Compare commits
4 Commits
258143b362
...
production
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52586ef28a | ||
|
|
8c4975481d | ||
|
|
a44a90c69d | ||
|
|
3a9f8f4cc5 |
@@ -62,9 +62,18 @@ jobs:
|
|||||||
CONTAINER_NAME="portfolio-app-dev"
|
CONTAINER_NAME="portfolio-app-dev"
|
||||||
HEALTH_PORT="3001"
|
HEALTH_PORT="3001"
|
||||||
IMAGE_NAME="${{ env.DOCKER_IMAGE }}:dev"
|
IMAGE_NAME="${{ env.DOCKER_IMAGE }}:dev"
|
||||||
|
BOT_CONTAINER="portfolio-discord-bot-dev"
|
||||||
|
BOT_IMAGE="portfolio-discord-bot:dev"
|
||||||
|
|
||||||
# Check for existing container
|
# Build discord-bot image
|
||||||
|
echo "🏗️ Building discord-bot image..."
|
||||||
|
DOCKER_BUILDKIT=1 docker build \
|
||||||
|
-t $BOT_IMAGE \
|
||||||
|
./discord-presence-bot
|
||||||
|
|
||||||
|
# Check for existing containers
|
||||||
EXISTING_CONTAINER=$(docker ps -aq -f name=$CONTAINER_NAME || echo "")
|
EXISTING_CONTAINER=$(docker ps -aq -f name=$CONTAINER_NAME || echo "")
|
||||||
|
EXISTING_BOT=$(docker ps -aq -f name=$BOT_CONTAINER || echo "")
|
||||||
|
|
||||||
# Ensure networks exist
|
# Ensure networks exist
|
||||||
echo "🌐 Ensuring networks exist..."
|
echo "🌐 Ensuring networks exist..."
|
||||||
@@ -78,13 +87,15 @@ jobs:
|
|||||||
echo "⚠️ Production database not reachable, app will use fallbacks"
|
echo "⚠️ Production database not reachable, app will use fallbacks"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Stop and remove existing container
|
# Stop and remove existing containers
|
||||||
if [ ! -z "$EXISTING_CONTAINER" ]; then
|
for C in $EXISTING_CONTAINER $EXISTING_BOT; do
|
||||||
echo "🛑 Stopping existing container..."
|
if [ ! -z "$C" ]; then
|
||||||
docker stop $EXISTING_CONTAINER 2>/dev/null || true
|
echo "🛑 Stopping existing container $C..."
|
||||||
docker rm $EXISTING_CONTAINER 2>/dev/null || true
|
docker stop $C 2>/dev/null || true
|
||||||
|
docker rm $C 2>/dev/null || true
|
||||||
sleep 3
|
sleep 3
|
||||||
fi
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
# Ensure port is free
|
# Ensure port is free
|
||||||
PORT_CONTAINER=$(docker ps -a --format "{{.ID}}\t{{.Ports}}" | grep -E "(:${HEALTH_PORT}->)" | awk '{print $1}' | head -1 || echo "")
|
PORT_CONTAINER=$(docker ps -a --format "{{.ID}}\t{{.Ports}}" | grep -E "(:${HEALTH_PORT}->)" | awk '{print $1}' | head -1 || echo "")
|
||||||
@@ -95,7 +106,18 @@ jobs:
|
|||||||
sleep 3
|
sleep 3
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Start new container
|
# Start discord-bot container
|
||||||
|
echo "🤖 Starting discord-bot container..."
|
||||||
|
docker run -d \
|
||||||
|
--name $BOT_CONTAINER \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--network portfolio_net \
|
||||||
|
-e DISCORD_BOT_TOKEN="${DISCORD_BOT_TOKEN}" \
|
||||||
|
-e DISCORD_USER_ID="${DISCORD_USER_ID:-172037532370862080}" \
|
||||||
|
-e BOT_PORT=3001 \
|
||||||
|
$BOT_IMAGE
|
||||||
|
|
||||||
|
# Start new portfolio container
|
||||||
echo "🆕 Starting new dev container..."
|
echo "🆕 Starting new dev container..."
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name $CONTAINER_NAME \
|
--name $CONTAINER_NAME \
|
||||||
@@ -159,6 +181,8 @@ jobs:
|
|||||||
N8N_API_KEY: ${{ vars.N8N_API_KEY || '' }}
|
N8N_API_KEY: ${{ vars.N8N_API_KEY || '' }}
|
||||||
DIRECTUS_URL: ${{ vars.DIRECTUS_URL || 'https://cms.dk0.dev' }}
|
DIRECTUS_URL: ${{ vars.DIRECTUS_URL || 'https://cms.dk0.dev' }}
|
||||||
DIRECTUS_STATIC_TOKEN: ${{ secrets.DIRECTUS_STATIC_TOKEN || '' }}
|
DIRECTUS_STATIC_TOKEN: ${{ secrets.DIRECTUS_STATIC_TOKEN || '' }}
|
||||||
|
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN || '' }}
|
||||||
|
DISCORD_USER_ID: ${{ vars.DISCORD_USER_ID || '172037532370862080' }}
|
||||||
|
|
||||||
- name: Cleanup
|
- name: Cleanup
|
||||||
run: docker image prune -f
|
run: docker image prune -f
|
||||||
@@ -209,10 +233,12 @@ jobs:
|
|||||||
export ADMIN_SESSION_SECRET="${ADMIN_SESSION_SECRET}"
|
export ADMIN_SESSION_SECRET="${ADMIN_SESSION_SECRET}"
|
||||||
export DIRECTUS_URL="${DIRECTUS_URL}"
|
export DIRECTUS_URL="${DIRECTUS_URL}"
|
||||||
export DIRECTUS_STATIC_TOKEN="${DIRECTUS_STATIC_TOKEN}"
|
export DIRECTUS_STATIC_TOKEN="${DIRECTUS_STATIC_TOKEN}"
|
||||||
|
export DISCORD_BOT_TOKEN="${DISCORD_BOT_TOKEN}"
|
||||||
|
export DISCORD_USER_ID="${DISCORD_USER_ID:-172037532370862080}"
|
||||||
|
|
||||||
# Start new container via compose
|
# Start new containers via compose
|
||||||
echo "🆕 Starting new production container..."
|
echo "🆕 Starting new production containers..."
|
||||||
docker compose -f $COMPOSE_FILE up -d portfolio
|
docker compose -f $COMPOSE_FILE up -d --build portfolio discord-bot
|
||||||
|
|
||||||
# Wait for health
|
# Wait for health
|
||||||
echo "⏳ Waiting for container to be healthy..."
|
echo "⏳ Waiting for container to be healthy..."
|
||||||
@@ -274,6 +300,8 @@ jobs:
|
|||||||
N8N_API_KEY: ${{ vars.N8N_API_KEY || '' }}
|
N8N_API_KEY: ${{ vars.N8N_API_KEY || '' }}
|
||||||
DIRECTUS_URL: ${{ vars.DIRECTUS_URL || 'https://cms.dk0.dev' }}
|
DIRECTUS_URL: ${{ vars.DIRECTUS_URL || 'https://cms.dk0.dev' }}
|
||||||
DIRECTUS_STATIC_TOKEN: ${{ secrets.DIRECTUS_STATIC_TOKEN || '' }}
|
DIRECTUS_STATIC_TOKEN: ${{ secrets.DIRECTUS_STATIC_TOKEN || '' }}
|
||||||
|
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN || '' }}
|
||||||
|
DISCORD_USER_ID: ${{ vars.DISCORD_USER_ID || '172037532370862080' }}
|
||||||
|
|
||||||
- name: Cleanup
|
- name: Cleanup
|
||||||
run: docker image prune -f
|
run: docker image prune -f
|
||||||
|
|||||||
@@ -123,6 +123,8 @@ N8N_SECRET_TOKEN=...
|
|||||||
N8N_API_KEY=...
|
N8N_API_KEY=...
|
||||||
DATABASE_URL=postgresql://...
|
DATABASE_URL=postgresql://...
|
||||||
REDIS_URL=redis://... # optional
|
REDIS_URL=redis://... # optional
|
||||||
|
DISCORD_BOT_TOKEN=... # Discord bot token for presence bot (replaces Lanyard)
|
||||||
|
DISCORD_USER_ID=172037532370862080 # Discord user ID to track
|
||||||
```
|
```
|
||||||
|
|
||||||
## Adding a CMS-managed Section
|
## Adding a CMS-managed Section
|
||||||
|
|||||||
@@ -214,7 +214,12 @@ export default function ActivityFeed({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 relative z-10">
|
<a
|
||||||
|
href={data.music.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex gap-4 relative z-10"
|
||||||
|
>
|
||||||
<div className="w-16 h-16 rounded-lg overflow-hidden shrink-0 shadow-md relative group-hover:shadow-xl transition-shadow duration-500">
|
<div className="w-16 h-16 rounded-lg overflow-hidden shrink-0 shadow-md relative group-hover:shadow-xl transition-shadow duration-500">
|
||||||
<Image
|
<Image
|
||||||
src={data.music.albumArt}
|
src={data.music.albumArt}
|
||||||
@@ -225,10 +230,10 @@ export default function ActivityFeed({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex flex-col justify-center">
|
<div className="min-w-0 flex flex-col justify-center">
|
||||||
<p className="font-bold text-[#1DB954] dark:text-[#1DB954] text-base truncate leading-tight mb-1">{data.music.track}</p>
|
<p className="font-bold text-[#1DB954] dark:text-[#1DB954] text-base truncate leading-tight mb-1 hover:underline">{data.music.track}</p>
|
||||||
<p className="text-sm text-stone-600 dark:text-white/60 truncate font-medium">{data.music.artist}</p>
|
<p className="text-sm text-stone-600 dark:text-white/60 truncate font-medium">{data.music.artist}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
{/* Subtle Spotify branding gradient */}
|
{/* Subtle Spotify branding gradient */}
|
||||||
<div className="absolute top-0 right-0 w-32 h-32 bg-[#1DB954]/5 blur-[60px] rounded-full -mr-16 -mt-16 pointer-events-none" />
|
<div className="absolute top-0 right-0 w-32 h-32 bg-[#1DB954]/5 blur-[60px] rounded-full -mr-16 -mt-16 pointer-events-none" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
1
discord-presence-bot/.gitignore
vendored
Normal file
1
discord-presence-bot/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
17
discord-presence-bot/Dockerfile
Normal file
17
discord-presence-bot/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
FROM node:25-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm ci --only=production && npm cache clean --force
|
||||||
|
|
||||||
|
COPY index.js .
|
||||||
|
|
||||||
|
USER node
|
||||||
|
|
||||||
|
EXPOSE 3001
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||||
|
CMD wget -qO- http://localhost:3001/presence || exit 1
|
||||||
|
|
||||||
|
CMD ["node", "index.js"]
|
||||||
110
discord-presence-bot/index.js
Normal file
110
discord-presence-bot/index.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
const { Client, GatewayIntentBits, ActivityType } = require("discord.js");
|
||||||
|
const http = require("http");
|
||||||
|
|
||||||
|
const TOKEN = process.env.DISCORD_BOT_TOKEN;
|
||||||
|
const TARGET_USER_ID = process.env.DISCORD_USER_ID || "172037532370862080";
|
||||||
|
const PORT = parseInt(process.env.BOT_PORT || "3001", 10);
|
||||||
|
|
||||||
|
if (!TOKEN) {
|
||||||
|
console.error("DISCORD_BOT_TOKEN is required");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
intents: [
|
||||||
|
GatewayIntentBits.Guilds,
|
||||||
|
GatewayIntentBits.GuildPresences,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let cachedData = {
|
||||||
|
discord_status: "offline",
|
||||||
|
listening_to_spotify: false,
|
||||||
|
spotify: null,
|
||||||
|
activities: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
function updatePresence(guild) {
|
||||||
|
const member = guild.members.cache.get(TARGET_USER_ID);
|
||||||
|
if (!member || !member.presence) return;
|
||||||
|
|
||||||
|
const presence = member.presence;
|
||||||
|
cachedData.discord_status = presence.status || "offline";
|
||||||
|
|
||||||
|
cachedData.activities = presence.activities
|
||||||
|
? presence.activities
|
||||||
|
.filter((a) => a.type !== ActivityType.Custom)
|
||||||
|
.map((a) => ({
|
||||||
|
name: a.name,
|
||||||
|
type: a.type,
|
||||||
|
details: a.details || null,
|
||||||
|
state: a.state || null,
|
||||||
|
assets: a.assets
|
||||||
|
? {
|
||||||
|
large_image: a.assets.largeImage || null,
|
||||||
|
large_text: a.assets.largeText || null,
|
||||||
|
small_image: a.assets.smallImage || null,
|
||||||
|
small_text: a.assets.smallText || null,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
timestamps: a.timestamps
|
||||||
|
? {
|
||||||
|
start: a.timestamps.start?.toISOString() || null,
|
||||||
|
end: a.timestamps.end?.toISOString() || null,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const spotifyActivity = presence.activities
|
||||||
|
? presence.activities.find((a) => a.type === ActivityType.Listening && a.name === "Spotify")
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (spotifyActivity && spotifyActivity.syncId) {
|
||||||
|
cachedData.listening_to_spotify = true;
|
||||||
|
cachedData.spotify = {
|
||||||
|
song: spotifyActivity.details || "",
|
||||||
|
artist: spotifyActivity.state ? spotifyActivity.state.replace(/; /g, "; ") : "",
|
||||||
|
album: spotifyActivity.assets?.largeText || "",
|
||||||
|
album_art_url: spotifyActivity.assets?.largeImage
|
||||||
|
? `https://i.scdn.co/image/${spotifyActivity.assets.largeImage.replace("spotify:", "")}`
|
||||||
|
: null,
|
||||||
|
track_id: spotifyActivity.syncId || null,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
cachedData.listening_to_spotify = false;
|
||||||
|
cachedData.spotify = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAll() {
|
||||||
|
for (const guild of client.guilds.cache.values()) {
|
||||||
|
updatePresence(guild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.on("ready", () => {
|
||||||
|
console.log(`Bot online as ${client.user.tag}`);
|
||||||
|
client.user.setActivity("Watching Presence", { type: ActivityType.Watching });
|
||||||
|
updateAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("presenceUpdate", () => {
|
||||||
|
updateAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
if (req.method === "GET" && req.url === "/presence") {
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ data: cachedData }));
|
||||||
|
} else {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end("Not found");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(`HTTP endpoint listening on port ${PORT}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.login(TOKEN);
|
||||||
324
discord-presence-bot/package-lock.json
generated
Normal file
324
discord-presence-bot/package-lock.json
generated
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
{
|
||||||
|
"name": "discord-presence-bot",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "discord-presence-bot",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"discord.js": "^14.18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/builders": {
|
||||||
|
"version": "1.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz",
|
||||||
|
"integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/formatters": "^0.6.2",
|
||||||
|
"@discordjs/util": "^1.2.0",
|
||||||
|
"@sapphire/shapeshift": "^4.0.0",
|
||||||
|
"discord-api-types": "^0.38.40",
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"ts-mixer": "^6.0.4",
|
||||||
|
"tslib": "^2.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.11.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/collection": {
|
||||||
|
"version": "1.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
|
||||||
|
"integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/formatters": {
|
||||||
|
"version": "0.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz",
|
||||||
|
"integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"discord-api-types": "^0.38.33"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.11.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/rest": {
|
||||||
|
"version": "2.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.1.tgz",
|
||||||
|
"integrity": "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/collection": "^2.1.1",
|
||||||
|
"@discordjs/util": "^1.2.0",
|
||||||
|
"@sapphire/async-queue": "^1.5.3",
|
||||||
|
"@sapphire/snowflake": "^3.5.5",
|
||||||
|
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||||
|
"discord-api-types": "^0.38.40",
|
||||||
|
"magic-bytes.js": "^1.13.0",
|
||||||
|
"tslib": "^2.6.3",
|
||||||
|
"undici": "6.24.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/rest/node_modules/@sapphire/snowflake": {
|
||||||
|
"version": "3.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz",
|
||||||
|
"integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v14.0.0",
|
||||||
|
"npm": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/util": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"discord-api-types": "^0.38.33"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/ws": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/collection": "^2.1.0",
|
||||||
|
"@discordjs/rest": "^2.5.1",
|
||||||
|
"@discordjs/util": "^1.1.0",
|
||||||
|
"@sapphire/async-queue": "^1.5.2",
|
||||||
|
"@types/ws": "^8.5.10",
|
||||||
|
"@vladfrangu/async_event_emitter": "^2.2.4",
|
||||||
|
"discord-api-types": "^0.38.1",
|
||||||
|
"tslib": "^2.6.2",
|
||||||
|
"ws": "^8.17.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.11.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sapphire/async-queue": {
|
||||||
|
"version": "1.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
|
||||||
|
"integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v14.0.0",
|
||||||
|
"npm": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sapphire/shapeshift": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sapphire/snowflake": {
|
||||||
|
"version": "3.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz",
|
||||||
|
"integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v14.0.0",
|
||||||
|
"npm": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "25.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
|
||||||
|
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~7.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vladfrangu/async_event_emitter": {
|
||||||
|
"version": "2.4.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz",
|
||||||
|
"integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v14.0.0",
|
||||||
|
"npm": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/discord-api-types": {
|
||||||
|
"version": "0.38.47",
|
||||||
|
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.47.tgz",
|
||||||
|
"integrity": "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"scripts/actions/documentation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/discord.js": {
|
||||||
|
"version": "14.26.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.26.3.tgz",
|
||||||
|
"integrity": "sha512-XEKtYn28YFsiJ5l4fLRyikdbo6RD5oFyqfVHQlvXz2104JhH/E8slN28dbky05w3DCrJcNVWvhVvcJCTSl/KIg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/builders": "^1.14.1",
|
||||||
|
"@discordjs/collection": "1.5.3",
|
||||||
|
"@discordjs/formatters": "^0.6.2",
|
||||||
|
"@discordjs/rest": "^2.6.1",
|
||||||
|
"@discordjs/util": "^1.2.0",
|
||||||
|
"@discordjs/ws": "^1.2.3",
|
||||||
|
"@sapphire/snowflake": "3.5.3",
|
||||||
|
"discord-api-types": "^0.38.40",
|
||||||
|
"fast-deep-equal": "3.1.3",
|
||||||
|
"lodash.snakecase": "4.1.1",
|
||||||
|
"magic-bytes.js": "^1.13.0",
|
||||||
|
"tslib": "^2.6.3",
|
||||||
|
"undici": "6.24.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fast-deep-equal": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||||
|
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.snakecase": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/magic-bytes.js": {
|
||||||
|
"version": "1.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz",
|
||||||
|
"integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/ts-mixer": {
|
||||||
|
"version": "6.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
|
||||||
|
"integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
|
"node_modules/undici": {
|
||||||
|
"version": "6.24.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
|
||||||
|
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "7.19.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
|
||||||
|
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||||
|
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
discord-presence-bot/package.json
Normal file
12
discord-presence-bot/package.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "discord-presence-bot",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"discord.js": "^14.18.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,6 +103,33 @@ services:
|
|||||||
memory: 128M
|
memory: 128M
|
||||||
cpus: '0.1'
|
cpus: '0.1'
|
||||||
|
|
||||||
|
discord-bot:
|
||||||
|
build:
|
||||||
|
context: ./discord-presence-bot
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: portfolio-discord-bot
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN}
|
||||||
|
- DISCORD_USER_ID=${DISCORD_USER_ID:-172037532370862080}
|
||||||
|
- BOT_PORT=3001
|
||||||
|
networks:
|
||||||
|
- portfolio_net
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:3001/presence"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 128M
|
||||||
|
cpus: '0.25'
|
||||||
|
reservations:
|
||||||
|
memory: 64M
|
||||||
|
cpus: '0.1'
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
portfolio_data:
|
portfolio_data:
|
||||||
driver: local
|
driver: local
|
||||||
|
|||||||
@@ -87,6 +87,33 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
start_period: 30s
|
start_period: 30s
|
||||||
|
|
||||||
|
discord-bot:
|
||||||
|
build:
|
||||||
|
context: ./discord-presence-bot
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: portfolio-discord-bot
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN}
|
||||||
|
- DISCORD_USER_ID=${DISCORD_USER_ID:-172037532370862080}
|
||||||
|
- BOT_PORT=3001
|
||||||
|
networks:
|
||||||
|
- portfolio_net
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:3001/presence"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 128M
|
||||||
|
cpus: '0.25'
|
||||||
|
reservations:
|
||||||
|
memory: 64M
|
||||||
|
cpus: '0.1'
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
portfolio_data:
|
portfolio_data:
|
||||||
driver: local
|
driver: local
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ N8N_WEBHOOK_URL=https://n8n.dk0.dev
|
|||||||
N8N_SECRET_TOKEN=your-n8n-secret-token
|
N8N_SECRET_TOKEN=your-n8n-secret-token
|
||||||
N8N_API_KEY=your-n8n-api-key
|
N8N_API_KEY=your-n8n-api-key
|
||||||
|
|
||||||
|
# Discord Presence Bot (replaces Lanyard)
|
||||||
|
DISCORD_BOT_TOKEN=your-discord-bot-token
|
||||||
|
DISCORD_USER_ID=172037532370862080
|
||||||
|
|
||||||
# Directus CMS (for i18n messages & content pages)
|
# Directus CMS (for i18n messages & content pages)
|
||||||
DIRECTUS_URL=https://cms.dk0.dev
|
DIRECTUS_URL=https://cms.dk0.dev
|
||||||
DIRECTUS_STATIC_TOKEN=your-static-token-here
|
DIRECTUS_STATIC_TOKEN=your-static-token-here
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const eslintConfig = [
|
|||||||
"coverage/**",
|
"coverage/**",
|
||||||
"scripts/**",
|
"scripts/**",
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
|
"discord-presence-bot/**",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"url": "https://api.lanyard.rest/v1/users/172037532370862080",
|
"url": "http://discord-bot:3001/presence",
|
||||||
"options": {}
|
"options": {}
|
||||||
},
|
},
|
||||||
"type": "n8n-nodes-base.httpRequest",
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
|||||||
Reference in New Issue
Block a user