Files
local-ai/configure-searxng-safesearch.sh
T
Claude 5bfedf2cee consolidate everything into the repo folder; add compose management comments
All scripts now default BASE to their own directory (SCRIPT_DIR) instead
of ~/docker/ai-stack, so after a git clone the user never needs to change
folders — docker-compose.yml, .env, settings, and data dirs all live next
to the scripts.

configure-searxng-safesearch.sh and configure-storage.sh do the same for
standalone use (still honoured when called with BASE= from a parent script).

docker-compose.yml (generated by both setup scripts) gains a comment block
at the top listing the everyday docker compose commands:
  up -d / down / restart / stop / logs -f / pull / ps
so the file itself is the reference for managing containers.

.gitignore added to exclude generated files (docker-compose.yml, .env,
requirements.txt, helper scripts) and data directories (workspace/, kiwix/,
searxng/, gitea/, etc.) from git tracking.

https://claude.ai/code/session_012gDnantBmFTWZGCiKyjazx
2026-03-21 15:38:20 +00:00

235 lines
9.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# configure-searxng-safesearch.sh
# Set SearXNG safe-search level; optionally disable categories or engines.
#
# Usage:
# ./configure-searxng-safesearch.sh [strict|moderate|none] [OPTIONS]
#
# Options:
# --disable-categories cat1,cat2 videos images news science social
# --disable-engines eng1,eng2 e.g. duckduckgo,bing,yandex
# --enable-engines eng1,eng2 re-enable engines auto-disabled by level
#
# Examples:
# ./configure-searxng-safesearch.sh strict
# ./configure-searxng-safesearch.sh moderate --disable-categories videos,images
# ./configure-searxng-safesearch.sh none --disable-engines bing,duckduckgo
# ./configure-searxng-safesearch.sh strict --disable-categories videos \
# --disable-engines bing --enable-engines yandex
set -euo pipefail
# ── Helpers ───────────────────────────────────────────────────────────────────
red() { printf '\e[31m%s\e[0m\n' "$*"; }
grn() { printf '\e[32m%s\e[0m\n' "$*"; }
blu() { printf '\e[34m%s\e[0m\n' "$*"; }
yel() { printf '\e[33m%s\e[0m\n' "$*"; }
die() { red "ERROR: $*"; exit 1; }
ok() { grn " ✓ $*"; }
info() { blu " → $*"; }
warn() { yel " ! $*"; }
# ── Defaults ──────────────────────────────────────────────────────────────────
LEVEL="moderate"
DISABLE_CATS=""
DISABLE_ENGINES_EXTRA=""
ENABLE_ENGINES_EXTRA=""
BASE="${BASE:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
# ── Parse arguments ───────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
strict|moderate|none) LEVEL="$1"; shift ;;
--disable-categories) DISABLE_CATS="$2"; shift 2 ;;
--disable-engines) DISABLE_ENGINES_EXTRA="$2"; shift 2 ;;
--enable-engines) ENABLE_ENGINES_EXTRA="$2"; shift 2 ;;
--base) BASE="$2"; shift 2 ;;
-h|--help)
sed -n '2,20p' "$0" | sed 's/^# \?//'
exit 0
;;
*) die "Unknown argument: $1 (run with --help)" ;;
esac
done
SETTINGS="$BASE/searxng/settings.yml"
COMPOSE="$BASE/docker-compose.yml"
# Ensure settings directory exists (fix ownership if Docker created it as root)
mkdir -p "$(dirname "$SETTINGS")" 2>/dev/null || \
sudo mkdir -p "$(dirname "$SETTINGS")"
if [[ -e "$SETTINGS" ]] && [[ ! -w "$SETTINGS" ]]; then
warn "settings.yml not writable — fixing ownership"
sudo chown "$(id -u):$(id -g)" "$SETTINGS" || \
die "Cannot write to $SETTINGS — run: sudo chown $USER $SETTINGS"
fi
# ── Level → integer ───────────────────────────────────────────────────────────
case "$LEVEL" in
none) SAFE_INT=0 ;;
moderate) SAFE_INT=1 ;;
strict) SAFE_INT=2 ;;
esac
info "Safe search: $LEVEL (${SAFE_INT})"
# ── Engines with no safe-search support ───────────────────────────────────────
# Auto-disabled when level is moderate or strict.
NO_SAFESEARCH_ENGINES=(
# Torrent / P2P — no filtering possible
"1337x" "piratebay" "nyaa" "torrentz" "kickass torrents"
# Web engines without safe-search API
"mojeek" "naver" "baidu"
# Yandex: parameter exists but not reliably enforced for non-Russian queries
"yandex" "yandex images"
# Video frontends — no safe-search passthrough
"invidious" "piped" "peertube" "sepiasearch"
)
# ── Category → engine lists ───────────────────────────────────────────────────
VIDEOS_ENGINES=(
"youtube" "bing videos" "brave videos" "duckduckgo videos" "google videos" "qwant videos"
"dailymotion" "media.ccc.de" "wikcommons.videos"
"vimeo" "odysee" "rumble" "bitchute"
"invidious" "piped" "peertube" "sepiasearch"
)
IMAGES_ENGINES=(
"google images" "bing images" "duckduckgo images" "brave images" "qwant images"
"startpage images" "mojeek images" "presearch images"
"openverse" "unsplash" "pexels" "pixabay images" "pinterest" "flickr"
"wikcommons.images" "artic" "yandex images"
"imgur" "deviantart" "artstation" "adobe stock"
)
NEWS_ENGINES=(
"google news" "bing news" "duckduckgo news" "brave news" "qwant news"
"startpage news" "presearch news" "mojeek news"
"reuters" "yahoo news" "wikinews" "yep news"
)
SCIENCE_ENGINES=(
"arxiv" "semantic scholar" "pubmed" "crossref" "base"
)
SOCIAL_ENGINES=(
"reddit" "lemmy" "mastodon"
)
# ── Build disable/enable maps ─────────────────────────────────────────────────
declare -A DISABLE_MAP # engine → 1
declare -A ENABLE_MAP # engine → 1 (overrides everything)
# Parse --enable-engines
if [[ -n "$ENABLE_ENGINES_EXTRA" ]]; then
IFS=',' read -ra _engs <<< "$ENABLE_ENGINES_EXTRA"
for e in "${_engs[@]}"; do
e="${e#"${e%%[![:space:]]*}"}"; e="${e%"${e##*[![:space:]]}"}" # trim
[[ -n "$e" ]] && ENABLE_MAP["$e"]=1
done
fi
# Helper: add to DISABLE_MAP unless explicitly re-enabled
mark_disabled() {
local eng="$1"
# Use set +u to safely check array key existence on bash < 4.4 (set -u quirk)
set +u; local _chk="${ENABLE_MAP[$eng]+x}"; set -u
[[ -n "$_chk" ]] && return # user said keep it
DISABLE_MAP["$eng"]=1
}
# Auto-disable no-safesearch engines for moderate/strict
if [[ "$LEVEL" != "none" ]]; then
for eng in "${NO_SAFESEARCH_ENGINES[@]}"; do
mark_disabled "$eng"
done
fi
# Category disables
if [[ -n "$DISABLE_CATS" ]]; then
IFS=',' read -ra _cats <<< "$DISABLE_CATS"
for cat in "${_cats[@]}"; do
cat="${cat#"${cat%%[![:space:]]*}"}"; cat="${cat%"${cat##*[![:space:]]}"}"
cat="${cat,,}"
case "$cat" in
videos) for e in "${VIDEOS_ENGINES[@]}"; do mark_disabled "$e"; done ;;
images) for e in "${IMAGES_ENGINES[@]}"; do mark_disabled "$e"; done ;;
news) for e in "${NEWS_ENGINES[@]}"; do mark_disabled "$e"; done ;;
science) for e in "${SCIENCE_ENGINES[@]}"; do mark_disabled "$e"; done ;;
social) for e in "${SOCIAL_ENGINES[@]}"; do mark_disabled "$e"; done ;;
"") ;;
*) warn "Unknown category '$cat' — valid: videos images news science social" ;;
esac
done
fi
# Extra engine disables
if [[ -n "$DISABLE_ENGINES_EXTRA" ]]; then
IFS=',' read -ra _engs <<< "$DISABLE_ENGINES_EXTRA"
for e in "${_engs[@]}"; do
e="${e#"${e%%[![:space:]]*}"}"; e="${e%"${e##*[![:space:]]}"}"
[[ -n "$e" ]] && mark_disabled "$e"
done
fi
# ── Preserve existing secret key ─────────────────────────────────────────────
SECRET_KEY=$(grep -oP '(?<=secret_key: ")[^"]+' "$SETTINGS" 2>/dev/null || true)
[[ -z "$SECRET_KEY" ]] && SECRET_KEY=$(openssl rand -hex 32)
# ── Build engine override block ───────────────────────────────────────────────
ENGINE_BLOCK=""
# set +u: iterating empty associative arrays throws "unbound variable" on bash <4.4
set +u
for eng in "${!DISABLE_MAP[@]}"; do
ENGINE_BLOCK+=" - name: ${eng}\n disabled: true\n"
done
for eng in "${!ENABLE_MAP[@]}"; do
ENGINE_BLOCK+=" - name: ${eng}\n disabled: false\n"
done
set -u
# ── Write settings.yml ────────────────────────────────────────────────────────
{
printf 'use_default_settings: true\n'
printf 'general:\n instance_name: "Local Search"\n'
printf 'server:\n secret_key: "%s"\n limiter: false\n' "$SECRET_KEY"
printf 'search:\n safe_search: %d\n default_lang: "en"\n formats: [html, json]\n' "$SAFE_INT"
# Lock the safe-search preference so users cannot override it via the UI
if [[ "$LEVEL" != "none" ]]; then
printf 'preferences:\n lock:\n - safesearch\n'
fi
if [[ -n "$ENGINE_BLOCK" ]]; then
printf 'engines:\n'
printf '%b' "$ENGINE_BLOCK"
fi
} > "$SETTINGS"
ok "Updated settings.yml (safe_search: $SAFE_INT)"
# bash < 4.4: ${#assoc[@]} on an empty declared array throws "unbound variable"
# under set -u — disable nounset for the rest of the reporting section
set +u
if [[ ${#DISABLE_MAP[@]} -gt 0 ]]; then
info "Disabled (${#DISABLE_MAP[@]}): $(printf '%s, ' "${!DISABLE_MAP[@]}" | sed 's/, $//')"
fi
if [[ ${#ENABLE_MAP[@]} -gt 0 ]]; then
info "Re-enabled: $(printf '%s, ' "${!ENABLE_MAP[@]}" | sed 's/, $//')"
fi
# ── Update &safesearch= in SEARXNG_QUERY_URL inside docker-compose.yml ────────
if [[ -f "$COMPOSE" ]]; then
sed -i -E \
"s|(SEARXNG_QUERY_URL=http://searxng:[0-9]+/search\?[^&[:space:]]*)(&safesearch=[0-9])?|\1\&safesearch=${SAFE_INT}|g" \
"$COMPOSE"
ok "Updated SEARXNG_QUERY_URL (&safesearch=${SAFE_INT})"
fi
# ── Restart SearXNG ───────────────────────────────────────────────────────────
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^searxng$'; then
info "Restarting SearXNG..."
docker restart searxng
ok "SearXNG restarted"
else
info "SearXNG not running — changes take effect on next start"
fi
echo
grn "Done — safe search: $LEVEL"
[[ "$LEVEL" != "none" ]] && \
info "Engines skipped (can't enforce '$LEVEL'): ${#DISABLE_MAP[@]} total"