- Implemented across bootstrap.php, chat frontend, and dashboard:
Per-tier audio bitrate: Free 64, Plus 96, Pro 128 kbps. Configurable text/voice reply threads and depth. Registration/login-only honeypot and internal CAPTCHA. Optional Google reCAPTCHA v2/v3 with server verification. Archive/export support for replies and bitrate. Dashboard controls for all settings. Verified JavaScript, JSON, SQLite migrations, desktop/mobile UI, and browser console. Native PHP lint was unavailable because PHP is not installed. Google implementation follows Siteverify and reCAPTCHA v3.
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
|
||||
define('ROOT_DIR', __DIR__);
|
||||
define('CONFIG_FILE', ROOT_DIR . '/config.json');
|
||||
define('ADMIN_VERSION', '2.1.0');
|
||||
define('ADMIN_VERSION', '2.2.0');
|
||||
require_once ROOT_DIR . '/bootstrap.php';
|
||||
require_once ROOT_DIR . '/lib/admin_platform.php';
|
||||
|
||||
@@ -47,6 +47,9 @@ function ensureAdminMessageColumns(PDO $d): void {
|
||||
'voice_file' => 'TEXT',
|
||||
'voice_mime' => 'TEXT',
|
||||
'voice_duration' => 'REAL',
|
||||
'voice_bitrate_kbps' => 'INTEGER',
|
||||
'reply_to_id' => 'INTEGER',
|
||||
'reply_depth' => 'INTEGER NOT NULL DEFAULT 0',
|
||||
] as $name => $definition) {
|
||||
if (!isset($columns[$name])) $d->exec("ALTER TABLE messages ADD COLUMN $name $definition");
|
||||
}
|
||||
@@ -62,6 +65,7 @@ function archiveDB(string $year, string $month, string $day): ?PDO {
|
||||
ensureAdminMessageColumns($a);
|
||||
removeLegacyGroupSchema($a, false);
|
||||
$a->exec("CREATE INDEX IF NOT EXISTS idx_archive_user ON messages(user_id,created_at)");
|
||||
$a->exec("CREATE INDEX IF NOT EXISTS idx_archive_reply ON messages(reply_to_id)");
|
||||
return $a;
|
||||
}
|
||||
|
||||
@@ -84,6 +88,9 @@ function createArchiveDB(string $year, string $month, string $day): PDO {
|
||||
voice_file TEXT,
|
||||
voice_mime TEXT,
|
||||
voice_duration REAL,
|
||||
voice_bitrate_kbps INTEGER,
|
||||
reply_to_id INTEGER,
|
||||
reply_depth INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL,
|
||||
day_key TEXT NOT NULL
|
||||
)");
|
||||
@@ -94,6 +101,7 @@ function createArchiveDB(string $year, string $month, string $day): PDO {
|
||||
value TEXT
|
||||
)");
|
||||
$a->exec("CREATE INDEX IF NOT EXISTS idx_archive_user ON messages(user_id,created_at)");
|
||||
$a->exec("CREATE INDEX IF NOT EXISTS idx_archive_reply ON messages(reply_to_id)");
|
||||
return $a;
|
||||
}
|
||||
|
||||
@@ -223,11 +231,13 @@ if ($isLoggedIn && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
if ($existingFile === false) {
|
||||
$insert = $adb->prepare("INSERT INTO messages
|
||||
(id,user_id,username,color,message,message_type,voice_file,voice_mime,voice_duration,created_at,day_key)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)");
|
||||
(id,user_id,username,color,message,message_type,voice_file,voice_mime,voice_duration,
|
||||
voice_bitrate_kbps,reply_to_id,reply_depth,created_at,day_key)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
|
||||
$insert->execute([
|
||||
$row['id'], $row['user_id'], $row['username'], $row['color'], $row['message'],
|
||||
$row['message_type'], $row['voice_file'], $row['voice_mime'], $row['voice_duration'],
|
||||
$row['voice_bitrate_kbps'], $row['reply_to_id'], $row['reply_depth'],
|
||||
$row['created_at'], $row['day_key']
|
||||
]);
|
||||
}
|
||||
@@ -1114,12 +1124,14 @@ td.actions{white-space:nowrap;width:1px}
|
||||
<td style="color:<?= esc($msg['color']) ?>;font-family:var(--mono);font-size:11px;white-space:nowrap"><?= esc($msg['username']) ?></td>
|
||||
<td>
|
||||
<?php if (($msg['message_type'] ?? 'text') === 'voice' && !empty($msg['voice_file'])): ?>
|
||||
<div class="badge badge-b" style="margin-bottom:4px">VOICE · <?= esc(round((float)$msg['voice_duration'], 1)) ?>s</div>
|
||||
<div class="badge badge-b" style="margin-bottom:4px">VOICE · <?= esc(round((float)$msg['voice_duration'], 1)) ?>s
|
||||
<?= !empty($msg['voice_bitrate_kbps']) ? ' · ' . (int)$msg['voice_bitrate_kbps'] . ' kbps' : '' ?></div>
|
||||
<audio class="admin-audio" controls preload="metadata"
|
||||
src="<?= esc(trim((string)cfgVal('voice.upload_dir', 'uploads/voice'), '/') . '/' . rawurlencode($msg['voice_file'])) ?>"></audio>
|
||||
<?php else: ?>
|
||||
<div class="msg-preview" title="<?= esc($msg['message']) ?>"><?= esc($msg['message']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($msg['reply_to_id'])): ?><div class="dim">Reply #<?= (int)$msg['reply_to_id'] ?> · depth <?= (int)$msg['reply_depth'] ?></div><?php endif; ?>
|
||||
</td>
|
||||
<td class="dim" style="white-space:nowrap"><?= fmtTime((int)$msg['created_at']) ?></td>
|
||||
<td class="actions">
|
||||
@@ -1392,12 +1404,14 @@ td.actions{white-space:nowrap;width:1px}
|
||||
<td style="color:<?= esc($msg['color']) ?>;font-family:var(--mono);font-size:11px"><?= esc($msg['username']) ?></td>
|
||||
<td>
|
||||
<?php if (($msg['message_type'] ?? 'text') === 'voice' && !empty($msg['voice_file'])): ?>
|
||||
<div class="badge badge-b" style="margin-bottom:4px">VOICE · <?= esc(round((float)$msg['voice_duration'], 1)) ?>s</div>
|
||||
<div class="badge badge-b" style="margin-bottom:4px">VOICE · <?= esc(round((float)$msg['voice_duration'], 1)) ?>s
|
||||
<?= !empty($msg['voice_bitrate_kbps']) ? ' · ' . (int)$msg['voice_bitrate_kbps'] . ' kbps' : '' ?></div>
|
||||
<audio class="admin-audio" controls preload="metadata"
|
||||
src="<?= esc(trim((string)cfgVal('voice.upload_dir', 'uploads/voice'), '/') . '/' . rawurlencode($msg['voice_file'])) ?>"></audio>
|
||||
<?php else: ?>
|
||||
<div class="msg-preview" title="<?= esc($msg['message']) ?>"><?= esc($msg['message']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($msg['reply_to_id'])): ?><div class="dim">Reply #<?= (int)$msg['reply_to_id'] ?> · depth <?= (int)$msg['reply_depth'] ?></div><?php endif; ?>
|
||||
</td>
|
||||
<td class="dim" style="white-space:nowrap"><?= fmtTime((int)$msg['created_at']) ?></td>
|
||||
<td class="actions">
|
||||
@@ -1724,6 +1738,8 @@ td.actions{white-space:nowrap;width:1px}
|
||||
<tr><td class="mono">chat.poll_timeout_ms</td><td class="dim">integer</td><td>Abort and retry a stalled poll after this many milliseconds</td></tr>
|
||||
<tr><td class="mono">chat.poll_max_backoff_ms</td><td class="dim">integer</td><td>Maximum delay after repeated poll failures</td></tr>
|
||||
<tr><td class="mono">chat.max_rendered_messages</td><td class="dim">integer</td><td>Maximum chat rows retained in the browser</td></tr>
|
||||
<tr><td class="mono">chat.replies_enabled</td><td class="dim">bool</td><td>Enable reply-to and threaded display</td></tr>
|
||||
<tr><td class="mono">chat.reply_max_depth</td><td class="dim">integer</td><td>Maximum nested reply depth</td></tr>
|
||||
<tr><td class="mono">chat.session_lock_ip</td><td class="dim">bool</td><td>Lock session to IP</td></tr>
|
||||
<tr><td class="mono">voice.enabled</td><td class="dim">bool</td><td>Show voice recording controls</td></tr>
|
||||
<tr><td class="mono">voice.max_duration_seconds</td><td class="dim">integer</td><td>Maximum voice clip duration</td></tr>
|
||||
@@ -1736,6 +1752,7 @@ td.actions{white-space:nowrap;width:1px}
|
||||
<tr><td class="mono">voice.transcoding.timeout_seconds</td><td class="dim">integer</td><td>FFmpeg upload conversion timeout</td></tr>
|
||||
<tr><td class="mono">voice.transcoding.fallback_to_source</td><td class="dim">bool</td><td>Retain WebM/MP4 source when conversion fails</td></tr>
|
||||
<tr><td class="mono">security.min_password_length</td><td class="dim">integer</td><td>Min password chars</td></tr>
|
||||
<tr><td class="mono">security.captcha.*</td><td class="dim">object</td><td>Registration/login honeypot, internal, and Google CAPTCHA settings</td></tr>
|
||||
<tr><td class="mono">security.session_timeout_hours</td><td class="dim">integer</td><td>Session expiry in hours</td></tr>
|
||||
<tr><td class="mono">security.bcrypt_cost</td><td class="dim">integer</td><td>bcrypt work factor (10–14)</td></tr>
|
||||
<tr><td class="mono">ui.app_title</td><td class="dim">string</td><td>Header title text</td></tr>
|
||||
|
||||
Reference in New Issue
Block a user