Commit b64ace7a0c16
Changed files (4)
pkgs/default.nix
@@ -37,6 +37,7 @@ in
jellyfin-manage-playlist = pkgs.callPackage ../tools/jellyfin-manage-playlist { };
music-playlist-dl = pkgs.callPackage ../tools/music-playlist-dl { };
nix-flake-update = pkgs.callPackage ../tools/nix-flake-update { };
+ slack-archive = pkgs.callPackage ../tools/slack-archive { };
nixpkgs-consolidate = pkgs.callPackage ../tools/nixpkgs-consolidate { };
gcal-to-org = pkgs.callPackage ../tools/gcal-to-org { };
review-tool = pkgs.callPackage ../tools/review-tool { };
systems/aomi/extra.nix
@@ -276,8 +276,55 @@
systemd.tmpfiles.rules = [
"d /var/lib/ollama-exporter 0755 root root -"
"d /var/lib/git-builds 0755 builder users -"
+ "d /var/lib/slack-archive 0750 vincent users -"
];
+ # Slack Archive - daily backup of public Slack channels
+ systemd.services.slack-archive = {
+ description = "Slack Public Channel Archiver";
+ after = [ "network-online.target" ];
+ wants = [ "network-online.target" ];
+
+ serviceConfig = {
+ Type = "oneshot";
+ User = "vincent";
+ Group = "users";
+ ExecStart = "${pkgs.slack-archive}/bin/slack-archive archive";
+ Environment = [
+ "SLACK_ARCHIVE_DIR=/var/lib/slack-archive"
+ "HOME=/home/vincent"
+ "XDG_CACHE_HOME=/home/vincent/.cache"
+ ];
+
+ # Security hardening
+ PrivateTmp = true;
+ ProtectSystem = "strict";
+ ProtectHome = "read-only";
+ ReadWritePaths = [
+ "/var/lib/slack-archive"
+ "/home/vincent/.cache/slackdump"
+ "/home/vincent/.local/cache/slackdump"
+ ];
+ NoNewPrivileges = true;
+
+ # Logging
+ StandardOutput = "journal";
+ StandardError = "journal";
+ SyslogIdentifier = "slack-archive";
+ };
+ };
+
+ systemd.timers.slack-archive = {
+ description = "Daily Slack Archive Timer";
+ wantedBy = [ "timers.target" ];
+
+ timerConfig = {
+ OnCalendar = "daily";
+ RandomizedDelaySec = 3600; # 0-1 hour random delay
+ Persistent = true;
+ };
+ };
+
systemd.services.ollama-exporter = {
description = "Ollama Prometheus Exporter";
after = [
tools/slack-archive/default.nix
@@ -0,0 +1,48 @@
+{
+ lib,
+ stdenv,
+ makeWrapper,
+ slackdump,
+ jq,
+ findutils,
+ gnused,
+ uv,
+ python3,
+}:
+
+stdenv.mkDerivation {
+ pname = "slack-archive";
+ version = "0.1.0";
+
+ src = ./.;
+
+ nativeBuildInputs = [ makeWrapper ];
+
+ installPhase = ''
+ runHook preInstall
+
+ mkdir -p $out/bin
+ cp slack-archive.sh $out/bin/slack-archive
+ chmod +x $out/bin/slack-archive
+
+ wrapProgram $out/bin/slack-archive \
+ --prefix PATH : ${
+ lib.makeBinPath [
+ slackdump
+ jq
+ findutils
+ gnused
+ uv
+ python3
+ ]
+ }
+
+ runHook postInstall
+ '';
+
+ meta = with lib; {
+ description = "Slack public channel archiver with static HTML export";
+ license = licenses.mit;
+ platforms = platforms.linux;
+ };
+}
tools/slack-archive/slack-archive.sh
@@ -0,0 +1,155 @@
+#!/usr/bin/env bash
+# Slack Public Channels Archive Script
+# Archives public channels incrementally and generates static HTML viewer
+set -euo pipefail
+
+# --- Configuration ---
+DATA_DIR="${SLACK_ARCHIVE_DIR:-/var/lib/slack-archive}"
+ARCHIVE_DIR="$DATA_DIR/archive"
+EXPORT_DIR="$DATA_DIR/exports"
+HTML_DIR="$DATA_DIR/html"
+CHANNELS_FILE="$DATA_DIR/public-channels.txt"
+CHANNELS_JSON="$DATA_DIR/channels.json"
+
+# How often to refresh channel list (in days)
+CHANNEL_REFRESH_DAYS="${CHANNEL_REFRESH_DAYS:-7}"
+
+# --- Functions ---
+
+log() {
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
+}
+
+die() {
+ log "ERROR: $*" >&2
+ exit 1
+}
+
+check_auth() {
+ # Check for cached credentials
+ local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/slackdump"
+ if [[ -f "$cache_dir/provider.bin" ]]; then
+ log "Using cached credentials from $cache_dir"
+ return 0
+ fi
+
+ die "No cached credentials found. Run 'slackdump login' interactively first."
+}
+
+setup_dirs() {
+ mkdir -p "$DATA_DIR" "$EXPORT_DIR" "$HTML_DIR"
+}
+
+refresh_channel_list() {
+ local should_refresh=false
+
+ if [[ ! -f "$CHANNELS_FILE" ]]; then
+ log "Channel list not found, fetching..."
+ should_refresh=true
+ elif [[ -n "$(find "$CHANNELS_FILE" -mtime +"$CHANNEL_REFRESH_DAYS" 2>/dev/null)" ]]; then
+ log "Channel list older than $CHANNEL_REFRESH_DAYS days, refreshing..."
+ should_refresh=true
+ fi
+
+ if [[ "$should_refresh" == "true" ]] || [[ "${FORCE_REFRESH:-}" == "true" ]]; then
+ log "Fetching channel list from Slack..."
+ slackdump list channels -format JSON -no-json > "$CHANNELS_JSON"
+
+ # Filter to public channels only (exclude private, DMs, group DMs)
+ jq -r '.[] | select(.is_private == false and .is_im == false and .is_mpim == false) | .id' \
+ "$CHANNELS_JSON" > "$CHANNELS_FILE"
+
+ local count
+ count=$(wc -l < "$CHANNELS_FILE")
+ log "Found $count public channels"
+ else
+ log "Using cached channel list ($(wc -l < "$CHANNELS_FILE") channels)"
+ fi
+}
+
+run_archive() {
+ if [[ -d "$ARCHIVE_DIR" ]]; then
+ log "Resuming archive from previous state..."
+ slackdump resume "$ARCHIVE_DIR"
+ else
+ log "Starting fresh archive..."
+ slackdump archive -o "$ARCHIVE_DIR" @"$CHANNELS_FILE"
+ fi
+}
+
+convert_to_export() {
+ EXPORT_FILE="$EXPORT_DIR/slack-export-$(date +%Y-%m-%d).zip"
+ log "Converting archive to export format: $EXPORT_FILE"
+ slackdump convert -f export -storage standard -o "$EXPORT_FILE" "$ARCHIVE_DIR"
+}
+
+generate_html() {
+ local export_file="$1"
+ log "Generating HTML viewer..."
+ uvx slack-export-viewer -z "$export_file" --html-only -o "$HTML_DIR"
+
+ # Fix empty anchor tags (slack-export-viewer bug)
+ # Converts <a href='URL'></a> to <a href='URL'>URL</a>
+ log "Fixing empty links..."
+ find "$HTML_DIR" -name "*.html" -exec sed -i -E "s|<a href='([^']+)'></a>|<a href='\\1'>\\1</a>|g" {} \;
+
+ log "HTML generated at: $HTML_DIR"
+}
+
+serve_html() {
+ local port="${1:-8080}"
+ log "Starting server at http://localhost:$port"
+ python3 -m http.server -d "$HTML_DIR" "$port"
+}
+
+# --- Main ---
+
+main() {
+ local cmd="${1:-archive}"
+
+ case "$cmd" in
+ archive)
+ check_auth
+ setup_dirs
+ refresh_channel_list
+ run_archive
+ convert_to_export
+ generate_html "$EXPORT_FILE"
+ log "Done! View at: $HTML_DIR/index.html"
+ ;;
+ channels)
+ check_auth
+ setup_dirs
+ FORCE_REFRESH=true refresh_channel_list
+ ;;
+ html)
+ local latest_export
+ latest_export=$(find "$EXPORT_DIR" -maxdepth 1 -name "*.zip" -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-)
+ if [[ -z "$latest_export" ]]; then
+ die "No export found. Run '$0 archive' first."
+ fi
+ generate_html "$latest_export"
+ ;;
+ serve)
+ serve_html "${2:-8080}"
+ ;;
+ help|--help|-h)
+ echo "Usage: $0 [command]"
+ echo ""
+ echo "Commands:"
+ echo " archive Fetch channels, archive, convert, generate HTML (default)"
+ echo " channels Refresh channel list only"
+ echo " html Regenerate HTML from latest export"
+ echo " serve Start local HTTP server (port 8080 or specify)"
+ echo " help Show this help"
+ echo ""
+ echo "Environment:"
+ echo " SLACK_ARCHIVE_DIR Data directory (default: /var/lib/slack-archive)"
+ ;;
+ *)
+ die "Unknown command: $cmd. Run '$0 help' for usage."
+ ;;
+ esac
+}
+
+main "$@"