diff --git a/.replit b/.replit index b28a4fdd..87a20613 100644 --- a/.replit +++ b/.replit @@ -60,6 +60,10 @@ externalPort = 3000 localPort = 40437 externalPort = 3001 +[[ports]] +localPort = 43237 +externalPort = 3002 + [deployment] deploymentTarget = "autoscale" run = ["node", "dist/server/production.mjs"] diff --git a/client/components/feed/ArmFeed.tsx b/client/components/feed/ArmFeed.tsx index 8b27db8c..e4237528 100644 --- a/client/components/feed/ArmFeed.tsx +++ b/client/components/feed/ArmFeed.tsx @@ -14,6 +14,7 @@ import { CardTitle, } from "@/components/ui/card"; import { useAuth } from "@/contexts/AuthContext"; +import { useToast } from "@/hooks/use-toast"; import { aethexSocialService } from "@/lib/aethex-social-service"; import { cn } from "@/lib/utils"; import { normalizeErrorMessage } from "@/lib/error-utils"; @@ -110,6 +111,9 @@ interface FeedItem { likes: number; comments: number; arm?: ArmType; + source?: "discord" | "web" | null; + discordChannelName?: string | null; + discordAuthorTag?: string | null; } function parseContent(content: string) { @@ -125,9 +129,12 @@ function parseContent(content: string) { ? "video" : "image" : "none"), + source: obj.source || null, + discordChannelName: obj.discord_channel_name || obj.discord_channel || null, + discordAuthorTag: obj.discord_author_tag || null, }; } catch { - return { text: content, mediaUrl: null, mediaType: "none" }; + return { text: content, mediaUrl: null, mediaType: "none", source: null }; } } @@ -137,7 +144,7 @@ interface ArmFeedProps { export default function ArmFeed({ arm }: ArmFeedProps) { const { user, loading } = useAuth(); - const { toast } = useAuth().toast || { toast: () => {} }; + const { toast } = useToast(); const [isLoading, setIsLoading] = useState(true); const [following, setFollowing] = useState([]); @@ -160,6 +167,9 @@ export default function ArmFeed({ arm }: ArmFeedProps) { likes: p.likes_count ?? 0, comments: p.comments_count ?? 0, arm: p.arm_affiliation || "labs", + source: meta.source, + discordChannelName: meta.discordChannelName, + discordAuthorTag: meta.discordAuthorTag, }; }), [], diff --git a/client/components/social/FeedItemCard.tsx b/client/components/social/FeedItemCard.tsx index e26fb574..0b0bfcb8 100644 --- a/client/components/social/FeedItemCard.tsx +++ b/client/components/social/FeedItemCard.tsx @@ -15,9 +15,15 @@ import { cn } from "@/lib/utils"; import { useAuth } from "@/contexts/AuthContext"; import { useToast } from "@/hooks/use-toast"; import { communityService } from "@/lib/supabase-service"; -import { Heart, MessageCircle, Share2, Volume2, VolumeX } from "lucide-react"; +import { Heart, MessageCircle, Share2, Volume2, VolumeX, MessageSquare } from "lucide-react"; import type { FeedItem } from "@/pages/Feed"; +const DiscordIcon = () => ( + + + +); + const ARM_COLORS: Record< string, { bg: string; border: string; badge: string; text: string } @@ -179,6 +185,14 @@ export function FeedItemCard({ > {armLabel} + {item.source === "discord" && ( + + + {item.discordChannelName ? `#${item.discordChannelName}` : "Discord"} + + )} diff --git a/client/pages/Feed.tsx b/client/pages/Feed.tsx index 24701441..aed6d23d 100644 --- a/client/pages/Feed.tsx +++ b/client/pages/Feed.tsx @@ -77,6 +77,9 @@ export interface FeedItem { likes: number; comments: number; arm?: ArmType; + source?: "discord" | "web" | null; + discordChannelName?: string | null; + discordAuthorTag?: string | null; } interface TrendingTopic { @@ -96,6 +99,9 @@ function parseContent(content: string): { text?: string; mediaUrl?: string | null; mediaType: "video" | "image" | "none"; + source?: "discord" | "web" | null; + discordChannelName?: string | null; + discordAuthorTag?: string | null; } { try { const obj = JSON.parse(content || "{}"); @@ -109,9 +115,12 @@ function parseContent(content: string): { ? "video" : "image" : "none"), + source: obj.source || null, + discordChannelName: obj.discord_channel_name || obj.discord_channel || null, + discordAuthorTag: obj.discord_author_tag || null, }; } catch { - return { text: content, mediaUrl: null, mediaType: "none" }; + return { text: content, mediaUrl: null, mediaType: "none", source: null }; } } @@ -147,6 +156,9 @@ export default function Feed() { likes: p.likes_count ?? 0, comments: p.comments_count ?? 0, arm: p.arm_affiliation || "labs", + source: meta.source, + discordChannelName: meta.discordChannelName, + discordAuthorTag: meta.discordAuthorTag, }; }), [], diff --git a/discord-bot/events/messageCreate-announcements.js b/discord-bot/events/messageCreate-announcements.js deleted file mode 100644 index 0193468e..00000000 --- a/discord-bot/events/messageCreate-announcements.js +++ /dev/null @@ -1,237 +0,0 @@ -const { createClient } = require("@supabase/supabase-js"); - -// Initialize Supabase -const supabase = createClient( - process.env.SUPABASE_URL, - process.env.SUPABASE_SERVICE_ROLE, -); - -const API_BASE = process.env.VITE_API_BASE || "https://api.aethex.dev"; - -// Channel IDs for syncing -const ANNOUNCEMENT_CHANNELS = process.env.DISCORD_ANNOUNCEMENT_CHANNELS - ? process.env.DISCORD_ANNOUNCEMENT_CHANNELS.split(",") - : ["1435667453244866702"]; // Default to feed channel if env not set - -// Arm affiliation mapping based on guild/channel name -const getArmAffiliation = (message) => { - const guildName = message.guild?.name?.toLowerCase() || ""; - const channelName = message.channel?.name?.toLowerCase() || ""; - - const searchString = `${guildName} ${channelName}`.toLowerCase(); - - if (searchString.includes("gameforge")) return "gameforge"; - if (searchString.includes("corp")) return "corp"; - if (searchString.includes("foundation")) return "foundation"; - if (searchString.includes("devlink") || searchString.includes("dev-link")) - return "devlink"; - if (searchString.includes("nexus")) return "nexus"; - if (searchString.includes("staff")) return "staff"; - - return "labs"; // Default -}; - -module.exports = { - name: "messageCreate", - async execute(message, client, supabase) { - try { - // Ignore bot messages - if (message.author.bot) return; - - // Only process messages in announcement channels - if (!ANNOUNCEMENT_CHANNELS.includes(message.channelId)) { - return; - } - - // Skip empty messages - if (!message.content && message.attachments.size === 0) { - return; - } - - console.log( - `[Announcements Sync] Processing message from ${message.author.tag} in #${message.channel.name}`, - ); - - // Get or create system admin user for announcements - let adminUser = await supabase - .from("user_profiles") - .select("id") - .eq("username", "aethex-announcements") - .single(); - - let authorId = adminUser.data?.id; - - if (!authorId) { - // Create a system user if it doesn't exist - const { data: newUser } = await supabase - .from("user_profiles") - .insert({ - username: "aethex-announcements", - full_name: "AeThex Announcements", - avatar_url: "https://aethex.dev/logo.png", - }) - .select("id"); - - authorId = newUser?.[0]?.id; - } - - if (!authorId) { - console.error("[Announcements Sync] Could not get author ID"); - return; - } - - // Prepare message content - let content = message.content || "Announcement from Discord"; - - // Handle embeds (convert to text) - if (message.embeds.length > 0) { - const embed = message.embeds[0]; - if (embed.title) content = embed.title + "\n\n" + content; - if (embed.description) content += "\n\n" + embed.description; - } - - // Handle attachments (images, videos) - let mediaUrl = null; - let mediaType = "none"; - - if (message.attachments.size > 0) { - const attachment = message.attachments.first(); - if (attachment) { - mediaUrl = attachment.url; - - 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"; - } - } - } - - // Determine arm affiliation - const armAffiliation = getArmAffiliation(message); - - // Prepare post content JSON - const postContent = JSON.stringify({ - text: content, - mediaUrl: mediaUrl, - mediaType: mediaType, - source: "discord", - discord_message_id: message.id, - discord_author: message.author.tag, - }); - - // Create post in AeThex - const { data: createdPost, error: insertError } = await supabase - .from("community_posts") - .insert({ - title: content.substring(0, 100) || "Discord Announcement", - content: postContent, - arm_affiliation: armAffiliation, - author_id: authorId, - tags: ["discord", "announcement"], - category: "announcement", - is_published: true, - likes_count: 0, - comments_count: 0, - }) - .select( - `id, title, content, arm_affiliation, author_id, created_at, likes_count, comments_count, - user_profiles!community_posts_author_id_fkey (id, username, full_name, avatar_url)`, - ); - - if (insertError) { - console.error( - "[Announcements Sync] Failed to create post:", - insertError, - ); - try { - await message.react("❌"); - } catch (reactionError) { - console.warn( - "[Announcements Sync] Could not add reaction:", - reactionError, - ); - } - return; - } - - // Sync to Discord feed webhook if configured - if (process.env.DISCORD_FEED_WEBHOOK_URL && createdPost?.[0]) { - try { - const post = createdPost[0]; - const armColors = { - labs: 0xfbbf24, - gameforge: 0x22c55e, - corp: 0x3b82f6, - foundation: 0xef4444, - devlink: 0x06b6d4, - nexus: 0xa855f7, - staff: 0x6366f1, - }; - - const embed = { - title: post.title, - description: content.substring(0, 1024), - color: armColors[armAffiliation] || 0x8b5cf6, - author: { - name: `${message.author.username} (${armAffiliation.toUpperCase()})`, - icon_url: message.author.displayAvatarURL(), - }, - footer: { - text: "Synced from Discord", - }, - timestamp: new Date().toISOString(), - }; - - await fetch(process.env.DISCORD_FEED_WEBHOOK_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - username: "AeThex Community Feed", - embeds: [embed], - }), - }); - } catch (webhookError) { - console.warn( - "[Announcements Sync] Failed to sync to webhook:", - webhookError, - ); - } - } - - console.log( - `[Announcements Sync] ✅ Posted announcement from Discord to AeThex (${armAffiliation})`, - ); - - // React with success emoji - try { - await message.react("✅"); - } catch (reactionError) { - console.warn( - "[Announcements Sync] Could not add success reaction:", - reactionError, - ); - } - } catch (error) { - console.error("[Announcements Sync] Unexpected error:", error); - - try { - await message.react("⚠️"); - } catch (reactionError) { - console.warn( - "[Announcements Sync] Could not add warning reaction:", - reactionError, - ); - } - } - }, -}; diff --git a/discord-bot/events/messageCreate.js b/discord-bot/events/messageCreate.js index 344c3b2e..e206978c 100644 --- a/discord-bot/events/messageCreate.js +++ b/discord-bot/events/messageCreate.js @@ -15,6 +15,11 @@ const ANNOUNCEMENT_CHANNELS = process.env.DISCORD_ANNOUNCEMENT_CHANNELS ? process.env.DISCORD_ANNOUNCEMENT_CHANNELS.split(",").map((id) => id.trim()) : []; +// Main chat channels - sync ALL messages from these channels to the feed +const MAIN_CHAT_CHANNELS = process.env.DISCORD_MAIN_CHAT_CHANNELS + ? process.env.DISCORD_MAIN_CHAT_CHANNELS.split(",").map((id) => id.trim()) + : []; + // Helper: Get arm affiliation from message context function getArmAffiliation(message) { const guildName = message.guild?.name?.toLowerCase() || ""; @@ -32,6 +37,147 @@ function getArmAffiliation(message) { return "labs"; } +// Handle main chat messages - sync ALL messages to feed +async function handleMainChatSync(message) { + try { + console.log( + `[Main Chat] Processing from ${message.author.tag} in #${message.channel.name}`, + ); + + // Check if user has linked account + const { data: linkedAccount } = await supabase + .from("discord_links") + .select("user_id") + .eq("discord_id", message.author.id) + .single(); + + let authorId = linkedAccount?.user_id; + let authorInfo = null; + + if (authorId) { + // Get linked user's profile + const { data: profile } = await supabase + .from("user_profiles") + .select("id, username, full_name, avatar_url") + .eq("id", authorId) + .single(); + authorInfo = profile; + } + + // If no linked account, use or create a Discord guest profile + if (!authorId) { + // Check if we have a discord guest profile for this user + const discordUsername = `discord-${message.author.id}`; + let { data: guestProfile } = await supabase + .from("user_profiles") + .select("id, username, full_name, avatar_url") + .eq("username", discordUsername) + .single(); + + if (!guestProfile) { + // Create guest profile + const { data: newProfile, error: createError } = await supabase + .from("user_profiles") + .insert({ + username: discordUsername, + full_name: message.author.displayName || message.author.username, + avatar_url: message.author.displayAvatarURL({ size: 256 }), + }) + .select("id, username, full_name, avatar_url") + .single(); + + if (createError) { + console.error("[Main Chat] Could not create guest profile:", createError); + return; + } + guestProfile = newProfile; + } + + authorId = guestProfile?.id; + authorInfo = guestProfile; + } + + if (!authorId) { + console.error("[Main Chat] Could not get author ID"); + return; + } + + // Prepare content + let content = message.content || "Shared a message on Discord"; + let mediaUrl = null; + let mediaType = "none"; + + if (message.attachments.size > 0) { + const attachment = message.attachments.first(); + if (attachment) { + mediaUrl = attachment.url; + const attachmentLower = attachment.name.toLowerCase(); + + if ( + [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((ext) => + attachmentLower.endsWith(ext), + ) + ) { + mediaType = "image"; + } else if ( + [".mp4", ".webm", ".mov", ".avi"].some((ext) => + attachmentLower.endsWith(ext), + ) + ) { + mediaType = "video"; + } + } + } + + // Determine arm affiliation + const armAffiliation = getArmAffiliation(message); + + // Prepare post content with Discord metadata + const postContent = JSON.stringify({ + text: content, + mediaUrl: mediaUrl, + mediaType: mediaType, + source: "discord", + discord_message_id: message.id, + discord_channel_id: message.channelId, + discord_channel_name: message.channel.name, + discord_guild_id: message.guildId, + discord_guild_name: message.guild?.name, + discord_author_id: message.author.id, + discord_author_tag: message.author.tag, + discord_author_avatar: message.author.displayAvatarURL({ size: 256 }), + is_linked_user: !!linkedAccount, + }); + + // Create post + const { data: createdPost, error: insertError } = await supabase + .from("community_posts") + .insert({ + title: content.substring(0, 100) || "Discord Message", + content: postContent, + arm_affiliation: armAffiliation, + author_id: authorId, + tags: ["discord", "main-chat"], + category: "discord", + is_published: true, + likes_count: 0, + comments_count: 0, + }) + .select("id"); + + if (insertError) { + console.error("[Main Chat] Post creation failed:", insertError); + return; + } + + console.log( + `[Main Chat] ✅ Synced message from ${message.author.tag} to AeThex feed`, + ); + } catch (error) { + console.error("[Main Chat] Error:", error); + } +} + // Handle announcements from designated channels async function handleAnnouncementSync(message) { try { @@ -169,6 +315,14 @@ module.exports = { return handleAnnouncementSync(message); } + // Check if this is a main chat channel - sync ALL messages + if ( + MAIN_CHAT_CHANNELS.length > 0 && + MAIN_CHAT_CHANNELS.includes(message.channelId) + ) { + return handleMainChatSync(message); + } + // Check if this is in the feed channel (for user-generated posts) if (FEED_CHANNEL_ID && message.channelId !== FEED_CHANNEL_ID) { return; @@ -183,7 +337,7 @@ module.exports = { const { data: linkedAccount, error } = await supabase .from("discord_links") .select("user_id") - .eq("discord_user_id", message.author.id) + .eq("discord_id", message.author.id) .single(); if (error || !linkedAccount) {