Files
openclaw-bms/scripts/bms-tickets.sh

455 lines
16 KiB
Bash

#!/usr/bin/env bash
# bms-tickets.sh — Kaseya BMS ticket CRUD operations
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BMS_API_BASE="${BMS_API_BASE:-https://api.bms.kaseya.com}"
# ─── Helpers ─────────────────────────────────────────────────────────────────
die() { echo "ERROR: $*" >&2; exit 1; }
get_token() {
bash "${SCRIPT_DIR}/bms-auth.sh" get-token
}
bms_curl() {
local method="$1"; shift
local path="$1"; shift
local token
token=$(get_token)
curl -sf -X "$method" \
"${BMS_API_BASE}${path}" \
-H "Authorization: Bearer ${token}" \
-H "Content-Type: application/json" \
"$@"
}
# Pretty-print a ticket list
format_ticket_list() {
jq -r '
(.Data // .Items // .) |
if type == "array" then .[] else . end |
"\(.TicketNumber // .Id)\t[\(.StatusName // "?")] \(.Title)\t| \(.AccountName // "?")\t| Assignee: \(.AssigneeName // "unassigned")\t| Priority: \(.PriorityName // "?")"
' | column -t -s $'\t'
}
format_ticket_detail() {
jq -r '
.Data // . |
"Ticket: \(.TicketNumber) (ID: \(.Id))
Title: \(.Title)
Status: \(.StatusName) (ID: \(.StatusId))
Priority: \(.PriorityName) (ID: \(.PriorityId))
Account: \(.AccountName) (ID: \(.AccountId))
Location: \(.LocationName // "N/A")
Contact: \(.ContactName // "N/A")
Queue: \(.QueueName // "N/A")
Assignee: \(.AssigneeName // "unassigned")
Type: \(.TypeName // "N/A")
Created: \(.CreatedOn)
Modified: \(.ModifiedOn)
Due: \(.DueDate // "none")
---
\(.Details // "(no details)")"
'
}
# ─── Commands ────────────────────────────────────────────────────────────────
cmd_list() {
local status="" assignee="" from="" to="" priority="" queue="" account=""
local page=1 page_size=25 format="table"
while [[ $# -gt 0 ]]; do
case "$1" in
--status) status="$2"; shift 2 ;;
--assignee) assignee="$2"; shift 2 ;;
--from) from="$2"; shift 2 ;;
--to) to="$2"; shift 2 ;;
--priority) priority="$2"; shift 2 ;;
--queue) queue="$2"; shift 2 ;;
--account) account="$2"; shift 2 ;;
--page) page="$2"; shift 2 ;;
--page-size) page_size="$2"; shift 2 ;;
--format) format="$2"; shift 2 ;;
*) die "Unknown option: $1" ;;
esac
done
# Build filter JSON
local filter="{"
local sep=""
[[ -n "$status" ]] && { filter+="${sep}\"StatusNames\":\"${status}\""; sep=","; }
[[ -n "$assignee" ]] && { filter+="${sep}\"AssigneeName\":\"${assignee}\""; sep=","; }
[[ -n "$from" ]] && { filter+="${sep}\"CreatedOnFrom\":\"${from}T00:00:00\""; sep=","; }
[[ -n "$to" ]] && { filter+="${sep}\"CreatedOnTo\":\"${to}T23:59:59\""; sep=","; }
[[ -n "$priority" ]] && { filter+="${sep}\"PriorityNames\":\"${priority}\""; sep=","; }
[[ -n "$queue" ]] && { filter+="${sep}\"QueueNames\":\"${queue}\""; sep=","; }
[[ -n "$account" ]] && { filter+="${sep}\"Account\":\"${account}\""; sep=","; }
filter+="}"
local body
body=$(jq -n \
--argjson filter "$filter" \
--argjson page "$page" \
--argjson page_size "$page_size" \
'{Filter: $filter, PageNumber: $page, PageSize: $page_size}')
local response
response=$(bms_curl POST "/v2/servicedesk/tickets/search" -d "$body")
if [[ "$format" == "json" ]]; then
echo "$response" | jq .
else
local total
total=$(echo "$response" | jq -r '.TotalCount // .Total // "?"')
echo "Tickets (page ${page}, ${page_size} per page, total: ${total}):" >&2
echo "$response" | format_ticket_list
fi
}
cmd_get() {
local ticket_id="${1:-}"
local format="${2:-table}"
[[ -n "$ticket_id" ]] || die "Usage: bms tickets get <ticketId>"
local response
response=$(bms_curl GET "/v2/servicedesk/tickets/${ticket_id}")
if [[ "$format" == "json" ]] || [[ "${2:-}" == "--json" ]]; then
echo "$response" | jq .
else
echo "$response" | format_ticket_detail
fi
}
# Prompt for a value if empty; first arg is field label, second is current value (by nameref)
# Usage: prompt_if_empty "Label" varname
prompt_if_empty() {
local label="$1"
local -n _ref="$2"
if [[ -z "${_ref:-}" ]]; then
read -r -p "${label}: " _ref
fi
}
cmd_create() {
local title="" details="" account_id="" location_id="" contact_id=""
local status_id="" priority_id="" type_id="" source_id=""
local assignee_id="" queue_id="" due_date="" open_date=""
local template_id=""
local interactive=false
while [[ $# -gt 0 ]]; do
case "$1" in
--template-id) template_id="$2"; shift 2 ;;
--title) title="$2"; shift 2 ;;
--details|--description) details="$2"; shift 2 ;;
--account-id) account_id="$2"; shift 2 ;;
--location-id) location_id="$2"; shift 2 ;;
--contact-id) contact_id="$2"; shift 2 ;;
--status-id) status_id="$2"; shift 2 ;;
--priority-id) priority_id="$2"; shift 2 ;;
--type-id) type_id="$2"; shift 2 ;;
--source-id) source_id="$2"; shift 2 ;;
--assignee-id) assignee_id="$2"; shift 2 ;;
--queue-id) queue_id="$2"; shift 2 ;;
--due-date) due_date="$2"; shift 2 ;;
--interactive) interactive=true; shift ;;
*) die "Unknown option: $1" ;;
esac
done
# ── Template pre-fill ────────────────────────────────────────────────────
if [[ -n "$template_id" ]]; then
echo "Fetching template ${template_id}..." >&2
local tmpl
tmpl=$(bms_curl GET "/v2/servicedesk/templates/tickets/${template_id}" | jq '.Data // .')
# Fill only fields not already set by CLI flags
[[ -z "$title" ]] && title=$(echo "$tmpl" | jq -r '.Title // empty')
[[ -z "$details" ]] && details=$(echo "$tmpl" | jq -r '.Details // empty')
[[ -z "$status_id" ]] && status_id=$(echo "$tmpl" | jq -r '.StatusId // empty')
[[ -z "$priority_id" ]] && priority_id=$(echo "$tmpl" | jq -r '.PriorityId // empty')
[[ -z "$type_id" ]] && type_id=$(echo "$tmpl" | jq -r '(.TypeId // .IssueTypeId) // empty')
[[ -z "$source_id" ]] && source_id=$(echo "$tmpl" | jq -r '.SourceId // empty')
[[ -z "$queue_id" ]] && queue_id=$(echo "$tmpl" | jq -r '.QueueId // empty')
[[ -z "$assignee_id" ]] && assignee_id=$(echo "$tmpl" | jq -r '.AssigneeId // empty')
[[ -z "$account_id" ]] && account_id=$(echo "$tmpl" | jq -r '.AccountId // empty')
[[ -z "$location_id" ]] && location_id=$(echo "$tmpl" | jq -r '.LocationId // empty')
[[ -z "$contact_id" ]] && contact_id=$(echo "$tmpl" | jq -r '.ContactId // empty')
fi
# ── Interactive prompts ───────────────────────────────────────────────────
if $interactive; then
prompt_if_empty "Title" title
prompt_if_empty "Details" details
prompt_if_empty "Account ID" account_id
prompt_if_empty "Location ID" location_id
prompt_if_empty "Status ID" status_id
prompt_if_empty "Priority ID" priority_id
prompt_if_empty "Type ID" type_id
prompt_if_empty "Source ID" source_id
prompt_if_empty "Assignee ID (optional, Enter to skip)" assignee_id
else
# When using a template, prompt only for fields still missing that are required
if [[ -n "$template_id" ]]; then
[[ -n "$title" ]] || { read -r -p "Title: " title; }
[[ -n "$details" ]] || { read -r -p "Details: " details; }
[[ -n "$account_id" ]] || { read -r -p "Account ID: " account_id; }
[[ -n "$location_id" ]] || { read -r -p "Location ID: " location_id; }
[[ -n "$status_id" ]] || { read -r -p "Status ID: " status_id; }
[[ -n "$priority_id" ]] || { read -r -p "Priority ID: " priority_id; }
[[ -n "$type_id" ]] || { read -r -p "Type ID: " type_id; }
[[ -n "$source_id" ]] || { read -r -p "Source ID: " source_id; }
fi
fi
[[ -n "$title" ]] || die "--title is required"
[[ -n "$details" ]] || die "--details is required"
[[ -n "$account_id" ]] || die "--account-id is required"
[[ -n "$location_id" ]] || die "--location-id is required"
[[ -n "$status_id" ]] || die "--status-id is required"
[[ -n "$priority_id" ]] || die "--priority-id is required"
[[ -n "$type_id" ]] || die "--type-id is required"
[[ -n "$source_id" ]] || die "--source-id is required"
open_date="${open_date:-$(date -u +%Y-%m-%dT%H:%M:%S)}"
local body
body=$(jq -n \
--arg title "$title" \
--arg details "$details" \
--argjson account_id "$account_id" \
--argjson location_id "$location_id" \
--argjson status_id "$status_id" \
--argjson priority_id "$priority_id" \
--argjson type_id "$type_id" \
--argjson source_id "$source_id" \
--arg open_date "$open_date" \
'{
Title: $title,
Details: $details,
AccountId: $account_id,
LocationId: $location_id,
StatusId: $status_id,
PriorityId: $priority_id,
TypeId: $type_id,
SourceId: $source_id,
OpenDate: $open_date
}')
# Optionally add non-required fields
[[ -n "$contact_id" ]] && body=$(echo "$body" | jq --argjson v "$contact_id" '. + {ContactId: $v}')
[[ -n "$assignee_id" ]] && body=$(echo "$body" | jq --argjson v "$assignee_id" '. + {AssigneeId: $v}')
[[ -n "$queue_id" ]] && body=$(echo "$body" | jq --argjson v "$queue_id" '. + {QueueId: $v}')
[[ -n "$due_date" ]] && body=$(echo "$body" | jq --arg v "$due_date" '. + {DueDate: $v}')
local response
response=$(bms_curl POST "/v2/servicedesk/tickets" -d "$body")
echo "$response" | jq -r '"Created ticket ID: \(.Data.Id // .Id) — \(.Data.TicketNumber // .TicketNumber // "N/A")"'
}
cmd_update() {
local ticket_id="${1:-}"
[[ -n "$ticket_id" ]] || die "Usage: bms tickets update <ticketId> [options]"
shift
# Fetch current ticket first so we can do a full PUT with changes merged
local current
current=$(bms_curl GET "/v2/servicedesk/tickets/${ticket_id}" | jq '.Data // .')
local patch="{}"
while [[ $# -gt 0 ]]; do
case "$1" in
--title) patch=$(echo "$patch" | jq --arg v "$2" '. + {Title: $v}'); shift 2 ;;
--details) patch=$(echo "$patch" | jq --arg v "$2" '. + {Details: $v}'); shift 2 ;;
--status-id) patch=$(echo "$patch" | jq --argjson v "$2" '. + {StatusId: $v}'); shift 2 ;;
--priority-id) patch=$(echo "$patch" | jq --argjson v "$2" '. + {PriorityId: $v}'); shift 2 ;;
--assignee-id) patch=$(echo "$patch" | jq --argjson v "$2" '. + {AssigneeId: $v}'); shift 2 ;;
--queue-id) patch=$(echo "$patch" | jq --argjson v "$2" '. + {QueueId: $v}'); shift 2 ;;
--due-date) patch=$(echo "$patch" | jq --arg v "$2" '. + {DueDate: $v}'); shift 2 ;;
*) die "Unknown option: $1" ;;
esac
done
# Merge patch onto current (keep required fields from current ticket)
local body
body=$(echo "$current" | jq \
--argjson patch "$patch" \
'{
Title: .Title,
Details: .Details,
AccountId: .AccountId,
LocationId: .LocationId,
StatusId: .StatusId,
PriorityId: .PriorityId,
TypeId: .TypeId,
SourceId: .SourceId,
OpenDate: .OpenDate,
AssigneeId: .AssigneeId,
QueueId: .QueueId,
ContactId: .ContactId
} * $patch')
local response
response=$(bms_curl PUT "/v2/servicedesk/tickets/${ticket_id}" -d "$body")
echo "$response" | jq -r '"Updated ticket \(.Data.Id // .Id // "'"$ticket_id"'")"'
}
cmd_note() {
local ticket_id="${1:-}"
[[ -n "$ticket_id" ]] || die "Usage: bms tickets note <ticketId> --message <text> [options]"
shift
local message="" is_internal=false type_id=1 status_id="" note_date=""
while [[ $# -gt 0 ]]; do
case "$1" in
--message|-m) message="$2"; shift 2 ;;
--internal) is_internal=true; shift ;;
--type-id) type_id="$2"; shift 2 ;;
--status-id) status_id="$2"; shift 2 ;;
*) die "Unknown option: $1" ;;
esac
done
[[ -n "$message" ]] || die "--message is required"
note_date=$(date -u +%Y-%m-%dT%H:%M:%S)
local body
body=$(jq -n \
--arg details "$message" \
--argjson is_internal "$is_internal" \
--argjson type_id "$type_id" \
--arg note_date "$note_date" \
'{
Details: $details,
IsInternal: $is_internal,
TypeId: $type_id,
NoteDate: $note_date
}')
[[ -n "$status_id" ]] && body=$(echo "$body" | jq --argjson v "$status_id" '. + {StatusId: $v}')
local response
response=$(bms_curl POST "/v2/servicedesk/tickets/${ticket_id}/notes" -d "$body")
echo "$response" | jq -r '"Note added (ID: \(.Data.Id // .Id // "ok"))"'
}
cmd_assign() {
local ticket_id="${1:-}"
[[ -n "$ticket_id" ]] || die "Usage: bms tickets assign <ticketId> [options]"
shift
local assignee_id="" queue_id="" note="" is_internal=false type_id=1 status_id=""
while [[ $# -gt 0 ]]; do
case "$1" in
--assignee-id) assignee_id="$2"; shift 2 ;;
--queue-id) queue_id="$2"; shift 2 ;;
--note|-n) note="$2"; shift 2 ;;
--internal) is_internal=true; shift ;;
--type-id) type_id="$2"; shift 2 ;;
--status-id) status_id="$2"; shift 2 ;;
*) die "Unknown option: $1" ;;
esac
done
[[ -n "$note" ]] || note="Ticket reassigned."
local note_date
note_date=$(date -u +%Y-%m-%dT%H:%M:%S)
local body
body=$(jq -n \
--arg details "$note" \
--argjson is_internal "$is_internal" \
--argjson type_id "$type_id" \
--arg note_date "$note_date" \
'{
Details: $details,
IsInternal: $is_internal,
TypeId: $type_id,
NoteDate: $note_date
}')
[[ -n "$status_id" ]] && body=$(echo "$body" | jq --argjson v "$status_id" '. + {StatusId: $v}')
[[ -n "$assignee_id" ]] && body=$(echo "$body" | jq --argjson v "$assignee_id" '. + {AssigneeId: $v}')
[[ -n "$queue_id" ]] && body=$(echo "$body" | jq --argjson v "$queue_id" '. + {QueueId: $v}')
local response
response=$(bms_curl POST "/v2/servicedesk/tickets/${ticket_id}/assignticket" -d "$body")
echo "$response" | jq -r '"Ticket \("'"$ticket_id"'") assigned."'
}
cmd_resolve() {
local ticket_id="${1:-}"
[[ -n "$ticket_id" ]] || die "Usage: bms tickets resolve <ticketId> --comment <text> [options]"
shift
local comment="" status_id="" publish_kb=false is_internal=false
while [[ $# -gt 0 ]]; do
case "$1" in
--comment|-c) comment="$2"; shift 2 ;;
--status-id) status_id="$2"; shift 2 ;;
--publish-kb) publish_kb=true; shift ;;
--internal) is_internal=true; shift ;;
*) die "Unknown option: $1" ;;
esac
done
[[ -n "$comment" ]] || die "--comment is required"
local body
body=$(jq -n \
--arg comment "$comment" \
--argjson publish_kb "$publish_kb" \
--argjson is_internal "$is_internal" \
'{Comment: $comment, IsPublishToKnowledgeBase: $publish_kb, IsInternal: $is_internal}')
[[ -n "$status_id" ]] && body=$(echo "$body" | jq --argjson v "$status_id" '. + {StatusId: $v}')
local response
response=$(bms_curl POST "/v2/servicedesk/tickets/${ticket_id}/resolve" -d "$body")
echo "$response" | jq -r '"Ticket \("'"$ticket_id"'") resolved."'
}
cmd_delete() {
local ids=("$@")
[[ ${#ids[@]} -gt 0 ]] || die "Usage: bms tickets delete <id> [id2 ...]"
if [[ ${#ids[@]} -eq 1 ]]; then
bms_curl DELETE "/v2/servicedesk/tickets/${ids[0]}" >/dev/null
echo "Deleted ticket ${ids[0]}"
else
local body
body=$(printf '%s\n' "${ids[@]}" | jq -Rs 'split("\n") | map(select(. != "")) | map(tonumber) | {Ids: .}')
bms_curl DELETE "/v2/servicedesk/tickets" -d "$body" >/dev/null
echo "Deleted tickets: ${ids[*]}"
fi
}
# ─── Dispatch ────────────────────────────────────────────────────────────────
subcmd="${1:-list}"
[[ $# -gt 0 ]] && shift
case "$subcmd" in
list) cmd_list "$@" ;;
get) cmd_get "$@" ;;
create) cmd_create "$@" ;;
update) cmd_update "$@" ;;
note) cmd_note "$@" ;;
assign) cmd_assign "$@" ;;
resolve) cmd_resolve "$@" ;;
delete) cmd_delete "$@" ;;
*)
echo "Usage: bms tickets <list|get|create|update|note|assign|resolve|delete>" >&2
exit 1
;;
esac