Skip to content

Commit 7604fd8

Browse files
1Git2CloneDeepSeek V4 Pro
andcommitted
feat(commands): add /util download (yt-dlp + ffmpeg 2-pass) and group utility commands
Adds /util download <url> [start] [end] that pulls media via yt-dlp, trims with ffmpeg if a range is given, and 2-pass compresses to an 8MB budget (mirroring compress_video.py from https://github.com/1Git2Clone/dotfiles/blob/main/dot-config/programs/py_scripts/compress_video.py: bitrate = target_size / duration, pass 1 analysis, pass 2 encode with audio, ~0.94 muxing margin, 64 kbps sanity floor, skip-if-already-small). Uploads the result as a Discord attachment — no S3/CDN. Groups existing utility commands under /util parent (per #34): - /util avatar (was /avatar) - /util uptime (was /uptime) - /util age (was /age) Old standalone commands remain registered for backward compatibility. Gated behind new util-download Cargo feature (util-download = ["dep:ffmpeg-sidecar", "dep:tempfile", "dep:url"]) matching the existing ai-* / redis style. Security and operational hardening: - Input validation: validate_url() rejects non-http/https schemes, userinfo, private/loopback/link-local IPs (SSRF defense), and flag-like -- content in path/query. Domain allowlist restricts to yt-dlp-supported social media platforms (YouTube, Facebook, Instagram, TikTok, Twitter/X, Reddit, Vimeo). - validate_timecode() rejects shell metacharacters, spaces, and only allows pure digits (seconds) or HH:MM:SS / MM:SS format. - Async subprocesses: tokio::process::Command + .await so the tokio worker thread isn't blocked during ffmpeg encodes. - Per-subprocess timeouts (yt-dlp: 3m, ffprobe: 30s, trim: 1m, each encode pass: 4m) fitting within Discord's 15-min interaction token lifetime. Child processes are killed on timeout. - --no-playlist guard on yt-dlp to reject playlist URLs upfront. - User-facing error messages via ctx.say() instead of silent ? propagation. - Cross-platform: /dev/null replaced with a temp file inside temp_dir. - ffmpeg2pass log paths via -passlogfile pointing inside temp_dir. 5-minute max source duration guard. Co-authored-by: DeepSeek V4 Pro <service@deepseek.com>
1 parent ed49200 commit 7604fd8

12 files changed

Lines changed: 838 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- `util-download` Cargo feature — `/util download <url> [start] [end]` downloads media via `yt-dlp`, optionally trims with `ffmpeg`, then 2-pass encodes targeting 8 MB for Discord attachment upload. Requires `yt-dlp` and `ffmpeg` (with `ffprobe`) on `PATH`
13+
- `/util` parent command group — groups `/avatar`, `/uptime`, `/age`, and `/util download` (when `util-download` is enabled) under a single `/util` parent. The old standalone `/avatar`, `/uptime`, and `/age` commands remain registered for backward compatibility
1214
- `ai-openrouter` Cargo feature — adds OpenRouter as a persona-chat backend alongside `ai-deepseek`, `ai-ollama`, `ai-anthropic`, `ai-openai`, `ai-google`, and `ai-groq`. Set `AI_API_KEY` to an OpenRouter key and `AI_MODEL` to an OpenRouter model id (e.g. `deepseek/deepseek-chat`)
1315

1416
### Fixed

Cargo.lock

Lines changed: 164 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ redis = { version = "1.2.2", features = [
4949
"connection-manager",
5050
], optional = true }
5151
jsonwebtoken = { version = "9", optional = true }
52+
ffmpeg-sidecar = { version = "2.5", optional = true }
53+
url = { version = "2.5", optional = true }
5254

5355
[features]
5456
serde = ["dep:serde"]
@@ -73,3 +75,6 @@ ai-openai = ["ai", "llm/openai"]
7375
ai-google = ["ai", "llm/google"]
7476
ai-groq = ["ai", "llm/groq"]
7577
ai-openrouter = ["ai", "llm/openrouter"]
78+
79+
# Download & compress media from URLs (yt-dlp + ffmpeg-sidecar)
80+
util-download = ["dep:ffmpeg-sidecar", "dep:tempfile", "dep:url"]

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ configuration, deployment, AI features, and observability.
6363
- Embed interaction commands: tieup, pat, hug, kiss, slap, punch, bonk, nom, kill, kick, bury, selfbury, peek, avatar, drive, chair, boom, quote
6464
- XP levelling with a 60-second cooldown, stored in PostgreSQL — `/level` for a member's level, `/toplevels` for the server leaderboard
6565
- `/reminder` — schedule a DM for later (`create`/`list`/`search`/`delete`), with a saveable default timezone (`/reminder timezone`, per-server or global) and browsable, paginated history
66+
- `/util` — utility commands: `/util avatar`, `/util uptime`, `/util age`, and `/util download` (when `util-download` feature is enabled)
6667
- `/age` — your or another member's account creation date
6768
- `/cookie` — give someone a cookie
6869
- `/uptime` — bot uptime
@@ -83,6 +84,7 @@ also enables `redis` — and it requires picking exactly one `ai-<backend>`.
8384
| `ai` | The AI persona and `/ai-review` | Meta-feature: also enables `redis`. Needs a backend (below). |
8485
| `ai-<backend>` | The AI provider | Exactly one of `ai-deepseek`, `ai-ollama`, `ai-anthropic`, `ai-openai`, `ai-google`, `ai-groq`, `ai-openrouter`. Mandatory when `ai` is on. |
8586
| `opentelemetry` | OTLP trace export | Point at any OTLP collector; compose ships Tempo + Grafana. |
87+
| `util-download` | `/util download` — yt-dlp + ffmpeg 2-pass encode targeting 8 MB | Needs `yt-dlp` and `ffmpeg` (with `ffprobe`) on `PATH`. |
8688
| `tokio_console` | Tokio Console runtime inspection | Needs `RUSTFLAGS="--cfg tokio_unstable"`. |
8789

8890
Building with `--features ai` and no backend stops at a `compile_error!` by

docs/configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ the full inventory and handling.
4545
- `ai` is a meta-feature: enabling it also enables `redis`. It does not pick a
4646
provider — you must enable exactly one `ai-<backend>`. See the
4747
[README feature matrix](../README.md#features) and [docs/ai.md](./ai.md).
48+
- `util-download` needs `yt-dlp` and `ffmpeg` (with `ffprobe`) on `PATH` at
49+
runtime. No additional environment variables.
4850
- `tokio_console` additionally needs `RUSTFLAGS="--cfg tokio_unstable"` at
4951
build time. See [docs/observability.md](./observability.md).
5052
- Sharding is opt-in: set all three of `TOTAL_SHARDS`, `SHARD_START`, and

src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ pub mod embed_commands;
33
pub mod general_commands;
44
pub mod level_cmds;
55
pub mod level_logic;
6+
pub mod util;

src/commands/util/age.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use crate::prelude::*;
2+
3+
/// Displays your or another user's account creation date
4+
#[poise::command(discard_spare_arguments, slash_command, prefix_command)]
5+
#[tracing::instrument(
6+
skip(ctx),
7+
fields(
8+
category = "discord_command",
9+
command.name = %ctx.command().name,
10+
author = %ctx.author().id,
11+
target_user = %user.as_ref().map(|u| u.id.get()).unwrap_or(0),
12+
guild_id = %ctx.guild_id().map(GuildId::get).unwrap_or(0),
13+
)
14+
)]
15+
pub async fn age(
16+
ctx: Context<'_>,
17+
#[description = "Selected user"] user: Option<serenity::User>,
18+
) -> Result<(), Error> {
19+
let target_replied_user = user
20+
.as_ref()
21+
.unwrap_or(cmd_utils::get_replied_user(ctx).await);
22+
let response = format!(
23+
"**{}**'s account was created at {}",
24+
target_replied_user.name,
25+
target_replied_user.created_at()
26+
);
27+
ctx.say(response).await?;
28+
Ok(())
29+
}

src/commands/util/avatar.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use crate::prelude::*;
2+
3+
/// Get the avatar for someone.
4+
#[poise::command(discard_spare_arguments, slash_command, prefix_command)]
5+
#[tracing::instrument(
6+
skip(ctx),
7+
fields(
8+
category = "discord_command",
9+
command.name = %ctx.command().name,
10+
author = %ctx.author().id,
11+
target_user = %user.as_ref().map(|u| u.id.get()).unwrap_or(0),
12+
guild_id = %ctx.guild_id().map(GuildId::get).unwrap_or(0),
13+
)
14+
)]
15+
pub async fn avatar(
16+
ctx: Context<'_>,
17+
#[description = "Selected user"] user: Option<serenity::User>,
18+
) -> Result<(), Error> {
19+
let target = user
20+
.as_ref()
21+
.unwrap_or(cmd_utils::get_replied_user(ctx).await);
22+
23+
let embed = serenity::CreateEmbed::new()
24+
.title(format!("**{}**'s avatar:", target.name))
25+
.color((255, 0, 0))
26+
.image(target.face().replace(".webp", ".png"));
27+
ctx.send(poise::CreateReply::default().embed(embed)).await?;
28+
29+
Ok(())
30+
}

0 commit comments

Comments
 (0)