Compare commits
6 Commits
edd8dc58ab
...
production
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52586ef28a | ||
|
|
8c4975481d | ||
|
|
a44a90c69d | ||
|
|
3a9f8f4cc5 | ||
|
|
258143b362 | ||
|
|
049dda8dc5 |
@@ -62,9 +62,18 @@ jobs:
|
||||
CONTAINER_NAME="portfolio-app-dev"
|
||||
HEALTH_PORT="3001"
|
||||
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_BOT=$(docker ps -aq -f name=$BOT_CONTAINER || echo "")
|
||||
|
||||
# Ensure networks exist
|
||||
echo "🌐 Ensuring networks exist..."
|
||||
@@ -78,13 +87,15 @@ jobs:
|
||||
echo "⚠️ Production database not reachable, app will use fallbacks"
|
||||
fi
|
||||
|
||||
# Stop and remove existing container
|
||||
if [ ! -z "$EXISTING_CONTAINER" ]; then
|
||||
echo "🛑 Stopping existing container..."
|
||||
docker stop $EXISTING_CONTAINER 2>/dev/null || true
|
||||
docker rm $EXISTING_CONTAINER 2>/dev/null || true
|
||||
sleep 3
|
||||
fi
|
||||
# Stop and remove existing containers
|
||||
for C in $EXISTING_CONTAINER $EXISTING_BOT; do
|
||||
if [ ! -z "$C" ]; then
|
||||
echo "🛑 Stopping existing container $C..."
|
||||
docker stop $C 2>/dev/null || true
|
||||
docker rm $C 2>/dev/null || true
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
|
||||
# Ensure port is free
|
||||
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
|
||||
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..."
|
||||
docker run -d \
|
||||
--name $CONTAINER_NAME \
|
||||
@@ -159,6 +181,8 @@ jobs:
|
||||
N8N_API_KEY: ${{ vars.N8N_API_KEY || '' }}
|
||||
DIRECTUS_URL: ${{ vars.DIRECTUS_URL || 'https://cms.dk0.dev' }}
|
||||
DIRECTUS_STATIC_TOKEN: ${{ secrets.DIRECTUS_STATIC_TOKEN || '' }}
|
||||
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN || '' }}
|
||||
DISCORD_USER_ID: ${{ vars.DISCORD_USER_ID || '172037532370862080' }}
|
||||
|
||||
- name: Cleanup
|
||||
run: docker image prune -f
|
||||
@@ -209,10 +233,12 @@ jobs:
|
||||
export ADMIN_SESSION_SECRET="${ADMIN_SESSION_SECRET}"
|
||||
export DIRECTUS_URL="${DIRECTUS_URL}"
|
||||
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
|
||||
echo "🆕 Starting new production container..."
|
||||
docker compose -f $COMPOSE_FILE up -d portfolio
|
||||
# Start new containers via compose
|
||||
echo "🆕 Starting new production containers..."
|
||||
docker compose -f $COMPOSE_FILE up -d --build portfolio discord-bot
|
||||
|
||||
# Wait for health
|
||||
echo "⏳ Waiting for container to be healthy..."
|
||||
@@ -274,6 +300,8 @@ jobs:
|
||||
N8N_API_KEY: ${{ vars.N8N_API_KEY || '' }}
|
||||
DIRECTUS_URL: ${{ vars.DIRECTUS_URL || 'https://cms.dk0.dev' }}
|
||||
DIRECTUS_STATIC_TOKEN: ${{ secrets.DIRECTUS_STATIC_TOKEN || '' }}
|
||||
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN || '' }}
|
||||
DISCORD_USER_ID: ${{ vars.DISCORD_USER_ID || '172037532370862080' }}
|
||||
|
||||
- name: Cleanup
|
||||
run: docker image prune -f
|
||||
|
||||
@@ -123,6 +123,8 @@ N8N_SECRET_TOKEN=...
|
||||
N8N_API_KEY=...
|
||||
DATABASE_URL=postgresql://...
|
||||
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
|
||||
|
||||
@@ -2,6 +2,19 @@ import type { Metadata } from "next";
|
||||
import HomePageServer from "../_ui/HomePageServer";
|
||||
import { getLanguageAlternates, toAbsoluteUrl } from "@/lib/seo";
|
||||
|
||||
const localeMetadata: Record<string, { title: string; description: string }> = {
|
||||
de: {
|
||||
title: "Dennis Konkol – Webentwickler Osnabrück",
|
||||
description:
|
||||
"Dennis Konkol – Software Engineer & Webentwickler in Osnabrück. Webentwicklung, Fullstack-Apps, Docker, Next.js, Flutter. Projekte ansehen und Kontakt aufnehmen.",
|
||||
},
|
||||
en: {
|
||||
title: "Dennis Konkol – Web Developer Osnabrück",
|
||||
description:
|
||||
"Dennis Konkol – Software Engineer & Web Developer in Osnabrück, Germany. Web development, fullstack apps, Docker, Next.js, Flutter.",
|
||||
},
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
@@ -9,7 +22,10 @@ export async function generateMetadata({
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const languages = getLanguageAlternates({ pathWithoutLocale: "" });
|
||||
const meta = localeMetadata[locale] ?? localeMetadata.en;
|
||||
return {
|
||||
title: meta.title,
|
||||
description: meta.description,
|
||||
alternates: {
|
||||
canonical: toAbsoluteUrl(`/${locale}`),
|
||||
languages,
|
||||
|
||||
@@ -13,7 +13,12 @@ export async function generateMetadata({
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const languages = getLanguageAlternates({ pathWithoutLocale: "projects" });
|
||||
const isDe = locale === "de";
|
||||
return {
|
||||
title: isDe ? "Projekte – Dennis Konkol" : "Projects – Dennis Konkol",
|
||||
description: isDe
|
||||
? "Webentwicklung, Fullstack-Apps und Mobile-Projekte von Dennis Konkol. Next.js, Flutter, Docker und mehr – Osnabrück."
|
||||
: "Web development, fullstack apps and mobile projects by Dennis Konkol. Next.js, Flutter, Docker and more – Osnabrück.",
|
||||
alternates: {
|
||||
canonical: toAbsoluteUrl(`/${locale}/projects`),
|
||||
languages,
|
||||
|
||||
@@ -31,20 +31,41 @@ export default async function HomePageServer({ locale }: HomePageServerProps) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Script
|
||||
id={"structured-data"}
|
||||
id={"structured-data-person"}
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Person",
|
||||
name: "Dennis Konkol",
|
||||
alternateName: ["dk0", "denshooter"],
|
||||
url: "https://dk0.dev",
|
||||
jobTitle: "Software Engineer",
|
||||
description:
|
||||
locale === "de"
|
||||
? "Software Engineer & Webentwickler in Osnabrück. Webentwicklung, Fullstack-Apps, Docker, Next.js, Flutter."
|
||||
: "Software Engineer & Web Developer in Osnabrück, Germany. Web development, fullstack apps, Docker, Next.js, Flutter.",
|
||||
address: {
|
||||
"@type": "PostalAddress",
|
||||
addressLocality: "Osnabrück",
|
||||
addressCountry: "Germany",
|
||||
addressRegion: "Niedersachsen",
|
||||
addressCountry: "DE",
|
||||
},
|
||||
knowsAbout: [
|
||||
"Webentwicklung",
|
||||
"Web Development",
|
||||
"Next.js",
|
||||
"React",
|
||||
"TypeScript",
|
||||
"Flutter",
|
||||
"Docker",
|
||||
"DevOps",
|
||||
"Self-Hosting",
|
||||
"CI/CD",
|
||||
"Fullstack Development",
|
||||
"Softwareentwicklung",
|
||||
"Informatik",
|
||||
],
|
||||
sameAs: [
|
||||
"https://github.com/Denshooter",
|
||||
"https://linkedin.com/in/dkonkol",
|
||||
@@ -52,6 +73,20 @@ export default async function HomePageServer({ locale }: HomePageServerProps) {
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
<Script
|
||||
id={"structured-data-website"}
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
name: "Dennis Konkol",
|
||||
alternateName: "dk0.dev",
|
||||
url: "https://dk0.dev",
|
||||
inLanguage: ["de", "en"],
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
<Header locale={locale} />
|
||||
{/* Spacer to prevent navbar overlap */}
|
||||
<div className="h-24 md:h-32" aria-hidden="true"></div>
|
||||
|
||||
@@ -214,7 +214,12 @@ export default function ActivityFeed({
|
||||
))}
|
||||
</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">
|
||||
<Image
|
||||
src={data.music.albumArt}
|
||||
@@ -225,10 +230,10 @@ export default function ActivityFeed({
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/* 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" />
|
||||
</motion.div>
|
||||
|
||||
@@ -49,23 +49,33 @@ export default async function RootLayout({
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(getBaseUrl()),
|
||||
title: {
|
||||
default: "Dennis Konkol | Portfolio",
|
||||
template: "%s | Dennis Konkol",
|
||||
default: "Dennis Konkol",
|
||||
template: "%s | dk0",
|
||||
},
|
||||
description:
|
||||
"Portfolio of Dennis Konkol, a student and software engineer based in Osnabrück, Germany. Passionate about technology, coding, and solving real-world problems.",
|
||||
"Dennis Konkol – Software Engineer & Webentwickler in Osnabrück. Webentwicklung, Fullstack-Apps, Docker, Next.js, Flutter. Portfolio mit Projekten und Kontakt.",
|
||||
keywords: [
|
||||
"Dennis Konkol",
|
||||
"dk0",
|
||||
"denshooter",
|
||||
"Webentwicklung Osnabrück",
|
||||
"Webentwicklung",
|
||||
"Softwareentwicklung Osnabrück",
|
||||
"Website erstellen Osnabrück",
|
||||
"Web Design Osnabrück",
|
||||
"Informatik Osnabrück",
|
||||
"Software Engineer",
|
||||
"Portfolio",
|
||||
"Student",
|
||||
"Web Development",
|
||||
"Full Stack Developer",
|
||||
"Osnabrück",
|
||||
"Germany",
|
||||
"React",
|
||||
"Frontend Developer Osnabrück",
|
||||
"Next.js",
|
||||
"React",
|
||||
"TypeScript",
|
||||
"Flutter",
|
||||
"Docker",
|
||||
"Self-Hosting",
|
||||
"DevOps",
|
||||
"Portfolio",
|
||||
"Osnabrück",
|
||||
],
|
||||
authors: [{ name: "Dennis Konkol", url: "https://dk0.dev" }],
|
||||
creator: "Dennis Konkol",
|
||||
@@ -82,26 +92,27 @@ export const metadata: Metadata = {
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
title: "Dennis Konkol | Portfolio",
|
||||
title: "Dennis Konkol",
|
||||
description:
|
||||
"Explore my projects and contact me for collaboration opportunities!",
|
||||
"Software Engineer & Webentwickler in Osnabrück. Next.js, Flutter, Docker, DevOps. Projekte ansehen und Kontakt aufnehmen.",
|
||||
url: "https://dk0.dev",
|
||||
siteName: "Dennis Konkol Portfolio",
|
||||
siteName: "Dennis Konkol",
|
||||
images: [
|
||||
{
|
||||
url: "https://dk0.dev/api/og",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "Dennis Konkol Portfolio",
|
||||
alt: "Dennis Konkol",
|
||||
},
|
||||
],
|
||||
locale: "en_US",
|
||||
locale: "de_DE",
|
||||
alternateLocale: ["en_US"],
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Dennis Konkol | Portfolio",
|
||||
description: "Student & Software Engineer based in Osnabrück, Germany.",
|
||||
title: "Dennis Konkol",
|
||||
description: "Software Engineer & Webentwickler in Osnabrück.",
|
||||
images: ["https://dk0.dev/api/og"],
|
||||
creator: "@denshooter",
|
||||
},
|
||||
@@ -110,5 +121,9 @@ export const metadata: Metadata = {
|
||||
},
|
||||
alternates: {
|
||||
canonical: "https://dk0.dev",
|
||||
languages: {
|
||||
de: "https://dk0.dev/de",
|
||||
en: "https://dk0.dev/en",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
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
|
||||
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:
|
||||
portfolio_data:
|
||||
driver: local
|
||||
|
||||
@@ -87,6 +87,33 @@ services:
|
||||
retries: 5
|
||||
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:
|
||||
portfolio_data:
|
||||
driver: local
|
||||
|
||||
@@ -30,6 +30,10 @@ N8N_WEBHOOK_URL=https://n8n.dk0.dev
|
||||
N8N_SECRET_TOKEN=your-n8n-secret-token
|
||||
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_URL=https://cms.dk0.dev
|
||||
DIRECTUS_STATIC_TOKEN=your-static-token-here
|
||||
|
||||
@@ -19,6 +19,7 @@ const eslintConfig = [
|
||||
"coverage/**",
|
||||
"scripts/**",
|
||||
"next-env.d.ts",
|
||||
"discord-presence-bot/**",
|
||||
],
|
||||
},
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"f2": "Docker Swarm & CI/CD",
|
||||
"f3": "Self-Hosted Infrastruktur"
|
||||
},
|
||||
"description": "Ich bin Dennis, Student aus Osnabrück und leidenschaftlicher Selfhoster. Ich entwickle Fullstack Apps und sorge am liebsten selbst dafür, dass sie auf meiner eigenen Infrastruktur perfekt laufen.",
|
||||
"description": "Ich bin Dennis Konkol, Informatik-Student und Webentwickler aus Osnabrück. Ich entwickle Fullstack-Apps mit Next.js und Flutter und betreibe meine eigene Infrastruktur mit Docker und CI/CD.",
|
||||
"ctaWork": "Meine Projekte",
|
||||
"ctaContact": "Kontakt"
|
||||
},
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"f2": "Docker Swarm & CI/CD",
|
||||
"f3": "Self-Hosted Infrastructure"
|
||||
},
|
||||
"description": "I'm Dennis, a student from Germany and a passionate selfhoster. I build fullstack applications and love the challenge of managing the infrastructure they run on.",
|
||||
"description": "I'm Dennis Konkol, a computer science student and web developer from Osnabrück, Germany. I build fullstack apps with Next.js and Flutter and love running my own infrastructure with Docker and CI/CD.",
|
||||
"ctaWork": "View Projects",
|
||||
"ctaContact": "Get in touch"
|
||||
},
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "https://api.lanyard.rest/v1/users/172037532370862080",
|
||||
"url": "http://discord-bot:3001/presence",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
|
||||
Reference in New Issue
Block a user