Jobber/scripts/jobber-pipeline-telegram.sh

203 lines
7.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Run Jobber pipeline, wait until it finishes, send summary + job links to Telegram.
# Secrets: copy scripts/jobber-cron.env.example to /root/.jobber-cron.env (chmod 600).
set -euo pipefail
ENV_FILE="${JOBBER_CRON_ENV:-/root/.jobber-cron.env}"
if [[ ! -f "$ENV_FILE" ]]; then
echo "Missing env file: $ENV_FILE (set JOBBER_CRON_ENV or create the default path)" >&2
exit 1
fi
# shellcheck source=/dev/null
source "$ENV_FILE"
: "${TELEGRAM_BOT_TOKEN:?Set TELEGRAM_BOT_TOKEN in $ENV_FILE}"
: "${TELEGRAM_CHAT_ID:?Set TELEGRAM_CHAT_ID in $ENV_FILE}"
BASE="${JOBOPS_URL:-http://127.0.0.1:3005}"
MAX_JOBS="${JOB_TELEGRAM_MAX_JOBS:-25}"
AUTH=()
if [[ -n "${BASIC_AUTH_USER:-}" && -n "${BASIC_AUTH_PASSWORD:-}" ]]; then
AUTH=(-u "${BASIC_AUTH_USER}:${BASIC_AUTH_PASSWORD}")
fi
tg_html_escape() {
printf '%s' "$1" | sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g'
}
tg_href_escape() {
printf '%s' "$1" | sed -e 's/&/\&amp;/g' -e 's/"/\&quot;/g'
}
send_tg_html() {
local msg="$1"
curl -sS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg c "$TELEGRAM_CHAT_ID" \
--arg t "$msg" \
'{chat_id: $c, text: $t, parse_mode: "HTML", disable_web_page_preview: true}')" >/dev/null
}
fetch_status() {
curl -sS "${AUTH[@]}" "${BASE}/api/pipeline/status"
}
fetch_jobs_list() {
curl -sS "${AUTH[@]}" "${BASE}/api/jobs?view=list"
}
build_job_lines_html() {
local jobs_json="$1"
local started="$2"
local completed="$3"
local max_n="$4"
# Pipeline run times are ISO-8601 (…T…Z). Jobs often use SQLite datetime('now'): "YYYY-MM-DD HH:MM:SS".
# Raw string compare treats space before the clock as sorting before "T", so every SQLite-style
# discoveredAt incorrectly falls *before* the run window. Normalize for comparison only.
echo "$jobs_json" | jq -c --arg s "$started" --arg e "$completed" --argjson max "$max_n" '
def pickurl:
if (.jobUrl // "") != "" then .jobUrl
elif (.applicationLink // "") != "" then .applicationLink
else "" end;
def normalizeTs:
if . == null or . == "" then ""
elif test("[Tt]") then .
else sub(" "; "T")
end;
def pickrows($all):
if ($s == "" or $s == null) then
{ rows: ($all | sort_by(.discoveredAt | normalizeTs) | reverse), usedFallback: true }
else
($all
| map(select(
($e != "" and ((.discoveredAt | normalizeTs) >= ($s | normalizeTs)) and ((.discoveredAt | normalizeTs) <= ($e | normalizeTs)))
))) as $win |
if ($win | length) > 0 then { rows: $win, usedFallback: false }
else
($all | map(select(((.discoveredAt | normalizeTs) >= ($s | normalizeTs))))) as $from |
if ($from | length) > 0 then { rows: $from, usedFallback: false }
else
{ rows: ($all | sort_by(.discoveredAt | normalizeTs) | reverse), usedFallback: true }
end
end
end;
(.data.jobs // []) as $all |
if ($all | length) == 0 then
{total: 0, lines: [], usedFallback: false}
else
pickrows($all) as $picked |
($picked.rows | sort_by(.discoveredAt | normalizeTs) | reverse) as $sorted |
($sorted | length) as $total |
($sorted | .[0:max]) as $slice |
{
total: $total,
usedFallback: $picked.usedFallback,
lines: [
$slice[] |
{
title: (.title // "Untitled"),
url: pickurl,
employer: (.employer // "")
}
]
}
end
'
}
append_lines_from_json() {
local sel="$1"
local -n _out="$2"
local item line title url emp
while IFS= read -r item; do
[[ -z "$item" || "$item" == "null" ]] && continue
title="$(echo "$item" | jq -r '.title // "Untitled"')"
url="$(echo "$item" | jq -r '.url // ""')"
emp="$(echo "$item" | jq -r '.employer // ""')"
if [[ -n "$url" && "$url" != "null" ]]; then
line="$(tg_html_escape "$emp") — <a href=\"$(tg_href_escape "$url")\">$(tg_html_escape "$title")</a>"
else
line="$(tg_html_escape "$emp")$(tg_html_escape "$title") <i>(no URL)</i>"
fi
_out+=$'\n'"${line}"
done < <(echo "$sel" | jq -c '.lines[]? // empty')
}
body="$(fetch_status)"
if ! echo "$body" | jq -e '.ok == true' >/dev/null 2>&1; then
send_tg_html "Jobber: /api/pipeline/status failed (before run). Check container."
exit 1
fi
if echo "$body" | jq -e '.data.isRunning == true' >/dev/null 2>&1; then
send_tg_html "Jobber: pipeline already running; skipping scheduled run."
exit 0
fi
resp="$(curl -sS "${AUTH[@]}" -X POST "${BASE}/api/pipeline/run" \
-H "Content-Type: application/json" -d '{}')"
if ! echo "$resp" | jq -e '.ok == true' >/dev/null 2>&1; then
_fail_json="$(echo "$resp" | jq -c . 2>/dev/null || echo "$resp")"
send_tg_html "Jobber: POST /api/pipeline/run failed: $(tg_html_escape "$_fail_json")"
exit 1
fi
was_running=0
for _ in $(seq 1 720); do
sleep 30
body="$(fetch_status)"
if ! echo "$body" | jq -e '.ok == true' >/dev/null 2>&1; then
send_tg_html "Jobber: status check failed mid-run."
exit 1
fi
running="$(echo "$body" | jq -r '.data.isRunning')"
if [[ "$running" == "true" ]]; then
was_running=1
elif [[ "$was_running" -eq 1 ]]; then
lr="$(echo "$body" | jq '.data.lastRun')"
st="$(echo "$lr" | jq -r '.status // "unknown"')"
disc="$(echo "$lr" | jq -r '.jobsDiscovered // 0')"
proc="$(echo "$lr" | jq -r '.jobsProcessed // 0')"
err="$(echo "$lr" | jq -r '.errorMessage // empty')"
started="$(echo "$lr" | jq -r '.startedAt // ""')"
completed="$(echo "$lr" | jq -r '.completedAt // ""')"
msg="<b>Jobber</b> pipeline: <b>$(tg_html_escape "$st")</b>"
msg+=$'\n'"Discovered: ${disc}, processed: ${proc}."
[[ -n "$err" ]] && msg+=$'\n'"<b>Error:</b> $(tg_html_escape "$err")"
jobs_resp="$(fetch_jobs_list)"
if echo "$jobs_resp" | jq -e '.ok == true' >/dev/null 2>&1; then
sel="$(build_job_lines_html "$jobs_resp" "$started" "$completed" "$MAX_JOBS")" ||
sel='{"total":0,"lines":[]}'
total="$(echo "$sel" | jq -r '.total // 0')"
shown="$(echo "$sel" | jq -r '.lines | length')"
used_fb="$(echo "$sel" | jq -r '.usedFallback // false')"
if [[ "$total" -gt 0 ]]; then
if [[ "$used_fb" == "true" ]]; then
msg+=$'\n\n'"<b>Recent jobs</b> (showing ${shown} of ${total}; time window did not match — links may include older discoveries):"
else
msg+=$'\n\n'"<b>Jobs in this run</b> (showing ${shown} of ${total}):"
fi
append_lines_from_json "$sel" msg
rest=$((total - shown))
if [[ "$rest" -gt 0 ]]; then
msg+=$'\n\n'"<i>…and ${rest} more not shown.</i>"
fi
else
msg+=$'\n\n'"<i>No job rows in the API list. Open the app for details.</i>"
fi
else
msg+=$'\n\n'"<i>Could not load GET /api/jobs for links.</i>"
fi
send_tg_html "$msg"
exit 0
fi
done
send_tg_html "Jobber: timed out waiting for pipeline (6h). Check server."
exit 1