Add a secure bot management panel and new Discord commands
Implement server-side proxy endpoints for bot management, add admin token authentication, and introduce new Discord slash commands for help, stats, leaderboards, and posting. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9203795e-937a-4306-b81d-b4d5c78c240e Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: f0eccab4-b258-4b1c-a2a5-e7b2b3c56c44 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7c94b7a0-29c7-4f2e-94ef-44b2153872b7/9203795e-937a-4306-b81d-b4d5c78c240e/ryY0zvi Replit-Helium-Checkpoint-Created: true
This commit is contained in:
commit
f3dc7dd642
10 changed files with 1491 additions and 10 deletions
8
.replit
8
.replit
|
|
@ -52,6 +52,10 @@ externalPort = 80
|
||||||
localPort = 8044
|
localPort = 8044
|
||||||
externalPort = 3003
|
externalPort = 3003
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 33499
|
||||||
|
externalPort = 3002
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 38557
|
localPort = 38557
|
||||||
externalPort = 3000
|
externalPort = 3000
|
||||||
|
|
@ -60,10 +64,6 @@ externalPort = 3000
|
||||||
localPort = 40437
|
localPort = 40437
|
||||||
externalPort = 3001
|
externalPort = 3001
|
||||||
|
|
||||||
[[ports]]
|
|
||||||
localPort = 43879
|
|
||||||
externalPort = 3002
|
|
||||||
|
|
||||||
[deployment]
|
[deployment]
|
||||||
deploymentTarget = "autoscale"
|
deploymentTarget = "autoscale"
|
||||||
run = ["node", "dist/server/production.mjs"]
|
run = ["node", "dist/server/production.mjs"]
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ import DiscordVerify from "./pages/DiscordVerify";
|
||||||
import { Analytics } from "@vercel/analytics/react";
|
import { Analytics } from "@vercel/analytics/react";
|
||||||
import CreatorDirectory from "./pages/creators/CreatorDirectory";
|
import CreatorDirectory from "./pages/creators/CreatorDirectory";
|
||||||
import CreatorProfile from "./pages/creators/CreatorProfile";
|
import CreatorProfile from "./pages/creators/CreatorProfile";
|
||||||
|
import BotPanel from "./pages/BotPanel";
|
||||||
import OpportunitiesHub from "./pages/opportunities/OpportunitiesHub";
|
import OpportunitiesHub from "./pages/opportunities/OpportunitiesHub";
|
||||||
import OpportunityDetail from "./pages/opportunities/OpportunityDetail";
|
import OpportunityDetail from "./pages/opportunities/OpportunityDetail";
|
||||||
import OpportunityPostForm from "./pages/opportunities/OpportunityPostForm";
|
import OpportunityPostForm from "./pages/opportunities/OpportunityPostForm";
|
||||||
|
|
@ -274,6 +275,14 @@ const App = () => (
|
||||||
path="/discord-verify"
|
path="/discord-verify"
|
||||||
element={<DiscordVerify />}
|
element={<DiscordVerify />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/bot-panel"
|
||||||
|
element={
|
||||||
|
<RequireAccess>
|
||||||
|
<BotPanel />
|
||||||
|
</RequireAccess>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route path="/activity" element={<Activity />} />
|
<Route path="/activity" element={<Activity />} />
|
||||||
<Route path="/discord" element={<DiscordActivity />} />
|
<Route path="/discord" element={<DiscordActivity />} />
|
||||||
<Route
|
<Route
|
||||||
|
|
|
||||||
628
client/pages/BotPanel.tsx
Normal file
628
client/pages/BotPanel.tsx
Normal file
|
|
@ -0,0 +1,628 @@
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
|
import {
|
||||||
|
Bot,
|
||||||
|
Server,
|
||||||
|
Terminal,
|
||||||
|
Users,
|
||||||
|
MessageSquare,
|
||||||
|
RefreshCw,
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
|
Clock,
|
||||||
|
Zap,
|
||||||
|
ArrowLeftRight,
|
||||||
|
Hash,
|
||||||
|
Activity,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
interface BotStatus {
|
||||||
|
status: string;
|
||||||
|
bot: {
|
||||||
|
tag: string;
|
||||||
|
id: string;
|
||||||
|
avatar: string;
|
||||||
|
};
|
||||||
|
guilds: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
memberCount: number;
|
||||||
|
icon: string;
|
||||||
|
}>;
|
||||||
|
guildCount: number;
|
||||||
|
commands: string[];
|
||||||
|
commandCount: number;
|
||||||
|
uptime: number;
|
||||||
|
feedBridge: {
|
||||||
|
enabled: boolean;
|
||||||
|
channelId: string;
|
||||||
|
};
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LinkedUser {
|
||||||
|
discord_id: string;
|
||||||
|
user_id: string;
|
||||||
|
primary_arm: string;
|
||||||
|
created_at: string;
|
||||||
|
profile: {
|
||||||
|
username: string;
|
||||||
|
full_name: string;
|
||||||
|
avatar_url: string;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FeedStats {
|
||||||
|
totalPosts: number;
|
||||||
|
discordPosts: number;
|
||||||
|
websitePosts: number;
|
||||||
|
recentPosts: Array<{
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
source: string;
|
||||||
|
created_at: string;
|
||||||
|
discord_author_name: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommandInfo {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
options: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const API_BASE = "/api/discord";
|
||||||
|
|
||||||
|
export default function BotPanel() {
|
||||||
|
const { user, loading: authLoading } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [botStatus, setBotStatus] = useState<BotStatus | null>(null);
|
||||||
|
const [linkedUsers, setLinkedUsers] = useState<LinkedUser[]>([]);
|
||||||
|
const [feedStats, setFeedStats] = useState<FeedStats | null>(null);
|
||||||
|
const [commands, setCommands] = useState<CommandInfo[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authLoading && !user) {
|
||||||
|
navigate("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchAllData();
|
||||||
|
}, [user, authLoading, navigate]);
|
||||||
|
|
||||||
|
const fetchAllData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
await Promise.all([
|
||||||
|
fetchBotStatus(),
|
||||||
|
fetchLinkedUsers(),
|
||||||
|
fetchFeedStats(),
|
||||||
|
fetchCommands(),
|
||||||
|
]);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setRefreshing(true);
|
||||||
|
await fetchAllData();
|
||||||
|
setRefreshing(false);
|
||||||
|
aethexToast.success({ description: "Data refreshed successfully" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchBotStatus = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/bot-status`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
setBotStatus(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch bot status:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchLinkedUsers = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/linked-users`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
setLinkedUsers(data.links || []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch linked users:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchFeedStats = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/feed-stats`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
setFeedStats(data.stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch feed stats:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchCommands = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/command-stats`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
setCommands(data.stats.commands || []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch commands:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerCommands = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/bot-register-commands`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
aethexToast.success({
|
||||||
|
title: "Commands Registered",
|
||||||
|
description: `Successfully registered ${data.count} commands`,
|
||||||
|
});
|
||||||
|
await fetchCommands();
|
||||||
|
} else {
|
||||||
|
throw new Error(data.error);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
aethexToast.error({
|
||||||
|
description: error?.message || "Failed to register commands",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatUptime = (seconds: number) => {
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
if (days > 0) return `${days}d ${hours}h ${minutes}m`;
|
||||||
|
if (hours > 0) return `${hours}h ${minutes}m`;
|
||||||
|
return `${minutes}m`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateStr: string) => {
|
||||||
|
return new Date(dateStr).toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (authLoading || loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-purple-900/20 to-gray-900 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<Bot className="w-16 h-16 text-purple-500 animate-pulse mx-auto mb-4" />
|
||||||
|
<p className="text-gray-400">Loading Bot Panel...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-purple-900/20 to-gray-900 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="p-3 bg-purple-500/20 rounded-xl">
|
||||||
|
<Bot className="w-8 h-8 text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-white">Bot Panel</h1>
|
||||||
|
<p className="text-gray-400">Manage your AeThex Discord bot</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleRefresh}
|
||||||
|
variant="outline"
|
||||||
|
className="border-purple-500/50 hover:bg-purple-500/20"
|
||||||
|
disabled={refreshing}
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-4 h-4 mr-2 ${refreshing ? "animate-spin" : ""}`} />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<Card className="bg-gray-800/50 border-gray-700">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400">Status</p>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
{botStatus?.status === "online" ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||||
|
<span className="text-xl font-bold text-green-500">Online</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<XCircle className="w-5 h-5 text-red-500" />
|
||||||
|
<span className="text-xl font-bold text-red-500">Offline</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Activity className="w-10 h-10 text-purple-500/50" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-gray-800/50 border-gray-700">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400">Servers</p>
|
||||||
|
<p className="text-2xl font-bold text-white mt-1">
|
||||||
|
{botStatus?.guildCount || 0}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Server className="w-10 h-10 text-blue-500/50" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-gray-800/50 border-gray-700">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400">Uptime</p>
|
||||||
|
<p className="text-2xl font-bold text-white mt-1">
|
||||||
|
{botStatus ? formatUptime(botStatus.uptime) : "--"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Clock className="w-10 h-10 text-yellow-500/50" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-gray-800/50 border-gray-700">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400">Linked Users</p>
|
||||||
|
<p className="text-2xl font-bold text-white mt-1">{linkedUsers.length}</p>
|
||||||
|
</div>
|
||||||
|
<Users className="w-10 h-10 text-green-500/50" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs defaultValue="overview" className="space-y-4">
|
||||||
|
<TabsList className="bg-gray-800/50 border border-gray-700">
|
||||||
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||||
|
<TabsTrigger value="servers">Servers</TabsTrigger>
|
||||||
|
<TabsTrigger value="commands">Commands</TabsTrigger>
|
||||||
|
<TabsTrigger value="users">Linked Users</TabsTrigger>
|
||||||
|
<TabsTrigger value="feed">Feed Bridge</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="overview" className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<Card className="bg-gray-800/50 border-gray-700">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-white flex items-center gap-2">
|
||||||
|
<Bot className="w-5 h-5 text-purple-400" />
|
||||||
|
Bot Information
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{botStatus?.bot && (
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{botStatus.bot.avatar && (
|
||||||
|
<img
|
||||||
|
src={botStatus.bot.avatar}
|
||||||
|
alt="Bot Avatar"
|
||||||
|
className="w-16 h-16 rounded-full border-2 border-purple-500"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<p className="text-xl font-bold text-white">{botStatus.bot.tag}</p>
|
||||||
|
<p className="text-sm text-gray-400">ID: {botStatus.bot.id}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Separator className="bg-gray-700" />
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400">Commands</p>
|
||||||
|
<p className="text-lg font-semibold text-white">
|
||||||
|
{botStatus?.commandCount || 0}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400">Feed Bridge</p>
|
||||||
|
<Badge
|
||||||
|
variant={botStatus?.feedBridge?.enabled ? "default" : "secondary"}
|
||||||
|
className={
|
||||||
|
botStatus?.feedBridge?.enabled
|
||||||
|
? "bg-green-500/20 text-green-400"
|
||||||
|
: "bg-gray-500/20 text-gray-400"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{botStatus?.feedBridge?.enabled ? "Enabled" : "Disabled"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="bg-gray-800/50 border-gray-700">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-white flex items-center gap-2">
|
||||||
|
<ArrowLeftRight className="w-5 h-5 text-purple-400" />
|
||||||
|
Feed Bridge Stats
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="text-center p-4 bg-gray-700/30 rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-white">{feedStats?.totalPosts || 0}</p>
|
||||||
|
<p className="text-sm text-gray-400">Total Posts</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-purple-500/10 rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-purple-400">
|
||||||
|
{feedStats?.discordPosts || 0}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-400">From Discord</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center p-4 bg-blue-500/10 rounded-lg">
|
||||||
|
<p className="text-2xl font-bold text-blue-400">
|
||||||
|
{feedStats?.websitePosts || 0}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-400">From Website</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="servers">
|
||||||
|
<Card className="bg-gray-800/50 border-gray-700">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-white flex items-center gap-2">
|
||||||
|
<Server className="w-5 h-5 text-blue-400" />
|
||||||
|
Connected Servers ({botStatus?.guildCount || 0})
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-gray-400">
|
||||||
|
All Discord servers where the bot is installed
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ScrollArea className="h-[400px]">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{botStatus?.guilds?.map((guild) => (
|
||||||
|
<div
|
||||||
|
key={guild.id}
|
||||||
|
className="flex items-center justify-between p-4 bg-gray-700/30 rounded-lg hover:bg-gray-700/50 transition"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{guild.icon ? (
|
||||||
|
<img
|
||||||
|
src={guild.icon}
|
||||||
|
alt={guild.name}
|
||||||
|
className="w-12 h-12 rounded-full"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-12 h-12 bg-gray-600 rounded-full flex items-center justify-center">
|
||||||
|
<Server className="w-6 h-6 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-white">{guild.name}</p>
|
||||||
|
<p className="text-sm text-gray-400">ID: {guild.id}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Users className="w-4 h-4 text-gray-400" />
|
||||||
|
<span className="text-gray-300">{guild.memberCount} members</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{(!botStatus?.guilds || botStatus.guilds.length === 0) && (
|
||||||
|
<div className="text-center py-8 text-gray-400">
|
||||||
|
No servers connected yet
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="commands">
|
||||||
|
<Card className="bg-gray-800/50 border-gray-700">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-white flex items-center gap-2">
|
||||||
|
<Terminal className="w-5 h-5 text-green-400" />
|
||||||
|
Slash Commands ({commands.length})
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-gray-400">
|
||||||
|
All available Discord slash commands
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={registerCommands}
|
||||||
|
className="bg-purple-600 hover:bg-purple-700"
|
||||||
|
>
|
||||||
|
<Zap className="w-4 h-4 mr-2" />
|
||||||
|
Register Commands
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
{commands.map((cmd) => (
|
||||||
|
<div
|
||||||
|
key={cmd.name}
|
||||||
|
className="p-4 bg-gray-700/30 rounded-lg hover:bg-gray-700/50 transition"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Hash className="w-4 h-4 text-purple-400" />
|
||||||
|
<span className="font-mono font-semibold text-white">/{cmd.name}</span>
|
||||||
|
{cmd.options > 0 && (
|
||||||
|
<Badge variant="outline" className="text-xs border-gray-600">
|
||||||
|
{cmd.options} options
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-400">{cmd.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="users">
|
||||||
|
<Card className="bg-gray-800/50 border-gray-700">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-white flex items-center gap-2">
|
||||||
|
<Users className="w-5 h-5 text-green-400" />
|
||||||
|
Linked Users ({linkedUsers.length})
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-gray-400">
|
||||||
|
Discord accounts linked to AeThex profiles
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ScrollArea className="h-[400px]">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{linkedUsers.map((link) => (
|
||||||
|
<div
|
||||||
|
key={link.discord_id}
|
||||||
|
className="flex items-center justify-between p-4 bg-gray-700/30 rounded-lg"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{link.profile?.avatar_url ? (
|
||||||
|
<img
|
||||||
|
src={link.profile.avatar_url}
|
||||||
|
alt="Avatar"
|
||||||
|
className="w-10 h-10 rounded-full"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-10 h-10 bg-gray-600 rounded-full flex items-center justify-center">
|
||||||
|
<Users className="w-5 h-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-white">
|
||||||
|
{link.profile?.full_name || link.profile?.username || "Unknown"}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
Discord ID: {link.discord_id}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{link.primary_arm && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="border-purple-500/50 text-purple-400"
|
||||||
|
>
|
||||||
|
{link.primary_arm}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<span className="text-sm text-gray-400">
|
||||||
|
{formatDate(link.created_at)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{linkedUsers.length === 0 && (
|
||||||
|
<div className="text-center py-8 text-gray-400">
|
||||||
|
No linked users yet
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="feed">
|
||||||
|
<Card className="bg-gray-800/50 border-gray-700">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-white flex items-center gap-2">
|
||||||
|
<MessageSquare className="w-5 h-5 text-purple-400" />
|
||||||
|
Recent Feed Activity
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-gray-400">
|
||||||
|
Latest posts synced between Discord and AeThex
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ScrollArea className="h-[400px]">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{feedStats?.recentPosts?.map((post) => (
|
||||||
|
<div
|
||||||
|
key={post.id}
|
||||||
|
className="p-4 bg-gray-700/30 rounded-lg"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Badge
|
||||||
|
className={
|
||||||
|
post.source === "discord"
|
||||||
|
? "bg-purple-500/20 text-purple-400"
|
||||||
|
: "bg-blue-500/20 text-blue-400"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{post.source === "discord" ? "Discord" : "Website"}
|
||||||
|
</Badge>
|
||||||
|
{post.discord_author_name && (
|
||||||
|
<span className="text-sm text-gray-400">
|
||||||
|
by {post.discord_author_name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="text-sm text-gray-500 ml-auto">
|
||||||
|
{formatDate(post.created_at)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-300 line-clamp-2">
|
||||||
|
{post.content?.slice(0, 200)}
|
||||||
|
{post.content?.length > 200 ? "..." : ""}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{(!feedStats?.recentPosts || feedStats.recentPosts.length === 0) && (
|
||||||
|
<div className="text-center py-8 text-gray-400">
|
||||||
|
No recent feed activity
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -172,6 +172,63 @@ const COMMANDS_TO_REGISTER = [
|
||||||
name: "verify-role",
|
name: "verify-role",
|
||||||
description: "Check your assigned Discord roles",
|
description: "Check your assigned Discord roles",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "help",
|
||||||
|
description: "View all AeThex bot commands and features",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stats",
|
||||||
|
description: "View your AeThex statistics and activity",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "leaderboard",
|
||||||
|
description: "View the top AeThex contributors",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "category",
|
||||||
|
type: 3,
|
||||||
|
description: "Leaderboard category",
|
||||||
|
required: false,
|
||||||
|
choices: [
|
||||||
|
{ name: "Most Active (Posts)", value: "posts" },
|
||||||
|
{ name: "Most Liked", value: "likes" },
|
||||||
|
{ name: "Top Creators", value: "creators" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "post",
|
||||||
|
description: "Create a post in the AeThex community feed",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: "content",
|
||||||
|
type: 3,
|
||||||
|
description: "Your post content",
|
||||||
|
required: true,
|
||||||
|
max_length: 500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "category",
|
||||||
|
type: 3,
|
||||||
|
description: "Post category",
|
||||||
|
required: false,
|
||||||
|
choices: [
|
||||||
|
{ name: "General", value: "general" },
|
||||||
|
{ name: "Project Update", value: "project_update" },
|
||||||
|
{ name: "Question", value: "question" },
|
||||||
|
{ name: "Idea", value: "idea" },
|
||||||
|
{ name: "Announcement", value: "announcement" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "image",
|
||||||
|
type: 11,
|
||||||
|
description: "Attach an image to your post",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Function to register commands with Discord
|
// Function to register commands with Discord
|
||||||
|
|
@ -255,10 +312,19 @@ async function registerDiscordCommands() {
|
||||||
|
|
||||||
// Start HTTP health check server
|
// Start HTTP health check server
|
||||||
const healthPort = process.env.HEALTH_PORT || 8044;
|
const healthPort = process.env.HEALTH_PORT || 8044;
|
||||||
|
const ADMIN_TOKEN = process.env.DISCORD_ADMIN_TOKEN || "aethex-bot-admin";
|
||||||
|
|
||||||
|
// Helper to check admin authentication
|
||||||
|
const checkAdminAuth = (req) => {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
return authHeader === `Bearer ${ADMIN_TOKEN}`;
|
||||||
|
};
|
||||||
|
|
||||||
http
|
http
|
||||||
.createServer((req, res) => {
|
.createServer((req, res) => {
|
||||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||||
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||||
res.setHeader("Content-Type", "application/json");
|
res.setHeader("Content-Type", "application/json");
|
||||||
|
|
||||||
if (req.method === "OPTIONS") {
|
if (req.method === "OPTIONS") {
|
||||||
|
|
@ -281,6 +347,179 @@ http
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET /bot-status - Comprehensive bot status for management panel (requires auth)
|
||||||
|
if (req.url === "/bot-status") {
|
||||||
|
if (!checkAdminAuth(req)) {
|
||||||
|
res.writeHead(401);
|
||||||
|
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelId = getFeedChannelId();
|
||||||
|
const guilds = client.guilds.cache.map((guild) => ({
|
||||||
|
id: guild.id,
|
||||||
|
name: guild.name,
|
||||||
|
memberCount: guild.memberCount,
|
||||||
|
icon: guild.iconURL(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
status: client.isReady() ? "online" : "offline",
|
||||||
|
bot: {
|
||||||
|
tag: client.user?.tag || "Not logged in",
|
||||||
|
id: client.user?.id,
|
||||||
|
avatar: client.user?.displayAvatarURL(),
|
||||||
|
},
|
||||||
|
guilds: guilds,
|
||||||
|
guildCount: client.guilds.cache.size,
|
||||||
|
commands: Array.from(client.commands.keys()),
|
||||||
|
commandCount: client.commands.size,
|
||||||
|
uptime: Math.floor(process.uptime()),
|
||||||
|
feedBridge: {
|
||||||
|
enabled: !!channelId,
|
||||||
|
channelId: channelId,
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /linked-users - Get all Discord-linked users (requires auth, sanitizes PII)
|
||||||
|
if (req.url === "/linked-users") {
|
||||||
|
if (!checkAdminAuth(req)) {
|
||||||
|
res.writeHead(401);
|
||||||
|
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const { data: links, error } = await supabase
|
||||||
|
.from("discord_links")
|
||||||
|
.select("discord_id, user_id, primary_arm, created_at")
|
||||||
|
.order("created_at", { ascending: false })
|
||||||
|
.limit(50);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
const enrichedLinks = await Promise.all(
|
||||||
|
(links || []).map(async (link) => {
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from("user_profiles")
|
||||||
|
.select("username, avatar_url")
|
||||||
|
.eq("id", link.user_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return {
|
||||||
|
discord_id: link.discord_id.slice(0, 6) + "***",
|
||||||
|
user_id: link.user_id.slice(0, 8) + "...",
|
||||||
|
primary_arm: link.primary_arm,
|
||||||
|
created_at: link.created_at,
|
||||||
|
profile: profile ? {
|
||||||
|
username: profile.username,
|
||||||
|
avatar_url: profile.avatar_url,
|
||||||
|
} : null,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(JSON.stringify({ success: true, links: enrichedLinks, count: enrichedLinks.length }));
|
||||||
|
} catch (error) {
|
||||||
|
res.writeHead(500);
|
||||||
|
res.end(JSON.stringify({ success: false, error: error.message }));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /command-stats - Get command usage statistics (requires auth)
|
||||||
|
if (req.url === "/command-stats") {
|
||||||
|
if (!checkAdminAuth(req)) {
|
||||||
|
res.writeHead(401);
|
||||||
|
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const stats = {
|
||||||
|
commands: COMMANDS_TO_REGISTER.map((cmd) => ({
|
||||||
|
name: cmd.name,
|
||||||
|
description: cmd.description,
|
||||||
|
options: cmd.options?.length || 0,
|
||||||
|
})),
|
||||||
|
totalCommands: COMMANDS_TO_REGISTER.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(JSON.stringify({ success: true, stats }));
|
||||||
|
} catch (error) {
|
||||||
|
res.writeHead(500);
|
||||||
|
res.end(JSON.stringify({ success: false, error: error.message }));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /feed-stats - Get feed bridge statistics (requires auth)
|
||||||
|
if (req.url === "/feed-stats") {
|
||||||
|
if (!checkAdminAuth(req)) {
|
||||||
|
res.writeHead(401);
|
||||||
|
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const { count: totalPosts } = await supabase
|
||||||
|
.from("community_posts")
|
||||||
|
.select("*", { count: "exact", head: true });
|
||||||
|
|
||||||
|
const { count: discordPosts } = await supabase
|
||||||
|
.from("community_posts")
|
||||||
|
.select("*", { count: "exact", head: true })
|
||||||
|
.eq("source", "discord");
|
||||||
|
|
||||||
|
const { count: websitePosts } = await supabase
|
||||||
|
.from("community_posts")
|
||||||
|
.select("*", { count: "exact", head: true })
|
||||||
|
.or("source.is.null,source.neq.discord");
|
||||||
|
|
||||||
|
const { data: recentPosts } = await supabase
|
||||||
|
.from("community_posts")
|
||||||
|
.select("id, content, source, created_at")
|
||||||
|
.order("created_at", { ascending: false })
|
||||||
|
.limit(10);
|
||||||
|
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
stats: {
|
||||||
|
totalPosts: totalPosts || 0,
|
||||||
|
discordPosts: discordPosts || 0,
|
||||||
|
websitePosts: websitePosts || 0,
|
||||||
|
recentPosts: (recentPosts || []).map(p => ({
|
||||||
|
id: p.id,
|
||||||
|
content: p.content?.slice(0, 100) + (p.content?.length > 100 ? "..." : ""),
|
||||||
|
source: p.source,
|
||||||
|
created_at: p.created_at,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
res.writeHead(500);
|
||||||
|
res.end(JSON.stringify({ success: false, error: error.message }));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// POST /send-to-discord - Send a post from AeThex to Discord channel
|
// POST /send-to-discord - Send a post from AeThex to Discord channel
|
||||||
if (req.url === "/send-to-discord" && req.method === "POST") {
|
if (req.url === "/send-to-discord" && req.method === "POST") {
|
||||||
let body = "";
|
let body = "";
|
||||||
|
|
@ -329,6 +568,11 @@ http
|
||||||
|
|
||||||
if (req.url === "/register-commands") {
|
if (req.url === "/register-commands") {
|
||||||
if (req.method === "GET") {
|
if (req.method === "GET") {
|
||||||
|
if (!checkAdminAuth(req)) {
|
||||||
|
res.writeHead(401);
|
||||||
|
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Show HTML form with button
|
// Show HTML form with button
|
||||||
res.writeHead(200, { "Content-Type": "text/html" });
|
res.writeHead(200, { "Content-Type": "text/html" });
|
||||||
res.end(`
|
res.end(`
|
||||||
|
|
@ -480,13 +724,10 @@ http
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "POST") {
|
if (req.method === "POST") {
|
||||||
// Verify admin token if provided
|
// Verify admin token
|
||||||
const authHeader = req.headers.authorization;
|
if (!checkAdminAuth(req)) {
|
||||||
const adminToken = process.env.DISCORD_ADMIN_REGISTER_TOKEN;
|
|
||||||
|
|
||||||
if (adminToken && authHeader !== `Bearer ${adminToken}`) {
|
|
||||||
res.writeHead(401);
|
res.writeHead(401);
|
||||||
res.end(JSON.stringify({ error: "Unauthorized" }));
|
res.end(JSON.stringify({ error: "Unauthorized - Admin token required" }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
55
discord-bot/commands/help.js
Normal file
55
discord-bot/commands/help.js
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("help")
|
||||||
|
.setDescription("View all AeThex bot commands and features"),
|
||||||
|
|
||||||
|
async execute(interaction) {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0x7289da)
|
||||||
|
.setTitle("🤖 AeThex Bot Commands")
|
||||||
|
.setDescription("Here are all the commands you can use with the AeThex Discord bot.")
|
||||||
|
.addFields(
|
||||||
|
{
|
||||||
|
name: "🔗 Account Linking",
|
||||||
|
value: [
|
||||||
|
"`/verify` - Link your Discord account to AeThex",
|
||||||
|
"`/unlink` - Disconnect your Discord from AeThex",
|
||||||
|
"`/profile` - View your linked AeThex profile",
|
||||||
|
].join("\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "⚔️ Realm Management",
|
||||||
|
value: [
|
||||||
|
"`/set-realm` - Choose your primary realm (Labs, GameForge, Corp, Foundation, Dev-Link)",
|
||||||
|
"`/verify-role` - Check your assigned Discord roles",
|
||||||
|
].join("\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "📊 Community",
|
||||||
|
value: [
|
||||||
|
"`/stats` - View your AeThex statistics and activity",
|
||||||
|
"`/leaderboard` - See the top contributors",
|
||||||
|
"`/post` - Create a post in the AeThex community feed",
|
||||||
|
].join("\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ℹ️ Information",
|
||||||
|
value: "`/help` - Show this help message",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.addFields({
|
||||||
|
name: "🔗 Quick Links",
|
||||||
|
value: [
|
||||||
|
"[AeThex Platform](https://aethex.dev)",
|
||||||
|
"[Creator Directory](https://aethex.dev/creators)",
|
||||||
|
"[Community Feed](https://aethex.dev/community/feed)",
|
||||||
|
].join(" | "),
|
||||||
|
})
|
||||||
|
.setFooter({ text: "AeThex | Build. Create. Connect." })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
|
},
|
||||||
|
};
|
||||||
155
discord-bot/commands/leaderboard.js
Normal file
155
discord-bot/commands/leaderboard.js
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("leaderboard")
|
||||||
|
.setDescription("View the top AeThex contributors")
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName("category")
|
||||||
|
.setDescription("Leaderboard category")
|
||||||
|
.setRequired(false)
|
||||||
|
.addChoices(
|
||||||
|
{ name: "🔥 Most Active (Posts)", value: "posts" },
|
||||||
|
{ name: "❤️ Most Liked", value: "likes" },
|
||||||
|
{ name: "🎨 Top Creators", value: "creators" }
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(interaction, supabase) {
|
||||||
|
await interaction.deferReply();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const category = interaction.options.getString("category") || "posts";
|
||||||
|
|
||||||
|
let leaderboardData = [];
|
||||||
|
let title = "";
|
||||||
|
let emoji = "";
|
||||||
|
|
||||||
|
if (category === "posts") {
|
||||||
|
title = "Most Active Posters";
|
||||||
|
emoji = "🔥";
|
||||||
|
|
||||||
|
const { data: posts } = await supabase
|
||||||
|
.from("community_posts")
|
||||||
|
.select("user_id")
|
||||||
|
.not("user_id", "is", null);
|
||||||
|
|
||||||
|
const postCounts = {};
|
||||||
|
posts?.forEach((post) => {
|
||||||
|
postCounts[post.user_id] = (postCounts[post.user_id] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedUsers = Object.entries(postCounts)
|
||||||
|
.sort(([, a], [, b]) => b - a)
|
||||||
|
.slice(0, 10);
|
||||||
|
|
||||||
|
for (const [userId, count] of sortedUsers) {
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from("user_profiles")
|
||||||
|
.select("username, full_name, avatar_url")
|
||||||
|
.eq("id", userId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (profile) {
|
||||||
|
leaderboardData.push({
|
||||||
|
name: profile.full_name || profile.username || "Anonymous",
|
||||||
|
value: `${count} posts`,
|
||||||
|
username: profile.username,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (category === "likes") {
|
||||||
|
title = "Most Liked Users";
|
||||||
|
emoji = "❤️";
|
||||||
|
|
||||||
|
const { data: posts } = await supabase
|
||||||
|
.from("community_posts")
|
||||||
|
.select("user_id, likes_count")
|
||||||
|
.not("user_id", "is", null)
|
||||||
|
.order("likes_count", { ascending: false });
|
||||||
|
|
||||||
|
const likeCounts = {};
|
||||||
|
posts?.forEach((post) => {
|
||||||
|
likeCounts[post.user_id] =
|
||||||
|
(likeCounts[post.user_id] || 0) + (post.likes_count || 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedUsers = Object.entries(likeCounts)
|
||||||
|
.sort(([, a], [, b]) => b - a)
|
||||||
|
.slice(0, 10);
|
||||||
|
|
||||||
|
for (const [userId, count] of sortedUsers) {
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from("user_profiles")
|
||||||
|
.select("username, full_name, avatar_url")
|
||||||
|
.eq("id", userId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (profile) {
|
||||||
|
leaderboardData.push({
|
||||||
|
name: profile.full_name || profile.username || "Anonymous",
|
||||||
|
value: `${count} likes received`,
|
||||||
|
username: profile.username,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (category === "creators") {
|
||||||
|
title = "Top Creators";
|
||||||
|
emoji = "🎨";
|
||||||
|
|
||||||
|
const { data: creators } = await supabase
|
||||||
|
.from("aethex_creators")
|
||||||
|
.select("user_id, total_projects, verified, featured")
|
||||||
|
.order("total_projects", { ascending: false })
|
||||||
|
.limit(10);
|
||||||
|
|
||||||
|
for (const creator of creators || []) {
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from("user_profiles")
|
||||||
|
.select("username, full_name, avatar_url")
|
||||||
|
.eq("id", creator.user_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (profile) {
|
||||||
|
const badges = [];
|
||||||
|
if (creator.verified) badges.push("✅");
|
||||||
|
if (creator.featured) badges.push("⭐");
|
||||||
|
|
||||||
|
leaderboardData.push({
|
||||||
|
name: profile.full_name || profile.username || "Anonymous",
|
||||||
|
value: `${creator.total_projects || 0} projects ${badges.join(" ")}`,
|
||||||
|
username: profile.username,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0x7289da)
|
||||||
|
.setTitle(`${emoji} ${title}`)
|
||||||
|
.setDescription(
|
||||||
|
leaderboardData.length > 0
|
||||||
|
? leaderboardData
|
||||||
|
.map(
|
||||||
|
(user, index) =>
|
||||||
|
`**${index + 1}.** ${user.name} - ${user.value}`
|
||||||
|
)
|
||||||
|
.join("\n")
|
||||||
|
: "No data available yet. Be the first to contribute!"
|
||||||
|
)
|
||||||
|
.setFooter({ text: "AeThex Leaderboard | Updated in real-time" })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.editReply({ embeds: [embed] });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Leaderboard command error:", error);
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0xff0000)
|
||||||
|
.setTitle("❌ Error")
|
||||||
|
.setDescription("Failed to fetch leaderboard. Please try again.");
|
||||||
|
|
||||||
|
await interaction.editReply({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
144
discord-bot/commands/post.js
Normal file
144
discord-bot/commands/post.js
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
const {
|
||||||
|
SlashCommandBuilder,
|
||||||
|
EmbedBuilder,
|
||||||
|
ModalBuilder,
|
||||||
|
TextInputBuilder,
|
||||||
|
TextInputStyle,
|
||||||
|
ActionRowBuilder,
|
||||||
|
} = require("discord.js");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("post")
|
||||||
|
.setDescription("Create a post in the AeThex community feed")
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName("content")
|
||||||
|
.setDescription("Your post content")
|
||||||
|
.setRequired(true)
|
||||||
|
.setMaxLength(500)
|
||||||
|
)
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName("category")
|
||||||
|
.setDescription("Post category")
|
||||||
|
.setRequired(false)
|
||||||
|
.addChoices(
|
||||||
|
{ name: "💬 General", value: "general" },
|
||||||
|
{ name: "🚀 Project Update", value: "project_update" },
|
||||||
|
{ name: "❓ Question", value: "question" },
|
||||||
|
{ name: "💡 Idea", value: "idea" },
|
||||||
|
{ name: "🎉 Announcement", value: "announcement" }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addAttachmentOption((option) =>
|
||||||
|
option
|
||||||
|
.setName("image")
|
||||||
|
.setDescription("Attach an image to your post")
|
||||||
|
.setRequired(false)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(interaction, supabase, client) {
|
||||||
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data: link } = await supabase
|
||||||
|
.from("discord_links")
|
||||||
|
.select("user_id, primary_arm")
|
||||||
|
.eq("discord_id", interaction.user.id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (!link) {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0xff6b6b)
|
||||||
|
.setTitle("❌ Not Linked")
|
||||||
|
.setDescription(
|
||||||
|
"You must link your Discord account to AeThex first.\nUse `/verify` to get started."
|
||||||
|
);
|
||||||
|
|
||||||
|
return await interaction.editReply({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from("user_profiles")
|
||||||
|
.select("username, full_name, avatar_url")
|
||||||
|
.eq("id", link.user_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
const content = interaction.options.getString("content");
|
||||||
|
const category = interaction.options.getString("category") || "general";
|
||||||
|
const attachment = interaction.options.getAttachment("image");
|
||||||
|
|
||||||
|
let imageUrl = null;
|
||||||
|
if (attachment && attachment.contentType?.startsWith("image/")) {
|
||||||
|
imageUrl = attachment.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryLabels = {
|
||||||
|
general: "General",
|
||||||
|
project_update: "Project Update",
|
||||||
|
question: "Question",
|
||||||
|
idea: "Idea",
|
||||||
|
announcement: "Announcement",
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: post, error } = await supabase
|
||||||
|
.from("community_posts")
|
||||||
|
.insert({
|
||||||
|
user_id: link.user_id,
|
||||||
|
content: content,
|
||||||
|
category: category,
|
||||||
|
arm_affiliation: link.primary_arm || "general",
|
||||||
|
image_url: imageUrl,
|
||||||
|
source: "discord",
|
||||||
|
discord_message_id: interaction.id,
|
||||||
|
discord_author_id: interaction.user.id,
|
||||||
|
discord_author_name: interaction.user.username,
|
||||||
|
discord_author_avatar: interaction.user.displayAvatarURL(),
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
const successEmbed = new EmbedBuilder()
|
||||||
|
.setColor(0x00ff00)
|
||||||
|
.setTitle("✅ Post Created!")
|
||||||
|
.setDescription(content.length > 100 ? content.slice(0, 100) + "..." : content)
|
||||||
|
.addFields(
|
||||||
|
{
|
||||||
|
name: "📁 Category",
|
||||||
|
value: categoryLabels[category],
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "⚔️ Realm",
|
||||||
|
value: link.primary_arm || "general",
|
||||||
|
inline: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (imageUrl) {
|
||||||
|
successEmbed.setImage(imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
successEmbed
|
||||||
|
.addFields({
|
||||||
|
name: "🔗 View Post",
|
||||||
|
value: `[Open in AeThex](https://aethex.dev/community/feed)`,
|
||||||
|
})
|
||||||
|
.setFooter({ text: "Your post is now live on AeThex!" })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.editReply({ embeds: [successEmbed] });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Post command error:", error);
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0xff0000)
|
||||||
|
.setTitle("❌ Error")
|
||||||
|
.setDescription("Failed to create post. Please try again.");
|
||||||
|
|
||||||
|
await interaction.editReply({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
140
discord-bot/commands/stats.js
Normal file
140
discord-bot/commands/stats.js
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("stats")
|
||||||
|
.setDescription("View your AeThex statistics and activity"),
|
||||||
|
|
||||||
|
async execute(interaction, supabase) {
|
||||||
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data: link } = await supabase
|
||||||
|
.from("discord_links")
|
||||||
|
.select("user_id, primary_arm, created_at")
|
||||||
|
.eq("discord_id", interaction.user.id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (!link) {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0xff6b6b)
|
||||||
|
.setTitle("❌ Not Linked")
|
||||||
|
.setDescription(
|
||||||
|
"You must link your Discord account to AeThex first.\nUse `/verify` to get started."
|
||||||
|
);
|
||||||
|
|
||||||
|
return await interaction.editReply({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from("user_profiles")
|
||||||
|
.select("*")
|
||||||
|
.eq("id", link.user_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
const { count: postCount } = await supabase
|
||||||
|
.from("community_posts")
|
||||||
|
.select("*", { count: "exact", head: true })
|
||||||
|
.eq("user_id", link.user_id);
|
||||||
|
|
||||||
|
const { count: likeCount } = await supabase
|
||||||
|
.from("community_likes")
|
||||||
|
.select("*", { count: "exact", head: true })
|
||||||
|
.eq("user_id", link.user_id);
|
||||||
|
|
||||||
|
const { count: commentCount } = await supabase
|
||||||
|
.from("community_comments")
|
||||||
|
.select("*", { count: "exact", head: true })
|
||||||
|
.eq("user_id", link.user_id);
|
||||||
|
|
||||||
|
const { data: creatorProfile } = await supabase
|
||||||
|
.from("aethex_creators")
|
||||||
|
.select("verified, featured, total_projects")
|
||||||
|
.eq("user_id", link.user_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
const armEmojis = {
|
||||||
|
labs: "🧪",
|
||||||
|
gameforge: "🎮",
|
||||||
|
corp: "💼",
|
||||||
|
foundation: "🤝",
|
||||||
|
devlink: "💻",
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkedDate = new Date(link.created_at);
|
||||||
|
const daysSinceLinked = Math.floor(
|
||||||
|
(Date.now() - linkedDate.getTime()) / (1000 * 60 * 60 * 24)
|
||||||
|
);
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0x7289da)
|
||||||
|
.setTitle(`📊 ${profile?.full_name || interaction.user.username}'s Stats`)
|
||||||
|
.setThumbnail(profile?.avatar_url || interaction.user.displayAvatarURL())
|
||||||
|
.addFields(
|
||||||
|
{
|
||||||
|
name: `${armEmojis[link.primary_arm] || "⚔️"} Primary Realm`,
|
||||||
|
value: link.primary_arm || "Not set",
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "👤 Account Type",
|
||||||
|
value: profile?.user_type || "community_member",
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "📅 Days Linked",
|
||||||
|
value: `${daysSinceLinked} days`,
|
||||||
|
inline: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.addFields(
|
||||||
|
{
|
||||||
|
name: "📝 Posts",
|
||||||
|
value: `${postCount || 0}`,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "❤️ Likes Given",
|
||||||
|
value: `${likeCount || 0}`,
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "💬 Comments",
|
||||||
|
value: `${commentCount || 0}`,
|
||||||
|
inline: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (creatorProfile) {
|
||||||
|
embed.addFields({
|
||||||
|
name: "🎨 Creator Status",
|
||||||
|
value: [
|
||||||
|
creatorProfile.verified ? "✅ Verified Creator" : "⏳ Pending Verification",
|
||||||
|
creatorProfile.featured ? "⭐ Featured" : "",
|
||||||
|
`📁 ${creatorProfile.total_projects || 0} Projects`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
embed
|
||||||
|
.addFields({
|
||||||
|
name: "🔗 Full Profile",
|
||||||
|
value: `[View on AeThex](https://aethex.dev/creators/${profile?.username || link.user_id})`,
|
||||||
|
})
|
||||||
|
.setFooter({ text: "AeThex | Your Creative Hub" })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.editReply({ embeds: [embed] });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Stats command error:", error);
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0xff0000)
|
||||||
|
.setTitle("❌ Error")
|
||||||
|
.setDescription("Failed to fetch stats. Please try again.");
|
||||||
|
|
||||||
|
await interaction.editReply({ embeds: [embed] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
20
replit.md
20
replit.md
|
|
@ -142,6 +142,26 @@ https://supabase.aethex.tech/auth/v1/callback
|
||||||
- `https://aethex.foundation/**`
|
- `https://aethex.foundation/**`
|
||||||
- `https://supabase.aethex.tech/auth/v1/callback`
|
- `https://supabase.aethex.tech/auth/v1/callback`
|
||||||
|
|
||||||
|
## Recent Changes (December 4, 2025)
|
||||||
|
- ✅ **Bot Panel** (`/bot-panel`): Comprehensive Discord bot management dashboard
|
||||||
|
- Overview tab: Bot info, feed bridge stats, uptime
|
||||||
|
- Servers tab: All connected Discord servers with member counts
|
||||||
|
- Commands tab: All slash commands with "Register Commands" button
|
||||||
|
- Linked Users tab: Discord-linked AeThex users (sanitized PII)
|
||||||
|
- Feed tab: Recent feed activity from Discord and website
|
||||||
|
- Protected with admin token authentication
|
||||||
|
- ✅ **New Discord Slash Commands**: Added 4 new commands
|
||||||
|
- `/help` - Shows all bot commands with descriptions
|
||||||
|
- `/stats` - View your AeThex statistics (posts, likes, comments)
|
||||||
|
- `/leaderboard` - Top contributors with category filter (posts, likes, creators)
|
||||||
|
- `/post` - Create a post directly from Discord with category and image support
|
||||||
|
- ✅ **Bot API Security**: Added authentication and CORS to management endpoints
|
||||||
|
- All management endpoints require admin token
|
||||||
|
- PII sanitized in linked users endpoint
|
||||||
|
- CORS headers added for browser access
|
||||||
|
- Server-side proxy endpoints (`/api/discord/bot-*`) to keep admin token secure
|
||||||
|
- Client uses proxied endpoints - no tokens exposed in frontend bundle
|
||||||
|
|
||||||
## Recent Changes (December 3, 2025)
|
## Recent Changes (December 3, 2025)
|
||||||
- ✅ **Discord Feed Bridge Bug Fix**: Fixed critical 14x duplicate post issue with three-layer protection
|
- ✅ **Discord Feed Bridge Bug Fix**: Fixed critical 14x duplicate post issue with three-layer protection
|
||||||
- Added polling lock to prevent overlapping poll cycles
|
- Added polling lock to prevent overlapping poll cycles
|
||||||
|
|
|
||||||
|
|
@ -2226,6 +2226,95 @@ export function createServer() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Bot Management Proxy Endpoints (session-authenticated)
|
||||||
|
const BOT_ADMIN_TOKEN = process.env.DISCORD_ADMIN_TOKEN || "aethex-bot-admin";
|
||||||
|
const getBotApiUrl = () => {
|
||||||
|
const urls = [
|
||||||
|
process.env.DISCORD_BOT_HEALTH_URL?.replace("/health", ""),
|
||||||
|
"http://localhost:8044",
|
||||||
|
].filter(Boolean);
|
||||||
|
return urls[0] || "http://localhost:8044";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Proxy to bot-status
|
||||||
|
app.get("/api/discord/bot-status", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const botUrl = getBotApiUrl();
|
||||||
|
const response = await fetch(`${botUrl}/bot-status`, {
|
||||||
|
headers: { Authorization: `Bearer ${BOT_ADMIN_TOKEN}` },
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`Bot returned ${response.status}`);
|
||||||
|
const data = await response.json();
|
||||||
|
res.json(data);
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(503).json({ error: error.message, status: "offline" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Proxy to linked-users
|
||||||
|
app.get("/api/discord/linked-users", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const botUrl = getBotApiUrl();
|
||||||
|
const response = await fetch(`${botUrl}/linked-users`, {
|
||||||
|
headers: { Authorization: `Bearer ${BOT_ADMIN_TOKEN}` },
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`Bot returned ${response.status}`);
|
||||||
|
const data = await response.json();
|
||||||
|
res.json(data);
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(503).json({ error: error.message, success: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Proxy to command-stats
|
||||||
|
app.get("/api/discord/command-stats", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const botUrl = getBotApiUrl();
|
||||||
|
const response = await fetch(`${botUrl}/command-stats`, {
|
||||||
|
headers: { Authorization: `Bearer ${BOT_ADMIN_TOKEN}` },
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`Bot returned ${response.status}`);
|
||||||
|
const data = await response.json();
|
||||||
|
res.json(data);
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(503).json({ error: error.message, success: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Proxy to feed-stats
|
||||||
|
app.get("/api/discord/feed-stats", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const botUrl = getBotApiUrl();
|
||||||
|
const response = await fetch(`${botUrl}/feed-stats`, {
|
||||||
|
headers: { Authorization: `Bearer ${BOT_ADMIN_TOKEN}` },
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`Bot returned ${response.status}`);
|
||||||
|
const data = await response.json();
|
||||||
|
res.json(data);
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(503).json({ error: error.message, success: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Proxy to register-commands
|
||||||
|
app.post("/api/discord/bot-register-commands", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const botUrl = getBotApiUrl();
|
||||||
|
const response = await fetch(`${botUrl}/register-commands`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${BOT_ADMIN_TOKEN}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`Bot returned ${response.status}`);
|
||||||
|
const data = await response.json();
|
||||||
|
res.json(data);
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(503).json({ error: error.message, success: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Discord Token Diagnostic Endpoint
|
// Discord Token Diagnostic Endpoint
|
||||||
app.get("/api/discord/diagnostic", async (req, res) => {
|
app.get("/api/discord/diagnostic", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue