Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68832bd914 | ||
|
|
eb4f306c8d | ||
|
|
3c6738376b | ||
|
|
5ccb5169a6 |
7
.env
Normal file
7
.env
Normal file
@ -0,0 +1,7 @@
|
||||
DISCORD_TOKEN=MTQ3Mjc2ODQ4NTQ0NzY5NjY1MA.GDeYeK.5LFkAzXfLpZjS2yn7976SQlU_Pr-zZmz4inptQ
|
||||
CLIENT_ID=1472768485447696650
|
||||
GUILD_ID=1428530728706117632
|
||||
VOICE_CHANNEL_ID=1457687430189682781
|
||||
SAHUR_HOUR=3
|
||||
SAHUR_MINUTE=0
|
||||
TEXT_CHANNEL_ID=1430856944004104326
|
||||
117
assets/css/custom.css
Normal file
117
assets/css/custom.css
Normal file
@ -0,0 +1,117 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #f7f7f6;
|
||||
--surface: #ffffff;
|
||||
--text: #161616;
|
||||
--muted: #6b6b6b;
|
||||
--border: #e5e5e5;
|
||||
--accent: #111111;
|
||||
--accent-soft: #f0f0f0;
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 10px;
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 24px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Space Grotesk", "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: var(--surface) !important;
|
||||
}
|
||||
|
||||
.hero {
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.section-head {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.card,
|
||||
.table {
|
||||
border-color: var(--border) !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-dark {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.btn-outline-secondary {
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-outline-secondary:hover {
|
||||
background: var(--accent-soft);
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-5);
|
||||
background: var(--surface);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 140px;
|
||||
transition: border-color 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.dropzone input[type="file"] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropzone.is-dragover {
|
||||
border-color: var(--text);
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.dropzone-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.badge {
|
||||
border-radius: 999px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
footer {
|
||||
background: var(--surface);
|
||||
}
|
||||
40
assets/js/main.js
Normal file
40
assets/js/main.js
Normal file
@ -0,0 +1,40 @@
|
||||
(() => {
|
||||
const dropzones = document.querySelectorAll('[data-dropzone]');
|
||||
dropzones.forEach((dropzone) => {
|
||||
const input = dropzone.querySelector('input[type="file"]');
|
||||
const fileLabel = dropzone.querySelector('[data-file-name]');
|
||||
if (!input || !fileLabel) return;
|
||||
|
||||
const updateName = (file) => {
|
||||
if (!file) {
|
||||
fileLabel.textContent = 'Belum ada file dipilih';
|
||||
return;
|
||||
}
|
||||
fileLabel.textContent = `${file.name} (${Math.round(file.size / 1024)} KB)`;
|
||||
};
|
||||
|
||||
input.addEventListener('change', (event) => {
|
||||
updateName(event.target.files[0]);
|
||||
});
|
||||
|
||||
['dragenter', 'dragover'].forEach((eventName) => {
|
||||
dropzone.addEventListener(eventName, (event) => {
|
||||
event.preventDefault();
|
||||
dropzone.classList.add('is-dragover');
|
||||
});
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach((eventName) => {
|
||||
dropzone.addEventListener(eventName, () => {
|
||||
dropzone.classList.remove('is-dragover');
|
||||
});
|
||||
});
|
||||
|
||||
dropzone.addEventListener('drop', (event) => {
|
||||
event.preventDefault();
|
||||
if (!event.dataTransfer.files.length) return;
|
||||
input.files = event.dataTransfer.files;
|
||||
updateName(event.dataTransfer.files[0]);
|
||||
});
|
||||
});
|
||||
})();
|
||||
26
bot.log
Normal file
26
bot.log
Normal file
@ -0,0 +1,26 @@
|
||||
[dotenv@17.3.1] injecting env (7) from .env -- tip: 🔐 prevent committing .env to code: https://dotenvx.com/precommit
|
||||
Keep-Alive server running on port 8080
|
||||
Bot logged in as XiaoMao#2565
|
||||
Mendaftarkan ulang slash commands (setelah reset)...
|
||||
(node:7936) DeprecationWarning: The ready event has been renamed to clientReady to distinguish it from the gateway READY event and will only emit under that name in v15. Please use clientReady instead.
|
||||
(Use `node --trace-deprecation ...` to show where the warning was created)
|
||||
Berhasil mendaftarkan command: /join, /test_sahur
|
||||
/home/ubuntu/executor/workspace/node_modules/@discordjs/voice/dist/index.js:746
|
||||
throw new Error(
|
||||
^
|
||||
|
||||
Error: Cannot utilize the DAVE protocol as the @snazzah/davey package has not been installed.
|
||||
- Use the generateDependencyReport() function for more information.
|
||||
|
||||
at new DAVESession (/home/ubuntu/executor/workspace/node_modules/@discordjs/voice/dist/index.js:746:13)
|
||||
at Networking.createDaveSession (/home/ubuntu/executor/workspace/node_modules/@discordjs/voice/dist/index.js:1467:21)
|
||||
at Networking.onWsPacket (/home/ubuntu/executor/workspace/node_modules/@discordjs/voice/dist/index.js:1602:20)
|
||||
at VoiceWebSocket.emit (node:events:530:35)
|
||||
at VoiceWebSocket.onMessage (/home/ubuntu/executor/workspace/node_modules/@discordjs/voice/dist/index.js:1239:10)
|
||||
at VoiceWebSocket.ws.onmessage (/home/ubuntu/executor/workspace/node_modules/@discordjs/voice/dist/index.js:1182:39)
|
||||
at callListener (/home/ubuntu/executor/workspace/node_modules/ws/lib/event-target.js:290:14)
|
||||
at WebSocket.onMessage (/home/ubuntu/executor/workspace/node_modules/ws/lib/event-target.js:209:9)
|
||||
at WebSocket.emit (node:events:518:28)
|
||||
at Receiver.receiverOnMessage (/home/ubuntu/executor/workspace/node_modules/ws/lib/websocket.js:1225:20)
|
||||
|
||||
Node.js v22.18.0
|
||||
102
event.php
Normal file
102
event.php
Normal file
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('Asia/Jakarta');
|
||||
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
$eventId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
|
||||
$pdo = db();
|
||||
|
||||
$event = null;
|
||||
if ($eventId) {
|
||||
$stmt = $pdo->prepare('SELECT id, event_type, message, meta, created_at FROM sahur_events WHERE id = :id');
|
||||
$stmt->execute([':id' => $eventId]);
|
||||
$event = $stmt->fetch();
|
||||
}
|
||||
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Detail Aktivitas Sahur</title>
|
||||
<?php if ($projectDescription): ?>
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/assets/css/custom.css?v=<?= htmlspecialchars((string)time()) ?>" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg bg-white border-bottom">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-semibold" href="/">Sahur Bot Control</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container py-5">
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted small mb-1">Detail Aktivitas</p>
|
||||
<h1 class="h4 fw-semibold mb-0">Log Sahur</h1>
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary" href="/">Kembali</a>
|
||||
</div>
|
||||
|
||||
<?php if (!$event): ?>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
Data aktivitas tidak ditemukan. Pilih event dari daftar log.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<p class="text-muted small mb-1">Tipe</p>
|
||||
<p class="fw-semibold mb-0 text-uppercase"><?= htmlspecialchars($event['event_type']) ?></p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<p class="text-muted small mb-1">Waktu</p>
|
||||
<p class="fw-semibold mb-0"><?= htmlspecialchars($event['created_at']) ?> WIB</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<p class="text-muted small mb-1">ID Event</p>
|
||||
<p class="fw-semibold mb-0">#<?= htmlspecialchars((string)$event['id']) ?></p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<p class="text-muted small mb-1">Pesan</p>
|
||||
<p class="mb-0"><?= htmlspecialchars($event['message']) ?></p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<p class="text-muted small mb-1">Meta</p>
|
||||
<pre class="bg-light border rounded p-3 mb-0 small"><?= htmlspecialchars($event['meta'] ?: 'Tidak ada data tambahan.') ?></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
|
||||
<footer class="border-top py-4">
|
||||
<div class="container">
|
||||
<span class="text-muted small">Sahur Bot Dashboard (MVP)</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
184
index.js
Normal file
184
index.js
Normal file
@ -0,0 +1,184 @@
|
||||
require('dotenv').config();
|
||||
const { Client, GatewayIntentBits, REST, Routes, SlashCommandBuilder } = require('discord.js');
|
||||
const {
|
||||
joinVoiceChannel,
|
||||
createAudioPlayer,
|
||||
createAudioResource,
|
||||
AudioPlayerStatus,
|
||||
NoSubscriberBehavior,
|
||||
VoiceConnectionStatus,
|
||||
enterState
|
||||
} = require('@discordjs/voice');
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// --- Configuration ---
|
||||
const DISCORD_TOKEN = process.env.DISCORD_TOKEN;
|
||||
const CLIENT_ID = process.env.CLIENT_ID;
|
||||
const GUILD_ID = process.env.GUILD_ID;
|
||||
|
||||
// Keep-Alive Server
|
||||
const app = express();
|
||||
const port = 8080;
|
||||
app.get('/', (req, res) => res.send('Bot Sahur is Alive! 🌙'));
|
||||
app.listen(port, () => console.log(`Keep-Alive server running on port ${port}`));
|
||||
|
||||
// Discord Client
|
||||
const client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildVoiceStates
|
||||
]
|
||||
});
|
||||
|
||||
let player = null;
|
||||
let connection = null;
|
||||
let isLooping = false;
|
||||
|
||||
// Function to play local sahur.mp3
|
||||
function playAudio() {
|
||||
try {
|
||||
if (!connection) {
|
||||
console.error('[Audio] Connection tidak ditemukan!');
|
||||
return false;
|
||||
}
|
||||
|
||||
const filePath = path.join(__dirname, 'sahur.mp3');
|
||||
console.log(`[Audio] Memutar file dari: ${filePath}`);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error(`[Audio] File TIDAK ditemukan di: ${filePath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!player) {
|
||||
console.log('[Audio] Membuat player baru...');
|
||||
player = createAudioPlayer({
|
||||
behaviors: {
|
||||
noSubscriber: NoSubscriberBehavior.Play,
|
||||
},
|
||||
});
|
||||
|
||||
player.on(AudioPlayerStatus.Idle, () => {
|
||||
if (isLooping && connection) {
|
||||
console.log('[Audio] Looping: Memutar ulang sahur.mp3');
|
||||
playAudio();
|
||||
}
|
||||
});
|
||||
|
||||
player.on('error', error => {
|
||||
console.error('[Audio] Player Error:', error.message);
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
const resource = createAudioResource(filePath);
|
||||
player.play(resource);
|
||||
connection.subscribe(player);
|
||||
console.log('[Audio] Berhasil memutar resource.');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[Audio] Exception di playAudio:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Slash Commands Definition
|
||||
const commands = [
|
||||
new SlashCommandBuilder()
|
||||
.setName('join')
|
||||
.setDescription('Membuat bot masuk ke Voice Channel kamu'),
|
||||
new SlashCommandBuilder()
|
||||
.setName('test_sahur')
|
||||
.setDescription('Memutar sahur.mp3 secara berulang (loop)'),
|
||||
].map(command => command.toJSON());
|
||||
|
||||
const rest = new REST({ version: '10' }).setToken(DISCORD_TOKEN);
|
||||
|
||||
async function registerCommands() {
|
||||
try {
|
||||
console.log('Mendaftarkan ulang slash commands (setelah reset)...');
|
||||
await rest.put(
|
||||
Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID),
|
||||
{ body: commands }
|
||||
);
|
||||
console.log('Berhasil mendaftarkan command: /join, /test_sahur');
|
||||
} catch (error) {
|
||||
console.error('Gagal mendaftarkan command:', error);
|
||||
}
|
||||
}
|
||||
|
||||
client.once('ready', () => {
|
||||
console.log(`Bot logged in as ${client.user.tag}`);
|
||||
registerCommands();
|
||||
|
||||
// Simple Keep-Alive log every 15 minutes
|
||||
setInterval(() => {
|
||||
console.log(`[Keep-Alive] Bot active at ${new Date().toISOString()}`);
|
||||
}, 15 * 60 * 1000);
|
||||
});
|
||||
|
||||
client.on('interactionCreate', async interaction => {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
const { commandName } = interaction;
|
||||
|
||||
try {
|
||||
if (commandName === 'join') {
|
||||
const member = interaction.member;
|
||||
const voiceChannel = member.voice.channel;
|
||||
|
||||
if (!voiceChannel) {
|
||||
return interaction.reply({ content: 'Kamu harus berada di Voice Channel!', ephemeral: true });
|
||||
}
|
||||
|
||||
connection = joinVoiceChannel({
|
||||
channelId: voiceChannel.id,
|
||||
guildId: interaction.guildId,
|
||||
adapterCreator: interaction.guild.voiceAdapterCreator,
|
||||
});
|
||||
|
||||
connection.on(VoiceConnectionStatus.Disconnected, async (oldState, newState) => {
|
||||
try {
|
||||
await Promise.race([
|
||||
enterState(connection, VoiceConnectionStatus.Signalling, 5_000),
|
||||
enterState(connection, VoiceConnectionStatus.Connecting, 5_000),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('[Voice] Gagal menyambung kembali:', error);
|
||||
if (connection) connection.destroy();
|
||||
connection = null;
|
||||
}
|
||||
});
|
||||
|
||||
await interaction.reply({ content: `Bot berhasil join ke ${voiceChannel.name}! ✅` });
|
||||
}
|
||||
|
||||
if (commandName === 'test_sahur') {
|
||||
if (!connection) {
|
||||
return interaction.reply({ content: 'Bot harus join ke Voice Channel dulu! Gunakan /join', ephemeral: true });
|
||||
}
|
||||
|
||||
isLooping = true;
|
||||
const success = playAudio();
|
||||
|
||||
if (success) {
|
||||
await interaction.reply({ content: 'Memutar sahur.mp3 dalam mode loop... 🔁' });
|
||||
} else {
|
||||
await interaction.reply({ content: 'Gagal memutar audio. Cek Logs untuk detailnya. ❌', ephemeral: true });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Interaction Error] Command: ${commandName}`, error);
|
||||
|
||||
const errorMsg = 'Terjadi kesalahan saat menjalankan command ini. Silakan coba lagi.';
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.followUp({ content: errorMsg, ephemeral: true });
|
||||
} else {
|
||||
await interaction.reply({ content: errorMsg, ephemeral: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.login(DISCORD_TOKEN);
|
||||
457
index.php
457
index.php
@ -2,149 +2,360 @@
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
@date_default_timezone_set('Asia/Jakarta');
|
||||
|
||||
session_start();
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
$pdo = db();
|
||||
$pdo->exec(
|
||||
'CREATE TABLE IF NOT EXISTS sahur_settings ('
|
||||
. 'id INT PRIMARY KEY, '
|
||||
. 'sahur_hour TINYINT NOT NULL, '
|
||||
. 'sahur_minute TINYINT NOT NULL, '
|
||||
. 'updated_at DATETIME NOT NULL'
|
||||
. ')'
|
||||
);
|
||||
$pdo->exec(
|
||||
'CREATE TABLE IF NOT EXISTS sahur_events ('
|
||||
. 'id INT AUTO_INCREMENT PRIMARY KEY, '
|
||||
. 'event_type VARCHAR(32) NOT NULL, '
|
||||
. 'message VARCHAR(255) NOT NULL, '
|
||||
. 'meta TEXT NULL, '
|
||||
. 'created_at DATETIME NOT NULL'
|
||||
. ')'
|
||||
);
|
||||
|
||||
$settings = $pdo->query('SELECT id, sahur_hour, sahur_minute, updated_at FROM sahur_settings WHERE id = 1')->fetch();
|
||||
if (!$settings) {
|
||||
$stmt = $pdo->prepare('INSERT INTO sahur_settings (id, sahur_hour, sahur_minute, updated_at) VALUES (1, :hour, :minute, NOW())');
|
||||
$stmt->execute([':hour' => 3, ':minute' => 0]);
|
||||
$settings = ['id' => 1, 'sahur_hour' => 3, 'sahur_minute' => 0, 'updated_at' => date('Y-m-d H:i:s')];
|
||||
}
|
||||
|
||||
$flash = $_SESSION['flash'] ?? null;
|
||||
unset($_SESSION['flash']);
|
||||
|
||||
function redirect_with_flash(string $type, string $message): void {
|
||||
$_SESSION['flash'] = ['type' => $type, 'message' => $message];
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
if ($action === 'upload') {
|
||||
if (!isset($_FILES['sahur_file']) || $_FILES['sahur_file']['error'] !== UPLOAD_ERR_OK) {
|
||||
redirect_with_flash('danger', 'Upload gagal. Pilih file MP3 yang valid.');
|
||||
}
|
||||
|
||||
$file = $_FILES['sahur_file'];
|
||||
if ($file['size'] > 15000000) {
|
||||
redirect_with_flash('danger', 'Ukuran file terlalu besar. Maksimum 15MB.');
|
||||
}
|
||||
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mime = $finfo ? finfo_file($finfo, $file['tmp_name']) : '';
|
||||
if ($finfo) {
|
||||
finfo_close($finfo);
|
||||
}
|
||||
|
||||
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||
if ($mime !== 'audio/mpeg' || $ext !== 'mp3') {
|
||||
redirect_with_flash('danger', 'Format file harus MP3 (audio/mpeg).');
|
||||
}
|
||||
|
||||
$uploadDir = __DIR__ . '/uploads';
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0775, true);
|
||||
}
|
||||
|
||||
$targetPath = $uploadDir . '/sahur.mp3';
|
||||
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
|
||||
redirect_with_flash('danger', 'Upload gagal saat menyimpan file.');
|
||||
}
|
||||
|
||||
$meta = json_encode([
|
||||
'original_name' => $file['name'],
|
||||
'size' => $file['size'],
|
||||
]);
|
||||
$stmt = $pdo->prepare('INSERT INTO sahur_events (event_type, message, meta, created_at) VALUES (:type, :message, :meta, NOW())');
|
||||
$stmt->execute([
|
||||
':type' => 'upload',
|
||||
':message' => 'File sahur.mp3 diperbarui',
|
||||
':meta' => $meta,
|
||||
]);
|
||||
|
||||
redirect_with_flash('success', 'File sahur.mp3 berhasil diunggah dan diganti.');
|
||||
}
|
||||
|
||||
if ($action === 'set_schedule') {
|
||||
$hour = filter_input(INPUT_POST, 'sahur_hour', FILTER_VALIDATE_INT);
|
||||
$minute = filter_input(INPUT_POST, 'sahur_minute', FILTER_VALIDATE_INT);
|
||||
|
||||
if ($hour === false || $minute === false || $hour < 0 || $hour > 23 || $minute < 0 || $minute > 59) {
|
||||
redirect_with_flash('danger', 'Jam sahur tidak valid. Masukkan 0-23 untuk jam dan 0-59 untuk menit.');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE sahur_settings SET sahur_hour = :hour, sahur_minute = :minute, updated_at = NOW() WHERE id = 1');
|
||||
$stmt->execute([':hour' => $hour, ':minute' => $minute]);
|
||||
|
||||
$meta = json_encode(['hour' => $hour, 'minute' => $minute]);
|
||||
$stmt = $pdo->prepare('INSERT INTO sahur_events (event_type, message, meta, created_at) VALUES (:type, :message, :meta, NOW())');
|
||||
$stmt->execute([
|
||||
':type' => 'schedule',
|
||||
':message' => 'Jadwal sahur diperbarui',
|
||||
':meta' => $meta,
|
||||
]);
|
||||
|
||||
redirect_with_flash('success', 'Jadwal sahur berhasil diperbarui.');
|
||||
}
|
||||
|
||||
if ($action === 'test_now') {
|
||||
$stmt = $pdo->prepare('INSERT INTO sahur_events (event_type, message, meta, created_at) VALUES (:type, :message, :meta, NOW())');
|
||||
$stmt->execute([
|
||||
':type' => 'test',
|
||||
':message' => 'Tes manual dipicu dari dashboard',
|
||||
':meta' => null,
|
||||
]);
|
||||
|
||||
redirect_with_flash('success', 'Tes sahur dicatat. Integrasi bot akan memutar audio saat tersedia.');
|
||||
}
|
||||
}
|
||||
|
||||
$audioPath = __DIR__ . '/uploads/sahur.mp3';
|
||||
$fileExists = is_file($audioPath);
|
||||
$fileSize = $fileExists ? filesize($audioPath) : null;
|
||||
$fileUpdated = $fileExists ? date('Y-m-d H:i', filemtime($audioPath)) : null;
|
||||
|
||||
$now = new DateTime('now');
|
||||
$nextRun = (clone $now)->setTime((int)$settings['sahur_hour'], (int)$settings['sahur_minute'], 0);
|
||||
if ($nextRun <= $now) {
|
||||
$nextRun->modify('+1 day');
|
||||
}
|
||||
|
||||
$events = $pdo->query('SELECT id, event_type, message, created_at FROM sahur_events ORDER BY id DESC LIMIT 8')->fetchAll();
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Sahur Bot Control Center</title>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<!-- Twitter image -->
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
@keyframes bg-pan {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.loader {
|
||||
margin: 1.25rem auto 1.25rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.hint {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
code {
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/assets/css/custom.css?v=<?= htmlspecialchars((string)time()) ?>" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
<nav class="navbar navbar-expand-lg bg-white border-bottom sticky-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-semibold" href="/">Sahur Bot Control</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMain" aria-controls="navMain" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navMain">
|
||||
<div class="navbar-nav ms-auto">
|
||||
<a class="nav-link" href="#status">Status</a>
|
||||
<a class="nav-link" href="#upload">Upload</a>
|
||||
<a class="nav-link" href="#schedule">Jadwal</a>
|
||||
<a class="nav-link" href="#log">Log</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<header class="hero border-bottom">
|
||||
<div class="container py-5">
|
||||
<div class="row align-items-center g-4">
|
||||
<div class="col-lg-7">
|
||||
<p class="text-uppercase text-muted small mb-2">Dashboard Sahur</p>
|
||||
<h1 class="display-6 fw-semibold mb-3">Kendalikan jadwal sahur dan audio bot dalam satu tempat.</h1>
|
||||
<p class="text-muted mb-4">Upload MP3, ubah jam sahur, dan catat tes manual. Status file dan jadwal berikutnya selalu terlihat jelas.</p>
|
||||
<div class="d-flex gap-3">
|
||||
<a class="btn btn-dark btn-lg" href="#upload">Upload MP3</a>
|
||||
<a class="btn btn-outline-secondary btn-lg" href="#schedule">Ubah Jadwal</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-1">Next run</p>
|
||||
<h2 class="h4 fw-semibold mb-2"><?= htmlspecialchars($nextRun->format('d M Y, H:i')) ?> WIB</h2>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<span class="badge bg-light text-dark border"><?= htmlspecialchars(sprintf('%02d:%02d', $settings['sahur_hour'], $settings['sahur_minute'])) ?> (jadwal aktif)</span>
|
||||
<span class="badge bg-light text-dark border"><?= $fileExists ? 'File MP3 tersedia' : 'File MP3 belum ada' ?></span>
|
||||
</div>
|
||||
<p class="text-muted small mt-3 mb-0">Pastikan bot Discord kamu membaca jadwal yang sama.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container py-5">
|
||||
<?php if ($flash): ?>
|
||||
<div class="alert alert-<?= htmlspecialchars($flash['type']) ?> alert-dismissible fade show" role="alert">
|
||||
<?= htmlspecialchars($flash['message']) ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<section id="status" class="mb-5">
|
||||
<div class="section-head">
|
||||
<h2 class="h5 fw-semibold mb-1">Status Saat Ini</h2>
|
||||
<p class="text-muted small">Pantau kesiapan audio dan jadwal sahur berikutnya.</p>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-1">Audio sahur</p>
|
||||
<h3 class="h6 fw-semibold mb-2"><?= $fileExists ? 'Tersedia' : 'Belum tersedia' ?></h3>
|
||||
<p class="text-muted small mb-0"><?= $fileExists ? 'Ukuran: ' . number_format($fileSize / 1024, 1) . ' KB' : 'Upload file sahur.mp3 agar bot dapat memutar.' ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-1">Update terakhir</p>
|
||||
<h3 class="h6 fw-semibold mb-2"><?= $fileUpdated ? htmlspecialchars($fileUpdated . ' WIB') : '—' ?></h3>
|
||||
<p class="text-muted small mb-0">Penggantian file terakhir.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-1">Next run</p>
|
||||
<h3 class="h6 fw-semibold mb-2"><?= htmlspecialchars($nextRun->format('d M Y, H:i')) ?> WIB</h3>
|
||||
<p class="text-muted small mb-0">Jadwal sahur berdasarkan setting saat ini.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!$fileExists): ?>
|
||||
<div class="alert alert-warning mt-3 mb-0" role="alert">
|
||||
File sahur.mp3 belum ada. Bot Discord harus menampilkan peringatan di channel teks saat file belum tersedia.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section id="upload" class="mb-5">
|
||||
<div class="section-head">
|
||||
<h2 class="h5 fw-semibold mb-1">Upload MP3 Sahur</h2>
|
||||
<p class="text-muted small">Drag & drop atau klik untuk memilih file. File akan ditimpa menjadi sahur.mp3.</p>
|
||||
</div>
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<form class="row g-3" action="/" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="action" value="upload" />
|
||||
<div class="col-12">
|
||||
<div class="dropzone" data-dropzone>
|
||||
<input class="form-control" type="file" name="sahur_file" accept=".mp3,audio/mpeg" required>
|
||||
<div class="dropzone-placeholder">
|
||||
<span class="fw-semibold">Tarik file ke sini</span>
|
||||
<span class="text-muted small">atau klik untuk memilih</span>
|
||||
<span class="text-muted small" data-file-name>Belum ada file dipilih</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-dark">Upload & Ganti File</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="schedule" class="mb-5">
|
||||
<div class="section-head">
|
||||
<h2 class="h5 fw-semibold mb-1">Jadwal Sahur</h2>
|
||||
<p class="text-muted small">Atur jam sahur harian yang akan digunakan bot.</p>
|
||||
</div>
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<form class="row g-3 align-items-end" action="/" method="post">
|
||||
<input type="hidden" name="action" value="set_schedule" />
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="sahur_hour">Jam</label>
|
||||
<input class="form-control" type="number" id="sahur_hour" name="sahur_hour" min="0" max="23" value="<?= htmlspecialchars((string)$settings['sahur_hour']) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="sahur_minute">Menit</label>
|
||||
<input class="form-control" type="number" id="sahur_minute" name="sahur_minute" min="0" max="59" value="<?= htmlspecialchars((string)$settings['sahur_minute']) ?>" required>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-dark flex-fill">Simpan Jadwal</button>
|
||||
<button type="submit" class="btn btn-outline-secondary flex-fill" name="action" value="test_now">Tes Sekarang</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="log" class="mb-5">
|
||||
<div class="section-head">
|
||||
<h2 class="h5 fw-semibold mb-1">Log Aktivitas</h2>
|
||||
<p class="text-muted small">Riwayat upload, perubahan jadwal, dan tes manual.</p>
|
||||
</div>
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<?php if (!$events): ?>
|
||||
<p class="text-muted mb-0">Belum ada aktivitas. Mulai dengan upload file sahur pertama.</p>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Waktu</th>
|
||||
<th scope="col">Tipe</th>
|
||||
<th scope="col">Pesan</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($events as $event): ?>
|
||||
<tr>
|
||||
<td class="text-muted small"><?= htmlspecialchars($event['created_at']) ?></td>
|
||||
<td><span class="badge bg-light text-dark border text-uppercase"><?= htmlspecialchars($event['event_type']) ?></span></td>
|
||||
<td><?= htmlspecialchars($event['message']) ?></td>
|
||||
<td class="text-end"><a class="link-dark" href="/event.php?id=<?= htmlspecialchars((string)$event['id']) ?>">Detail</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
|
||||
<footer class="border-top py-4">
|
||||
<div class="container d-flex flex-column flex-md-row justify-content-between gap-2">
|
||||
<span class="text-muted small">Sahur Bot Dashboard (MVP)</span>
|
||||
<span class="text-muted small">Zona waktu: WIB (Asia/Jakarta)</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/assets/js/main.js?v=<?= htmlspecialchars((string)time()) ?>" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
1522
package-lock.json
generated
Normal file
1522
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
package.json
Normal file
22
package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "workspace",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node index.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@discordjs/voice": "^0.19.0",
|
||||
"discord.js": "^14.25.1",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"ffmpeg-static": "^5.3.0",
|
||||
"libsodium-wrappers": "^0.8.2",
|
||||
"opusscript": "^0.0.8"
|
||||
}
|
||||
}
|
||||
1
uploads/README.txt
Normal file
1
uploads/README.txt
Normal file
@ -0,0 +1 @@
|
||||
Place sahur.mp3 in this folder.
|
||||
BIN
uploads/sahur.mp3
Normal file
BIN
uploads/sahur.mp3
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user