| Marc Kupietz | 34c5e41 | 2025-12-03 17:52:29 +0100 | [diff] [blame] | 1 | #!/bin/bash |
| 2 | |
| 3 | # Default values |
| 4 | NC_URL="${NC_TALK_URL:-https://cloud.ids-mannheim.de}" |
| 5 | CONVERSATION="${NC_TALK_CONVERSATION:-o6toyqx7}" |
| 6 | SECRET="${NC_TALK_SECRET}" |
| Marc Kupietz | b7c62c1 | 2026-07-16 11:56:36 +0200 | [diff] [blame^] | 7 | NC_USER="${NC_TALK_USER}" |
| 8 | NC_APP_PASSWORD="${NC_TALK_APP_PASSWORD}" |
| 9 | ATTACHMENT_FOLDER="${NC_TALK_ATTACHMENT_FOLDER:-/Talk}" |
| 10 | ATTACHMENTS=() |
| Marc Kupietz | 34c5e41 | 2025-12-03 17:52:29 +0100 | [diff] [blame] | 11 | MESSAGE="" |
| 12 | DEBUG=false |
| 13 | SILENT=false |
| 14 | |
| 15 | # Function to display help |
| 16 | show_help() { |
| 17 | cat << EOF |
| 18 | Usage: $(basename "$0") [OPTIONS] [message] |
| 19 | |
| 20 | Sends <message> to a Nextcloud Talk room. Use - for reading message from stdin |
| 21 | |
| 22 | Options: |
| 23 | -h, --help Show this help message |
| 24 | -d, --debug Output extra debugging |
| Marc Kupietz | b7c62c1 | 2026-07-16 11:56:36 +0200 | [diff] [blame^] | 25 | -c, --conversation <token> Send message to conversation (default: $CONVERSATION) |
| Marc Kupietz | 34c5e41 | 2025-12-03 17:52:29 +0100 | [diff] [blame] | 26 | -s, --secret <secret> Bot secret |
| Marc Kupietz | b7c62c1 | 2026-07-16 11:56:36 +0200 | [diff] [blame^] | 27 | -u, --url <url> Nextcloud URL (default: $NC_URL) |
| 28 | -a, --attachment <file> Attach a file (PDF, image, ...); may be given |
| 29 | multiple times. Requires a Nextcloud user |
| 30 | account (see -U/-P), since the bot API cannot |
| 31 | upload files. Attachments are uploaded to the |
| 32 | user's attachment folder and shared into the |
| 33 | conversation as that user (not as the bot). |
| 34 | -U, --user <user> Nextcloud user for attachment upload |
| 35 | -P, --password <password> App password for attachment upload |
| Marc Kupietz | 34c5e41 | 2025-12-03 17:52:29 +0100 | [diff] [blame] | 36 | --silent Send message silently (no notification) |
| 37 | |
| 38 | Environment variables: |
| Marc Kupietz | b7c62c1 | 2026-07-16 11:56:36 +0200 | [diff] [blame^] | 39 | NC_TALK_URL Nextcloud URL |
| 40 | NC_TALK_CONVERSATION Conversation token |
| 41 | NC_TALK_SECRET Bot secret |
| 42 | NC_TALK_USER Nextcloud user for attachment upload |
| 43 | NC_TALK_APP_PASSWORD App password for attachment upload |
| 44 | NC_TALK_ATTACHMENT_FOLDER Remote folder for uploads (default: /Talk) |
| Marc Kupietz | 34c5e41 | 2025-12-03 17:52:29 +0100 | [diff] [blame] | 45 | |
| 46 | EOF |
| 47 | exit 0 |
| 48 | } |
| 49 | |
| 50 | # Parse command line arguments |
| 51 | while [[ $# -gt 0 ]]; do |
| 52 | case $1 in |
| 53 | -h|--help) |
| 54 | show_help |
| 55 | ;; |
| 56 | -d|--debug) |
| 57 | DEBUG=true |
| 58 | shift |
| 59 | ;; |
| 60 | -c|--conversation) |
| 61 | CONVERSATION="$2" |
| 62 | shift 2 |
| 63 | ;; |
| 64 | -s|--secret) |
| 65 | SECRET="$2" |
| 66 | shift 2 |
| 67 | ;; |
| 68 | -u|--url) |
| 69 | NC_URL="$2" |
| 70 | shift 2 |
| 71 | ;; |
| Marc Kupietz | b7c62c1 | 2026-07-16 11:56:36 +0200 | [diff] [blame^] | 72 | -a|--attachment) |
| 73 | ATTACHMENTS+=("$2") |
| 74 | shift 2 |
| 75 | ;; |
| 76 | -U|--user) |
| 77 | NC_USER="$2" |
| 78 | shift 2 |
| 79 | ;; |
| 80 | -P|--password) |
| 81 | NC_APP_PASSWORD="$2" |
| 82 | shift 2 |
| 83 | ;; |
| Marc Kupietz | 34c5e41 | 2025-12-03 17:52:29 +0100 | [diff] [blame] | 84 | --silent) |
| 85 | SILENT=true |
| 86 | shift |
| 87 | ;; |
| 88 | -*) |
| 89 | echo "Unknown option: $1" >&2 |
| 90 | show_help |
| 91 | ;; |
| 92 | *) |
| 93 | MESSAGE="$1" |
| 94 | shift |
| 95 | ;; |
| 96 | esac |
| 97 | done |
| 98 | |
| Marc Kupietz | b7c62c1 | 2026-07-16 11:56:36 +0200 | [diff] [blame^] | 99 | # Check if we should read from stdin (before consuming stdin). |
| 100 | # With attachments, an empty message is allowed and does not trigger stdin reading. |
| 101 | if [[ "$MESSAGE" == "-" ]] || { [[ -z "$MESSAGE" ]] && [[ ${#ATTACHMENTS[@]} -eq 0 ]]; }; then |
| Marc Kupietz | 34c5e41 | 2025-12-03 17:52:29 +0100 | [diff] [blame] | 102 | if [[ "$DEBUG" == true ]]; then |
| 103 | echo "Reading from stdin" >&2 |
| 104 | fi |
| 105 | # Read from stdin and wrap in code blocks like slack.js |
| 106 | MESSAGE=$'```\n'"$(cat)"$'\n```' |
| 107 | fi |
| 108 | |
| 109 | # Check required parameters |
| 110 | if [[ -z "$CONVERSATION" ]]; then |
| 111 | echo "Error: Conversation token is required. Use -c/--conversation option or set NC_TALK_CONVERSATION environment variable." >&2 |
| 112 | exit 1 |
| 113 | fi |
| 114 | |
| 115 | if [[ -z "$SECRET" ]]; then |
| 116 | echo "Error: Bot secret is required. Use -s/--secret option or set NC_TALK_SECRET environment variable." >&2 |
| 117 | exit 1 |
| 118 | fi |
| 119 | |
| Marc Kupietz | b7c62c1 | 2026-07-16 11:56:36 +0200 | [diff] [blame^] | 120 | if [[ -z "$MESSAGE" ]] && [[ ${#ATTACHMENTS[@]} -eq 0 ]]; then |
| Marc Kupietz | 34c5e41 | 2025-12-03 17:52:29 +0100 | [diff] [blame] | 121 | echo "Error: Message is required." >&2 |
| 122 | exit 1 |
| 123 | fi |
| 124 | |
| Marc Kupietz | b7c62c1 | 2026-07-16 11:56:36 +0200 | [diff] [blame^] | 125 | # Check attachment prerequisites |
| 126 | if [[ ${#ATTACHMENTS[@]} -gt 0 ]]; then |
| 127 | if [[ -z "$NC_USER" ]] || [[ -z "$NC_APP_PASSWORD" ]]; then |
| 128 | echo "Error: Attachments require a Nextcloud user and app password (the bot API cannot upload files)." >&2 |
| 129 | echo "Use -U/--user and -P/--password or set NC_TALK_USER and NC_TALK_APP_PASSWORD." >&2 |
| 130 | exit 1 |
| 131 | fi |
| 132 | for FILE in "${ATTACHMENTS[@]}"; do |
| 133 | if [[ ! -f "$FILE" ]]; then |
| 134 | echo "Error: Attachment not found: $FILE" >&2 |
| 135 | exit 1 |
| 136 | fi |
| 137 | done |
| 138 | fi |
| 139 | |
| Marc Kupietz | 34c5e41 | 2025-12-03 17:52:29 +0100 | [diff] [blame] | 140 | if [[ "$DEBUG" == true ]]; then |
| 141 | echo "URL: $NC_URL" >&2 |
| 142 | echo "Conversation: $CONVERSATION" >&2 |
| 143 | echo "Message: $MESSAGE" >&2 |
| 144 | echo "Silent: $SILENT" >&2 |
| Marc Kupietz | b7c62c1 | 2026-07-16 11:56:36 +0200 | [diff] [blame^] | 145 | if [[ ${#ATTACHMENTS[@]} -gt 0 ]]; then |
| 146 | echo "Attachments: ${ATTACHMENTS[*]}" >&2 |
| 147 | echo "Upload user: $NC_USER" >&2 |
| 148 | echo "Attachment folder: $ATTACHMENT_FOLDER" >&2 |
| 149 | fi |
| Marc Kupietz | 34c5e41 | 2025-12-03 17:52:29 +0100 | [diff] [blame] | 150 | fi |
| 151 | |
| Marc Kupietz | b7c62c1 | 2026-07-16 11:56:36 +0200 | [diff] [blame^] | 152 | if [[ -n "$MESSAGE" ]]; then |
| 153 | # Generate a random header and signature |
| 154 | RANDOM_HEADER=$(openssl rand -hex 32) |
| 155 | MESSAGE_TO_SIGN="${RANDOM_HEADER}${MESSAGE}" |
| 156 | SIGNATURE=$(echo -n "${MESSAGE_TO_SIGN}" | openssl dgst -sha256 -hmac "${SECRET}" | cut -d' ' -f2) |
| Marc Kupietz | 34c5e41 | 2025-12-03 17:52:29 +0100 | [diff] [blame] | 157 | |
| Marc Kupietz | b7c62c1 | 2026-07-16 11:56:36 +0200 | [diff] [blame^] | 158 | # Escape the message for JSON (replace backslashes, quotes, and newlines) |
| 159 | MESSAGE_ESCAPED=$(echo -n "$MESSAGE" | jq -Rs .) |
| Marc Kupietz | 34c5e41 | 2025-12-03 17:52:29 +0100 | [diff] [blame] | 160 | |
| Marc Kupietz | b7c62c1 | 2026-07-16 11:56:36 +0200 | [diff] [blame^] | 161 | # Send the message |
| 162 | curl -X POST \ |
| 163 | "${NC_URL}/ocs/v2.php/apps/spreed/api/v1/bot/${CONVERSATION}/message" \ |
| 164 | -H "Content-Type: application/json" \ |
| 165 | -H "Accept: application/json" \ |
| 166 | -H "OCS-APIRequest: true" \ |
| 167 | -H "X-Nextcloud-Talk-Bot-Random: ${RANDOM_HEADER}" \ |
| 168 | -H "X-Nextcloud-Talk-Bot-Signature: ${SIGNATURE}" \ |
| 169 | -d '{"message":'"${MESSAGE_ESCAPED}"',"silent":'"$SILENT"'}' |
| 170 | fi |
| 171 | |
| 172 | # Upload and share attachments. The bot API cannot upload files, so this uses |
| 173 | # the WebDAV and OCS share APIs with a regular user account: upload the file |
| 174 | # to the user's attachment folder, then share it into the conversation |
| 175 | # (shareType 10 = Talk room). |
| 176 | if [[ ${#ATTACHMENTS[@]} -gt 0 ]]; then |
| 177 | # The WebDAV path needs the internal user ID, which can differ from the |
| 178 | # login name (e.g. a UUID for LDAP accounts), so resolve it via OCS. |
| 179 | DAV_USER=$(curl -s -u "${NC_USER}:${NC_APP_PASSWORD}" \ |
| 180 | -H "OCS-APIRequest: true" \ |
| 181 | "${NC_URL}/ocs/v2.php/cloud/user?format=json" | jq -r '.ocs.data.id // empty') |
| 182 | if [[ -z "$DAV_USER" ]]; then |
| 183 | echo "Error: Could not resolve user ID for $NC_USER. Check user name and app password." >&2 |
| 184 | exit 1 |
| 185 | fi |
| 186 | if [[ "$DEBUG" == true ]]; then |
| 187 | echo "DAV user ID: $DAV_USER" >&2 |
| 188 | fi |
| 189 | |
| 190 | # Make sure the attachment folder exists (405 = already exists, ignored) |
| 191 | curl -s -o /dev/null -X MKCOL -u "${NC_USER}:${NC_APP_PASSWORD}" \ |
| 192 | "${NC_URL}/remote.php/dav/files/${DAV_USER}${ATTACHMENT_FOLDER}" |
| 193 | |
| 194 | for FILE in "${ATTACHMENTS[@]}"; do |
| 195 | BASENAME=$(basename "$FILE") |
| 196 | # Prefix with a timestamp to avoid overwriting or re-share conflicts |
| 197 | REMOTE_NAME="$(date +%Y%m%d-%H%M%S)_${BASENAME}" |
| 198 | REMOTE_NAME_ENCODED=$(jq -rn --arg v "$REMOTE_NAME" '$v|@uri') |
| 199 | |
| 200 | if [[ "$DEBUG" == true ]]; then |
| 201 | echo "Uploading $FILE as ${ATTACHMENT_FOLDER}/${REMOTE_NAME}" >&2 |
| 202 | fi |
| 203 | |
| 204 | HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ |
| 205 | -u "${NC_USER}:${NC_APP_PASSWORD}" \ |
| 206 | -T "$FILE" \ |
| 207 | "${NC_URL}/remote.php/dav/files/${DAV_USER}${ATTACHMENT_FOLDER}/${REMOTE_NAME_ENCODED}") |
| 208 | if [[ "$HTTP_CODE" != 2* ]]; then |
| 209 | echo "Error: Upload of $FILE failed (HTTP $HTTP_CODE)." >&2 |
| 210 | exit 1 |
| 211 | fi |
| 212 | |
| 213 | SHARE_RESPONSE=$(curl -s -X POST \ |
| 214 | -u "${NC_USER}:${NC_APP_PASSWORD}" \ |
| 215 | -H "OCS-APIRequest: true" \ |
| 216 | -H "Accept: application/json" \ |
| 217 | "${NC_URL}/ocs/v2.php/apps/files_sharing/api/v1/shares" \ |
| 218 | -d "shareType=10" \ |
| 219 | -d "shareWith=${CONVERSATION}" \ |
| 220 | --data-urlencode "path=${ATTACHMENT_FOLDER}/${REMOTE_NAME}") |
| 221 | SHARE_STATUS=$(echo "$SHARE_RESPONSE" | jq -r '.ocs.meta.statuscode // empty') |
| 222 | if [[ "$SHARE_STATUS" != "200" ]]; then |
| 223 | echo "Error: Sharing $FILE into conversation failed:" >&2 |
| 224 | echo "$SHARE_RESPONSE" | jq -r '.ocs.meta.message // .' >&2 |
| 225 | exit 1 |
| 226 | fi |
| 227 | if [[ "$DEBUG" == true ]]; then |
| 228 | echo "Shared ${REMOTE_NAME} into conversation ${CONVERSATION}" >&2 |
| 229 | fi |
| 230 | done |
| 231 | fi |
| Marc Kupietz | 34c5e41 | 2025-12-03 17:52:29 +0100 | [diff] [blame] | 232 | |