#!/usr/bin/env bash set -euo pipefail # POS: communication matrix-sender — Send messages to a Matrix room via the client-server API (send, test, login) # POS_SUBCMDS: send test login # POS_CONFIG: matrix | matrix.env | MATRIX_HOMESERVER=:Homeserver URL (https://matrix.example.org)::https://matrix.example.org | MATRIX_ACCESS_TOKEN=secret:Access token (from 'pos communication matrix sender login' or a Matrix client) | MATRIX_USER_ID=:Your Matrix user id (set by login)::@you:example.org | MATRIX_ROOM_ID=:Room id or alias::#pos:example.org CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}" CONFIG_FILE="$CONFIG_DIR/matrix.env" # Shared config loader (canonical KEY=VALUE parser, env-wins precedence) source "$(dirname "$0")/../lib/config-ui.sh" 2>/dev/null || source "$(dirname "$0")/config-ui.sh" usage() { cat < [options] Send a text message to the configured room test Send a test message using the current config login --user <@id> Get an access token (password prompt) and save it Options (send): --markdown Send with org.matrix.custom.html formatting (best-effort markdown → HTML conversion) --room Override the room for one send Config: $CONFIG_FILE (MATRIX_HOMESERVER, MATRIX_ACCESS_TOKEN, MATRIX_USER_ID, MATRIX_ROOM_ID) — edit it with 'pos config matrix' Precedence: CLI flags > environment > config file. Examples: pos communication matrix sender send "Backup finished" pos communication matrix sender send "**disk full**" --markdown pos communication matrix sender send "hello" --room '#ops:example.org' pos communication matrix sender login --user @you:example.org pos communication matrix sender test EOF exit 0 } err() { echo "ERROR: $*" >&2; exit 1; } log() { echo "[+] $*"; } load_config() { load_env_file "$CONFIG_FILE" } save_config() { mkdir -p "$CONFIG_DIR" local key="$1" val="$2" tmp tmp="$(mktemp)" grep -v "^${key}=" "$CONFIG_FILE" 2>/dev/null >"$tmp" || true printf '%s="%s"\n' "$key" "$val" >>"$tmp" mv "$tmp" "$CONFIG_FILE" chmod 600 "$CONFIG_FILE" } urlencode() { local s="$1" s="${s//#/%23}" s="${s//\"/%22}" s="${s// /%20}" printf '%s' "$s" } # Best-effort markdown → HTML for org.matrix.custom.html. Handles code # fences, inline code, bold/italic/strike, links, headers, and lists. The # conversion is deliberately simple — it must never fail the send. md_to_html() { local t="$1" t="$(printf '%s' "$t" | sed -E \ -e 's/&/\&/g' \ -e 's//\>/g' \ -e 's/```([^`]*)```/\n
\1<\/code><\/pre>\n/g' \
        -e 's/`([^`]*)`/\1<\/code>/g' \
        -e 's/\*\*([^*]*)\*\*/\1<\/strong>/g' \
        -e 's/__([^_]*)__/\1<\/strong>/g' \
        -e 's/\*([^*]*)\*/\1<\/em>/g' \
        -e 's/_([^_]*)_/\1<\/em>/g' \
        -e 's/~~([^~]*)~~/\1<\/del>/g' \
        -e 's/\[([^]]*)\]\(([^)]*)\)/\1<\/a>/g' \
        -e 's/^[[:space:]]*#{1,6}[[:space:]]+/

/' \ -e 's/^[[:space:]]*([-*+]|[0-9]+\.)[[:space:]]+/
  • /' \ -e 's/$//')" printf '%s' "$t" } api_base() { local base="${MATRIX_HOMESERVER:-}" [ -n "$base" ] || err "No homeserver — run 'pos config matrix' (MATRIX_HOMESERVER)" printf '%s' "${base%/}" } send_room_message() { local room="$1" text="$2" markdown="$3" [ -n "${MATRIX_ACCESS_TOKEN:-}" ] || err "No access token — run 'pos communication matrix sender login'" [ -n "$room" ] || err "No room — run 'pos config matrix' (MATRIX_ROOM_ID) or pass --room" local base txn body fmt base="$(api_base)" txn="$(date +%s%N)" room="$(urlencode "$room")" if [ "$markdown" -eq 1 ]; then fmt="$(md_to_html "$text")" body="$(jq -n --arg body "$text" --arg html "$fmt" \ '{msgtype:"m.text", body:$body, format:"org.matrix.custom.html", formatted_body:$html}')" else body="$(jq -n --arg body "$text" '{msgtype:"m.text", body:$body}')" fi curl -fsS -m 60 -X PUT \ -H "Authorization: Bearer ${MATRIX_ACCESS_TOKEN}" \ -H "Content-Type: application/json" \ --data "$body" \ "${base}/_matrix/client/v3/rooms/${room}/send/m.room.message/${txn}" >/dev/null } cmd_send() { local value="" markdown=0 room="${MATRIX_ROOM_ID:-}" while [ $# -gt 0 ]; do case "$1" in --markdown) markdown=1; shift ;; --room) [ $# -ge 2 ] || err "--room needs a value" room="$2"; shift 2 ;; -h|--help) usage ;; --) shift; [ $# -ge 1 ] || err "No value given for send"; value="$1"; shift ;; -*) err "Unknown option '$1'" ;; *) [ -z "$value" ] || err "Unexpected extra argument '$1'"; value="$1"; shift ;; esac done [ -n "$value" ] || err "No value given for send" load_config [ -n "${room:-}" ] || room="${MATRIX_ROOM_ID:-}" send_room_message "$room" "$value" "$markdown" log "m.text sent to room ${room}" } cmd_login() { local user="" password while [ $# -gt 0 ]; do case "$1" in --user) [ $# -ge 2 ] || err "--user needs a value" user="$2"; shift 2 ;; -h|--help) usage ;; -*) err "Unknown option '$1'" ;; *) err "Unexpected argument '$1' (use --user @you:server)" ;; esac done [ -n "$user" ] || err "--user is required (e.g. --user @you:example.org)" [[ "$user" == @* ]] || user="@$user" load_config local base base="$(api_base)" read -rsp "Password for ${user}: " password &2 [ -n "$password" ] || err "empty password" local body code resp token uid body="$(mktemp)" code="$(curl -sS -o "$body" -w '%{http_code}' -m 60 -X POST \ -H "Content-Type: application/json" \ --data "$(jq -n --arg user "$user" --arg pw "$password" \ '{type:"m.login.password", identifier:{type:"m.id.user", user:$user}, password:$pw, initial_device_display_name:"pos"}')" \ "${base}/_matrix/client/v3/login")" || { rm -f "$body"; err "login request failed (homeserver unreachable?)"; } if [ "$code" != "200" ]; then local reason reason="$(jq -r 'if .errcode then .errcode + (if .error then ": " + .error else "" end) else "HTTP '"$code"'" end' "$body" 2>/dev/null || echo "HTTP $code")" rm -f "$body" err "login failed — $reason" fi resp="$(cat "$body")" rm -f "$body" token="$(printf '%s' "$resp" | jq -r '.access_token // empty')" uid="$(printf '%s' "$resp" | jq -r '.user_id // empty')" [ -n "$token" ] || err "login succeeded but no access_token in response" [ -n "$uid" ] || uid="$user" save_config MATRIX_ACCESS_TOKEN "$token" save_config MATRIX_USER_ID "$uid" log "logged in as $uid — token saved to $CONFIG_FILE" } cmd_test() { load_config send_room_message "${MATRIX_ROOM_ID:-}" "Test message from pos $(date '+%Y-%m-%d %H:%M:%S')" 0 log "test message sent to room ${MATRIX_ROOM_ID:-}" } cmd="${1:-}" case "$cmd" in -h|--help) usage ;; send) shift cmd_send "$@" ;; test) shift cmd_test "$@" ;; login) shift cmd_login "$@" ;; "") usage ;; *) echo "ERROR: Unknown matrix command '$cmd'" echo "Run 'pos communication matrix-sender --help' for usage." exit 1 ;; esac