Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"permissions": {
"ask": ["Bash(./prod.sh*)"]
},
"hooks": {
"Stop": [
{
Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bunx oxlint --deny-warnings
# download-video.ts writes its info cache under /storage at import time
- run: sudo mkdir -p -m 777 /storage
- run: bun test
env:
BOT_TOKEN: dummy
OWNER_ID: '0'

# NOTE: no e2e job. Tried in 9dce83d: reddit/youtube hard-block GitHub's
# datacenter IPs (403 / auth walls), so e2e only works from residential
# IPs. It runs as the pre-push git hook instead (see .simple-git-hooks.json).
3 changes: 2 additions & 1 deletion .simple-git-hooks.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"pre-commit": "bun run lint"
"pre-commit": "./check.sh",
"pre-push": "./e2e.sh"
}
14 changes: 14 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# mp4ify-bot

Telegram bot (@mp4ify_bot): send it a video link, it replies with the mp4.
Bun + Telegraf + yt-dlp, deployed via Docker Compose with a local
telegram-bot-api server.

- Tests only work inside the test container (/storage is root-owned on the
host):
`UID=$(id -u) GID=$(id -g) docker compose run --rm --no-deps test bun test`
- Everything goes on a branch + PR (main is protected). Before opening the
PR, run /pr-review-toolkit:review-pr, then /code-review --fix.
- Never assume Telegram API behavior from the docs — verify against real
payloads and keep MockBotApi (test/simulate-bot-api.ts) in parity.
- X/Twitter is deliberately unsupported, for moral reasons. Do not add it.
4 changes: 4 additions & 0 deletions bunfig.e2e.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# bunfig for e2e runs (./e2e.sh): no coverage thresholds, since a partial
# run can't meet whole-suite coverage. Keep preload in sync with bunfig.toml.
[test]
preload = ["./test/simulate-bot-api"]
3 changes: 3 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
# This has to load first so that the mock of node-fetch applies to Telegraf:
preload = ["./test/simulate-bot-api"]
coverage = true
# Quality gate: bun test fails if any file's coverage drops below these.
# If they block you, write better tests - don't lower them.
coverageThreshold = { lines = 0.95, functions = 0.9 }
16 changes: 16 additions & 0 deletions check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/sh
# Quality gate: lint, secret scan, full test suite (with coverage thresholds).
# Runs as the pre-commit hook; can also be run manually at any time.
set -e

bun run lint

if command -v gitleaks >/dev/null 2>&1; then
gitleaks git --pre-commit --staged --no-banner --redact
else
echo "ERROR: gitleaks not installed - refusing to commit unscanned changes." >&2
echo "Install: https://github.com/gitleaks/gitleaks/releases (single static binary)" >&2
exit 1
fi

UID=$(id -u) GID=$(id -g) docker compose run --rm --no-deps test bun test
18 changes: 17 additions & 1 deletion e2e.sh
Original file line number Diff line number Diff line change
@@ -1,2 +1,18 @@
#!/bin/sh
docker compose run --remove-orphans --rm -it test bash -c 'TEST_E2E=true bun test e2e'
# Usage: ./e2e.sh [full] [-u]
# full also test rate-limit-prone sites (youtube rejects more than a few
# hits per hour). Used by the deploy gate (prod.sh); the pre-push
# hook runs the reduced set.
# -u refresh snapshots. Implies full: `bun test -u` deletes snapshots of
# tests it didn't run, so a reduced -u run would silently prune the
# full-mode snapshots.
FULL=""
UPDATE=""
for arg in "$@"; do
case "$arg" in
full) FULL=1 ;;
-u) UPDATE="-u"; FULL=1 ;;
*) echo "unknown argument: $arg" >&2 && exit 64 ;;
esac
done
docker compose run --remove-orphans --rm -T -e TEST_E2E=true -e TEST_E2E_FULL="$FULL" test bun --config=bunfig.e2e.toml test e2e $UPDATE
3 changes: 1 addition & 2 deletions prod.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
#!/bin/sh
#./e2e.sh && UID=$(id -u) GID=$(id -g) docker compose up prod bot-api --build -d
UID=$(id -u) GID=$(id -g) docker compose up prod bot-api --build -d
./e2e.sh full && UID=$(id -u) GID=$(id -g) docker compose up prod bot-api --build -d
7 changes: 1 addition & 6 deletions src/download-video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,12 @@ export type VideoInfo = {
}[];
};

// Cloud environments (e.g. Claude Code Web) often have SSL interception
const certFlags = () =>
Bun.env.CLAUDE_CODE_REMOTE === 'true' ? ['--no-check-certificates'] : [];

// Self-update yt-dlp so extractors keep up with site changes (e.g. Reddit
// requiring auth from older versions). Never throws: a failed update just
// means we keep using the current version.
export const updateYtdlp = async () => {
try {
const proc = Bun.spawn(['yt-dlp', '--update', ...certFlags()], {
const proc = Bun.spawn(['yt-dlp', '--update'], {
stdout: 'pipe',
stderr: 'pipe',
timeout: 120_000,
Expand Down Expand Up @@ -85,7 +81,6 @@ const execYtdlp = async (
'yt-dlp',
url,
verbose ? '--verbose' : '--no-warnings',
...certFlags(),
...extraArgs,
];
console.debug(command.join(' '));
Expand Down
Loading
Loading