wywalenie tej wersji bota
This commit is contained in:
@@ -1,89 +0,0 @@
|
|||||||
# Git
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
|
|
||||||
# Documentation
|
|
||||||
README.md
|
|
||||||
*.md
|
|
||||||
|
|
||||||
# Environment files
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
# Dependencies
|
|
||||||
node_modules
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# Build outputs
|
|
||||||
frontend/build
|
|
||||||
dist
|
|
||||||
build
|
|
||||||
|
|
||||||
# Development tools
|
|
||||||
.vscode
|
|
||||||
.idea
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
|
|
||||||
# Testing
|
|
||||||
coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# Docker
|
|
||||||
Dockerfile*
|
|
||||||
docker-compose*
|
|
||||||
.dockerignore
|
|
||||||
|
|
||||||
# OS generated files
|
|
||||||
.DS_Store
|
|
||||||
.DS_Store?
|
|
||||||
._*
|
|
||||||
.Spotlight-V100
|
|
||||||
.Trashes
|
|
||||||
ehthumbs.db
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Temporary files
|
|
||||||
*.tmp
|
|
||||||
*.temp
|
|
||||||
|
|
||||||
# Database files
|
|
||||||
*.db
|
|
||||||
*.sqlite
|
|
||||||
|
|
||||||
# SSL certificates (for security)
|
|
||||||
ssl/
|
|
||||||
*.pem
|
|
||||||
*.crt
|
|
||||||
*.key
|
|
||||||
|
|
||||||
# Backup files
|
|
||||||
*.bak
|
|
||||||
*.backup
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Discord Bot Configuration
|
|
||||||
DISCORD_TOKEN=your_discord_bot_token_here
|
|
||||||
DISCORD_CLIENT_ID=your_discord_client_id_here
|
|
||||||
DISCORD_CLIENT_SECRET=your_discord_client_secret_here
|
|
||||||
|
|
||||||
# Database Configuration
|
|
||||||
DATABASE_URL=postgresql://username:password@localhost:5432/skrzynka_impostora
|
|
||||||
DB_HOST=localhost
|
|
||||||
DB_PORT=5432
|
|
||||||
DB_NAME=skrzynka_impostora
|
|
||||||
DB_USER=username
|
|
||||||
DB_PASSWORD=password
|
|
||||||
|
|
||||||
# Web Panel Configuration
|
|
||||||
JWT_SECRET=your_jwt_secret_here
|
|
||||||
SESSION_SECRET=your_session_secret_here
|
|
||||||
WEB_PORT=3001
|
|
||||||
API_PORT=3000
|
|
||||||
|
|
||||||
# OAuth2 Configuration
|
|
||||||
OAUTH2_REDIRECT_URI=http://localhost:3001/auth/discord/callback
|
|
||||||
|
|
||||||
# Environment
|
|
||||||
NODE_ENV=development
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
LOG_LEVEL=info
|
|
||||||
|
|
||||||
# Redis Configuration (optional)
|
|
||||||
REDIS_URL=redis://localhost:6379
|
|
||||||
|
|
||||||
# Docker specific overrides (uncomment for Docker usage)
|
|
||||||
# DB_HOST=postgres
|
|
||||||
# REDIS_URL=redis://redis:6379
|
|
||||||
|
|
||||||
# Production domain (for OAuth2 and CORS)
|
|
||||||
# PRODUCTION_DOMAIN=https://your-domain.com
|
|
||||||
85
bot/.github/copilot-instructions.md
vendored
85
bot/.github/copilot-instructions.md
vendored
@@ -1,85 +0,0 @@
|
|||||||
<!-- Use this file to provide workspace-specific custom instructions to Copilot. For more details, visit https://code.visualstudio.com/docs/copilot/copilot-customization#_use-a-githubcopilotinstructionsmd-file -->
|
|
||||||
|
|
||||||
# Skrzynka Impostora Bot - Instrukcje dla Copilot
|
|
||||||
|
|
||||||
## Opis projektu
|
|
||||||
To jest projekt Discord Bot o nazwie "Skrzynka Impostora Bot" służący do zarządzania wiadomościami powiatalnymi na serwerach Discord. Projekt składa się z:
|
|
||||||
|
|
||||||
- **Backend**: Node.js + Express z biblioteką discord.js
|
|
||||||
- **Frontend**: React SPA (Single Page Application)
|
|
||||||
- **Baza danych**: PostgreSQL
|
|
||||||
- **Funkcjonalności**: Discord slash commands, panel web OAuth2, edytor wiadomości
|
|
||||||
|
|
||||||
## Struktura projektu
|
|
||||||
```
|
|
||||||
/backend/ - Serwer Node.js i bot Discord
|
|
||||||
/commands/ - Slash commands dla Discord
|
|
||||||
/database/ - Zarządzanie bazą danych
|
|
||||||
/web/ - API serwer dla panelu web
|
|
||||||
/frontend/ - React aplikacja (panel administracyjny)
|
|
||||||
/src/components/ - Komponenty React
|
|
||||||
/src/pages/ - Strony aplikacji
|
|
||||||
/src/services/ - Serwisy API
|
|
||||||
/src/hooks/ - React hooks
|
|
||||||
/database/ - Skrypty migracji i seedowania
|
|
||||||
/shared/ - Współdzielone utilities
|
|
||||||
```
|
|
||||||
|
|
||||||
## Kluczowe technologie
|
|
||||||
- **Discord.js v14** - Biblioteka Discord API
|
|
||||||
- **Express.js** - Web framework
|
|
||||||
- **PostgreSQL** - Baza danych relacyjna
|
|
||||||
- **React 18** - Frontend framework
|
|
||||||
- **JWT** - Autoryzacja
|
|
||||||
- **OAuth2** - Logowanie przez Discord
|
|
||||||
|
|
||||||
## Główne funkcjonalności
|
|
||||||
1. **Slash Commands**:
|
|
||||||
- `/skrzynka` - komendy użytkownika
|
|
||||||
- `/skrzynka-adm` - komendy administracyjne
|
|
||||||
|
|
||||||
2. **Panel Web**:
|
|
||||||
- Logowanie przez Discord OAuth2
|
|
||||||
- Edytor wiadomości z podglądem
|
|
||||||
- Zarządzanie serwerami
|
|
||||||
- Historia zmian
|
|
||||||
|
|
||||||
3. **Zarządzanie wiadomościami**:
|
|
||||||
- Wsparcie Discord Markdown
|
|
||||||
- Live preview
|
|
||||||
- Walidacja długości (2000 znaków)
|
|
||||||
- Automatyczna aktualizacja na Discord
|
|
||||||
|
|
||||||
## Style kodowania
|
|
||||||
- Używaj ES6+ składni
|
|
||||||
- Preferuj async/await nad Promise.then()
|
|
||||||
- Zastosuj destructuring tam gdzie możliwe
|
|
||||||
- Używaj const/let zamiast var
|
|
||||||
- Komponenty React jako funkcyjne z hooks
|
|
||||||
- Nazwy plików: PascalCase dla komponentów, camelCase dla utilities
|
|
||||||
|
|
||||||
## Bezpieczeństwo
|
|
||||||
- Wszystkie zapytania API wymagają JWT token
|
|
||||||
- Walidacja danych wejściowych przez express-validator
|
|
||||||
- Helmet.js dla zabezpieczeń HTTP
|
|
||||||
- CORS skonfigurowany dla specific origins
|
|
||||||
- SQL prepared statements (pg library)
|
|
||||||
|
|
||||||
## Środowisko i konfiguracja
|
|
||||||
- Zmienne środowiskowe w pliku .env
|
|
||||||
- Development: localhost:3000 (API), localhost:3001 (React)
|
|
||||||
- Production: buildy React serwowane przez Express
|
|
||||||
|
|
||||||
## Konwencje nazewnictwa
|
|
||||||
- Tabele bazy: snake_case (np. welcome_messages)
|
|
||||||
- Kolumny bazy: snake_case (np. guild_id)
|
|
||||||
- JavaScript: camelCase (np. guildId)
|
|
||||||
- Komponenty React: PascalCase (np. MessageEditor)
|
|
||||||
- CSS classes: kebab-case (np. discord-button)
|
|
||||||
|
|
||||||
## Pomocne wskazówki
|
|
||||||
- Bot wymaga uprawnień MANAGE_CHANNELS do działania
|
|
||||||
- Discord API ma limit 2000 znaków na wiadomość
|
|
||||||
- Używaj PostgreSQL JSONB dla embed_data
|
|
||||||
- Zawsze dodawaj error handling dla Discord API calls
|
|
||||||
- Frontend używa Tailwind-like CSS classes w App.css
|
|
||||||
73
bot/.gitignore
vendored
73
bot/.gitignore
vendored
@@ -1,73 +0,0 @@
|
|||||||
# Dependencies
|
|
||||||
node_modules/
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# Production builds
|
|
||||||
frontend/build/
|
|
||||||
dist/
|
|
||||||
|
|
||||||
# Environment variables
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
# Database
|
|
||||||
*.db
|
|
||||||
*.sqlite
|
|
||||||
backup_*.sql
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
logs/
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids/
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage/
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.vscode/settings.json
|
|
||||||
.vscode/launch.json
|
|
||||||
.idea/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
|
|
||||||
# OS generated files
|
|
||||||
.DS_Store
|
|
||||||
.DS_Store?
|
|
||||||
._*
|
|
||||||
.Spotlight-V100
|
|
||||||
.Trashes
|
|
||||||
ehthumbs.db
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Temporary files
|
|
||||||
*.tmp
|
|
||||||
*.temp
|
|
||||||
|
|
||||||
# Discord bot specific
|
|
||||||
config.json
|
|
||||||
database/data/
|
|
||||||
|
|
||||||
# Docker
|
|
||||||
docker-compose.override.yml
|
|
||||||
.docker/
|
|
||||||
|
|
||||||
# SSL certificates
|
|
||||||
ssl/
|
|
||||||
*.pem
|
|
||||||
*.crt
|
|
||||||
*.key
|
|
||||||
*.p12
|
|
||||||
|
|
||||||
# Backup files
|
|
||||||
*.bak
|
|
||||||
*.backup
|
|
||||||
150
bot/.vscode/tasks.json
vendored
150
bot/.vscode/tasks.json
vendored
@@ -1,150 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"label": "Start Development Server",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "npm",
|
|
||||||
"args": [
|
|
||||||
"run",
|
|
||||||
"dev"
|
|
||||||
],
|
|
||||||
"group": "build",
|
|
||||||
"isBackground": true,
|
|
||||||
"problemMatcher": [],
|
|
||||||
"presentation": {
|
|
||||||
"echo": true,
|
|
||||||
"reveal": "always",
|
|
||||||
"focus": false,
|
|
||||||
"panel": "new",
|
|
||||||
"showReuseMessage": true,
|
|
||||||
"clear": false
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Docker: Start Development",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "docker",
|
|
||||||
"args": [
|
|
||||||
"compose",
|
|
||||||
"-f",
|
|
||||||
"docker-compose.dev.yml",
|
|
||||||
"up",
|
|
||||||
"--build"
|
|
||||||
],
|
|
||||||
"group": "build",
|
|
||||||
"isBackground": true,
|
|
||||||
"problemMatcher": [],
|
|
||||||
"presentation": {
|
|
||||||
"echo": true,
|
|
||||||
"reveal": "always",
|
|
||||||
"focus": false,
|
|
||||||
"panel": "new",
|
|
||||||
"showReuseMessage": true,
|
|
||||||
"clear": false
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Docker: Stop Development",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "docker",
|
|
||||||
"args": [
|
|
||||||
"compose",
|
|
||||||
"-f",
|
|
||||||
"docker-compose.dev.yml",
|
|
||||||
"down"
|
|
||||||
],
|
|
||||||
"group": "build",
|
|
||||||
"problemMatcher": [],
|
|
||||||
"presentation": {
|
|
||||||
"echo": true,
|
|
||||||
"reveal": "always",
|
|
||||||
"focus": false,
|
|
||||||
"panel": "new",
|
|
||||||
"showReuseMessage": true,
|
|
||||||
"clear": false
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Docker: Build Image",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "docker",
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"-t",
|
|
||||||
"skrzynka-impostora-bot:latest",
|
|
||||||
"."
|
|
||||||
],
|
|
||||||
"group": "build",
|
|
||||||
"problemMatcher": [],
|
|
||||||
"presentation": {
|
|
||||||
"echo": true,
|
|
||||||
"reveal": "always",
|
|
||||||
"focus": false,
|
|
||||||
"panel": "new",
|
|
||||||
"showReuseMessage": true,
|
|
||||||
"clear": false
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Docker: View Logs",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "docker",
|
|
||||||
"args": [
|
|
||||||
"compose",
|
|
||||||
"-f",
|
|
||||||
"docker-compose.dev.yml",
|
|
||||||
"logs",
|
|
||||||
"-f"
|
|
||||||
],
|
|
||||||
"group": "test",
|
|
||||||
"isBackground": true,
|
|
||||||
"problemMatcher": [],
|
|
||||||
"presentation": {
|
|
||||||
"echo": true,
|
|
||||||
"reveal": "always",
|
|
||||||
"focus": false,
|
|
||||||
"panel": "new",
|
|
||||||
"showReuseMessage": true,
|
|
||||||
"clear": false
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Deploy Commands to Discord",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "npm",
|
|
||||||
"args": [
|
|
||||||
"run",
|
|
||||||
"deploy"
|
|
||||||
],
|
|
||||||
"group": "build",
|
|
||||||
"problemMatcher": [],
|
|
||||||
"presentation": {
|
|
||||||
"echo": true,
|
|
||||||
"reveal": "always",
|
|
||||||
"focus": false,
|
|
||||||
"panel": "new",
|
|
||||||
"showReuseMessage": true,
|
|
||||||
"clear": false
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
# Multi-stage build dla optymalizacji
|
|
||||||
FROM node:18-alpine AS base
|
|
||||||
|
|
||||||
# Ustaw workdir
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Kopiuj package.json dla głównego projektu
|
|
||||||
COPY package*.json ./
|
|
||||||
|
|
||||||
# Zainstaluj zależności głównego projektu
|
|
||||||
RUN npm ci --only=production
|
|
||||||
|
|
||||||
# Stage dla frontendu
|
|
||||||
FROM node:18-alpine AS frontend-build
|
|
||||||
|
|
||||||
WORKDIR /app/frontend
|
|
||||||
|
|
||||||
# Kopiuj package.json frontendu
|
|
||||||
COPY frontend/package*.json ./
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
# Kopiuj kod frontendu
|
|
||||||
COPY frontend/ ./
|
|
||||||
|
|
||||||
# Zbuduj frontend
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Stage dla backend
|
|
||||||
FROM node:18-alpine AS backend
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Kopiuj zależności z base stage
|
|
||||||
COPY --from=base /app/node_modules ./node_modules
|
|
||||||
COPY package*.json ./
|
|
||||||
|
|
||||||
# Kopiuj kod backend
|
|
||||||
COPY backend/ ./backend/
|
|
||||||
COPY database/ ./database/
|
|
||||||
COPY shared/ ./shared/
|
|
||||||
|
|
||||||
# Kopiuj zbudowany frontend
|
|
||||||
COPY --from=frontend-build /app/frontend/build ./frontend/build
|
|
||||||
|
|
||||||
# Stwórz użytkownika non-root
|
|
||||||
RUN addgroup -g 1001 -S nodejs
|
|
||||||
RUN adduser -S nodejs -u 1001
|
|
||||||
|
|
||||||
# Zmień ownership plików
|
|
||||||
RUN chown -R nodejs:nodejs /app
|
|
||||||
USER nodejs
|
|
||||||
|
|
||||||
# Expose port
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
||||||
CMD node -e "require('http').get('http://localhost:3000/api/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"
|
|
||||||
|
|
||||||
# Uruchom aplikację
|
|
||||||
CMD ["node", "backend/index.js"]
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# Development Dockerfile z hot reload
|
|
||||||
FROM node:18-alpine
|
|
||||||
|
|
||||||
# Zainstaluj nodemon globalnie
|
|
||||||
RUN npm install -g nodemon concurrently
|
|
||||||
|
|
||||||
# Ustaw workdir
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Kopiuj package.json files
|
|
||||||
COPY package*.json ./
|
|
||||||
COPY frontend/package*.json ./frontend/
|
|
||||||
|
|
||||||
# Zainstaluj wszystkie zależności (including dev dependencies)
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
# Zainstaluj zależności frontendu
|
|
||||||
WORKDIR /app/frontend
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
# Wróć do głównego katalogu
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Expose ports
|
|
||||||
EXPOSE 3000 3001 9229
|
|
||||||
|
|
||||||
# Kopiuj pozostałe pliki (będzie nadpisane przez volume w dev)
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Stwórz katalogi dla logów
|
|
||||||
RUN mkdir -p logs
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
||||||
CMD node -e "require('http').get('http://localhost:3000/api/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })" || exit 1
|
|
||||||
|
|
||||||
# Default command (może być nadpisane w docker-compose)
|
|
||||||
CMD ["npm", "run", "dev"]
|
|
||||||
102
bot/Makefile
102
bot/Makefile
@@ -1,102 +0,0 @@
|
|||||||
# Makefile dla Skrzynka Impostora Bot
|
|
||||||
# Użycie: make <target>
|
|
||||||
|
|
||||||
.PHONY: help dev dev-down prod prod-down build logs clean deploy-commands
|
|
||||||
|
|
||||||
# Default target
|
|
||||||
help: ## Pokaż dostępne komendy
|
|
||||||
@echo "Dostępne komendy dla Skrzynka Impostora Bot:"
|
|
||||||
@echo ""
|
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
|
||||||
|
|
||||||
dev: ## Uruchom środowisko development
|
|
||||||
@echo "🚀 Uruchamianie środowiska development..."
|
|
||||||
@if [ ! -f .env ]; then \
|
|
||||||
echo "⚠️ Kopiuję .env.example do .env"; \
|
|
||||||
cp .env.example .env; \
|
|
||||||
echo "📝 Uzupełnij zmienne w pliku .env przed kontynuowaniem"; \
|
|
||||||
fi
|
|
||||||
docker compose -f docker-compose.dev.yml up --build -d
|
|
||||||
@echo "✅ Środowisko uruchomione!"
|
|
||||||
@echo "📋 Dostępne usługi:"
|
|
||||||
@echo " 🤖 Bot + Panel: http://localhost:3000"
|
|
||||||
@echo " 📊 pgAdmin: http://localhost:8080"
|
|
||||||
|
|
||||||
dev-down: ## Zatrzymaj środowisko development
|
|
||||||
@echo "🛑 Zatrzymywanie środowiska development..."
|
|
||||||
docker compose -f docker-compose.dev.yml down
|
|
||||||
|
|
||||||
dev-rebuild: ## Przebuduj i uruchom development
|
|
||||||
@echo "🔄 Przebudowywanie środowiska development..."
|
|
||||||
docker compose -f docker-compose.dev.yml down
|
|
||||||
docker compose -f docker-compose.dev.yml up --build -d
|
|
||||||
|
|
||||||
prod: ## Uruchom środowisko produkcyjne
|
|
||||||
@echo "🚀 Uruchamianie środowiska produkcyjnego..."
|
|
||||||
docker compose up --build -d
|
|
||||||
@echo "✅ Produkcja uruchomiona!"
|
|
||||||
|
|
||||||
prod-down: ## Zatrzymaj środowisko produkcyjne
|
|
||||||
@echo "🛑 Zatrzymywanie środowiska produkcyjnego..."
|
|
||||||
docker compose down
|
|
||||||
|
|
||||||
build: ## Zbuduj obraz Docker
|
|
||||||
@echo "🏗️ Budowanie obrazu Docker..."
|
|
||||||
docker build -t skrzynka-impostora-bot:latest .
|
|
||||||
|
|
||||||
logs: ## Pokaż logi development
|
|
||||||
docker compose -f docker-compose.dev.yml logs -f
|
|
||||||
|
|
||||||
logs-prod: ## Pokaż logi produkcji
|
|
||||||
docker compose logs -f
|
|
||||||
|
|
||||||
clean: ## Wyczyść wszystkie kontenery i volumes
|
|
||||||
@echo "🧹 Czyszczenie kontenerów i volumes..."
|
|
||||||
docker compose -f docker-compose.dev.yml down -v
|
|
||||||
docker compose down -v
|
|
||||||
docker system prune -f
|
|
||||||
|
|
||||||
clean-all: ## Wyczyść wszystko włącznie z obrazami
|
|
||||||
@echo "🧹 Czyszczenie wszystkiego..."
|
|
||||||
docker compose -f docker-compose.dev.yml down -v
|
|
||||||
docker compose down -v
|
|
||||||
docker rmi skrzynka-impostora-bot:latest 2>/dev/null || true
|
|
||||||
docker system prune -af
|
|
||||||
|
|
||||||
deploy-commands: ## Deploy komend Discord (wymaga DISCORD_TOKEN)
|
|
||||||
@if [ -z "$(DISCORD_TOKEN)" ]; then \
|
|
||||||
echo "❌ Brak DISCORD_TOKEN. Użyj: make deploy-commands DISCORD_TOKEN=twoj_token"; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
@echo "📡 Deployowanie komend Discord..."
|
|
||||||
docker run --rm \
|
|
||||||
-e DISCORD_TOKEN="$(DISCORD_TOKEN)" \
|
|
||||||
-e DISCORD_CLIENT_ID="$(DISCORD_CLIENT_ID)" \
|
|
||||||
skrzynka-impostora-bot:latest \
|
|
||||||
node backend/deploy-commands.js
|
|
||||||
|
|
||||||
shell: ## Wejdź do kontenera bota (development)
|
|
||||||
docker compose -f docker-compose.dev.yml exec bot-dev sh
|
|
||||||
|
|
||||||
shell-prod: ## Wejdź do kontenera bota (produkcja)
|
|
||||||
docker compose exec bot sh
|
|
||||||
|
|
||||||
db-shell: ## Wejdź do bazy danych PostgreSQL
|
|
||||||
docker compose -f docker-compose.dev.yml exec postgres-dev psql -U dev_user -d skrzynka_impostora_dev
|
|
||||||
|
|
||||||
backup-db: ## Stwórz backup bazy danych
|
|
||||||
@echo "💾 Tworzenie backupu bazy danych..."
|
|
||||||
docker compose -f docker-compose.dev.yml exec postgres-dev pg_dump -U dev_user skrzynka_impostora_dev > backup_$(shell date +%Y%m%d_%H%M%S).sql
|
|
||||||
@echo "✅ Backup utworzony!"
|
|
||||||
|
|
||||||
status: ## Pokaż status kontenerów
|
|
||||||
@echo "📊 Status kontenerów development:"
|
|
||||||
docker compose -f docker-compose.dev.yml ps
|
|
||||||
@echo ""
|
|
||||||
@echo "📊 Status kontenerów produkcji:"
|
|
||||||
docker compose ps
|
|
||||||
|
|
||||||
# Aliasy dla wygody
|
|
||||||
up: dev ## Alias dla dev
|
|
||||||
down: dev-down ## Alias dla dev-down
|
|
||||||
restart: dev-rebuild ## Alias dla dev-rebuild
|
|
||||||
373
bot/README.md
373
bot/README.md
@@ -1,373 +0,0 @@
|
|||||||
# 🎭 Skrzynka Impostora Bot
|
|
||||||
|
|
||||||
Kompleksowy bot Discord do zarządzania wiadomościami powaitalnymi z panelem web administracyjnym.
|
|
||||||
|
|
||||||
## 📋 Funkcjonalności
|
|
||||||
|
|
||||||
### ✨ Wersja 1.0
|
|
||||||
- ✅ Integracja z Discord API (discord.js v14)
|
|
||||||
- ✅ Slash commands (`/skrzynka`, `/skrzynka-adm`)
|
|
||||||
- ✅ Panel web do zarządzania wiadomościami
|
|
||||||
- ✅ Edytor z podglądem Discord Markdown
|
|
||||||
- ✅ Autoryzacja OAuth2 Discord
|
|
||||||
- ✅ Historia zmian wiadomości
|
|
||||||
- ✅ Baza danych PostgreSQL
|
|
||||||
|
|
||||||
### 🚀 Planowane funkcjonalności
|
|
||||||
- Wielojęzyczność
|
|
||||||
- Szablony wiadomości
|
|
||||||
- Harmonogramy wysyłki
|
|
||||||
- Statystyki i analizy
|
|
||||||
- System ról użytkowników
|
|
||||||
|
|
||||||
## 🛠️ Technologie
|
|
||||||
|
|
||||||
- **Backend**: Node.js, Express.js, discord.js
|
|
||||||
- **Frontend**: React 18, React Router
|
|
||||||
- **Baza danych**: PostgreSQL
|
|
||||||
- **Autoryzacja**: JWT, OAuth2 Discord
|
|
||||||
- **Styling**: Custom CSS (Discord-like)
|
|
||||||
|
|
||||||
## 📦 Instalacja
|
|
||||||
|
|
||||||
### Wymagania
|
|
||||||
|
|
||||||
**Dla Docker (Rekomendowane):**
|
|
||||||
- Docker Desktop >= 4.0
|
|
||||||
- Docker Compose v2
|
|
||||||
- Konto Discord Developer
|
|
||||||
|
|
||||||
**Dla lokalnego uruchomienia:**
|
|
||||||
- Node.js >= 18.0.0
|
|
||||||
- PostgreSQL >= 12
|
|
||||||
- npm >= 8.0.0
|
|
||||||
- Konto Discord Developer
|
|
||||||
|
|
||||||
### 1. Sklonuj repozytorium
|
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd skrzynka-impostora-bot
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Zainstaluj zależności
|
|
||||||
```bash
|
|
||||||
# Backend dependencies
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# Frontend dependencies
|
|
||||||
cd frontend
|
|
||||||
npm install
|
|
||||||
cd ..
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Konfiguracja Discord Bot
|
|
||||||
|
|
||||||
1. Idź do [Discord Developer Portal](https://discord.com/developers/applications)
|
|
||||||
2. Utwórz nową aplikację
|
|
||||||
3. W sekcji "Bot" utwórz bota i skopiuj token
|
|
||||||
4. W sekcji "OAuth2" dodaj redirect URI: `http://localhost:3001/auth/discord/callback`
|
|
||||||
|
|
||||||
### 4. Konfiguracja bazy danych
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Utwórz bazę danych PostgreSQL
|
|
||||||
createdb skrzynka_impostora
|
|
||||||
|
|
||||||
# Lub w psql:
|
|
||||||
CREATE DATABASE skrzynka_impostora;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Zmienne środowiskowe
|
|
||||||
|
|
||||||
Skopiuj `.env.example` do `.env` i wypełnij:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
Edytuj `.env`:
|
|
||||||
```env
|
|
||||||
# Discord Bot Configuration
|
|
||||||
DISCORD_TOKEN=your_discord_bot_token_here
|
|
||||||
DISCORD_CLIENT_ID=your_discord_client_id_here
|
|
||||||
DISCORD_CLIENT_SECRET=your_discord_client_secret_here
|
|
||||||
|
|
||||||
# Database Configuration
|
|
||||||
DATABASE_URL=postgresql://username:password@localhost:5432/skrzynka_impostora
|
|
||||||
DB_HOST=localhost
|
|
||||||
DB_PORT=5432
|
|
||||||
DB_NAME=skrzynka_impostora
|
|
||||||
DB_USER=your_username
|
|
||||||
DB_PASSWORD=your_password
|
|
||||||
|
|
||||||
# Web Panel Configuration
|
|
||||||
JWT_SECRET=your_secure_random_string_here
|
|
||||||
SESSION_SECRET=another_secure_random_string
|
|
||||||
WEB_PORT=3001
|
|
||||||
API_PORT=3000
|
|
||||||
|
|
||||||
# OAuth2 Configuration
|
|
||||||
OAUTH2_REDIRECT_URI=http://localhost:3001/auth/discord/callback
|
|
||||||
|
|
||||||
# Environment
|
|
||||||
NODE_ENV=development
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Inicjalizacja bazy danych
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Uruchom migracje
|
|
||||||
npm run db:migrate
|
|
||||||
|
|
||||||
# Opcjonalnie: dodaj przykładowe dane
|
|
||||||
npm run db:seed
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. Deploy komend Discord
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 Uruchomienie
|
|
||||||
|
|
||||||
### 🐳 Przez Docker (Rekomendowane)
|
|
||||||
|
|
||||||
**Development:**
|
|
||||||
```bash
|
|
||||||
# Automatyczny skrypt (Linux/macOS)
|
|
||||||
./scripts/dev-start.sh
|
|
||||||
|
|
||||||
# Automatyczny skrypt (Windows)
|
|
||||||
scripts\dev-start.bat
|
|
||||||
|
|
||||||
# Lub ręcznie
|
|
||||||
docker compose -f docker-compose.dev.yml up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
**Produkcja:**
|
|
||||||
```bash
|
|
||||||
# Skrypt deploy (Linux/macOS)
|
|
||||||
./scripts/prod-deploy.sh
|
|
||||||
|
|
||||||
# Lub ręcznie
|
|
||||||
docker compose up --build -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### 💻 Lokalnie (bez Docker)
|
|
||||||
|
|
||||||
### Development (równoczesne uruchomienie backend + frontend)
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Osobno Backend i Frontend
|
|
||||||
|
|
||||||
**Backend:**
|
|
||||||
```bash
|
|
||||||
npm run dev:backend
|
|
||||||
```
|
|
||||||
|
|
||||||
**Frontend:**
|
|
||||||
```bash
|
|
||||||
npm run dev:frontend
|
|
||||||
```
|
|
||||||
|
|
||||||
### Produkcja
|
|
||||||
```bash
|
|
||||||
# Zbuduj frontend
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Uruchom serwer
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📖 Użytkowanie
|
|
||||||
|
|
||||||
### 1. Zaproszenie bota na serwer
|
|
||||||
|
|
||||||
Użyj linku z odpowiednimi uprawnieniami:
|
|
||||||
```
|
|
||||||
https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=2147484672&scope=bot%20applications.commands
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Konfiguracja kanału powitalnego
|
|
||||||
|
|
||||||
Na serwerze Discord użyj komendy:
|
|
||||||
```
|
|
||||||
/skrzynka-adm set-welcome channel:#witamy
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Zarządzanie przez panel web
|
|
||||||
|
|
||||||
1. Idź do `http://localhost:3001` (development)
|
|
||||||
2. Zaloguj się przez Discord
|
|
||||||
3. Wybierz serwer
|
|
||||||
4. Edytuj wiadomość powitalną
|
|
||||||
|
|
||||||
### 4. Komendy Discord
|
|
||||||
|
|
||||||
**Komendy użytkownika:**
|
|
||||||
- `/skrzynka info` - Informacje o bocie
|
|
||||||
- `/skrzynka help` - Pomoc
|
|
||||||
|
|
||||||
**Komendy administracyjne:**
|
|
||||||
- `/skrzynka-adm set-welcome <channel>` - Ustaw kanał powitalny
|
|
||||||
- `/skrzynka-adm welcome` - Wyślij wiadomość powitalną
|
|
||||||
- `/skrzynka-adm welcome-update` - Zaktualizuj wiadomość
|
|
||||||
- `/skrzynka-adm status` - Status konfiguracji
|
|
||||||
|
|
||||||
## 🗄️ Struktura bazy danych
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Serwery Discord
|
|
||||||
guilds (id, name, created_at, updated_at)
|
|
||||||
|
|
||||||
-- Kanały powitalne
|
|
||||||
welcome_channels (id, guild_id, channel_id, channel_name, is_active, created_at, updated_at)
|
|
||||||
|
|
||||||
-- Wiadomości powitalne
|
|
||||||
welcome_messages (id, guild_id, content, embed_data, message_id, is_active, created_at, updated_at)
|
|
||||||
|
|
||||||
-- Użytkownicy panelu
|
|
||||||
users (id, discord_id, username, discriminator, avatar, email, role, is_active, last_login, created_at, updated_at)
|
|
||||||
|
|
||||||
-- Historia zmian
|
|
||||||
message_revisions (id, message_id, content, embed_data, user_id, revision_number, created_at)
|
|
||||||
|
|
||||||
-- Uprawnienia użytkowników
|
|
||||||
user_guild_permissions (id, user_id, guild_id, role, granted_by, created_at)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Development
|
|
||||||
|
|
||||||
### Dostępne skrypty
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run start # Uruchom produkcyjnie
|
|
||||||
npm run dev # Development (backend + frontend)
|
|
||||||
npm run dev:backend # Tylko backend
|
|
||||||
npm run dev:frontend # Tylko frontend
|
|
||||||
npm run build # Zbuduj frontend
|
|
||||||
npm run deploy # Deploy komend Discord
|
|
||||||
npm run db:migrate # Migracja bazy
|
|
||||||
npm run db:seed # Seed bazy danych
|
|
||||||
|
|
||||||
# Docker skrypty
|
|
||||||
npm run docker:dev # Uruchom development w Docker
|
|
||||||
npm run docker:dev:down # Zatrzymaj development Docker
|
|
||||||
npm run docker:prod # Uruchom produkcję w Docker
|
|
||||||
npm run docker:build # Zbuduj obraz Docker
|
|
||||||
npm run docker:logs # Pokaż logi Docker
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🐳 Docker Development
|
|
||||||
|
|
||||||
**Dostępne usługi w trybie development:**
|
|
||||||
- **Bot + API**: http://localhost:3000
|
|
||||||
- **Panel Web**: http://localhost:3001
|
|
||||||
- **PostgreSQL**: localhost:5433
|
|
||||||
- **pgAdmin**: http://localhost:8080 (admin@skrzynka.local / admin)
|
|
||||||
- **Redis**: localhost:6380
|
|
||||||
|
|
||||||
**Użyteczne komendy Docker:**
|
|
||||||
```bash
|
|
||||||
# Wyświetl logi konkretnej usługi
|
|
||||||
docker compose -f docker-compose.dev.yml logs -f bot-dev
|
|
||||||
|
|
||||||
# Wejdź do kontenera bota
|
|
||||||
docker compose -f docker-compose.dev.yml exec bot-dev sh
|
|
||||||
|
|
||||||
# Restart konkretnej usługi
|
|
||||||
docker compose -f docker-compose.dev.yml restart bot-dev
|
|
||||||
|
|
||||||
# Wyczyść wszystko i rozpocznij od nowa
|
|
||||||
docker compose -f docker-compose.dev.yml down -v
|
|
||||||
docker compose -f docker-compose.dev.yml up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🔄 Hot Reload
|
|
||||||
|
|
||||||
W trybie development Docker automatycznie:
|
|
||||||
- Monitoruje zmiany w kodzie (volume mount)
|
|
||||||
- Restartuje backend przy zmianach (nodemon)
|
|
||||||
- Przebudowuje frontend na żywo
|
|
||||||
- Automatycznie stosuje migracje bazy danych
|
|
||||||
|
|
||||||
### Struktura projektu
|
|
||||||
|
|
||||||
```
|
|
||||||
├── backend/ # Serwer Node.js
|
|
||||||
│ ├── index.js # Główny plik bota
|
|
||||||
│ ├── deploy-commands.js # Deploy slash commands
|
|
||||||
│ ├── commands/ # Slash commands
|
|
||||||
│ ├── database/ # Zarządzanie bazą
|
|
||||||
│ └── web/ # API serwer
|
|
||||||
├── frontend/ # React aplikacja
|
|
||||||
│ ├── src/
|
|
||||||
│ │ ├── components/ # Komponenty React
|
|
||||||
│ │ ├── pages/ # Strony aplikacji
|
|
||||||
│ │ ├── services/ # API client
|
|
||||||
│ │ └── hooks/ # React hooks
|
|
||||||
│ └── public/ # Pliki statyczne
|
|
||||||
├── database/ # Skrypty bazy danych
|
|
||||||
├── shared/ # Współdzielone utilities
|
|
||||||
└── .github/ # GitHub konfiguracja
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔒 Bezpieczeństwo
|
|
||||||
|
|
||||||
- JWT tokens dla autoryzacji API
|
|
||||||
- OAuth2 Discord dla logowania
|
|
||||||
- Helmet.js dla zabezpieczeń HTTP
|
|
||||||
- CORS protection
|
|
||||||
- SQL injection protection (prepared statements)
|
|
||||||
- Input validation (express-validator)
|
|
||||||
|
|
||||||
## 📊 Monitoring i Logi
|
|
||||||
|
|
||||||
Bot loguje ważne zdarzenia:
|
|
||||||
- Startup/shutdown
|
|
||||||
- Command execution
|
|
||||||
- Database operations
|
|
||||||
- API requests
|
|
||||||
- Errors and warnings
|
|
||||||
|
|
||||||
## 🤝 Contribucje
|
|
||||||
|
|
||||||
1. Fork repository
|
|
||||||
2. Utwórz feature branch (`git checkout -b feature/amazing-feature`)
|
|
||||||
3. Commit changes (`git commit -m 'Add amazing feature'`)
|
|
||||||
4. Push branch (`git push origin feature/amazing-feature`)
|
|
||||||
5. Otwórz Pull Request
|
|
||||||
|
|
||||||
## 📄 Licencja
|
|
||||||
|
|
||||||
MIT License - zobacz plik `LICENSE`
|
|
||||||
|
|
||||||
## 🆘 Pomoc i wsparcie
|
|
||||||
|
|
||||||
- Sprawdź [Issues](../../issues) dla znanych problemów
|
|
||||||
- Utwórz nowy Issue dla bugów lub feature requests
|
|
||||||
- Przeczytaj dokumentację Discord.js: https://discord.js.org/
|
|
||||||
|
|
||||||
## 📈 Roadmap
|
|
||||||
|
|
||||||
### v1.1 - Zarządzanie
|
|
||||||
- [ ] System ról użytkowników
|
|
||||||
- [ ] Bulk operations
|
|
||||||
- [ ] Advanced permissions
|
|
||||||
|
|
||||||
### v1.2 - Funkcjonalności
|
|
||||||
- [ ] Message templates
|
|
||||||
- [ ] Scheduled messages
|
|
||||||
- [ ] Multiple languages
|
|
||||||
|
|
||||||
### v1.3 - Analytics
|
|
||||||
- [ ] Usage statistics
|
|
||||||
- [ ] User engagement metrics
|
|
||||||
- [ ] Performance monitoring
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Stworzono z ❤️ dla społeczności Discord
|
|
||||||
@@ -1,440 +0,0 @@
|
|||||||
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
|
|
||||||
|
|
||||||
class SlashCommands {
|
|
||||||
constructor(database) {
|
|
||||||
this.db = database;
|
|
||||||
this.commands = this.setupCommands();
|
|
||||||
}
|
|
||||||
|
|
||||||
setupCommands() {
|
|
||||||
return [
|
|
||||||
// Komenda informacyjna dla użytkowników
|
|
||||||
new SlashCommandBuilder()
|
|
||||||
.setName('skrzynka')
|
|
||||||
.setDescription('Informacje o bocie Skrzynka Impostora')
|
|
||||||
.addSubcommand(subcommand =>
|
|
||||||
subcommand
|
|
||||||
.setName('info')
|
|
||||||
.setDescription('Pokaż informacje o bocie')
|
|
||||||
)
|
|
||||||
.addSubcommand(subcommand =>
|
|
||||||
subcommand
|
|
||||||
.setName('help')
|
|
||||||
.setDescription('Pokaż dostępne komendy')
|
|
||||||
),
|
|
||||||
|
|
||||||
// Komendy administracyjne
|
|
||||||
new SlashCommandBuilder()
|
|
||||||
.setName('skrzynka-adm')
|
|
||||||
.setDescription('Komendy administracyjne dla bota Skrzynka Impostora')
|
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels)
|
|
||||||
.addSubcommand(subcommand =>
|
|
||||||
subcommand
|
|
||||||
.setName('set-welcome')
|
|
||||||
.setDescription('Ustaw kanał powitalny')
|
|
||||||
.addChannelOption(option =>
|
|
||||||
option
|
|
||||||
.setName('channel')
|
|
||||||
.setDescription('Kanał na którym będzie wyświetlana wiadomość powitalna')
|
|
||||||
.setRequired(true)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.addSubcommand(subcommand =>
|
|
||||||
subcommand
|
|
||||||
.setName('welcome')
|
|
||||||
.setDescription('Wyślij wiadomość powitalną')
|
|
||||||
)
|
|
||||||
.addSubcommand(subcommand =>
|
|
||||||
subcommand
|
|
||||||
.setName('welcome-update')
|
|
||||||
.setDescription('Zaktualizuj wiadomość powitalną z bazy danych')
|
|
||||||
)
|
|
||||||
.addSubcommand(subcommand =>
|
|
||||||
subcommand
|
|
||||||
.setName('status')
|
|
||||||
.setDescription('Pokaż status konfiguracji bota')
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
getCommandsData() {
|
|
||||||
return this.commands.map(command => command.toJSON());
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleCommand(interaction) {
|
|
||||||
const { commandName, options } = interaction;
|
|
||||||
|
|
||||||
switch (commandName) {
|
|
||||||
case 'skrzynka':
|
|
||||||
await this.handleUserCommands(interaction, options);
|
|
||||||
break;
|
|
||||||
case 'skrzynka-adm':
|
|
||||||
await this.handleAdminCommands(interaction, options);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
await interaction.reply({
|
|
||||||
content: '❌ Nieznana komenda.',
|
|
||||||
ephemeral: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleUserCommands(interaction, options) {
|
|
||||||
const subcommand = options.getSubcommand();
|
|
||||||
|
|
||||||
switch (subcommand) {
|
|
||||||
case 'info':
|
|
||||||
await this.showBotInfo(interaction);
|
|
||||||
break;
|
|
||||||
case 'help':
|
|
||||||
await this.showHelp(interaction);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleAdminCommands(interaction, options) {
|
|
||||||
const subcommand = options.getSubcommand();
|
|
||||||
|
|
||||||
// Sprawdź uprawnienia użytkownika
|
|
||||||
if (!interaction.member.permissions.has(PermissionFlagsBits.ManageChannels)) {
|
|
||||||
await interaction.reply({
|
|
||||||
content: '❌ Nie masz uprawnień do używania komend administracyjnych.',
|
|
||||||
ephemeral: true
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (subcommand) {
|
|
||||||
case 'set-welcome':
|
|
||||||
await this.setWelcomeChannel(interaction, options);
|
|
||||||
break;
|
|
||||||
case 'welcome':
|
|
||||||
await this.sendWelcomeMessage(interaction);
|
|
||||||
break;
|
|
||||||
case 'welcome-update':
|
|
||||||
await this.updateWelcomeMessage(interaction);
|
|
||||||
break;
|
|
||||||
case 'status':
|
|
||||||
await this.showStatus(interaction);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async showBotInfo(interaction) {
|
|
||||||
const embed = new EmbedBuilder()
|
|
||||||
.setColor(0x00AE86)
|
|
||||||
.setTitle('🎭 Skrzynka Impostora Bot')
|
|
||||||
.setDescription('Bot do zarządzania wiadomościami powaitalnymi na serwerze Discord.')
|
|
||||||
.addFields(
|
|
||||||
{ name: '📝 Wersja', value: '1.0.0', inline: true },
|
|
||||||
{ name: '🔧 Panel Web', value: 'Dostępny dla administratorów', inline: true },
|
|
||||||
{ name: '⚡ Status', value: 'Online', inline: true },
|
|
||||||
{ name: '💻 Funkcje', value: '• Automatyczne wiadomości powitalne\n• Panel zarządzania web\n• Historia zmian\n• Konfiguracja kanałów', inline: false }
|
|
||||||
)
|
|
||||||
.setThumbnail(interaction.client.user.displayAvatarURL())
|
|
||||||
.setFooter({
|
|
||||||
text: 'Skrzynka Impostora Bot',
|
|
||||||
iconURL: interaction.client.user.displayAvatarURL()
|
|
||||||
})
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed] });
|
|
||||||
}
|
|
||||||
|
|
||||||
async showHelp(interaction) {
|
|
||||||
const embed = new EmbedBuilder()
|
|
||||||
.setColor(0x0099FF)
|
|
||||||
.setTitle('📚 Pomoc - Dostępne komendy')
|
|
||||||
.addFields(
|
|
||||||
{
|
|
||||||
name: '👥 Komendy użytkownika',
|
|
||||||
value: '`/skrzynka info` - Informacje o bocie\n`/skrzynka help` - Ta pomoc',
|
|
||||||
inline: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '🛠️ Komendy administracyjne',
|
|
||||||
value: '`/skrzynka-adm set-welcome` - Ustaw kanał powitalny\n`/skrzynka-adm welcome` - Wyślij wiadomość powitalną\n`/skrzynka-adm welcome-update` - Zaktualizuj wiadomość\n`/skrzynka-adm status` - Status konfiguracji',
|
|
||||||
inline: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '🌐 Panel Web',
|
|
||||||
value: 'Zarządzanie treścią wiadomości dostępne przez panel web.',
|
|
||||||
inline: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.setFooter({
|
|
||||||
text: 'Potrzebujesz pomocy? Skontaktuj się z administratorem serwera.',
|
|
||||||
iconURL: interaction.client.user.displayAvatarURL()
|
|
||||||
})
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed], ephemeral: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
async setWelcomeChannel(interaction, options) {
|
|
||||||
const channel = options.getChannel('channel');
|
|
||||||
const guildId = interaction.guild.id;
|
|
||||||
const guildName = interaction.guild.name;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Sprawdź czy bot ma uprawnienia do pisania na kanale
|
|
||||||
if (!channel.permissionsFor(interaction.client.user).has(PermissionFlagsBits.SendMessages)) {
|
|
||||||
await interaction.reply({
|
|
||||||
content: '❌ Bot nie ma uprawnień do pisania na wybranym kanale.',
|
|
||||||
ephemeral: true
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dodaj/zaktualizuj serwer w bazie
|
|
||||||
await this.db.addGuild(guildId, guildName);
|
|
||||||
|
|
||||||
// Ustaw kanał powitalny
|
|
||||||
await this.db.setWelcomeChannel(guildId, channel.id, channel.name);
|
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
|
||||||
.setColor(0x00FF00)
|
|
||||||
.setTitle('✅ Kanał powitalny ustawiony')
|
|
||||||
.setDescription(`Kanał ${channel} został ustawiony jako kanał powitalny.`)
|
|
||||||
.addFields(
|
|
||||||
{ name: 'Kanał', value: `${channel.name} (${channel.id})`, inline: false },
|
|
||||||
{ name: 'Następne kroki', value: 'Skonfiguruj treść wiadomości przez panel web lub użyj `/skrzynka-adm welcome` aby wysłać domyślną wiadomość.', inline: false }
|
|
||||||
)
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed] });
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Błąd podczas ustawiania kanału powitalnego:', error);
|
|
||||||
await interaction.reply({
|
|
||||||
content: '❌ Wystąpił błąd podczas ustawiania kanału powitalnego.',
|
|
||||||
ephemeral: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendWelcomeMessage(interaction) {
|
|
||||||
const guildId = interaction.guild.id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await interaction.deferReply({ ephemeral: true });
|
|
||||||
|
|
||||||
// Pobierz konfigurację kanału
|
|
||||||
const welcomeChannelConfig = await this.db.getWelcomeChannel(guildId);
|
|
||||||
if (!welcomeChannelConfig) {
|
|
||||||
await interaction.editReply({
|
|
||||||
content: '❌ Kanał powitalny nie został skonfigurowany. Użyj `/skrzynka-adm set-welcome` aby go ustawić.'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pobierz kanał
|
|
||||||
const channel = interaction.guild.channels.cache.get(welcomeChannelConfig.channel_id);
|
|
||||||
if (!channel) {
|
|
||||||
await interaction.editReply({
|
|
||||||
content: '❌ Skonfigurowany kanał powitalny nie istnieje lub bot nie ma do niego dostępu.'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pobierz wiadomość z bazy lub użyj domyślnej
|
|
||||||
let welcomeMessage = await this.db.getWelcomeMessage(guildId);
|
|
||||||
let messageContent, embedData;
|
|
||||||
|
|
||||||
if (welcomeMessage) {
|
|
||||||
messageContent = welcomeMessage.content;
|
|
||||||
embedData = welcomeMessage.embed_data;
|
|
||||||
} else {
|
|
||||||
messageContent = this.getDefaultWelcomeMessage(interaction.guild);
|
|
||||||
embedData = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wyślij wiadomość
|
|
||||||
const messageOptions = { content: messageContent };
|
|
||||||
if (embedData) {
|
|
||||||
messageOptions.embeds = [new EmbedBuilder(embedData)];
|
|
||||||
}
|
|
||||||
|
|
||||||
const sentMessage = await channel.send(messageOptions);
|
|
||||||
|
|
||||||
// Zapisz lub zaktualizuj wiadomość w bazie
|
|
||||||
if (welcomeMessage) {
|
|
||||||
await this.db.updateWelcomeMessage(guildId, messageContent, embedData, sentMessage.id);
|
|
||||||
} else {
|
|
||||||
await this.db.saveWelcomeMessage(guildId, messageContent, embedData, sentMessage.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const successEmbed = new EmbedBuilder()
|
|
||||||
.setColor(0x00FF00)
|
|
||||||
.setTitle('✅ Wiadomość powitalna wysłana')
|
|
||||||
.setDescription(`Wiadomość została wysłana na kanał ${channel}.`)
|
|
||||||
.addFields(
|
|
||||||
{ name: 'Kanał', value: `${channel.name}`, inline: true },
|
|
||||||
{ name: 'ID wiadomości', value: `${sentMessage.id}`, inline: true }
|
|
||||||
)
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [successEmbed] });
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Błąd podczas wysyłania wiadomości powitalnej:', error);
|
|
||||||
await interaction.editReply({
|
|
||||||
content: '❌ Wystąpił błąd podczas wysyłania wiadomości powitalnej.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateWelcomeMessage(interaction) {
|
|
||||||
const guildId = interaction.guild.id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await interaction.deferReply({ ephemeral: true });
|
|
||||||
|
|
||||||
// Pobierz konfigurację i wiadomość
|
|
||||||
const welcomeChannelConfig = await this.db.getWelcomeChannel(guildId);
|
|
||||||
const welcomeMessage = await this.db.getWelcomeMessage(guildId);
|
|
||||||
|
|
||||||
if (!welcomeChannelConfig || !welcomeMessage) {
|
|
||||||
await interaction.editReply({
|
|
||||||
content: '❌ Kanał powitalny lub wiadomość nie zostały skonfigurowane.'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const channel = interaction.guild.channels.cache.get(welcomeChannelConfig.channel_id);
|
|
||||||
if (!channel) {
|
|
||||||
await interaction.editReply({
|
|
||||||
content: '❌ Skonfigurowany kanał powitalny nie istnieje.'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Znajdź i zaktualizuj wiadomość
|
|
||||||
if (welcomeMessage.message_id) {
|
|
||||||
try {
|
|
||||||
const message = await channel.messages.fetch(welcomeMessage.message_id);
|
|
||||||
|
|
||||||
const messageOptions = { content: welcomeMessage.content };
|
|
||||||
if (welcomeMessage.embed_data) {
|
|
||||||
messageOptions.embeds = [new EmbedBuilder(welcomeMessage.embed_data)];
|
|
||||||
}
|
|
||||||
|
|
||||||
await message.edit(messageOptions);
|
|
||||||
|
|
||||||
const successEmbed = new EmbedBuilder()
|
|
||||||
.setColor(0x00FF00)
|
|
||||||
.setTitle('✅ Wiadomość zaktualizowana')
|
|
||||||
.setDescription(`Wiadomość powitalna została zaktualizowana na kanale ${channel}.`)
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [successEmbed] });
|
|
||||||
|
|
||||||
} catch (fetchError) {
|
|
||||||
// Jeśli nie można znaleźć wiadomości, wyślij nową
|
|
||||||
await this.sendWelcomeMessage(interaction);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Jeśli nie ma ID wiadomości, wyślij nową
|
|
||||||
await this.sendWelcomeMessage(interaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Błąd podczas aktualizacji wiadomości powitalnej:', error);
|
|
||||||
await interaction.editReply({
|
|
||||||
content: '❌ Wystąpił błąd podczas aktualizacji wiadomości powitalnej.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async showStatus(interaction) {
|
|
||||||
const guildId = interaction.guild.id;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const welcomeChannelConfig = await this.db.getWelcomeChannel(guildId);
|
|
||||||
const welcomeMessage = await this.db.getWelcomeMessage(guildId);
|
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
|
||||||
.setColor(0x0099FF)
|
|
||||||
.setTitle('📊 Status konfiguracji bota')
|
|
||||||
.setDescription(`Status dla serwera: **${interaction.guild.name}**`);
|
|
||||||
|
|
||||||
if (welcomeChannelConfig) {
|
|
||||||
const channel = interaction.guild.channels.cache.get(welcomeChannelConfig.channel_id);
|
|
||||||
embed.addFields({
|
|
||||||
name: '📍 Kanał powitalny',
|
|
||||||
value: channel ? `${channel.name} (${channel.id})` : `❌ Kanał niedostępny (${welcomeChannelConfig.channel_id})`,
|
|
||||||
inline: false
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
embed.addFields({
|
|
||||||
name: '📍 Kanał powitalny',
|
|
||||||
value: '❌ Nie skonfigurowany',
|
|
||||||
inline: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (welcomeMessage) {
|
|
||||||
embed.addFields(
|
|
||||||
{
|
|
||||||
name: '💬 Wiadomość powitalna',
|
|
||||||
value: '✅ Skonfigurowana w bazie danych',
|
|
||||||
inline: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '🆔 ID wiadomości',
|
|
||||||
value: welcomeMessage.message_id || 'Brak',
|
|
||||||
inline: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '📅 Ostatnia aktualizacja',
|
|
||||||
value: new Date(welcomeMessage.updated_at).toLocaleString('pl-PL'),
|
|
||||||
inline: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
embed.addFields({
|
|
||||||
name: '💬 Wiadomość powitalna',
|
|
||||||
value: '❌ Nie skonfigurowana (będzie użyta domyślna)',
|
|
||||||
inline: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
embed.setFooter({
|
|
||||||
text: 'Użyj panelu web aby zarządzać treścią wiadomości',
|
|
||||||
iconURL: interaction.client.user.displayAvatarURL()
|
|
||||||
})
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed], ephemeral: true });
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Błąd podczas pobierania statusu:', error);
|
|
||||||
await interaction.reply({
|
|
||||||
content: '❌ Wystąpił błąd podczas pobierania statusu.',
|
|
||||||
ephemeral: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultWelcomeMessage(guild) {
|
|
||||||
return `# 🎭 Witamy na serwerze ${guild.name}!
|
|
||||||
|
|
||||||
Miło Cię tutaj widzieć! 👋
|
|
||||||
|
|
||||||
## 📋 Najważniejsze informacje:
|
|
||||||
• Przeczytaj regulamin serwera
|
|
||||||
• Przedstaw się w odpowiednim kanale
|
|
||||||
• Baw się dobrze i szanuj innych członków
|
|
||||||
|
|
||||||
## 🎮 Funkcje serwera:
|
|
||||||
• Kanały tematyczne
|
|
||||||
• System ról
|
|
||||||
• Eventy i konkursy
|
|
||||||
|
|
||||||
---
|
|
||||||
*Ta wiadomość została wygenerowana przez Skrzynka Impostora Bot*
|
|
||||||
*Administratorzy mogą edytować treść przez panel web*`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { SlashCommands };
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
const { Pool } = require('pg');
|
|
||||||
require('dotenv').config();
|
|
||||||
|
|
||||||
class DatabaseManager {
|
|
||||||
constructor() {
|
|
||||||
this.pool = new Pool({
|
|
||||||
connectionString: process.env.DATABASE_URL,
|
|
||||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async initialize() {
|
|
||||||
try {
|
|
||||||
await this.createTables();
|
|
||||||
console.log('Tabele bazy danych utworzone pomyślnie');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Błąd podczas inicjalizacji bazy danych:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createTables() {
|
|
||||||
const createTablesSQL = `
|
|
||||||
-- Tabela serwerów Discord
|
|
||||||
CREATE TABLE IF NOT EXISTS guilds (
|
|
||||||
id VARCHAR(20) PRIMARY KEY,
|
|
||||||
name VARCHAR(100) NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela kanałów powitalnych
|
|
||||||
CREATE TABLE IF NOT EXISTS welcome_channels (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
guild_id VARCHAR(20) REFERENCES guilds(id) ON DELETE CASCADE,
|
|
||||||
channel_id VARCHAR(20) NOT NULL,
|
|
||||||
channel_name VARCHAR(100),
|
|
||||||
is_active BOOLEAN DEFAULT true,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE(guild_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela wiadomości powitalnych
|
|
||||||
CREATE TABLE IF NOT EXISTS welcome_messages (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
guild_id VARCHAR(20) REFERENCES guilds(id) ON DELETE CASCADE,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
embed_data JSONB,
|
|
||||||
message_id VARCHAR(20),
|
|
||||||
is_active BOOLEAN DEFAULT true,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela użytkowników panelu web
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
discord_id VARCHAR(20) UNIQUE NOT NULL,
|
|
||||||
username VARCHAR(32) NOT NULL,
|
|
||||||
discriminator VARCHAR(4),
|
|
||||||
avatar VARCHAR(255),
|
|
||||||
email VARCHAR(255),
|
|
||||||
role VARCHAR(20) DEFAULT 'viewer',
|
|
||||||
is_active BOOLEAN DEFAULT true,
|
|
||||||
last_login TIMESTAMP,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela historii wersji wiadomości
|
|
||||||
CREATE TABLE IF NOT EXISTS message_revisions (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
message_id INTEGER REFERENCES welcome_messages(id) ON DELETE CASCADE,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
embed_data JSONB,
|
|
||||||
user_id INTEGER REFERENCES users(id),
|
|
||||||
revision_number INTEGER NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela uprawnień użytkowników do serwerów
|
|
||||||
CREATE TABLE IF NOT EXISTS user_guild_permissions (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
guild_id VARCHAR(20) REFERENCES guilds(id) ON DELETE CASCADE,
|
|
||||||
role VARCHAR(20) DEFAULT 'viewer',
|
|
||||||
granted_by INTEGER REFERENCES users(id),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE(user_id, guild_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Indeksy dla wydajności
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_guilds_id ON guilds(id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_welcome_channels_guild_id ON welcome_channels(guild_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_welcome_messages_guild_id ON welcome_messages(guild_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_discord_id ON users(discord_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_message_revisions_message_id ON message_revisions(message_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_guild_permissions_user_guild ON user_guild_permissions(user_id, guild_id);
|
|
||||||
`;
|
|
||||||
|
|
||||||
await this.pool.query(createTablesSQL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metody dla zarządzania serwerami
|
|
||||||
async addGuild(guildId, guildName) {
|
|
||||||
const query = `
|
|
||||||
INSERT INTO guilds (id, name)
|
|
||||||
VALUES ($1, $2)
|
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
|
||||||
name = EXCLUDED.name,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
`;
|
|
||||||
await this.pool.query(query, [guildId, guildName]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getGuild(guildId) {
|
|
||||||
const query = 'SELECT * FROM guilds WHERE id = $1';
|
|
||||||
const result = await this.pool.query(query, [guildId]);
|
|
||||||
return result.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metody dla kanałów powitalnych
|
|
||||||
async setWelcomeChannel(guildId, channelId, channelName) {
|
|
||||||
const query = `
|
|
||||||
INSERT INTO welcome_channels (guild_id, channel_id, channel_name)
|
|
||||||
VALUES ($1, $2, $3)
|
|
||||||
ON CONFLICT (guild_id) DO UPDATE SET
|
|
||||||
channel_id = EXCLUDED.channel_id,
|
|
||||||
channel_name = EXCLUDED.channel_name,
|
|
||||||
is_active = true,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
`;
|
|
||||||
await this.pool.query(query, [guildId, channelId, channelName]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getWelcomeChannel(guildId) {
|
|
||||||
const query = 'SELECT * FROM welcome_channels WHERE guild_id = $1 AND is_active = true';
|
|
||||||
const result = await this.pool.query(query, [guildId]);
|
|
||||||
return result.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metody dla wiadomości powitalnych
|
|
||||||
async saveWelcomeMessage(guildId, content, embedData = null, messageId = null) {
|
|
||||||
const query = `
|
|
||||||
INSERT INTO welcome_messages (guild_id, content, embed_data, message_id)
|
|
||||||
VALUES ($1, $2, $3, $4)
|
|
||||||
RETURNING *
|
|
||||||
`;
|
|
||||||
const result = await this.pool.query(query, [guildId, content, embedData, messageId]);
|
|
||||||
return result.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateWelcomeMessage(guildId, content, embedData = null, messageId = null) {
|
|
||||||
const query = `
|
|
||||||
UPDATE welcome_messages
|
|
||||||
SET content = $2, embed_data = $3, message_id = $4, updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE guild_id = $1 AND is_active = true
|
|
||||||
RETURNING *
|
|
||||||
`;
|
|
||||||
const result = await this.pool.query(query, [guildId, content, embedData, messageId]);
|
|
||||||
return result.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async getWelcomeMessage(guildId) {
|
|
||||||
const query = 'SELECT * FROM welcome_messages WHERE guild_id = $1 AND is_active = true ORDER BY created_at DESC LIMIT 1';
|
|
||||||
const result = await this.pool.query(query, [guildId]);
|
|
||||||
return result.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metody dla użytkowników
|
|
||||||
async saveUser(discordUser) {
|
|
||||||
const query = `
|
|
||||||
INSERT INTO users (discord_id, username, discriminator, avatar, email)
|
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
|
||||||
ON CONFLICT (discord_id) DO UPDATE SET
|
|
||||||
username = EXCLUDED.username,
|
|
||||||
discriminator = EXCLUDED.discriminator,
|
|
||||||
avatar = EXCLUDED.avatar,
|
|
||||||
email = EXCLUDED.email,
|
|
||||||
last_login = CURRENT_TIMESTAMP,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
RETURNING *
|
|
||||||
`;
|
|
||||||
const result = await this.pool.query(query, [
|
|
||||||
discordUser.id,
|
|
||||||
discordUser.username,
|
|
||||||
discordUser.discriminator || '0000',
|
|
||||||
discordUser.avatar,
|
|
||||||
discordUser.email
|
|
||||||
]);
|
|
||||||
return result.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUser(discordId) {
|
|
||||||
const query = 'SELECT * FROM users WHERE discord_id = $1';
|
|
||||||
const result = await this.pool.query(query, [discordId]);
|
|
||||||
return result.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metody dla historii wersji
|
|
||||||
async saveMessageRevision(messageId, content, embedData, userId) {
|
|
||||||
// Pobierz aktualny numer rewizji
|
|
||||||
const revisionQuery = 'SELECT COALESCE(MAX(revision_number), 0) + 1 as next_revision FROM message_revisions WHERE message_id = $1';
|
|
||||||
const revisionResult = await this.pool.query(revisionQuery, [messageId]);
|
|
||||||
const nextRevision = revisionResult.rows[0].next_revision;
|
|
||||||
|
|
||||||
const query = `
|
|
||||||
INSERT INTO message_revisions (message_id, content, embed_data, user_id, revision_number)
|
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
|
||||||
RETURNING *
|
|
||||||
`;
|
|
||||||
const result = await this.pool.query(query, [messageId, content, embedData, userId, nextRevision]);
|
|
||||||
return result.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMessageRevisions(messageId) {
|
|
||||||
const query = `
|
|
||||||
SELECT mr.*, u.username
|
|
||||||
FROM message_revisions mr
|
|
||||||
LEFT JOIN users u ON mr.user_id = u.id
|
|
||||||
WHERE mr.message_id = $1
|
|
||||||
ORDER BY mr.revision_number DESC
|
|
||||||
`;
|
|
||||||
const result = await this.pool.query(query, [messageId]);
|
|
||||||
return result.rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
async close() {
|
|
||||||
await this.pool.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { DatabaseManager };
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
const { REST, Routes } = require('discord.js');
|
|
||||||
const { SlashCommands } = require('./commands');
|
|
||||||
require('dotenv').config();
|
|
||||||
|
|
||||||
async function deployCommands() {
|
|
||||||
if (!process.env.DISCORD_TOKEN || !process.env.DISCORD_CLIENT_ID) {
|
|
||||||
console.error('❌ Brak wymaganych zmiennych środowiskowych (DISCORD_TOKEN, DISCORD_CLIENT_ID)');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const commands = new SlashCommands();
|
|
||||||
const commandsData = commands.getCommandsData();
|
|
||||||
|
|
||||||
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(`🚀 Rozpoczęto deploy ${commandsData.length} komend slash...`);
|
|
||||||
|
|
||||||
// Deploy globalnych komend
|
|
||||||
const data = await rest.put(
|
|
||||||
Routes.applicationCommands(process.env.DISCORD_CLIENT_ID),
|
|
||||||
{ body: commandsData }
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`✅ Pomyślnie zdeployowano ${data.length} komend slash globalnie.`);
|
|
||||||
|
|
||||||
// Wyświetl listę komend
|
|
||||||
console.log('\n📋 Zdeployowane komendy:');
|
|
||||||
data.forEach(command => {
|
|
||||||
console.log(` • /${command.name} - ${command.description}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Błąd podczas deployowania komend:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jeśli skrypt jest uruchamiany bezpośrednio
|
|
||||||
if (require.main === module) {
|
|
||||||
deployCommands();
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { deployCommands };
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
const { Client, GatewayIntentBits, REST, Routes, EmbedBuilder } = require('discord.js');
|
|
||||||
const express = require('express');
|
|
||||||
const cors = require('cors');
|
|
||||||
const helmet = require('helmet');
|
|
||||||
const morgan = require('morgan');
|
|
||||||
const compression = require('compression');
|
|
||||||
require('dotenv').config();
|
|
||||||
|
|
||||||
const { DatabaseManager } = require('./database/DatabaseManager');
|
|
||||||
const { SlashCommands } = require('./commands');
|
|
||||||
const { WebPanel } = require('./web/server');
|
|
||||||
|
|
||||||
class SkrzynkaImpostoraBot {
|
|
||||||
constructor() {
|
|
||||||
this.client = new Client({
|
|
||||||
intents: [
|
|
||||||
GatewayIntentBits.Guilds,
|
|
||||||
GatewayIntentBits.GuildMessages,
|
|
||||||
GatewayIntentBits.MessageContent,
|
|
||||||
GatewayIntentBits.GuildMembers
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
this.db = new DatabaseManager();
|
|
||||||
this.commands = new SlashCommands(this.db);
|
|
||||||
this.webPanel = new WebPanel(this.db);
|
|
||||||
|
|
||||||
this.setupEventHandlers();
|
|
||||||
}
|
|
||||||
|
|
||||||
setupEventHandlers() {
|
|
||||||
this.client.once('ready', () => {
|
|
||||||
console.log(`✅ Bot zalogowany jako ${this.client.user.tag}`);
|
|
||||||
this.client.user.setActivity('Zarządzanie wiadomościami powiatalnymi', { type: 'WATCHING' });
|
|
||||||
});
|
|
||||||
|
|
||||||
this.client.on('guildCreate', async (guild) => {
|
|
||||||
console.log(`Dodano do nowego serwera: ${guild.name} (${guild.id})`);
|
|
||||||
await this.db.addGuild(guild.id, guild.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.client.on('interactionCreate', async (interaction) => {
|
|
||||||
if (!interaction.isChatInputCommand()) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.commands.handleCommand(interaction);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Błąd podczas wykonywania komendy:', error);
|
|
||||||
|
|
||||||
const errorEmbed = new EmbedBuilder()
|
|
||||||
.setColor(0xFF0000)
|
|
||||||
.setTitle('❌ Błąd')
|
|
||||||
.setDescription('Wystąpił błąd podczas wykonywania komendy.')
|
|
||||||
.setTimestamp();
|
|
||||||
|
|
||||||
if (interaction.replied || interaction.deferred) {
|
|
||||||
await interaction.followUp({ embeds: [errorEmbed], ephemeral: true });
|
|
||||||
} else {
|
|
||||||
await interaction.reply({ embeds: [errorEmbed], ephemeral: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.client.on('error', console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
async start() {
|
|
||||||
try {
|
|
||||||
// Inicjalizacja bazy danych
|
|
||||||
await this.db.initialize();
|
|
||||||
console.log('✅ Baza danych zainicjalizowana');
|
|
||||||
|
|
||||||
// Uruchomienie panelu web
|
|
||||||
await this.webPanel.start();
|
|
||||||
console.log('✅ Panel web uruchomiony');
|
|
||||||
|
|
||||||
// Logowanie bota
|
|
||||||
await this.client.login(process.env.DISCORD_TOKEN);
|
|
||||||
console.log('✅ Bot Discord uruchomiony');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Błąd podczas uruchamiania bota:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async deployCommands() {
|
|
||||||
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('Rozpoczęto odświeżanie komend slash...');
|
|
||||||
|
|
||||||
const commands = this.commands.getCommandsData();
|
|
||||||
|
|
||||||
await rest.put(
|
|
||||||
Routes.applicationCommands(process.env.DISCORD_CLIENT_ID),
|
|
||||||
{ body: commands }
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('✅ Komendy slash zostały pomyślnie odświeżone.');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Błąd podczas odświeżania komend:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Uruchomienie bota
|
|
||||||
if (require.main === module) {
|
|
||||||
const bot = new SkrzynkaImpostoraBot();
|
|
||||||
bot.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { SkrzynkaImpostoraBot };
|
|
||||||
@@ -1,372 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const cors = require('cors');
|
|
||||||
const helmet = require('helmet');
|
|
||||||
const morgan = require('morgan');
|
|
||||||
const compression = require('compression');
|
|
||||||
const jwt = require('jsonwebtoken');
|
|
||||||
const { body, validationResult } = require('express-validator');
|
|
||||||
require('dotenv').config();
|
|
||||||
|
|
||||||
class WebPanel {
|
|
||||||
constructor(database) {
|
|
||||||
this.db = database;
|
|
||||||
this.app = express();
|
|
||||||
this.setupMiddleware();
|
|
||||||
this.setupRoutes();
|
|
||||||
}
|
|
||||||
|
|
||||||
setupMiddleware() {
|
|
||||||
// Bezpieczeństwo
|
|
||||||
this.app.use(helmet({
|
|
||||||
contentSecurityPolicy: {
|
|
||||||
directives: {
|
|
||||||
defaultSrc: ["'self'"],
|
|
||||||
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
|
||||||
scriptSrc: ["'self'"],
|
|
||||||
fontSrc: ["'self'", "https://fonts.gstatic.com"],
|
|
||||||
imgSrc: ["'self'", "data:", "https:"],
|
|
||||||
connectSrc: ["'self'", "https://discord.com", "https://discordapp.com"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// CORS
|
|
||||||
this.app.use(cors({
|
|
||||||
origin: process.env.NODE_ENV === 'production'
|
|
||||||
? ['https://your-domain.com']
|
|
||||||
: ['http://localhost:3001', 'http://127.0.0.1:3001', 'http://localhost:3000'],
|
|
||||||
credentials: true
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Middleware podstawowe
|
|
||||||
this.app.use(compression());
|
|
||||||
this.app.use(morgan('combined'));
|
|
||||||
this.app.use(express.json({ limit: '10mb' }));
|
|
||||||
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
||||||
|
|
||||||
// Serwowanie plików statycznych tylko w production
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
this.app.use(express.static('frontend/build'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupRoutes() {
|
|
||||||
// API Routes
|
|
||||||
this.app.use('/api/auth', this.createAuthRoutes());
|
|
||||||
this.app.use('/api/guilds', this.createGuildRoutes());
|
|
||||||
this.app.use('/api/messages', this.createMessageRoutes());
|
|
||||||
|
|
||||||
// Health check
|
|
||||||
this.app.get('/api/health', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
status: 'OK',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
version: '1.0.0'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Catch all handler dla React Router - tylko w production
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
this.app.get('*', (req, res) => {
|
|
||||||
res.sendFile('index.html', { root: 'frontend/build' });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error handler
|
|
||||||
this.app.use(this.errorHandler.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
createAuthRoutes() {
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
// Discord OAuth2 login
|
|
||||||
router.get('/discord', (req, res) => {
|
|
||||||
const discordAuthUrl = `https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT_ID}&redirect_uri=${encodeURIComponent(process.env.OAUTH2_REDIRECT_URI)}&response_type=code&scope=identify%20guilds`;
|
|
||||||
res.redirect(discordAuthUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Discord OAuth2 callback
|
|
||||||
router.get('/discord/callback', async (req, res) => {
|
|
||||||
const { code } = req.query;
|
|
||||||
|
|
||||||
if (!code) {
|
|
||||||
return res.status(400).json({ error: 'Brak kodu autoryzacji' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Wymiana kodu na token
|
|
||||||
const tokenResponse = await fetch('https://discord.com/api/oauth2/token', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
body: new URLSearchParams({
|
|
||||||
client_id: process.env.DISCORD_CLIENT_ID,
|
|
||||||
client_secret: process.env.DISCORD_CLIENT_SECRET,
|
|
||||||
code,
|
|
||||||
grant_type: 'authorization_code',
|
|
||||||
redirect_uri: process.env.OAUTH2_REDIRECT_URI,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const tokenData = await tokenResponse.json();
|
|
||||||
|
|
||||||
if (!tokenData.access_token) {
|
|
||||||
throw new Error('Nie udało się uzyskać tokenu dostępu');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pobierz dane użytkownika
|
|
||||||
const userResponse = await fetch('https://discord.com/api/users/@me', {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${tokenData.access_token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const userData = await userResponse.json();
|
|
||||||
|
|
||||||
// Pobierz serwery użytkownika
|
|
||||||
const guildsResponse = await fetch('https://discord.com/api/users/@me/guilds', {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${tokenData.access_token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const guildsData = await guildsResponse.json();
|
|
||||||
|
|
||||||
// Zapisz użytkownika w bazie
|
|
||||||
const user = await this.db.saveUser(userData);
|
|
||||||
|
|
||||||
// Generuj JWT token
|
|
||||||
const jwtToken = jwt.sign(
|
|
||||||
{
|
|
||||||
userId: user.id,
|
|
||||||
discordId: user.discord_id,
|
|
||||||
username: user.username,
|
|
||||||
role: user.role
|
|
||||||
},
|
|
||||||
process.env.JWT_SECRET,
|
|
||||||
{ expiresIn: '7d' }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Przekieruj z tokenem
|
|
||||||
res.redirect(`${process.env.NODE_ENV === 'production' ? 'https://your-domain.com' : 'http://localhost:3001'}/dashboard?token=${jwtToken}`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Błąd podczas autoryzacji Discord:', error);
|
|
||||||
res.status(500).json({ error: 'Błąd podczas autoryzacji' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wylogowanie
|
|
||||||
router.post('/logout', (req, res) => {
|
|
||||||
res.json({ message: 'Wylogowano pomyślnie' });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Weryfikacja tokenu
|
|
||||||
router.get('/verify', this.authenticateToken, (req, res) => {
|
|
||||||
res.json({ user: req.user });
|
|
||||||
});
|
|
||||||
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
|
|
||||||
createGuildRoutes() {
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
// Pobierz serwery użytkownika
|
|
||||||
router.get('/', this.authenticateToken, async (req, res) => {
|
|
||||||
try {
|
|
||||||
// Tu powinieneś pobrać serwery z Discord API i porównać z bazą
|
|
||||||
// Na razie zwróć mockowane dane
|
|
||||||
res.json([
|
|
||||||
{
|
|
||||||
id: '123456789',
|
|
||||||
name: 'Test Server',
|
|
||||||
icon: null,
|
|
||||||
hasBot: true,
|
|
||||||
userPermissions: ['MANAGE_CHANNELS']
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Błąd podczas pobierania serwerów:', error);
|
|
||||||
res.status(500).json({ error: 'Błąd podczas pobierania serwerów' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pobierz konfigurację serwera
|
|
||||||
router.get('/:guildId/config', this.authenticateToken, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { guildId } = req.params;
|
|
||||||
|
|
||||||
const welcomeChannel = await this.db.getWelcomeChannel(guildId);
|
|
||||||
const welcomeMessage = await this.db.getWelcomeMessage(guildId);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
welcomeChannel,
|
|
||||||
welcomeMessage
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Błąd podczas pobierania konfiguracji:', error);
|
|
||||||
res.status(500).json({ error: 'Błąd podczas pobierania konfiguracji' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
|
|
||||||
createMessageRoutes() {
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
// Pobierz wiadomość powitalną
|
|
||||||
router.get('/:guildId', this.authenticateToken, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { guildId } = req.params;
|
|
||||||
const message = await this.db.getWelcomeMessage(guildId);
|
|
||||||
|
|
||||||
res.json(message || {
|
|
||||||
content: this.getDefaultWelcomeMessage(),
|
|
||||||
embed_data: null
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Błąd podczas pobierania wiadomości:', error);
|
|
||||||
res.status(500).json({ error: 'Błąd podczas pobierania wiadomości' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Zapisz wiadomość powitalną
|
|
||||||
router.post('/:guildId',
|
|
||||||
this.authenticateToken,
|
|
||||||
[
|
|
||||||
body('content').notEmpty().withMessage('Treść wiadomości nie może być pusta'),
|
|
||||||
body('content').isLength({ max: 2000 }).withMessage('Treść wiadomości nie może przekraczać 2000 znaków')
|
|
||||||
],
|
|
||||||
async (req, res) => {
|
|
||||||
try {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
return res.status(400).json({ errors: errors.array() });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { guildId } = req.params;
|
|
||||||
const { content, embed_data } = req.body;
|
|
||||||
|
|
||||||
// Sprawdź czy wiadomość już istnieje
|
|
||||||
const existingMessage = await this.db.getWelcomeMessage(guildId);
|
|
||||||
let message;
|
|
||||||
|
|
||||||
if (existingMessage) {
|
|
||||||
// Zapisz rewizję przed aktualizacją
|
|
||||||
await this.db.saveMessageRevision(
|
|
||||||
existingMessage.id,
|
|
||||||
existingMessage.content,
|
|
||||||
existingMessage.embed_data,
|
|
||||||
req.user.userId
|
|
||||||
);
|
|
||||||
|
|
||||||
message = await this.db.updateWelcomeMessage(guildId, content, embed_data);
|
|
||||||
} else {
|
|
||||||
message = await this.db.saveWelcomeMessage(guildId, content, embed_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
message: 'Wiadomość została zapisana pomyślnie',
|
|
||||||
data: message
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Błąd podczas zapisywania wiadomości:', error);
|
|
||||||
res.status(500).json({ error: 'Błąd podczas zapisywania wiadomości' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Historia wersji wiadomości
|
|
||||||
router.get('/:guildId/revisions', this.authenticateToken, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { guildId } = req.params;
|
|
||||||
const message = await this.db.getWelcomeMessage(guildId);
|
|
||||||
|
|
||||||
if (!message) {
|
|
||||||
return res.json([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const revisions = await this.db.getMessageRevisions(message.id);
|
|
||||||
res.json(revisions);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Błąd podczas pobierania historii:', error);
|
|
||||||
res.status(500).json({ error: 'Błąd podczas pobierania historii' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Middleware autoryzacji
|
|
||||||
authenticateToken(req, res, next) {
|
|
||||||
const authHeader = req.headers['authorization'];
|
|
||||||
const token = authHeader && authHeader.split(' ')[1];
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return res.status(401).json({ error: 'Brak tokenu dostępu' });
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
|
|
||||||
if (err) {
|
|
||||||
return res.status(403).json({ error: 'Nieprawidłowy token' });
|
|
||||||
}
|
|
||||||
req.user = user;
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error handler
|
|
||||||
errorHandler(error, req, res, next) {
|
|
||||||
console.error('Błąd serwera:', error);
|
|
||||||
|
|
||||||
if (error.type === 'entity.parse.failed') {
|
|
||||||
return res.status(400).json({ error: 'Nieprawidłowy format JSON' });
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(500).json({
|
|
||||||
error: 'Wewnętrzny błąd serwera',
|
|
||||||
...(process.env.NODE_ENV === 'development' && { details: error.message })
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultWelcomeMessage() {
|
|
||||||
return `# 🎭 Witamy na naszym serwerze!
|
|
||||||
|
|
||||||
Miło Cię tutaj widzieć! 👋
|
|
||||||
|
|
||||||
## 📋 Najważniejsze informacje:
|
|
||||||
• Przeczytaj regulamin serwera
|
|
||||||
• Przedstaw się w odpowiednim kanale
|
|
||||||
• Baw się dobrze i szanuj innych członków
|
|
||||||
|
|
||||||
## 🎮 Funkcje serwera:
|
|
||||||
• Kanały tematyczne
|
|
||||||
• System ról
|
|
||||||
• Eventy i konkursy
|
|
||||||
|
|
||||||
---
|
|
||||||
*Ta wiadomość może być edytowana przez panel administracyjny*`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async start() {
|
|
||||||
const port = process.env.API_PORT || 3000;
|
|
||||||
|
|
||||||
this.server = this.app.listen(port, () => {
|
|
||||||
console.log(`🌐 Panel web uruchomiony na porcie ${port}`);
|
|
||||||
console.log(`📱 URL panelu: http://localhost:${port}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.server;
|
|
||||||
}
|
|
||||||
|
|
||||||
async stop() {
|
|
||||||
if (this.server) {
|
|
||||||
this.server.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { WebPanel };
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
const { DatabaseManager } = require('../backend/database/DatabaseManager');
|
|
||||||
require('dotenv').config();
|
|
||||||
|
|
||||||
async function migrate() {
|
|
||||||
console.log('🚀 Rozpoczynam migrację bazy danych...');
|
|
||||||
|
|
||||||
const db = new DatabaseManager();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await db.initialize();
|
|
||||||
console.log('✅ Migracja zakończona pomyślnie');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Błąd podczas migracji:', error);
|
|
||||||
process.exit(1);
|
|
||||||
} finally {
|
|
||||||
await db.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (require.main === module) {
|
|
||||||
migrate();
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { migrate };
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
const { DatabaseManager } = require('../backend/database/DatabaseManager');
|
|
||||||
require('dotenv').config();
|
|
||||||
|
|
||||||
async function seed() {
|
|
||||||
console.log('🌱 Rozpoczynam seedowanie bazy danych...');
|
|
||||||
|
|
||||||
const db = new DatabaseManager();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await db.initialize();
|
|
||||||
|
|
||||||
// Dodaj przykładowy serwer
|
|
||||||
await db.addGuild('123456789', 'Test Server');
|
|
||||||
console.log('✅ Dodano przykładowy serwer');
|
|
||||||
|
|
||||||
// Dodaj przykładowy kanał powitalny
|
|
||||||
await db.setWelcomeChannel('123456789', '987654321', 'witamy');
|
|
||||||
console.log('✅ Dodano przykładowy kanał powitalny');
|
|
||||||
|
|
||||||
// Dodaj przykładową wiadomość
|
|
||||||
const defaultMessage = `# 🎭 Witamy na naszym serwerze!
|
|
||||||
|
|
||||||
Miło Cię tutaj widzieć! 👋
|
|
||||||
|
|
||||||
## 📋 Najważniejsze informacje:
|
|
||||||
• Przeczytaj regulamin serwera
|
|
||||||
• Przedstaw się w odpowiednim kanale
|
|
||||||
• Baw się dobrze i szanuj innych członków
|
|
||||||
|
|
||||||
## 🎮 Funkcje serwera:
|
|
||||||
• Kanały tematyczne
|
|
||||||
• System ról
|
|
||||||
• Eventy i konkursy
|
|
||||||
|
|
||||||
---
|
|
||||||
*Ta wiadomość została wygenerowana przez Skrzynka Impostora Bot*`;
|
|
||||||
|
|
||||||
await db.saveWelcomeMessage('123456789', defaultMessage);
|
|
||||||
console.log('✅ Dodano przykładową wiadomość powitalną');
|
|
||||||
|
|
||||||
console.log('✅ Seedowanie zakończone pomyślnie');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Błąd podczas seedowania:', error);
|
|
||||||
process.exit(1);
|
|
||||||
} finally {
|
|
||||||
await db.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (require.main === module) {
|
|
||||||
seed();
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { seed };
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
# PostgreSQL Database dla development
|
|
||||||
postgres-dev:
|
|
||||||
image: postgres:15-alpine
|
|
||||||
container_name: skrzynka-postgres-dev
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: skrzynka_impostora_dev
|
|
||||||
POSTGRES_USER: dev_user
|
|
||||||
POSTGRES_PASSWORD: dev_password
|
|
||||||
PGDATA: /var/lib/postgresql/data/pgdata
|
|
||||||
volumes:
|
|
||||||
- postgres_dev_data:/var/lib/postgresql/data
|
|
||||||
- ./database/init:/docker-entrypoint-initdb.d
|
|
||||||
ports:
|
|
||||||
- "5433:5432" # Inny port żeby nie kolidować z lokalnym PostgreSQL
|
|
||||||
networks:
|
|
||||||
- skrzynka-dev-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U dev_user -d skrzynka_impostora_dev"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
# Redis dla development
|
|
||||||
redis-dev:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: skrzynka-redis-dev
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "6380:6379" # Inny port
|
|
||||||
volumes:
|
|
||||||
- redis_dev_data:/data
|
|
||||||
networks:
|
|
||||||
- skrzynka-dev-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
# Bot w trybie development z hot reload
|
|
||||||
bot-dev:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile.dev
|
|
||||||
container_name: skrzynka-bot-dev
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
# Database
|
|
||||||
DATABASE_URL: postgresql://dev_user:dev_password@postgres-dev:5432/skrzynka_impostora_dev
|
|
||||||
DB_HOST: postgres-dev
|
|
||||||
DB_PORT: 5432
|
|
||||||
DB_NAME: skrzynka_impostora_dev
|
|
||||||
DB_USER: dev_user
|
|
||||||
DB_PASSWORD: dev_password
|
|
||||||
|
|
||||||
# Discord Bot
|
|
||||||
DISCORD_TOKEN: ${DISCORD_TOKEN}
|
|
||||||
DISCORD_CLIENT_ID: ${DISCORD_CLIENT_ID}
|
|
||||||
DISCORD_CLIENT_SECRET: ${DISCORD_CLIENT_SECRET}
|
|
||||||
|
|
||||||
# Web Panel
|
|
||||||
JWT_SECRET: dev_jwt_secret_key
|
|
||||||
SESSION_SECRET: dev_session_secret_key
|
|
||||||
WEB_PORT: 3001
|
|
||||||
API_PORT: 3000
|
|
||||||
|
|
||||||
# OAuth2
|
|
||||||
OAUTH2_REDIRECT_URI: http://localhost:3001/auth/discord/callback # Environment
|
|
||||||
NODE_ENV: development
|
|
||||||
LOG_LEVEL: debug
|
|
||||||
|
|
||||||
# Redis
|
|
||||||
REDIS_URL: redis://redis-dev:6379
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
- "3001:3001"
|
|
||||||
- "9229:9229" # Node.js debugger port
|
|
||||||
depends_on:
|
|
||||||
postgres-dev:
|
|
||||||
condition: service_healthy
|
|
||||||
redis-dev:
|
|
||||||
condition: service_healthy
|
|
||||||
networks:
|
|
||||||
- skrzynka-dev-network
|
|
||||||
volumes:
|
|
||||||
- .:/app
|
|
||||||
- /app/node_modules
|
|
||||||
- /app/frontend/node_modules
|
|
||||||
- ./logs:/app/logs
|
|
||||||
command: npm run dev
|
|
||||||
|
|
||||||
# pgAdmin dla zarządzania bazą danych
|
|
||||||
pgadmin:
|
|
||||||
image: dpage/pgadmin4:latest
|
|
||||||
container_name: skrzynka-pgadmin
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
PGADMIN_DEFAULT_EMAIL: admin@skrzynka.local
|
|
||||||
PGADMIN_DEFAULT_PASSWORD: admin
|
|
||||||
PGADMIN_CONFIG_SERVER_MODE: 'False'
|
|
||||||
ports:
|
|
||||||
- "8080:80"
|
|
||||||
depends_on:
|
|
||||||
- postgres-dev
|
|
||||||
networks:
|
|
||||||
- skrzynka-dev-network
|
|
||||||
volumes:
|
|
||||||
- pgadmin_data:/var/lib/pgadmin
|
|
||||||
|
|
||||||
# Volumes
|
|
||||||
volumes:
|
|
||||||
postgres_dev_data:
|
|
||||||
driver: local
|
|
||||||
redis_dev_data:
|
|
||||||
driver: local
|
|
||||||
pgadmin_data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
# Networks
|
|
||||||
networks:
|
|
||||||
skrzynka-dev-network:
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
# PostgreSQL Database
|
|
||||||
postgres:
|
|
||||||
image: postgres:15-alpine
|
|
||||||
container_name: skrzynka-postgres
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: skrzynka_impostora
|
|
||||||
POSTGRES_USER: skrzynka_user
|
|
||||||
POSTGRES_PASSWORD: skrzynka_password
|
|
||||||
PGDATA: /var/lib/postgresql/data/pgdata
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
- ./database/init:/docker-entrypoint-initdb.d
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
networks:
|
|
||||||
- skrzynka-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U skrzynka_user -d skrzynka_impostora"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
# Redis for caching (optional)
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: skrzynka-redis
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
networks:
|
|
||||||
- skrzynka-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
# Discord Bot Application
|
|
||||||
bot:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: skrzynka-bot
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
# Database
|
|
||||||
DATABASE_URL: postgresql://skrzynka_user:skrzynka_password@postgres:5432/skrzynka_impostora
|
|
||||||
DB_HOST: postgres
|
|
||||||
DB_PORT: 5432
|
|
||||||
DB_NAME: skrzynka_impostora
|
|
||||||
DB_USER: skrzynka_user
|
|
||||||
DB_PASSWORD: skrzynka_password
|
|
||||||
|
|
||||||
# Discord Bot (należy ustawić w .env lub przez docker secrets)
|
|
||||||
DISCORD_TOKEN: ${DISCORD_TOKEN}
|
|
||||||
DISCORD_CLIENT_ID: ${DISCORD_CLIENT_ID}
|
|
||||||
DISCORD_CLIENT_SECRET: ${DISCORD_CLIENT_SECRET}
|
|
||||||
|
|
||||||
# Web Panel
|
|
||||||
JWT_SECRET: ${JWT_SECRET:-super_secret_jwt_key_change_in_production}
|
|
||||||
SESSION_SECRET: ${SESSION_SECRET:-super_secret_session_key_change_in_production}
|
|
||||||
WEB_PORT: 3001
|
|
||||||
API_PORT: 3000
|
|
||||||
|
|
||||||
# OAuth2
|
|
||||||
OAUTH2_REDIRECT_URI: ${OAUTH2_REDIRECT_URI:-http://localhost:3001/auth/discord/callback}
|
|
||||||
|
|
||||||
# Environment
|
|
||||||
NODE_ENV: ${NODE_ENV:-production}
|
|
||||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
|
||||||
|
|
||||||
# Redis (optional)
|
|
||||||
REDIS_URL: redis://redis:6379
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
- "3001:3001"
|
|
||||||
depends_on:
|
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
redis:
|
|
||||||
condition: service_healthy
|
|
||||||
networks:
|
|
||||||
- skrzynka-network
|
|
||||||
volumes:
|
|
||||||
- ./logs:/app/logs
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
|
|
||||||
# Nginx Reverse Proxy (production)
|
|
||||||
nginx:
|
|
||||||
image: nginx:alpine
|
|
||||||
container_name: skrzynka-nginx
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
- "443:443"
|
|
||||||
volumes:
|
|
||||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
|
||||||
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
|
||||||
- ./ssl:/etc/nginx/ssl:ro
|
|
||||||
- nginx_logs:/var/log/nginx
|
|
||||||
depends_on:
|
|
||||||
- bot
|
|
||||||
networks:
|
|
||||||
- skrzynka-network
|
|
||||||
profiles:
|
|
||||||
- production
|
|
||||||
|
|
||||||
# Volumes
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
driver: local
|
|
||||||
redis_data:
|
|
||||||
driver: local
|
|
||||||
nginx_logs:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
# Networks
|
|
||||||
networks:
|
|
||||||
skrzynka-network:
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "skrzynka-impostora-panel",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"private": true,
|
|
||||||
"homepage": ".",
|
|
||||||
"dependencies": {
|
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
|
||||||
"@testing-library/react": "^13.4.0",
|
|
||||||
"@testing-library/user-event": "^13.5.0",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-router-dom": "^6.8.1",
|
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"axios": "^1.3.4",
|
|
||||||
"react-markdown": "^8.0.5",
|
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
|
||||||
"@monaco-editor/react": "^4.4.6",
|
|
||||||
"lucide-react": "^0.321.0",
|
|
||||||
"clsx": "^1.2.1"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"start": "react-scripts start",
|
|
||||||
"build": "react-scripts build",
|
|
||||||
"test": "react-scripts test",
|
|
||||||
"eject": "react-scripts eject"
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"proxy": "http://localhost:3000"
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="pl">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#5865f2" />
|
|
||||||
<meta name="description" content="Panel zarządzania bota Discord - Skrzynka Impostora" />
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<title>Skrzynka Impostora - Panel Administracyjny</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>Musisz włączyć JavaScript aby korzystać z tej aplikacji.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"short_name": "Skrzynka Impostora",
|
|
||||||
"name": "Skrzynka Impostora Bot Panel",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "favicon.ico",
|
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
|
||||||
"type": "image/x-icon"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": ".",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#5865f2",
|
|
||||||
"background_color": "#ffffff"
|
|
||||||
}
|
|
||||||
@@ -1,708 +0,0 @@
|
|||||||
/* Discord-inspired CSS for Skrzynka Impostora Bot Panel */
|
|
||||||
|
|
||||||
/* CSS Variables */
|
|
||||||
:root {
|
|
||||||
--discord-blue: #5865f2;
|
|
||||||
--discord-green: #57f287;
|
|
||||||
--discord-red: #ed4245;
|
|
||||||
--discord-yellow: #fee75c;
|
|
||||||
--discord-purple: #eb459e;
|
|
||||||
|
|
||||||
--discord-dark: #2c2f33;
|
|
||||||
--discord-darker: #23272a;
|
|
||||||
--discord-light: #36393f;
|
|
||||||
--discord-lighter: #40444b;
|
|
||||||
|
|
||||||
--text-primary: #ffffff;
|
|
||||||
--text-secondary: #b9bbbe;
|
|
||||||
--text-muted: #72767d;
|
|
||||||
|
|
||||||
--background-primary: #36393f;
|
|
||||||
--background-secondary: #2f3136;
|
|
||||||
--background-tertiary: #202225;
|
|
||||||
|
|
||||||
--border-color: #202225;
|
|
||||||
--hover-color: rgba(79, 84, 92, 0.16);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Base styles */
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
background-color: var(--background-primary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Layout */
|
|
||||||
.flex {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.min-h-screen {
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sidebar */
|
|
||||||
.sidebar {
|
|
||||||
width: 260px;
|
|
||||||
min-height: 100vh;
|
|
||||||
background-color: var(--background-secondary);
|
|
||||||
padding: 1.5rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar h2 {
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 1.25rem;
|
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar p {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
margin: 0 0 2rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation */
|
|
||||||
.nav-link {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link:hover {
|
|
||||||
background-color: var(--hover-color);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link.active {
|
|
||||||
background-color: var(--discord-blue);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main content */
|
|
||||||
.main-content {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 260px;
|
|
||||||
padding: 2rem;
|
|
||||||
background-color: var(--background-primary);
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cards */
|
|
||||||
.discord-card {
|
|
||||||
background-color: var(--background-secondary);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.guild-card {
|
|
||||||
background-color: var(--background-secondary);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1.5rem;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.guild-card:hover {
|
|
||||||
border-color: var(--discord-blue);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Guild icon */
|
|
||||||
.guild-icon {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: var(--discord-blue);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: white;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-right: 1rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.discord-button {
|
|
||||||
background-color: var(--discord-blue);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
text-decoration: none;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-button:hover {
|
|
||||||
background-color: #4752c4;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-button:disabled {
|
|
||||||
background-color: var(--text-muted);
|
|
||||||
cursor: not-allowed;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-button.secondary {
|
|
||||||
background-color: var(--background-lighter);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-button.secondary:hover {
|
|
||||||
background-color: var(--hover-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-button.danger {
|
|
||||||
background-color: var(--discord-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-button.danger:hover {
|
|
||||||
background-color: #c73e41;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-button.success {
|
|
||||||
background-color: var(--discord-green);
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-button.success:hover {
|
|
||||||
background-color: #4ac486;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Forms */
|
|
||||||
.discord-textarea {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 200px;
|
|
||||||
background-color: var(--background-tertiary);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 0.75rem;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--discord-blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-textarea::placeholder {
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Preview area */
|
|
||||||
.preview-area {
|
|
||||||
background-color: var(--background-tertiary);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 1rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-area h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0 0 1rem 0;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-area h2 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0 0 0.75rem 0;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-area h3 {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-area p {
|
|
||||||
margin: 0 0 1rem 0;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-area strong {
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-area em {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-area code {
|
|
||||||
background-color: var(--background-secondary);
|
|
||||||
color: var(--discord-yellow);
|
|
||||||
padding: 0.125rem 0.25rem;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
||||||
font-size: 0.8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-area pre {
|
|
||||||
background-color: var(--background-secondary);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 0.75rem;
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-area pre code {
|
|
||||||
background: none;
|
|
||||||
color: var(--discord-green);
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Grid system */
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cols-1 {
|
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cols-2 {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cols-3 {
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cols-4 {
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap-4 {
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap-6 {
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Spacing */
|
|
||||||
.mb-2 { margin-bottom: 0.5rem; }
|
|
||||||
.mb-4 { margin-bottom: 1rem; }
|
|
||||||
.mb-6 { margin-bottom: 1.5rem; }
|
|
||||||
.mb-8 { margin-bottom: 2rem; }
|
|
||||||
.mt-4 { margin-top: 1rem; }
|
|
||||||
.mt-6 { margin-top: 1.5rem; }
|
|
||||||
.mt-8 { margin-top: 2rem; }
|
|
||||||
.mr-2 { margin-right: 0.5rem; }
|
|
||||||
.mr-3 { margin-right: 0.75rem; }
|
|
||||||
.mr-4 { margin-right: 1rem; }
|
|
||||||
.ml-4 { margin-left: 1rem; }
|
|
||||||
|
|
||||||
/* Text utilities */
|
|
||||||
.text-center { text-align: center; }
|
|
||||||
.text-sm { font-size: 0.875rem; }
|
|
||||||
.text-lg { font-size: 1.125rem; }
|
|
||||||
.text-xl { font-size: 1.25rem; }
|
|
||||||
.text-2xl { font-size: 1.5rem; }
|
|
||||||
.text-3xl { font-size: 1.875rem; }
|
|
||||||
|
|
||||||
.font-medium { font-weight: 500; }
|
|
||||||
.font-semibold { font-weight: 600; }
|
|
||||||
.font-bold { font-weight: 700; }
|
|
||||||
|
|
||||||
.text-gray-900 { color: var(--text-primary); }
|
|
||||||
.text-gray-600 { color: var(--text-secondary); }
|
|
||||||
.text-gray-500 { color: var(--text-muted); }
|
|
||||||
|
|
||||||
/* Status indicators */
|
|
||||||
.loading {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 2rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
background-color: rgba(237, 66, 69, 0.1);
|
|
||||||
border: 1px solid var(--discord-red);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 1rem;
|
|
||||||
color: var(--discord-red);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success {
|
|
||||||
background-color: rgba(87, 242, 135, 0.1);
|
|
||||||
border: 1px solid var(--discord-green);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 1rem;
|
|
||||||
color: var(--discord-green);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.sidebar {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cols-2,
|
|
||||||
.grid-cols-3,
|
|
||||||
.grid-cols-4 {
|
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.grid-cols-4 {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animations */
|
|
||||||
@keyframes fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-card,
|
|
||||||
.guild-card {
|
|
||||||
animation: fade-in 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom scrollbar */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: var(--background-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--text-muted);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
|
||||||
monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App {
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Discord-like styling */
|
|
||||||
.discord-card {
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-button {
|
|
||||||
background: #5865f2;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 10px 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-button:hover {
|
|
||||||
background: #4752c4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-button:disabled {
|
|
||||||
background: #a5a5a5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-button.secondary {
|
|
||||||
background: #4f545c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-button.secondary:hover {
|
|
||||||
background: #5d6269;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-button.danger {
|
|
||||||
background: #ed4245;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-button.danger:hover {
|
|
||||||
background: #c03537;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-input {
|
|
||||||
background: #f2f3f5;
|
|
||||||
border: 1px solid #e3e5e8;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 10px;
|
|
||||||
font-size: 16px;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #5865f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-textarea {
|
|
||||||
background: #f2f3f5;
|
|
||||||
border: 1px solid #e3e5e8;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 200px;
|
|
||||||
resize: vertical;
|
|
||||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #5865f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
width: 250px;
|
|
||||||
background: #2f3136;
|
|
||||||
color: white;
|
|
||||||
height: 100vh;
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
padding: 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
margin-left: 250px;
|
|
||||||
padding: 20px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
color: #b9bbbe;
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link:hover {
|
|
||||||
background: #393c43;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link.active {
|
|
||||||
background: #5865f2;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-area {
|
|
||||||
background: #36393f;
|
|
||||||
color: white;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
min-height: 200px;
|
|
||||||
font-family: 'Whitney', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-area h1, .preview-area h2, .preview-area h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 200px;
|
|
||||||
font-size: 18px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
background: #fef2f2;
|
|
||||||
border: 1px solid #fecaca;
|
|
||||||
color: #dc2626;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success {
|
|
||||||
background: #f0f9ff;
|
|
||||||
border: 1px solid #bae6fd;
|
|
||||||
color: #0369a1;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.guild-card {
|
|
||||||
background: white;
|
|
||||||
border: 1px solid #e3e5e8;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.guild-card:hover {
|
|
||||||
border-color: #5865f2;
|
|
||||||
box-shadow: 0 2px 8px rgba(88, 101, 242, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.guild-icon {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #5865f2;
|
|
||||||
color: white;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 18px;
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.items-center {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.justify-between {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-sm {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-gray-500 {
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-green-600 {
|
|
||||||
color: #059669;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-red-600 {
|
|
||||||
color: #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-medium {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-bold {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-2 {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-4 {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mt-4 {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
height: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-panel {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-panel h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
color: #4f545c;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.sidebar {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-container {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
|
||||||
import { AuthProvider } from './hooks/useAuth';
|
|
||||||
import Layout from './components/Layout';
|
|
||||||
import Login from './pages/Login';
|
|
||||||
import Dashboard from './pages/Dashboard';
|
|
||||||
import MessageEditor from './pages/MessageEditor';
|
|
||||||
import ServerSelect from './pages/ServerSelect';
|
|
||||||
import './App.css';
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return (
|
|
||||||
<AuthProvider>
|
|
||||||
<Router>
|
|
||||||
<div className="App">
|
|
||||||
<Routes>
|
|
||||||
<Route path="/login" element={<Login />} />
|
|
||||||
<Route path="/" element={<Layout />}>
|
|
||||||
<Route index element={<Navigate to="/dashboard" replace />} />
|
|
||||||
<Route path="dashboard" element={<Dashboard />} />
|
|
||||||
<Route path="servers" element={<ServerSelect />} />
|
|
||||||
<Route path="servers/:guildId/messages" element={<MessageEditor />} />
|
|
||||||
</Route>
|
|
||||||
</Routes>
|
|
||||||
</div>
|
|
||||||
</Router>
|
|
||||||
</AuthProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
|
|
||||||
import { useAuth } from '../hooks/useAuth';
|
|
||||||
import { LogOut, Home, Server, Settings } from 'lucide-react';
|
|
||||||
|
|
||||||
function Layout() {
|
|
||||||
const { user, logout, isAuthenticated } = useAuth();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
navigate('/login');
|
|
||||||
}
|
|
||||||
}, [isAuthenticated, navigate]);
|
|
||||||
|
|
||||||
const handleLogout = () => {
|
|
||||||
logout();
|
|
||||||
navigate('/login');
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex">
|
|
||||||
<aside className="sidebar">
|
|
||||||
<div className="mb-4">
|
|
||||||
<h2 className="text-xl font-bold text-white mb-2">🎭 Skrzynka Impostora</h2>
|
|
||||||
<p className="text-gray-300 text-sm">Panel administratora</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav className="mb-8">
|
|
||||||
<NavLink to="/dashboard" className="nav-link">
|
|
||||||
<Home size={16} className="inline mr-2" />
|
|
||||||
Dashboard
|
|
||||||
</NavLink>
|
|
||||||
<NavLink to="/servers" className="nav-link">
|
|
||||||
<Server size={16} className="inline mr-2" />
|
|
||||||
Serwery
|
|
||||||
</NavLink>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div className="mt-auto">
|
|
||||||
<div className="border-t border-gray-600 pt-4">
|
|
||||||
<div className="text-sm text-gray-300 mb-2">
|
|
||||||
Zalogowany jako:
|
|
||||||
</div>
|
|
||||||
<div className="text-white font-medium mb-4">
|
|
||||||
{user?.username}#{user?.discriminator}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={handleLogout}
|
|
||||||
className="discord-button secondary w-full flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<LogOut size={16} className="mr-2" />
|
|
||||||
Wyloguj
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<main className="main-content">
|
|
||||||
<Outlet />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Layout;
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
|
||||||
import { api } from '../services/api';
|
|
||||||
|
|
||||||
const AuthContext = createContext();
|
|
||||||
|
|
||||||
export function useAuth() {
|
|
||||||
const context = useContext(AuthContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useAuth must be used within an AuthProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AuthProvider({ children }) {
|
|
||||||
const [user, setUser] = useState(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [token, setToken] = useState(localStorage.getItem('auth_token'));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (token) {
|
|
||||||
api.setAuthToken(token);
|
|
||||||
verifyToken();
|
|
||||||
} else {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [token]);
|
|
||||||
|
|
||||||
const verifyToken = async () => {
|
|
||||||
try {
|
|
||||||
const response = await api.verifyToken();
|
|
||||||
setUser(response.data.user);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Token verification failed:', error);
|
|
||||||
logout();
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const login = (authToken) => {
|
|
||||||
setToken(authToken);
|
|
||||||
localStorage.setItem('auth_token', authToken);
|
|
||||||
api.setAuthToken(authToken);
|
|
||||||
};
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
setToken(null);
|
|
||||||
setUser(null);
|
|
||||||
localStorage.removeItem('auth_token');
|
|
||||||
api.setAuthToken(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const value = {
|
|
||||||
user,
|
|
||||||
token,
|
|
||||||
loading,
|
|
||||||
login,
|
|
||||||
logout,
|
|
||||||
isAuthenticated: !!user
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AuthContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</AuthContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
|
||||||
monospace;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom/client';
|
|
||||||
import './index.css';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
||||||
root.render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { api } from '../services/api';
|
|
||||||
import { Server, MessageSquare, Users, Activity } from 'lucide-react';
|
|
||||||
|
|
||||||
function Dashboard() {
|
|
||||||
const [stats, setStats] = useState({
|
|
||||||
totalServers: 0,
|
|
||||||
totalMessages: 0,
|
|
||||||
totalUsers: 0,
|
|
||||||
botStatus: 'online'
|
|
||||||
});
|
|
||||||
const [recentActivity, setRecentActivity] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadDashboardData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadDashboardData = async () => {
|
|
||||||
try {
|
|
||||||
// Mock data - w prawdziwej aplikacji pobierz z API
|
|
||||||
setStats({
|
|
||||||
totalServers: 5,
|
|
||||||
totalMessages: 23,
|
|
||||||
totalUsers: 1,
|
|
||||||
botStatus: 'online'
|
|
||||||
});
|
|
||||||
|
|
||||||
setRecentActivity([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
type: 'message_updated',
|
|
||||||
server: 'Test Server',
|
|
||||||
timestamp: new Date(),
|
|
||||||
description: 'Zaktualizowano wiadomość powitalną'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
type: 'channel_configured',
|
|
||||||
server: 'Test Server',
|
|
||||||
timestamp: new Date(Date.now() - 3600000),
|
|
||||||
description: 'Skonfigurowano kanał #witamy'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading dashboard data:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <div className="loading">Ładowanie dashboard...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="mb-8">
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Dashboard</h1>
|
|
||||||
<p className="text-gray-600">
|
|
||||||
Przegląd stanu bota i ostatnich aktywności
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stats Cards */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
|
||||||
<div className="discord-card">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="p-3 bg-blue-100 rounded-lg mr-4">
|
|
||||||
<Server className="text-blue-600" size={24} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600">Serwery</p>
|
|
||||||
<p className="text-2xl font-bold text-gray-900">{stats.totalServers}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="discord-card">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="p-3 bg-green-100 rounded-lg mr-4">
|
|
||||||
<MessageSquare className="text-green-600" size={24} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600">Wiadomości</p>
|
|
||||||
<p className="text-2xl font-bold text-gray-900">{stats.totalMessages}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="discord-card">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="p-3 bg-purple-100 rounded-lg mr-4">
|
|
||||||
<Users className="text-purple-600" size={24} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600">Użytkownicy</p>
|
|
||||||
<p className="text-2xl font-bold text-gray-900">{stats.totalUsers}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="discord-card">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="p-3 bg-yellow-100 rounded-lg mr-4">
|
|
||||||
<Activity className="text-yellow-600" size={24} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600">Status Bota</p>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className={`w-3 h-3 rounded-full mr-2 ${
|
|
||||||
stats.botStatus === 'online' ? 'bg-green-500' : 'bg-red-500'
|
|
||||||
}`}></div>
|
|
||||||
<p className="text-lg font-semibold text-gray-900 capitalize">
|
|
||||||
{stats.botStatus === 'online' ? 'Online' : 'Offline'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
{/* Quick Actions */}
|
|
||||||
<div className="discord-card">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Szybkie akcje</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Link
|
|
||||||
to="/servers"
|
|
||||||
className="block p-3 border border-gray-200 rounded-lg hover:border-blue-300 hover:bg-blue-50 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Server className="text-blue-600 mr-3" size={20} />
|
|
||||||
<div>
|
|
||||||
<p className="font-medium text-gray-900">Zarządzaj serwerami</p>
|
|
||||||
<p className="text-sm text-gray-600">Konfiguruj kanały i wiadomości</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className="block p-3 border border-gray-200 rounded-lg bg-gray-50">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<MessageSquare className="text-gray-400 mr-3" size={20} />
|
|
||||||
<div>
|
|
||||||
<p className="font-medium text-gray-500">Szablony wiadomości</p>
|
|
||||||
<p className="text-sm text-gray-400">Wkrótce dostępne</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="block p-3 border border-gray-200 rounded-lg bg-gray-50">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Activity className="text-gray-400 mr-3" size={20} />
|
|
||||||
<div>
|
|
||||||
<p className="font-medium text-gray-500">Statystyki</p>
|
|
||||||
<p className="text-sm text-gray-400">Wkrótce dostępne</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Recent Activity */}
|
|
||||||
<div className="discord-card">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Ostatnia aktywność</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{recentActivity.length > 0 ? (
|
|
||||||
recentActivity.map((activity) => (
|
|
||||||
<div key={activity.id} className="flex items-start p-3 bg-gray-50 rounded-lg">
|
|
||||||
<div className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3"></div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-sm font-medium text-gray-900">
|
|
||||||
{activity.description}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-600">
|
|
||||||
{activity.server} • {activity.timestamp.toLocaleString('pl-PL')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<p className="text-gray-500 text-center py-4">
|
|
||||||
Brak ostatnich aktywności
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bot Info */}
|
|
||||||
<div className="discord-card mt-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Informacje o bocie</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600">Wersja</p>
|
|
||||||
<p className="font-medium">1.0.0</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600">Czas działania</p>
|
|
||||||
<p className="font-medium">99.5%</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-600">Ostatnia aktualizacja</p>
|
|
||||||
<p className="font-medium">Dzisiaj</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Dashboard;
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
import { useAuth } from '../hooks/useAuth';
|
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
function Login() {
|
|
||||||
const { login, isAuthenticated } = useAuth();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Sprawdź czy w URL jest token (po przekierowaniu z OAuth)
|
|
||||||
const token = searchParams.get('token');
|
|
||||||
if (token) {
|
|
||||||
login(token);
|
|
||||||
navigate('/dashboard');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jeśli już zalogowany, przekieruj na dashboard
|
|
||||||
if (isAuthenticated) {
|
|
||||||
navigate('/dashboard');
|
|
||||||
}
|
|
||||||
}, [searchParams, login, navigate, isAuthenticated]);
|
|
||||||
|
|
||||||
const handleDiscordLogin = () => {
|
|
||||||
const apiUrl = process.env.NODE_ENV === 'production'
|
|
||||||
? '/api/auth/discord'
|
|
||||||
: 'http://localhost:3000/api/auth/discord';
|
|
||||||
|
|
||||||
window.location.href = apiUrl;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
|
||||||
<div className="max-w-md w-full space-y-8">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-6xl mb-4">🎭</div>
|
|
||||||
<h2 className="text-3xl font-bold text-gray-900">
|
|
||||||
Skrzynka Impostora Bot
|
|
||||||
</h2>
|
|
||||||
<p className="mt-2 text-gray-600">
|
|
||||||
Panel zarządzania wiadomościami powaitalnymi
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="discord-card">
|
|
||||||
<div className="text-center">
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
|
||||||
Zaloguj się za pomocą Discord
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-600 mb-6">
|
|
||||||
Aby zarządzać wiadomościami powaitalnymi, musisz się zalogować przez Discord.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleDiscordLogin}
|
|
||||||
className="discord-button w-full flex items-center justify-center text-lg py-3"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="w-6 h-6 mr-3"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.010c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/>
|
|
||||||
</svg>
|
|
||||||
Zaloguj przez Discord
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center text-sm text-gray-500">
|
|
||||||
<p>
|
|
||||||
Potrzebujesz uprawnień administratora na serwerze Discord,
|
|
||||||
aby zarządzać wiadomościami powaitalnymi.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Login;
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
|
||||||
import { api } from '../services/api';
|
|
||||||
import { Save, Eye, History, ArrowLeft, RefreshCw } from 'lucide-react';
|
|
||||||
import ReactMarkdown from 'react-markdown';
|
|
||||||
|
|
||||||
function MessageEditor() {
|
|
||||||
const { guildId } = useParams();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [message, setMessage] = useState({
|
|
||||||
content: '',
|
|
||||||
embed_data: null
|
|
||||||
});
|
|
||||||
const [originalMessage, setOriginalMessage] = useState(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
const [success, setSuccess] = useState(null);
|
|
||||||
const [previewMode, setPreviewMode] = useState('discord');
|
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadMessage();
|
|
||||||
}, [guildId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (originalMessage) {
|
|
||||||
setIsDirty(message.content !== originalMessage.content);
|
|
||||||
}
|
|
||||||
}, [message.content, originalMessage]);
|
|
||||||
|
|
||||||
const loadMessage = async () => {
|
|
||||||
try {
|
|
||||||
const response = await api.getMessage(guildId);
|
|
||||||
const loadedMessage = response.data;
|
|
||||||
|
|
||||||
setMessage(loadedMessage);
|
|
||||||
setOriginalMessage(loadedMessage);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading message:', error);
|
|
||||||
setError('Nie udało się załadować wiadomości');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveMessage = async () => {
|
|
||||||
if (!message.content.trim()) {
|
|
||||||
setError('Treść wiadomości nie może być pusta');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.content.length > 2000) {
|
|
||||||
setError('Treść wiadomości nie może przekraczać 2000 znaków');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSaving(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.saveMessage(guildId, message.content, message.embed_data);
|
|
||||||
setSuccess('Wiadomość została zapisana pomyślnie');
|
|
||||||
setOriginalMessage({ ...message });
|
|
||||||
setIsDirty(false);
|
|
||||||
|
|
||||||
// Ukryj komunikat po 3 sekundach
|
|
||||||
setTimeout(() => setSuccess(null), 3000);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving message:', error);
|
|
||||||
setError('Nie udało się zapisać wiadomości');
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetMessage = () => {
|
|
||||||
if (originalMessage) {
|
|
||||||
setMessage({ ...originalMessage });
|
|
||||||
setIsDirty(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderDiscordPreview = (content) => {
|
|
||||||
// Podstawowa konwersja Discord Markdown do HTML
|
|
||||||
let html = content
|
|
||||||
// Headers
|
|
||||||
.replace(/^# (.*$)/gim, '<h1 class="text-xl font-bold mb-2">$1</h1>')
|
|
||||||
.replace(/^## (.*$)/gim, '<h2 class="text-lg font-bold mb-2">$1</h2>')
|
|
||||||
.replace(/^### (.*$)/gim, '<h3 class="text-base font-bold mb-1">$1</h3>')
|
|
||||||
// Bold
|
|
||||||
.replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>')
|
|
||||||
// Italic
|
|
||||||
.replace(/\*(.*?)\*/gim, '<em>$1</em>')
|
|
||||||
// Underline
|
|
||||||
.replace(/__(.*?)__/gim, '<u>$1</u>')
|
|
||||||
// Strikethrough
|
|
||||||
.replace(/~~(.*?)~~/gim, '<del>$1</del>')
|
|
||||||
// Code blocks
|
|
||||||
.replace(/```([\s\S]*?)```/gim, '<pre class="bg-gray-800 text-green-400 p-2 rounded text-sm my-2"><code>$1</code></pre>')
|
|
||||||
// Inline code
|
|
||||||
.replace(/`(.*?)`/gim, '<code class="bg-gray-700 text-gray-200 px-1 rounded text-sm">$1</code>')
|
|
||||||
// Lists
|
|
||||||
.replace(/^\• (.*$)/gim, '<li class="ml-4">• $1</li>')
|
|
||||||
// Line breaks
|
|
||||||
.replace(/\n/gim, '<br>');
|
|
||||||
|
|
||||||
return { __html: html };
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <div className="loading">Ładowanie edytora wiadomości...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center mb-6">
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/servers')}
|
|
||||||
className="discord-button secondary mr-4 flex items-center"
|
|
||||||
>
|
|
||||||
<ArrowLeft size={16} className="mr-2" />
|
|
||||||
Powrót do serwerów
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Edytor wiadomości</h1>
|
|
||||||
<p className="text-gray-600">
|
|
||||||
Edytuj wiadomość powitalną dla serwera
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="error">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{success && (
|
|
||||||
<div className="success">
|
|
||||||
{success}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
{/* Editor Panel */}
|
|
||||||
<div className="discord-card">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h3 className="text-lg font-semibold">Edytor</h3>
|
|
||||||
<div className="flex items-center space-x-2 text-sm text-gray-600">
|
|
||||||
<span>{message.content.length}/2000</span>
|
|
||||||
{isDirty && (
|
|
||||||
<span className="text-orange-600 font-medium">
|
|
||||||
• Niezapisane zmiany
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<textarea
|
|
||||||
value={message.content}
|
|
||||||
onChange={(e) => setMessage({ ...message, content: e.target.value })}
|
|
||||||
className="discord-textarea"
|
|
||||||
placeholder="Wprowadź treść wiadomości powitalnej...
|
|
||||||
|
|
||||||
Możesz używać Discord Markdown:
|
|
||||||
# Nagłówek 1
|
|
||||||
## Nagłówek 2
|
|
||||||
**Pogrubienie**
|
|
||||||
*Kursywa*
|
|
||||||
__Podkreślenie__
|
|
||||||
~~Przekreślenie~~
|
|
||||||
`kod`
|
|
||||||
```
|
|
||||||
blok kodu
|
|
||||||
```
|
|
||||||
• Lista
|
|
||||||
• Punktowana"
|
|
||||||
style={{ height: '400px' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between mt-4">
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={resetMessage}
|
|
||||||
disabled={!isDirty}
|
|
||||||
className="discord-button secondary flex items-center"
|
|
||||||
>
|
|
||||||
<RefreshCw size={16} className="mr-2" />
|
|
||||||
Resetuj
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={saveMessage}
|
|
||||||
disabled={saving || !message.content.trim() || !isDirty}
|
|
||||||
className="discord-button flex items-center"
|
|
||||||
>
|
|
||||||
<Save size={16} className="mr-2" />
|
|
||||||
{saving ? 'Zapisywanie...' : 'Zapisz'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Preview Panel */}
|
|
||||||
<div className="discord-card">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h3 className="text-lg font-semibold">Podgląd</h3>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={() => setPreviewMode('discord')}
|
|
||||||
className={`px-3 py-1 text-sm rounded ${
|
|
||||||
previewMode === 'discord'
|
|
||||||
? 'bg-blue-100 text-blue-700'
|
|
||||||
: 'bg-gray-100 text-gray-600'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Discord
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setPreviewMode('markdown')}
|
|
||||||
className={`px-3 py-1 text-sm rounded ${
|
|
||||||
previewMode === 'markdown'
|
|
||||||
? 'bg-blue-100 text-blue-700'
|
|
||||||
: 'bg-gray-100 text-gray-600'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Markdown
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="preview-area" style={{ height: '400px', overflowY: 'auto' }}>
|
|
||||||
{message.content ? (
|
|
||||||
previewMode === 'discord' ? (
|
|
||||||
<div dangerouslySetInnerHTML={renderDiscordPreview(message.content)} />
|
|
||||||
) : (
|
|
||||||
<ReactMarkdown>{message.content}</ReactMarkdown>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<p className="text-gray-400 italic">
|
|
||||||
Wprowadź treść wiadomości, aby zobaczyć podgląd
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tips */}
|
|
||||||
<div className="discord-card mt-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
|
||||||
💡 Wskazówki dotyczące formatowania
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">Podstawowe formatowanie:</h4>
|
|
||||||
<ul className="space-y-1 text-gray-600">
|
|
||||||
<li><code>**tekst**</code> → <strong>pogrubienie</strong></li>
|
|
||||||
<li><code>*tekst*</code> → <em>kursywa</em></li>
|
|
||||||
<li><code>__tekst__</code> → <u>podkreślenie</u></li>
|
|
||||||
<li><code>~~tekst~~</code> → <del>przekreślenie</del></li>
|
|
||||||
<li><code>`kod`</code> → kod wewnątrz tekstu</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">Zaawansowane:</h4>
|
|
||||||
<ul className="space-y-1 text-gray-600">
|
|
||||||
<li><code># Nagłówek</code> → duży nagłówek</li>
|
|
||||||
<li><code>## Nagłówek</code> → średni nagłówek</li>
|
|
||||||
<li><code>### Nagłówek</code> → mały nagłówek</li>
|
|
||||||
<li><code>• Element</code> → lista punktowana</li>
|
|
||||||
<li><code>```kod```</code> → blok kodu</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Character limit warning */}
|
|
||||||
{message.content.length > 1800 && (
|
|
||||||
<div className="discord-card mt-4 border-l-4 border-orange-400 bg-orange-50">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="text-orange-600 mr-3">⚠️</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-medium text-orange-800">
|
|
||||||
Uwaga: Zbliżasz się do limitu znaków
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-orange-700">
|
|
||||||
Discord ogranicza wiadomości do 2000 znaków.
|
|
||||||
Pozostało: {2000 - message.content.length} znaków
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MessageEditor;
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { api } from '../services/api';
|
|
||||||
import { Server, MessageSquare, Settings, CheckCircle, XCircle } from 'lucide-react';
|
|
||||||
|
|
||||||
function ServerSelect() {
|
|
||||||
const [guilds, setGuilds] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadGuilds();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadGuilds = async () => {
|
|
||||||
try {
|
|
||||||
const response = await api.getGuilds();
|
|
||||||
setGuilds(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading guilds:', error);
|
|
||||||
setError('Nie udało się załadować listy serwerów');
|
|
||||||
|
|
||||||
// Mock data dla rozwoju
|
|
||||||
setGuilds([
|
|
||||||
{
|
|
||||||
id: '123456789',
|
|
||||||
name: 'Test Server',
|
|
||||||
icon: null,
|
|
||||||
hasBot: true,
|
|
||||||
userPermissions: ['MANAGE_CHANNELS'],
|
|
||||||
memberCount: 150,
|
|
||||||
welcomeChannelConfigured: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '987654321',
|
|
||||||
name: 'Drugi Serwer',
|
|
||||||
icon: null,
|
|
||||||
hasBot: false,
|
|
||||||
userPermissions: ['MANAGE_CHANNELS'],
|
|
||||||
memberCount: 50,
|
|
||||||
welcomeChannelConfigured: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '555666777',
|
|
||||||
name: 'Gaming Community',
|
|
||||||
icon: null,
|
|
||||||
hasBot: true,
|
|
||||||
userPermissions: ['ADMINISTRATOR'],
|
|
||||||
memberCount: 300,
|
|
||||||
welcomeChannelConfigured: false
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getServerIcon = (guild) => {
|
|
||||||
if (guild.icon) {
|
|
||||||
return `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getServerInitials = (name) => {
|
|
||||||
return name
|
|
||||||
.split(' ')
|
|
||||||
.map(word => word[0])
|
|
||||||
.join('')
|
|
||||||
.substring(0, 2)
|
|
||||||
.toUpperCase();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <div className="loading">Ładowanie serwerów...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="mb-8">
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Twoje serwery</h1>
|
|
||||||
<p className="text-gray-600">
|
|
||||||
Wybierz serwer, aby zarządzać wiadomościami powaitalnymi
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="error mb-6">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{guilds.length === 0 ? (
|
|
||||||
<div className="discord-card text-center">
|
|
||||||
<Server className="mx-auto text-gray-400 mb-4" size={48} />
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
|
||||||
Brak dostępnych serwerów
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 mb-4">
|
|
||||||
Nie znaleziono serwerów z odpowiednimi uprawnieniami.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Upewnij się, że masz uprawnienia administracyjne na serwerze i że bot jest zaproszony.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
{guilds.map((guild) => (
|
|
||||||
<div key={guild.id} className="guild-card">
|
|
||||||
<div className="flex items-start justify-between mb-4">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="guild-icon">
|
|
||||||
{getServerIcon(guild) ? (
|
|
||||||
<img
|
|
||||||
src={getServerIcon(guild)}
|
|
||||||
alt={guild.name}
|
|
||||||
className="w-full h-full rounded-full object-cover"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
getServerInitials(guild.name)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-bold text-gray-900">{guild.name}</h3>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
{guild.memberCount || 0} członków
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col items-end space-y-1">
|
|
||||||
<div className="flex items-center">
|
|
||||||
{guild.hasBot ? (
|
|
||||||
<CheckCircle className="text-green-600" size={16} />
|
|
||||||
) : (
|
|
||||||
<XCircle className="text-red-600" size={16} />
|
|
||||||
)}
|
|
||||||
<span className={`text-xs ml-1 ${
|
|
||||||
guild.hasBot ? 'text-green-600' : 'text-red-600'
|
|
||||||
}`}>
|
|
||||||
{guild.hasBot ? 'Bot aktywny' : 'Bot nieaktywny'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{guild.welcomeChannelConfigured && (
|
|
||||||
<div className="flex items-center">
|
|
||||||
<MessageSquare className="text-blue-600" size={16} />
|
|
||||||
<span className="text-xs text-blue-600 ml-1">
|
|
||||||
Kanał skonfigurowany
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
{guild.hasBot ? (
|
|
||||||
<>
|
|
||||||
<Link
|
|
||||||
to={`/servers/${guild.id}/messages`}
|
|
||||||
className="discord-button w-full flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<MessageSquare size={16} className="mr-2" />
|
|
||||||
Zarządzaj wiadomościami
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button
|
|
||||||
className="discord-button secondary flex-1 flex items-center justify-center text-sm"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
<Settings size={14} className="mr-1" />
|
|
||||||
Ustawienia
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-sm text-gray-600 mb-2">
|
|
||||||
Bot nie jest zaproszony na ten serwer
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
className="discord-button w-full"
|
|
||||||
onClick={() => {
|
|
||||||
const inviteUrl = `https://discord.com/api/oauth2/authorize?client_id=${process.env.REACT_APP_DISCORD_CLIENT_ID}&permissions=2147484672&scope=bot%20applications.commands&guild_id=${guild.id}`;
|
|
||||||
window.open(inviteUrl, '_blank');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Zaproś bota
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-3 pt-3 border-t border-gray-200">
|
|
||||||
<div className="text-xs text-gray-500">
|
|
||||||
Uprawnienia: {guild.userPermissions?.includes('ADMINISTRATOR')
|
|
||||||
? 'Administrator'
|
|
||||||
: guild.userPermissions?.join(', ') || 'Brak'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="discord-card mt-8">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
|
||||||
Nie widzisz swojego serwera?
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-2 text-sm text-gray-600">
|
|
||||||
<p>• Upewnij się, że masz uprawnienia administracyjne na serwerze</p>
|
|
||||||
<p>• Sprawdź czy bot jest zaproszony na serwer z odpowiednimi uprawnieniami</p>
|
|
||||||
<p>• Wyloguj się i zaloguj ponownie, aby odświeżyć listę serwerów</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ServerSelect;
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
class ApiService {
|
|
||||||
constructor() {
|
|
||||||
this.client = axios.create({
|
|
||||||
baseURL: process.env.NODE_ENV === 'production'
|
|
||||||
? '/api'
|
|
||||||
: 'http://localhost:3000/api',
|
|
||||||
timeout: 10000,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setupInterceptors();
|
|
||||||
}
|
|
||||||
|
|
||||||
setupInterceptors() {
|
|
||||||
// Request interceptor
|
|
||||||
this.client.interceptors.request.use(
|
|
||||||
(config) => {
|
|
||||||
const token = localStorage.getItem('auth_token');
|
|
||||||
if (token) {
|
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Response interceptor
|
|
||||||
this.client.interceptors.response.use(
|
|
||||||
(response) => response,
|
|
||||||
(error) => {
|
|
||||||
if (error.response?.status === 401) {
|
|
||||||
// Token expired or invalid
|
|
||||||
localStorage.removeItem('auth_token');
|
|
||||||
window.location.href = '/login';
|
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setAuthToken(token) {
|
|
||||||
if (token) {
|
|
||||||
this.client.defaults.headers.Authorization = `Bearer ${token}`;
|
|
||||||
} else {
|
|
||||||
delete this.client.defaults.headers.Authorization;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auth endpoints
|
|
||||||
async verifyToken() {
|
|
||||||
return this.client.get('/auth/verify');
|
|
||||||
}
|
|
||||||
|
|
||||||
async logout() {
|
|
||||||
return this.client.post('/auth/logout');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Guild endpoints
|
|
||||||
async getGuilds() {
|
|
||||||
return this.client.get('/guilds');
|
|
||||||
}
|
|
||||||
|
|
||||||
async getGuildConfig(guildId) {
|
|
||||||
return this.client.get(`/guilds/${guildId}/config`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message endpoints
|
|
||||||
async getMessage(guildId) {
|
|
||||||
return this.client.get(`/messages/${guildId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveMessage(guildId, content, embedData = null) {
|
|
||||||
return this.client.post(`/messages/${guildId}`, {
|
|
||||||
content,
|
|
||||||
embed_data: embedData
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMessageRevisions(guildId) {
|
|
||||||
return this.client.get(`/messages/${guildId}/revisions`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Health check
|
|
||||||
async healthCheck() {
|
|
||||||
return this.client.get('/health');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const api = new ApiService();
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
upstream backend {
|
|
||||||
server bot:3000;
|
|
||||||
keepalive 32;
|
|
||||||
}
|
|
||||||
|
|
||||||
# HTTP to HTTPS redirect
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
# Health check endpoint
|
|
||||||
location /health {
|
|
||||||
access_log off;
|
|
||||||
return 200 "healthy\n";
|
|
||||||
add_header Content-Type text/plain;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Redirect all HTTP traffic to HTTPS
|
|
||||||
location / {
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main server block
|
|
||||||
server {
|
|
||||||
listen 443 ssl http2;
|
|
||||||
server_name your-domain.com www.your-domain.com;
|
|
||||||
|
|
||||||
# SSL configuration (uncomment and configure for production)
|
|
||||||
# ssl_certificate /etc/nginx/ssl/cert.pem;
|
|
||||||
# ssl_certificate_key /etc/nginx/ssl/key.pem;
|
|
||||||
# ssl_session_timeout 1d;
|
|
||||||
# ssl_session_cache shared:SSL:50m;
|
|
||||||
# ssl_session_tickets off;
|
|
||||||
# ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
# ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
|
|
||||||
# ssl_prefer_server_ciphers off;
|
|
||||||
|
|
||||||
# Security headers
|
|
||||||
add_header Strict-Transport-Security "max-age=63072000" always;
|
|
||||||
|
|
||||||
# Increase max body size for uploads
|
|
||||||
client_max_body_size 10M;
|
|
||||||
|
|
||||||
# API endpoints with rate limiting
|
|
||||||
location /api/ {
|
|
||||||
limit_req zone=api burst=20 nodelay;
|
|
||||||
|
|
||||||
proxy_pass http://backend;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
|
|
||||||
# Timeouts
|
|
||||||
proxy_connect_timeout 5s;
|
|
||||||
proxy_send_timeout 60s;
|
|
||||||
proxy_read_timeout 60s;
|
|
||||||
}
|
|
||||||
|
|
||||||
# OAuth callback with stricter rate limiting
|
|
||||||
location /api/auth/ {
|
|
||||||
limit_req zone=login burst=5 nodelay;
|
|
||||||
|
|
||||||
proxy_pass http://backend;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Static files (React app)
|
|
||||||
location / {
|
|
||||||
proxy_pass http://backend;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
|
|
||||||
# Cache static assets
|
|
||||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
||||||
proxy_pass http://backend;
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
location /health {
|
|
||||||
access_log off;
|
|
||||||
proxy_pass http://backend/api/health;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Development server block (uncomment for development with SSL)
|
|
||||||
# server {
|
|
||||||
# listen 443 ssl http2;
|
|
||||||
# server_name localhost;
|
|
||||||
#
|
|
||||||
# # Self-signed certificate for development
|
|
||||||
# ssl_certificate /etc/nginx/ssl/localhost.crt;
|
|
||||||
# ssl_certificate_key /etc/nginx/ssl/localhost.key;
|
|
||||||
#
|
|
||||||
# location / {
|
|
||||||
# proxy_pass http://backend;
|
|
||||||
# proxy_http_version 1.1;
|
|
||||||
# proxy_set_header Upgrade $http_upgrade;
|
|
||||||
# proxy_set_header Connection 'upgrade';
|
|
||||||
# proxy_set_header Host $host;
|
|
||||||
# proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
# proxy_cache_bypass $http_upgrade;
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
user nginx;
|
|
||||||
worker_processes auto;
|
|
||||||
error_log /var/log/nginx/error.log notice;
|
|
||||||
pid /var/run/nginx.pid;
|
|
||||||
|
|
||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
use epoll;
|
|
||||||
multi_accept on;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
|
|
||||||
# Logging format
|
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
|
||||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log main;
|
|
||||||
|
|
||||||
# Performance optimizations
|
|
||||||
sendfile on;
|
|
||||||
tcp_nopush on;
|
|
||||||
tcp_nodelay on;
|
|
||||||
keepalive_timeout 65;
|
|
||||||
types_hash_max_size 2048;
|
|
||||||
server_tokens off;
|
|
||||||
|
|
||||||
# Gzip compression
|
|
||||||
gzip on;
|
|
||||||
gzip_vary on;
|
|
||||||
gzip_min_length 10240;
|
|
||||||
gzip_proxied expired no-cache no-store private must-revalidate auth;
|
|
||||||
gzip_types
|
|
||||||
text/plain
|
|
||||||
text/css
|
|
||||||
text/xml
|
|
||||||
text/javascript
|
|
||||||
application/x-javascript
|
|
||||||
application/xml+rss
|
|
||||||
application/javascript
|
|
||||||
application/json;
|
|
||||||
|
|
||||||
# Security headers
|
|
||||||
add_header X-Frame-Options DENY always;
|
|
||||||
add_header X-Content-Type-Options nosniff always;
|
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
||||||
|
|
||||||
# Rate limiting
|
|
||||||
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
|
|
||||||
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
|
|
||||||
|
|
||||||
# Include additional configuration files
|
|
||||||
include /etc/nginx/conf.d/*.conf;
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "skrzynka-impostora-bot",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Discord bot with web panel for welcome messages management",
|
|
||||||
"main": "backend/index.js",
|
|
||||||
"scripts": {
|
|
||||||
"start": "node backend/index.js",
|
|
||||||
"dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"",
|
|
||||||
"dev:backend": "nodemon backend/index.js",
|
|
||||||
"dev:frontend": "cd frontend && npm start",
|
|
||||||
"build": "cd frontend && npm run build",
|
|
||||||
"deploy": "node backend/deploy-commands.js",
|
|
||||||
"db:migrate": "node database/migrate.js",
|
|
||||||
"db:seed": "node database/seed.js",
|
|
||||||
"docker:dev": "docker compose -f docker-compose.dev.yml up --build",
|
|
||||||
"docker:dev:down": "docker compose -f docker-compose.dev.yml down",
|
|
||||||
"docker:prod": "docker compose up --build -d",
|
|
||||||
"docker:prod:down": "docker compose down",
|
|
||||||
"docker:build": "docker build -t skrzynka-impostora-bot:latest .",
|
|
||||||
"docker:logs": "docker compose logs -f",
|
|
||||||
"docker:logs:dev": "docker compose -f docker-compose.dev.yml logs -f"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"discord",
|
|
||||||
"bot",
|
|
||||||
"welcome",
|
|
||||||
"panel",
|
|
||||||
"management"
|
|
||||||
],
|
|
||||||
"author": "Skrzynka Impostora Team",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"discord.js": "^14.14.1",
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"helmet": "^7.1.0",
|
|
||||||
"dotenv": "^16.3.1",
|
|
||||||
"pg": "^8.11.3",
|
|
||||||
"bcryptjs": "^2.4.3",
|
|
||||||
"jsonwebtoken": "^9.0.2",
|
|
||||||
"express-validator": "^7.0.1",
|
|
||||||
"morgan": "^1.10.0",
|
|
||||||
"compression": "^1.7.4"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"nodemon": "^3.0.2",
|
|
||||||
"concurrently": "^8.2.2",
|
|
||||||
"@types/node": "^20.10.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0",
|
|
||||||
"npm": ">=8.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
@echo off
|
|
||||||
REM Skript do uruchamiania środowiska development na Windows
|
|
||||||
|
|
||||||
echo 🚀 Uruchamianie środowiska development...
|
|
||||||
|
|
||||||
REM Sprawdź czy Docker jest uruchomiony
|
|
||||||
docker info >nul 2>&1
|
|
||||||
if %errorlevel% neq 0 (
|
|
||||||
echo ❌ Docker nie jest uruchomiony. Uruchom Docker Desktop i spróbuj ponownie.
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
REM Sprawdź czy plik .env istnieje
|
|
||||||
if not exist .env (
|
|
||||||
echo ⚠️ Plik .env nie istnieje. Kopiuję z .env.example...
|
|
||||||
copy .env.example .env
|
|
||||||
echo 📝 Plik .env został utworzony. Uzupełnij wymagane zmienne przed kontynuowaniem.
|
|
||||||
echo Szczególnie ważne: DISCORD_TOKEN, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET
|
|
||||||
echo.
|
|
||||||
set /p continue="Czy chcesz kontynuować? (y/N): "
|
|
||||||
if /i not "%continue%"=="y" exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
REM Uruchom docker-compose dla development
|
|
||||||
echo 🐳 Uruchamianie kontenerów...
|
|
||||||
docker compose -f docker-compose.dev.yml up --build -d
|
|
||||||
|
|
||||||
echo ✅ Środowisko development zostało uruchomione!
|
|
||||||
echo.
|
|
||||||
echo 📋 Dostępne usługi:
|
|
||||||
echo 🤖 Bot + Panel Web: http://localhost:3000
|
|
||||||
echo 🗄️ PostgreSQL: localhost:5433
|
|
||||||
echo 📊 pgAdmin: http://localhost:8080 (admin@skrzynka.local / admin)
|
|
||||||
echo 🔄 Redis: localhost:6380
|
|
||||||
echo.
|
|
||||||
echo 📝 Aby zobaczyć logi:
|
|
||||||
echo docker compose -f docker-compose.dev.yml logs -f
|
|
||||||
echo.
|
|
||||||
echo 🛑 Aby zatrzymać:
|
|
||||||
echo docker compose -f docker-compose.dev.yml down
|
|
||||||
echo.
|
|
||||||
pause
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Skript do uruchamiania środowiska development
|
|
||||||
echo "🚀 Uruchamianie środowiska development..."
|
|
||||||
|
|
||||||
# Sprawdź czy Docker jest uruchomiony
|
|
||||||
if ! docker info > /dev/null 2>&1; then
|
|
||||||
echo "❌ Docker nie jest uruchomiony. Uruchom Docker Desktop i spróbuj ponownie."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Sprawdź czy plik .env istnieje
|
|
||||||
if [ ! -f .env ]; then
|
|
||||||
echo "⚠️ Plik .env nie istnieje. Kopiuję z .env.example..."
|
|
||||||
cp .env.example .env
|
|
||||||
echo "📝 Plik .env został utworzony. Uzupełnij wymagane zmienne przed kontynuowaniem."
|
|
||||||
echo " Szczególnie ważne: DISCORD_TOKEN, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET"
|
|
||||||
echo ""
|
|
||||||
read -p "Czy chcesz kontynuować? (y/N): " -n 1 -r
|
|
||||||
echo
|
|
||||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Uruchom docker-compose dla development
|
|
||||||
echo "🐳 Uruchamianie kontenerów..."
|
|
||||||
docker compose -f docker-compose.dev.yml up --build -d
|
|
||||||
|
|
||||||
echo "✅ Środowisko development zostało uruchomione!"
|
|
||||||
echo ""
|
|
||||||
echo "📋 Dostępne usługi:"
|
|
||||||
echo " 🔧 API Backend: http://localhost:3000"
|
|
||||||
echo " 🎨 React Frontend: http://localhost:3001"
|
|
||||||
echo " 🗄️ PostgreSQL: localhost:5433"
|
|
||||||
echo " 📊 pgAdmin: http://localhost:8080 (boratsc@gmail.com / admin)"
|
|
||||||
echo " 🔄 Redis: localhost:6380"
|
|
||||||
echo ""
|
|
||||||
echo "📝 Aby zobaczyć logi:"
|
|
||||||
echo " docker compose -f docker-compose.dev.yml logs -f"
|
|
||||||
echo ""
|
|
||||||
echo "🛑 Aby zatrzymać:"
|
|
||||||
echo " docker compose -f docker-compose.dev.yml down"
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Skript do deployowania na produkcję
|
|
||||||
echo "🚀 Przygotowywanie do deployowania na produkcję..."
|
|
||||||
|
|
||||||
# Sprawdź czy Docker jest uruchomiony
|
|
||||||
if ! docker info > /dev/null 2>&1; then
|
|
||||||
echo "❌ Docker nie jest uruchomiony."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Sprawdź czy wszystkie wymagane zmienne są ustawione
|
|
||||||
if [ -z "$DISCORD_TOKEN" ] || [ -z "$DISCORD_CLIENT_ID" ] || [ -z "$DISCORD_CLIENT_SECRET" ]; then
|
|
||||||
echo "❌ Nie wszystkie wymagane zmienne środowiskowe są ustawione."
|
|
||||||
echo " Wymagane: DISCORD_TOKEN, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build obrazu
|
|
||||||
echo "🏗️ Budowanie obrazu..."
|
|
||||||
docker build -t skrzynka-impostora-bot:latest .
|
|
||||||
|
|
||||||
# Deploy komend Discord
|
|
||||||
echo "📡 Deployowanie komend Discord..."
|
|
||||||
docker run --rm \
|
|
||||||
-e DISCORD_TOKEN="$DISCORD_TOKEN" \
|
|
||||||
-e DISCORD_CLIENT_ID="$DISCORD_CLIENT_ID" \
|
|
||||||
skrzynka-impostora-bot:latest \
|
|
||||||
node backend/deploy-commands.js
|
|
||||||
|
|
||||||
# Uruchom środowisko produkcyjne
|
|
||||||
echo "🚀 Uruchamianie środowiska produkcyjnego..."
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
echo "✅ Aplikacja została wdrożona!"
|
|
||||||
echo ""
|
|
||||||
echo "📋 Dostępne usługi:"
|
|
||||||
echo " 🌐 Aplikacja: http://localhost (przez Nginx)"
|
|
||||||
echo " 🗄️ PostgreSQL: localhost:5432"
|
|
||||||
echo ""
|
|
||||||
echo "📝 Aby zobaczyć logi:"
|
|
||||||
echo " docker compose logs -f"
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
// Database configuration
|
|
||||||
database: {
|
|
||||||
development: {
|
|
||||||
host: process.env.DB_HOST || 'localhost',
|
|
||||||
port: process.env.DB_PORT || 5432,
|
|
||||||
database: process.env.DB_NAME || 'skrzynka_impostora',
|
|
||||||
username: process.env.DB_USER || 'postgres',
|
|
||||||
password: process.env.DB_PASSWORD || '',
|
|
||||||
dialect: 'postgres',
|
|
||||||
logging: console.log
|
|
||||||
},
|
|
||||||
production: {
|
|
||||||
use_env_variable: 'DATABASE_URL',
|
|
||||||
dialect: 'postgres',
|
|
||||||
logging: false,
|
|
||||||
dialectOptions: {
|
|
||||||
ssl: {
|
|
||||||
require: true,
|
|
||||||
rejectUnauthorized: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Bot configuration
|
|
||||||
bot: {
|
|
||||||
token: process.env.DISCORD_TOKEN,
|
|
||||||
clientId: process.env.DISCORD_CLIENT_ID,
|
|
||||||
clientSecret: process.env.DISCORD_CLIENT_SECRET,
|
|
||||||
permissions: {
|
|
||||||
required: [
|
|
||||||
'SEND_MESSAGES',
|
|
||||||
'MANAGE_MESSAGES',
|
|
||||||
'EMBED_LINKS',
|
|
||||||
'READ_MESSAGE_HISTORY',
|
|
||||||
'USE_SLASH_COMMANDS'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Web panel configuration
|
|
||||||
web: {
|
|
||||||
port: process.env.API_PORT || 3000,
|
|
||||||
frontendPort: process.env.WEB_PORT || 3001,
|
|
||||||
jwtSecret: process.env.JWT_SECRET,
|
|
||||||
sessionSecret: process.env.SESSION_SECRET,
|
|
||||||
oauth: {
|
|
||||||
redirectUri: process.env.OAUTH2_REDIRECT_URI
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Application settings
|
|
||||||
app: {
|
|
||||||
name: 'Skrzynka Impostora Bot',
|
|
||||||
version: '1.0.0',
|
|
||||||
environment: process.env.NODE_ENV || 'development',
|
|
||||||
logLevel: process.env.LOG_LEVEL || 'info'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
30
zalozenia.md
30
zalozenia.md
@@ -13,11 +13,7 @@ Celem projektu jest stworzenie kompleksowego bota Discordowego „Skrzynka Impos
|
|||||||
1. Integracja bota z Discord API.
|
1. Integracja bota z Discord API.
|
||||||
2. Wysyłanie domyślnej wiadomości powitalnej na wskazany kanał #witamy.
|
2. Wysyłanie domyślnej wiadomości powitalnej na wskazany kanał #witamy.
|
||||||
3. Panel web do edycji treści i konfiguracji kanału powitalnego.
|
3. Panel web do edycji treści i konfiguracji kanału powitalnego.
|
||||||
|
4. Obsługa wielu wariantów wiadomości (częściowo dzielona, carouseli, linki, emoji).
|
||||||
- **Funkcjonalności docelowe:**
|
|
||||||
- Obsługa wielu wariantów wiadomości (częściowo dzielona, carouseli, linki, emoji).
|
|
||||||
- Harmonogramy wysyłki (poranne, wieczorne przypomnienia, rotacje sezonowe).
|
|
||||||
- Wielojęzyczność.
|
|
||||||
|
|
||||||
## 4. Wymagania funkcjonalne
|
## 4. Wymagania funkcjonalne
|
||||||
1. **Konfiguracja kanału powitalnego**
|
1. **Konfiguracja kanału powitalnego**
|
||||||
@@ -29,32 +25,29 @@ Celem projektu jest stworzenie kompleksowego bota Discordowego „Skrzynka Impos
|
|||||||
3. **Publikacja wiadomości**
|
3. **Publikacja wiadomości**
|
||||||
- Usuwanie / aktualizacja poprzedniej wersji.
|
- Usuwanie / aktualizacja poprzedniej wersji.
|
||||||
- Automatyczne wysyłanie po zapisaniu zmian.
|
- Automatyczne wysyłanie po zapisaniu zmian.
|
||||||
4. **Historia zmian**
|
|
||||||
- Rejestracja timestamp i użytkownika, który wprowadził modyfikacje.
|
|
||||||
5. **Uprawnienia użytkowników**
|
|
||||||
- Role: administrator, edytor, przeglądający.
|
|
||||||
|
|
||||||
## 5. Wymagania niefunkcjonalne
|
## 5. Wymagania niefunkcjonalne
|
||||||
- **Wydajność:** Wysłanie i aktualizacja < 500 ms.
|
- **Wydajność:** Wysłanie i aktualizacja < 500 ms.
|
||||||
- **Skalowalność:** Obsługa wielu serwerów jednocześnie.
|
|
||||||
- **Dostępność:** ≥ 99,5% czasu działania.
|
- **Dostępność:** ≥ 99,5% czasu działania.
|
||||||
- **Bezpieczeństwo:** Autoryzacja użytkowników (JWT / OAuth2), ochrona przed XSS w edytorze.
|
- **Bezpieczeństwo:** login i hasło zahashowane, możliwość zmiany hasła,
|
||||||
- **Łatwość utrzymania:** Kod: JavaScript (Node.js), bazy danych SQL (PostgreSQL/MySQL).
|
- Kod: JavaScript (Node.js), bazy danych SQL (PostgreSQL/MySQL).
|
||||||
|
- Bot uruchamiany przez docker compose (v2)
|
||||||
|
|
||||||
## 6. Technologia i architektura
|
## 6. Technologia i architektura
|
||||||
- **Backend:** Node.js + Express/Koa.
|
- **Backend:** Node.js + Express/Koa.
|
||||||
- **Baza danych:** SQL (PostgreSQL lub MySQL).
|
- **Baza danych:** SQL (PostgreSQL lub MySQL).
|
||||||
- **Frontend panelu web:** React lub Vue.
|
- **Frontend panelu web:** Prosty web panel html + js.
|
||||||
- **Integracja Discord API:** Biblioteka discord.js.
|
- **Integracja Discord API:** Biblioteka discord.js.
|
||||||
- **Hosting:** Cloud (Heroku, AWS, Azure).
|
|
||||||
|
|
||||||
## 7. Formatowanie wiadomości powitalnej
|
## 7. Formatowanie wiadomości powitalnej
|
||||||
- Wsparcie Discord Markdown (nagłówki, listy, linki, kanały, wzmianki, emoji).
|
- Wsparcie Discord Markdown (nagłówki, listy, linki, kanały, wzmianki, emoji).
|
||||||
- Możliwość dzielenia treści na kilka embedów, jeśli przekracza limit znaków (2000).
|
- Automatyczne dzielenie treści na kilka embedów, jeśli przekracza limit znaków (2000).
|
||||||
- Wbudowane zmienne szablonowe (np. `{{user}}`, `{{server}}`, `{{date}}`).
|
- Wbudowane zmienne szablonowe (np. `{{user}}`, `{{server}}`, `{{date}}`).
|
||||||
|
|
||||||
## 8. Panel web
|
## 8. Panel web
|
||||||
- **Logowanie:** OAuth2 Discord.
|
- **Logowanie:** Login i hasło
|
||||||
- **Dashboard:**
|
- **Dashboard:**
|
||||||
- Wybór serwera (lista guilds).
|
- Wybór serwera (lista guilds).
|
||||||
- Konfiguracja kanału powitalnego.
|
- Konfiguracja kanału powitalnego.
|
||||||
@@ -83,8 +76,3 @@ Zarządzanie kanałem i treścią odbywa się przez panel web po zapisaniu zmian
|
|||||||
- Panel web pozwala edytować treść i wybór kanału.
|
- Panel web pozwala edytować treść i wybór kanału.
|
||||||
- Przekroczenie limitów Discord (2000 znaków) jest obsłużone.
|
- Przekroczenie limitów Discord (2000 znaków) jest obsłużone.
|
||||||
- Dokumentacja API i instrukcja użytkownika.
|
- Dokumentacja API i instrukcja użytkownika.
|
||||||
|
|
||||||
## 12. Rozwój w kolejnych etapach
|
|
||||||
- **Rotacje i harmonogramy:** Zaplanowane wysyłki.
|
|
||||||
- **Wersje językowe:** Automatyczne wykrywanie preferencji.
|
|
||||||
- **Analizy:** Statystyki odsłon i reakcji użytkowników.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user