envs
This commit is contained in:
7
.github/workflows/security.yml
vendored
7
.github/workflows/security.yml
vendored
@@ -34,13 +34,7 @@ jobs:
|
|||||||
- name: Run yarn audit
|
- name: Run yarn audit
|
||||||
run: yarn audit --level high
|
run: yarn audit --level high
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v3
|
|
||||||
with:
|
|
||||||
languages: javascript
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v3
|
|
||||||
|
|
||||||
docker-security:
|
docker-security:
|
||||||
name: Docker Security Scan
|
name: Docker Security Scan
|
||||||
@@ -59,4 +53,3 @@ jobs:
|
|||||||
image-ref: 'quixotic:scan'
|
image-ref: 'quixotic:scan'
|
||||||
format: 'sarif'
|
format: 'sarif'
|
||||||
output: 'trivy-results.sarif'
|
output: 'trivy-results.sarif'
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
31
.serena/memories/layout_issue_analysis.md
Normal file
31
.serena/memories/layout_issue_analysis.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Layout Issue Analysis
|
||||||
|
|
||||||
|
## Problem Description
|
||||||
|
User reports that the application layout is broken:
|
||||||
|
1. "Nothing found" notification shows always
|
||||||
|
2. Search field that was at bottom is now at top
|
||||||
|
|
||||||
|
## Current Structure Analysis
|
||||||
|
Based on code review of public/index.html and public/script.ts:
|
||||||
|
|
||||||
|
### HTML Structure (Current):
|
||||||
|
1. Search form (input + button) - at TOP
|
||||||
|
2. Welcome placeholder - initially visible
|
||||||
|
3. Loading spinner - hidden by default
|
||||||
|
4. Results list - hidden by default
|
||||||
|
5. No results placeholder - hidden by default
|
||||||
|
|
||||||
|
### JavaScript Logic Issues Found:
|
||||||
|
1. **No Results always showing**: In `showNoResults()` method, the noResults element is made visible with `classList.remove('tg-hidden')` but there's no logic to hide it when new search starts or results are found.
|
||||||
|
|
||||||
|
2. **Search field position**: The search form is structurally at the top in HTML, which matches user complaint.
|
||||||
|
|
||||||
|
### Display Logic Flow:
|
||||||
|
- `showLoading()`: Hides welcome, shows loading, hides results, hides noResults
|
||||||
|
- `displayResults()`: Calls `hideLoading()`, shows results if any, otherwise calls `showNoResults()`
|
||||||
|
- `showNoResults()`: Calls `hideLoading()`, hides results, SHOWS noResults (but doesn't hide welcome)
|
||||||
|
|
||||||
|
## Root Causes:
|
||||||
|
1. **NoResults visibility issue**: The noResults element is never hidden when starting a new search
|
||||||
|
2. **Welcome placeholder management**: Not properly hidden when showing noResults
|
||||||
|
3. **Search position**: Structurally at top, needs to be moved to bottom if that was the intended design
|
||||||
18
.serena/memories/telegram_token_fix.md
Normal file
18
.serena/memories/telegram_token_fix.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Telegram Bot Token Fix
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
Docker container was showing "⚠️ TELEGRAM_BOT_TOKEN not found or invalid - bot will not start" error despite the token being present in `.env.docker` file.
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
The `docker-compose.yml` file was missing the `env_file` configuration to load environment variables from `.env.docker`.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
Added `env_file: - .env.docker` at the top level of docker-compose.yml to ensure all services load environment variables from the .env.docker file.
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
- `docker-compose.yml`: Added env_file configuration
|
||||||
|
|
||||||
|
## Token Details
|
||||||
|
- Token is present in `.env.docker`: `8262100335:AAFUBycadhKwV4oWPtF_Uq3c_R95SfWUElM`
|
||||||
|
- WEB_APP_URL is configured: `https://quixy.uk`
|
||||||
|
- Environment variables are properly referenced in services
|
||||||
@@ -4,6 +4,8 @@ services:
|
|||||||
image: traefik:v3.0
|
image: traefik:v3.0
|
||||||
container_name: quixotic-traefik
|
container_name: quixotic-traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env.docker
|
||||||
command:
|
command:
|
||||||
- --api.dashboard=true
|
- --api.dashboard=true
|
||||||
- --api.insecure=false
|
- --api.insecure=false
|
||||||
@@ -37,6 +39,8 @@ services:
|
|||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
container_name: quixotic-postgres
|
container_name: quixotic-postgres
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env.docker
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-quixotic}
|
POSTGRES_DB: ${POSTGRES_DB:-quixotic}
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-quixotic}
|
POSTGRES_USER: ${POSTGRES_USER:-quixotic}
|
||||||
@@ -59,10 +63,13 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: quixotic-app
|
container_name: quixotic-app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env.docker
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- PORT=3000
|
- PORT=3000
|
||||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-quixotic}:${POSTGRES_PASSWORD:-quixotic123}@postgres:5432/${POSTGRES_DB:-quixotic}
|
- DATABASE_URL=postgresql://${POSTGRES_USER:-quixotic}:${POSTGRES_PASSWORD:-quixotic123}@postgres:5432/${POSTGRES_DB:-quixotic}
|
||||||
|
- DATABASE_SSL=false
|
||||||
volumes:
|
volumes:
|
||||||
- downloads:/app/downloads
|
- downloads:/app/downloads
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -12,34 +12,28 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="tg-root">
|
<div class="tg-root">
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tg-content">
|
<div class="tg-content">
|
||||||
<div class="tg-placeholder" id="welcomePlaceholder">
|
<div class="tg-placeholder" id="welcomePlaceholder">
|
||||||
<div class="tg-placeholder__icon">🎵</div>
|
<div class="tg-placeholder__icon">🎵</div>
|
||||||
<div class="tg-placeholder__title">Найти музыку</div>
|
<div class="tg-placeholder__title">Найти музыку</div>
|
||||||
<div class="tg-placeholder__description">Введите название песни или исполнителя для поиска на YouTube</div>
|
<div class="tg-placeholder__description">Введите название песни или исполнителя для поиска на SoundCloud</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="tg-button tg-button--primary tg-button--large" id="searchBtn">
|
<div class="tg-spinner tg-hidden" id="loading">
|
||||||
<span class="tg-button__text">Найти</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tg-spinner" id="loading">
|
|
||||||
<div class="tg-spinner__icon"></div>
|
<div class="tg-spinner__icon"></div>
|
||||||
<div class="tg-spinner__text">Поиск музыки...</div>
|
<div class="tg-spinner__text">Поиск музыки...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tg-list" id="results">
|
<div class="tg-list tg-hidden" id="results">
|
||||||
<!-- Search results will appear here -->
|
<!-- Search results will appear here -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tg-placeholder tg-placeholder--secondary" id="noResults">
|
<div class="tg-placeholder tg-placeholder--secondary tg-hidden" id="noResults">
|
||||||
<div class="tg-placeholder__icon">🔍</div>
|
<div class="tg-placeholder__icon">🔍</div>
|
||||||
<div class="tg-placeholder__title">Ничего не найдено</div>
|
<div class="tg-placeholder__title">Ничего не найдено</div>
|
||||||
<div class="tg-placeholder__description">Попробуйте изменить поисковый запрос</div>
|
<div class="tg-placeholder__description">Попробуйте изменить поисковый запрос</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tg-form">
|
<div class="tg-form">
|
||||||
<div class="tg-input-wrapper">
|
<div class="tg-input-wrapper">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ class QuixoticApp {
|
|||||||
this.welcomePlaceholder.classList.add('tg-hidden');
|
this.welcomePlaceholder.classList.add('tg-hidden');
|
||||||
this.loading.classList.remove('tg-hidden');
|
this.loading.classList.remove('tg-hidden');
|
||||||
this.loading.classList.add('tg-spinner--visible');
|
this.loading.classList.add('tg-spinner--visible');
|
||||||
|
this.results.classList.add('tg-hidden');
|
||||||
this.results.classList.remove('tg-list--visible');
|
this.results.classList.remove('tg-list--visible');
|
||||||
this.noResults.classList.add('tg-hidden');
|
this.noResults.classList.add('tg-hidden');
|
||||||
this.searchBtn.disabled = true;
|
this.searchBtn.disabled = true;
|
||||||
@@ -147,12 +148,14 @@ class QuixoticApp {
|
|||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|
||||||
|
this.results.classList.remove('tg-hidden');
|
||||||
this.results.classList.add('tg-list--visible');
|
this.results.classList.add('tg-list--visible');
|
||||||
this.noResults.classList.add('tg-hidden');
|
this.noResults.classList.add('tg-hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
private showNoResults(): void {
|
private showNoResults(): void {
|
||||||
this.hideLoading();
|
this.hideLoading();
|
||||||
|
this.results.classList.add('tg-hidden');
|
||||||
this.results.classList.remove('tg-list--visible');
|
this.results.classList.remove('tg-list--visible');
|
||||||
this.noResults.classList.remove('tg-hidden');
|
this.noResults.classList.remove('tg-hidden');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ body {
|
|||||||
.tg-content {
|
.tg-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: var(--tg-spacing-lg);
|
padding: var(--tg-spacing-lg);
|
||||||
|
padding-bottom: 140px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--tg-spacing-xl);
|
gap: var(--tg-spacing-xl);
|
||||||
@@ -74,9 +75,17 @@ body {
|
|||||||
|
|
||||||
/* Form components */
|
/* Form components */
|
||||||
.tg-form {
|
.tg-form {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--tg-spacing-md);
|
gap: var(--tg-spacing-md);
|
||||||
|
padding: var(--tg-spacing-lg);
|
||||||
|
background: var(--tg-color-bg);
|
||||||
|
border-top: 1px solid var(--tg-color-secondary-bg);
|
||||||
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tg-input-wrapper {
|
.tg-input-wrapper {
|
||||||
|
|||||||
22
src/bot.ts
22
src/bot.ts
@@ -84,7 +84,7 @@ export class QuixoticBot {
|
|||||||
|
|
||||||
await this.bot.sendMessage(chatId,
|
await this.bot.sendMessage(chatId,
|
||||||
'🎵 Добро пожаловать в Quixotic!\n\n' +
|
'🎵 Добро пожаловать в Quixotic!\n\n' +
|
||||||
'Найди любую песню на YouTube и получи MP3 файл прямо в чат.\n\n' +
|
'Найди любую песню на SoundCloud и получи MP3 файл прямо в чат.\n\n' +
|
||||||
'Нажми кнопку ниже, чтобы начать поиск:',
|
'Нажми кнопку ниже, чтобы начать поиск:',
|
||||||
{ reply_markup: keyboard }
|
{ reply_markup: keyboard }
|
||||||
);
|
);
|
||||||
@@ -98,7 +98,7 @@ export class QuixoticBot {
|
|||||||
this.bot.onText(/\/help/, async (msg: Message) => {
|
this.bot.onText(/\/help/, async (msg: Message) => {
|
||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
|
|
||||||
const helpText = `🎵 *Quixotic - YouTube to MP3*
|
const helpText = `🎵 *Quixotic - SoundCloud to MP3*
|
||||||
|
|
||||||
*Как пользоваться:*
|
*Как пользоваться:*
|
||||||
1️⃣ Нажми кнопку "Открыть Quixotic"
|
1️⃣ Нажми кнопку "Открыть Quixotic"
|
||||||
@@ -112,8 +112,8 @@ export class QuixoticBot {
|
|||||||
/history - История поиска
|
/history - История поиска
|
||||||
|
|
||||||
*Возможности:*
|
*Возможности:*
|
||||||
✅ Поиск по YouTube
|
✅ Поиск по SoundCloud
|
||||||
✅ Высокое качество MP3 (128kbps)
|
✅ Высокое качество MP3 (192kbps)
|
||||||
✅ Быстрая конвертация
|
✅ Быстрая конвертация
|
||||||
✅ История поиска`;
|
✅ История поиска`;
|
||||||
|
|
||||||
@@ -218,19 +218,7 @@ export class QuixoticBot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getSearchHistory(userId: number): Promise<SearchResult[]> {
|
private async getSearchHistory(userId: number): Promise<SearchResult[]> {
|
||||||
return new Promise((resolve, reject) => {
|
return this.db.getSearchHistory(userId);
|
||||||
this.db['db'].all(
|
|
||||||
`SELECT query, created_at FROM search_history
|
|
||||||
WHERE user_id = ?
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT 10`,
|
|
||||||
[userId],
|
|
||||||
(err: Error | null, rows: SearchResult[]) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
else resolve(rows || []);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sendAudioFile(chatId: number, audioUrl: string, title: string): Promise<void> {
|
private async sendAudioFile(chatId: number, audioUrl: string, title: string): Promise<void> {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Pool, QueryResult } from 'pg';
|
import { Pool } from 'pg';
|
||||||
|
|
||||||
interface TelegramUser {
|
interface TelegramUser {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -52,7 +52,7 @@ export class Database {
|
|||||||
await this.pool.query(`CREATE TABLE IF NOT EXISTS downloads (
|
await this.pool.query(`CREATE TABLE IF NOT EXISTS downloads (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
user_id INTEGER REFERENCES users(id),
|
user_id INTEGER REFERENCES users(id),
|
||||||
youtube_id TEXT NOT NULL,
|
track_id TEXT NOT NULL,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
file_path TEXT,
|
file_path TEXT,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
@@ -85,10 +85,10 @@ export class Database {
|
|||||||
return result.rows[0].id;
|
return result.rows[0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addDownload(userId: number, youtubeId: string, title: string, filePath: string): Promise<number> {
|
async addDownload(userId: number, trackId: string, title: string, filePath: string): Promise<number> {
|
||||||
const result = await this.pool.query(
|
const result = await this.pool.query(
|
||||||
'INSERT INTO downloads (user_id, youtube_id, title, file_path) VALUES ($1, $2, $3, $4) RETURNING id',
|
'INSERT INTO downloads (user_id, track_id, title, file_path) VALUES ($1, $2, $3, $4) RETURNING id',
|
||||||
[userId, youtubeId, title, filePath]
|
[userId, trackId, title, filePath]
|
||||||
);
|
);
|
||||||
return result.rows[0].id;
|
return result.rows[0].id;
|
||||||
}
|
}
|
||||||
@@ -101,6 +101,14 @@ export class Database {
|
|||||||
return result.rows[0] || undefined;
|
return result.rows[0] || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSearchHistory(userId: number, limit: number = 10): Promise<{query: string, created_at: string}[]> {
|
||||||
|
const result = await this.pool.query(
|
||||||
|
'SELECT query, created_at FROM search_history WHERE user_id = $1 ORDER BY created_at DESC LIMIT $2',
|
||||||
|
[userId, limit]
|
||||||
|
);
|
||||||
|
return result.rows;
|
||||||
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
await this.pool.end();
|
await this.pool.end();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,8 +198,8 @@ app.get('/health', (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Error handler
|
// Error handler
|
||||||
app.use((err: Error, req: Request, res: Response, next: any) => {
|
app.use((_err: Error, _req: Request, res: Response, _next: any) => {
|
||||||
console.error(err.stack);
|
console.error(_err.stack);
|
||||||
res.status(500).json({ error: 'Something went wrong!' });
|
res.status(500).json({ error: 'Something went wrong!' });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -238,11 +238,16 @@ app.listen(port, () => {
|
|||||||
const botToken = process.env.TELEGRAM_BOT_TOKEN;
|
const botToken = process.env.TELEGRAM_BOT_TOKEN;
|
||||||
const webAppUrl = process.env.WEB_APP_URL || `http://localhost:${port}`;
|
const webAppUrl = process.env.WEB_APP_URL || `http://localhost:${port}`;
|
||||||
|
|
||||||
if (botToken) {
|
if (botToken && botToken.length > 10) {
|
||||||
const bot = new QuixoticBot(botToken, webAppUrl);
|
try {
|
||||||
console.log('🤖 Telegram bot started');
|
new QuixoticBot(botToken, webAppUrl);
|
||||||
|
console.log('🤖 Telegram bot started');
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ Bot initialization failed:', error.message);
|
||||||
|
console.warn('⚠️ Bot disabled due to error');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('⚠️ TELEGRAM_BOT_TOKEN not found - bot will not start');
|
console.warn('⚠️ TELEGRAM_BOT_TOKEN not found or invalid - bot will not start');
|
||||||
}
|
}
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
Reference in New Issue
Block a user