diff --git a/triggers/dad-joke-greeter/README.md b/triggers/dad-joke-greeter/README.md new file mode 100644 index 0000000..a5599bd --- /dev/null +++ b/triggers/dad-joke-greeter/README.md @@ -0,0 +1,116 @@ +# Dad Joke Greeter + +Greet participants with a random dad joke when they join your Tuple room — spoken aloud so everyone on the call hears it. + +Jokes are fetched from [icanhazdadjoke.com](https://icanhazdadjoke.com/) and played through macOS text-to-speech. Audio is routed to both your local speakers and a virtual audio device so remote participants hear the joke through Tuple. + +## Installation + +Copy the trigger into your Tuple triggers directory: + +```bash +cp -r triggers/dad-joke-greeter ~/.tuple/triggers/dad-joke-greeter +``` + +> If you installed this trigger from the [Tuple Triggers Directory](https://tuple.app/triggers), it's already in place — skip to Quick Setup. + +## How it works + +- When **you** join a room, the trigger records which room you're in (no joke yet — you're alone). +- When **someone else** joins the same room, a dad joke is fetched and spoken aloud. +- When **you** leave the room, state is cleared and jokes stop. + +## Quick setup + +Run the included setup script to check prerequisites, create the audio device, and validate everything: + +```bash +bash ~/.tuple/triggers/dad-joke-greeter/setup.sh +``` + +The script will walk you through each step interactively. If you prefer to set things up manually, follow the steps below. + +## Prerequisites + +- macOS (uses `say` for text-to-speech) +- [jq](https://jqlang.github.io/jq/) (`brew install jq`) +- [BlackHole 2ch](https://existential.audio/blackhole/) (free virtual audio driver) +> **Note:** After installing BlackHole, you may need to restart your Mac (or log out and back in) before it appears as an audio device. + +## Manual audio setup + +The key trick is routing the `say` audio into Tuple's microphone input so remote participants hear the joke. This requires a virtual audio loopback device. + +### 1. Install BlackHole + +```bash +brew install blackhole-2ch +``` + +### 2. Create an aggregate audio device + +Open **Audio MIDI Setup** (Spotlight → "Audio MIDI Setup") and create a new aggregate device: + +1. Click the **+** button in the bottom-left → **Create Aggregate Device** +2. Name it **"BH + Mic Input"** +3. Check **your preferred microphone first** — this can be any hardware mic (e.g. "MacBook Pro Microphone", an external USB mic, etc.) +4. Check **"BlackHole 2ch" second** +5. Enable **Drift Correction** on the BlackHole 2ch row +6. Set the **Clock Source** to the microphone you selected in step 3 + +> **Order matters.** Your hardware mic must be added first and used as the clock source. BlackHole needs drift correction enabled because it runs on a virtual clock that can drift from the hardware mic. Getting this wrong can cause audio glitches or silence. + +### 3. Configure Tuple + +In Tuple, go to **Preferences → Audio → Input Device** and select **"BH + Mic Input"**. + +Now Tuple receives both your voice (real mic) and the joke audio (BlackHole). + +### 4. Speaker device name + +During setup, the script automatically detects your audio output devices and writes the selected device into the `room-joined` script. No manual editing needed. + +If you skipped setup or need to change it later, update `SPEAKER_DEVICE` in the `room-joined` script. Find your device name with: + +```bash +system_profiler SPAudioDataType | grep -A2 "Output Channels" | grep -v "Output Channels" +``` + +## Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `SPEAKER_DEVICE` | `MacBook Pro Speakers` | Local speaker device name | +| `BLACKHOLE_DEVICE` | `BlackHole 2ch` | Virtual audio device name | + +## Limiting to specific rooms + +By default, jokes trigger in any room you join. To restrict to specific rooms, create `~/.tuple/tracked-rooms` with one room name per line: + +``` +Engineering +Design Crit +``` +> **Room names are case-sensitive and must match exactly.** Use the room name as it appears in Tuple. + +When this file exists, only rooms listed in it will trigger jokes. Remove the file to go back to all rooms. + +## Disabling temporarily + +Create a disable file to pause jokes without removing the trigger: + +```bash +# Disable +touch ~/.tuple/.dad-jokes-disabled + +# Re-enable +rm ~/.tuple/.dad-jokes-disabled +``` + +> Tuple spawns trigger scripts as subprocesses, so environment variables set in your terminal don't propagate. The touch-file approach works reliably regardless of how the script is launched. + +Alternatively, remove or rename the trigger files in `~/.tuple/triggers/dad-joke-greeter/`. + +## Known limitations + +- **Switching microphones:** The aggregate audio device is configured with a specific mic. If you swap between multiple inputs throughout the day (e.g. AirPods, built-in mic, headset), you'll need to update the aggregate device in Audio MIDI Setup to use the new mic. A future version may automate this by detecting the current input and rebuilding the aggregate on the fly. diff --git a/triggers/dad-joke-greeter/assets/icon.png b/triggers/dad-joke-greeter/assets/icon.png new file mode 100644 index 0000000..ba33d4e Binary files /dev/null and b/triggers/dad-joke-greeter/assets/icon.png differ diff --git a/triggers/dad-joke-greeter/config.json b/triggers/dad-joke-greeter/config.json new file mode 100644 index 0000000..002a409 --- /dev/null +++ b/triggers/dad-joke-greeter/config.json @@ -0,0 +1,6 @@ +{ + "name": "Dad Joke Greeter", + "description": "Greet participants with a dad joke when they join your room — spoken aloud so everyone on the call hears it.", + "platforms": ["macos"], + "language": "bash" +} diff --git a/triggers/dad-joke-greeter/room-joined b/triggers/dad-joke-greeter/room-joined new file mode 100755 index 0000000..31b9790 --- /dev/null +++ b/triggers/dad-joke-greeter/room-joined @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Dad Joke Greeter — room-joined trigger +# +# Fetches a random dad joke and speaks it aloud when someone joins the +# room you're in. Audio is routed to both your speakers and BlackHole 2ch +# so remote Tuple participants hear the joke too. +# +# See README.md for audio setup instructions. +set -uo pipefail + +# Audio devices — adjust to match your system +SPEAKER_DEVICE="MacBook Pro Speakers" +BLACKHOLE_DEVICE="BlackHole 2ch" + +API_URL="https://icanhazdadjoke.com/" +STATE_DIR="$HOME/.tuple/.state/dad-joke-greeter" +MY_ROOM_FILE="$STATE_DIR/my-room" +TRACKED_ROOMS_FILE="$HOME/.tuple/tracked-rooms" + +# Optional: touch ~/.tuple/.dad-jokes-disabled to disable without removing the trigger +[[ ! -f "$HOME/.tuple/.dad-jokes-disabled" ]] || exit 0 + +is_self="${TUPLE_TRIGGER_IS_SELF:-false}" +room_name="${TUPLE_TRIGGER_ROOM_NAME:-}" + +# If a tracked-rooms file exists, only trigger for rooms listed in it +if [[ -f "$TRACKED_ROOMS_FILE" ]] && ! grep -qxF "$room_name" "$TRACKED_ROOMS_FILE"; then + exit 0 +fi + +mkdir -p "$STATE_DIR" + +if [[ "$is_self" == "true" ]]; then + # Record that we're in this room + printf '%s' "$room_name" > "$MY_ROOM_FILE" + # Don't tell a joke when joining alone — wait for someone else + exit 0 +fi + +# Someone else joined — only greet if we're in that same room +if [[ ! -f "$MY_ROOM_FILE" ]]; then + exit 0 +fi +my_room=$(<"$MY_ROOM_FILE") +if [[ "$my_room" != "$room_name" ]]; then + exit 0 +fi + +# Fetch a random joke +joke=$( + curl -sfS --connect-timeout 3 --max-time 5 \ + -H "Accept: application/json" \ + -H "User-Agent: TupleDadJokeGreeter/1.0 (https://github.com/tupleapp/community-triggers)" \ + "$API_URL" | jq -r '.joke // empty' +) + +if [[ -z "$joke" ]]; then + echo "$(date -Iseconds) [dad-joke-greeter] Failed to fetch joke" >&2 + exit 1 +fi + +# Sanitize joiner name (letters and spaces only) +raw_joiner="${TUPLE_TRIGGER_FULL_NAME:-someone}" +joiner="$(printf '%s' "$raw_joiner" | tr -cd '[:alpha:] ')" +joiner="${joiner:-someone}" + +greeting="${joiner} just joined. Here is a dad joke." +full_text="${greeting} ${joke}" + +# Play to local speakers and BlackHole in parallel so both you and +# remote participants hear the joke. Falls back gracefully if either +# device is missing. +say -a "$SPEAKER_DEVICE" -- "$full_text" 2>> "$HOME/.tuple/.state/dad-joke-greeter/say-errors.log" & +say -a "$BLACKHOLE_DEVICE" -- "$full_text" 2>> "$HOME/.tuple/.state/dad-joke-greeter/say-errors.log" & +wait + +echo "$(date -Iseconds) [dad-joke-greeter] Told joke to ${joiner}: ${joke}" diff --git a/triggers/dad-joke-greeter/room-left b/triggers/dad-joke-greeter/room-left new file mode 100755 index 0000000..5bc64d4 --- /dev/null +++ b/triggers/dad-joke-greeter/room-left @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Dad Joke Greeter — room-left trigger +# +# Clears room state when we leave so jokes stop firing. +set -uo pipefail + +STATE_DIR="$HOME/.tuple/.state/dad-joke-greeter" +MY_ROOM_FILE="$STATE_DIR/my-room" + +is_self="${TUPLE_TRIGGER_IS_SELF:-false}" + +if [[ "$is_self" == "true" ]] && [[ -f "$MY_ROOM_FILE" ]]; then + rm -f "$MY_ROOM_FILE" +fi diff --git a/triggers/dad-joke-greeter/setup.sh b/triggers/dad-joke-greeter/setup.sh new file mode 100755 index 0000000..216a86d --- /dev/null +++ b/triggers/dad-joke-greeter/setup.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2059 # ANSI colour vars in printf format strings are intentional +# Dad Joke Greeter — setup helper +# +# Validates prerequisites and walks you through the one-time audio setup +# so remote Tuple participants can hear the jokes. +set -uo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BOLD='\033[1m' +RESET='\033[0m' + +pass() { printf "${GREEN} ✓ %s${RESET}\n" "$1"; } +fail() { printf "${RED} ✗ %s${RESET}\n" "$1"; } +warn() { printf "${YELLOW} ! %s${RESET}\n" "$1"; } +step() { printf "\n${BOLD}%s${RESET}\n" "$1"; } + +errors=0 + +# ── Prerequisites ────────────────────────────────────────────────────── + +step "Checking prerequisites..." + +# jq +if command -v jq &>/dev/null; then + pass "jq installed ($(jq --version))" +else + fail "jq not found — install with: brew install jq" + errors=$((errors + 1)) +fi + +# BlackHole 2ch +if system_profiler SPAudioDataType 2>/dev/null | grep -q "BlackHole 2ch"; then + pass "BlackHole 2ch installed" +else + fail "BlackHole 2ch not found — install with: brew install blackhole-2ch" + errors=$((errors + 1)) + warn "If you just installed BlackHole, you may need to restart your Mac (or log out" + warn "and back in) before it appears as an audio device. Then re-run this script." +fi + +# curl +if command -v curl &>/dev/null; then + pass "curl installed" +else + fail "curl not found" + errors=$((errors + 1)) +fi + +if [[ "$errors" -gt 0 ]]; then + printf "\n${RED}Fix the issues above and re-run this script.${RESET}\n" + exit 1 +fi + +# ── Aggregate audio device ───────────────────────────────────────────── + +step "Checking for aggregate audio device..." + +DEVICE_NAME="BH + Mic Input" + +if system_profiler SPAudioDataType 2>/dev/null | grep -qi "$DEVICE_NAME"; then + pass "\"$DEVICE_NAME\" aggregate device exists" +else + warn "\"$DEVICE_NAME\" not found — let's create it" + + # List available microphones (filter out virtual/aggregate devices) + printf "\n${BOLD}Available microphones:${RESET}\n" + mics=() + while IFS= read -r mic; do + # Skip BlackHole, aggregate, and Zoom virtual devices + case "$mic" in + *BlackHole*|*"$DEVICE_NAME"*|*ZoomAudio*) continue ;; + esac + mics+=("$mic") + printf " ${GREEN}%d)${RESET} %s\n" "${#mics[@]}" "$mic" + done < <( + system_profiler SPAudioDataType 2>/dev/null \ + | grep -B2 "Input Channels" \ + | grep -v "Input Channels" \ + | grep -v "^--$" \ + | sed 's/^ *//;s/:$//' \ + | grep -v '^$' + ) + + if [[ ${#mics[@]} -eq 0 ]]; then + fail "No microphones found" + errors=$((errors + 1)) + else + printf "\n" + mic_choice="" + while [[ -z "$mic_choice" ]]; do + read -rp "Select your microphone [1-${#mics[@]}]: " pick + if [[ "$pick" =~ ^[0-9]+$ ]] && [[ "$pick" -ge 1 ]] && [[ "$pick" -le ${#mics[@]} ]]; then + candidate="${mics[$((pick - 1))]}" + read -rp "Use \"$candidate\"? [Y/n] " confirm + if [[ -z "$confirm" || "$confirm" =~ ^[Yy] ]]; then + mic_choice="$candidate" + fi + else + printf " ${RED}Invalid choice. Enter a number between 1 and %d.${RESET}\n" "${#mics[@]}" + fi + done + pass "Using microphone: \"$mic_choice\"" + fi + + read -rp "Press Enter to open Audio MIDI Setup (instructions will follow)..." + open -a "Audio MIDI Setup" + + printf "\n${BOLD}Follow these steps in Audio MIDI Setup:${RESET}\n" + printf "\n" + printf " 1. Click the ${BOLD}+${RESET} button (bottom-left) → ${BOLD}Create Aggregate Device${RESET}\n" + printf " 2. Rename it to ${BOLD}\"$DEVICE_NAME\"${RESET}\n" + printf " 3. Check ${BOLD}\"%s\" FIRST${RESET}\n" "${mic_choice:-your built-in microphone}" + printf " 4. Check ${BOLD}\"BlackHole 2ch\" SECOND${RESET}\n" + printf " 5. Enable ${BOLD}Drift Correction${RESET} on the BlackHole 2ch row\n" + printf " 6. Set ${BOLD}Clock Source${RESET} to ${BOLD}\"%s\"${RESET}\n" "${mic_choice:-your built-in microphone}" + printf "\n" + printf " ${YELLOW}Order matters!${RESET} The mic must be added first and used as the\n" + printf " clock source. BlackHole needs drift correction enabled because\n" + printf " it runs on a virtual clock that can drift from the hardware mic.\n" + printf "\n" + + read -rp "Press Enter once you've created the device..." + + if system_profiler SPAudioDataType 2>/dev/null | grep -qi "$DEVICE_NAME"; then + pass "\"$DEVICE_NAME\" created successfully" + else + fail "The device \"$DEVICE_NAME\" was not found." + warn "Make sure you: (1) created it in Audio MIDI Setup, (2) named it exactly" + warn "\"$DEVICE_NAME\", (3) clicked Done and closed the dialog." + errors=$((errors + 1)) + fi +fi + +# ── Tuple audio input ────────────────────────────────────────────────── + +step "Tuple configuration..." + +printf "\n Set Tuple's audio input to the aggregate device:\n" +printf " ${BOLD}Tuple → Preferences → Audio → Input Device → \"$DEVICE_NAME\"${RESET}\n\n" +read -rp "Press Enter once configured (or if already done)..." + +# ── Speaker device ───────────────────────────────────────────────────── + +step "Detecting speaker device..." + +# Enumerate audio output devices dynamically +DETECTED_SPEAKER="" +output_devices=() +while IFS= read -r dev; do + # Skip BlackHole, aggregate, and Zoom virtual devices + case "$dev" in + *BlackHole*|*"$DEVICE_NAME"*|*ZoomAudio*) continue ;; + esac + output_devices+=("$dev") +done < <( + system_profiler SPAudioDataType 2>/dev/null \ + | grep -B2 "Output Channels" \ + | grep -v "Output Channels" \ + | grep -v "^--$" \ + | sed 's/^ *//;s/:$//' \ + | grep -v '^$' +) + +if [[ ${#output_devices[@]} -eq 0 ]]; then + warn "Could not enumerate audio output devices." +else + echo "Available audio output devices:" + for i in "${!output_devices[@]}"; do + printf " %d) %s\n" "$((i+1))" "${output_devices[$i]}" + done + printf " 0) Skip (set manually later)\n" + read -rp "Select speaker device [0-${#output_devices[@]}]: " speaker_choice + if [[ "$speaker_choice" =~ ^[1-9][0-9]*$ ]] && [[ "$speaker_choice" -le "${#output_devices[@]}" ]]; then + DETECTED_SPEAKER="${output_devices[$((speaker_choice-1))]}" + fi +fi + +if [[ -n "$DETECTED_SPEAKER" ]]; then + sed -i '' "s|^SPEAKER_DEVICE=.*|SPEAKER_DEVICE='${DETECTED_SPEAKER}'|" "$SCRIPT_DIR/room-joined" + pass "Updated SPEAKER_DEVICE in room-joined to: ${DETECTED_SPEAKER}" +else + warn "SPEAKER_DEVICE not set. To configure manually, edit room-joined and set:" + warn " SPEAKER_DEVICE='Your Device Name'" + warn "Find your device name with: say -a '?' 2>&1" +fi + +# ── API test ─────────────────────────────────────────────────────────── + +step "Testing joke API..." + +joke=$( + curl -sfS --connect-timeout 3 --max-time 5 \ + -H "Accept: application/json" \ + -H "User-Agent: TupleDadJokeGreeter/1.0" \ + "https://icanhazdadjoke.com/" | jq -r '.joke // empty' +) + +if [[ -n "$joke" ]]; then + pass "API working" + printf " ${BOLD}Sample joke:${RESET} %s\n" "$joke" +else + fail "Could not fetch a joke — check your internet connection" + errors=$((errors + 1)) +fi + +# ── Summary ──────────────────────────────────────────────────────────── + +step "Setup summary" + +if [[ "$errors" -gt 0 ]]; then + printf "\n${RED}Setup incomplete — fix the issues above and re-run.${RESET}\n" + exit 1 +else + printf "\n${GREEN}All good! Dad jokes are ready to roll.${RESET}\n" + printf " Join a Tuple room and wait for someone else — they'll be greeted.\n" +fi