blob: 3c2d3e70b49025d2547c6be7320b23d62725ae4b [file] [log] [blame]
Marc Kupietz34c5e412025-12-03 17:52:29 +01001#!/bin/bash
2
3# Default values
4NC_URL="${NC_TALK_URL:-https://cloud.ids-mannheim.de}"
5CONVERSATION="${NC_TALK_CONVERSATION:-o6toyqx7}"
6SECRET="${NC_TALK_SECRET}"
Marc Kupietzb7c62c12026-07-16 11:56:36 +02007NC_USER="${NC_TALK_USER}"
8NC_APP_PASSWORD="${NC_TALK_APP_PASSWORD}"
9ATTACHMENT_FOLDER="${NC_TALK_ATTACHMENT_FOLDER:-/Talk}"
10ATTACHMENTS=()
Marc Kupietz34c5e412025-12-03 17:52:29 +010011MESSAGE=""
12DEBUG=false
13SILENT=false
14
15# Function to display help
16show_help() {
17 cat << EOF
18Usage: $(basename "$0") [OPTIONS] [message]
19
20Sends <message> to a Nextcloud Talk room. Use - for reading message from stdin
21
22Options:
23 -h, --help Show this help message
24 -d, --debug Output extra debugging
Marc Kupietzb7c62c12026-07-16 11:56:36 +020025 -c, --conversation <token> Send message to conversation (default: $CONVERSATION)
Marc Kupietz34c5e412025-12-03 17:52:29 +010026 -s, --secret <secret> Bot secret
Marc Kupietzb7c62c12026-07-16 11:56:36 +020027 -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 Kupietz34c5e412025-12-03 17:52:29 +010036 --silent Send message silently (no notification)
37
38Environment variables:
Marc Kupietzb7c62c12026-07-16 11:56:36 +020039 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 Kupietz34c5e412025-12-03 17:52:29 +010045
46EOF
47 exit 0
48}
49
50# Parse command line arguments
51while [[ $# -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 Kupietzb7c62c12026-07-16 11:56:36 +020072 -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 Kupietz34c5e412025-12-03 17:52:29 +010084 --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
97done
98
Marc Kupietzb7c62c12026-07-16 11:56:36 +020099# Check if we should read from stdin (before consuming stdin).
100# With attachments, an empty message is allowed and does not trigger stdin reading.
101if [[ "$MESSAGE" == "-" ]] || { [[ -z "$MESSAGE" ]] && [[ ${#ATTACHMENTS[@]} -eq 0 ]]; }; then
Marc Kupietz34c5e412025-12-03 17:52:29 +0100102 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```'
107fi
108
109# Check required parameters
110if [[ -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
113fi
114
115if [[ -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
118fi
119
Marc Kupietzb7c62c12026-07-16 11:56:36 +0200120if [[ -z "$MESSAGE" ]] && [[ ${#ATTACHMENTS[@]} -eq 0 ]]; then
Marc Kupietz34c5e412025-12-03 17:52:29 +0100121 echo "Error: Message is required." >&2
122 exit 1
123fi
124
Marc Kupietzb7c62c12026-07-16 11:56:36 +0200125# Check attachment prerequisites
126if [[ ${#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
138fi
139
Marc Kupietz34c5e412025-12-03 17:52:29 +0100140if [[ "$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 Kupietzb7c62c12026-07-16 11:56:36 +0200145 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 Kupietz34c5e412025-12-03 17:52:29 +0100150fi
151
Marc Kupietzb7c62c12026-07-16 11:56:36 +0200152if [[ -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 Kupietz34c5e412025-12-03 17:52:29 +0100157
Marc Kupietzb7c62c12026-07-16 11:56:36 +0200158 # Escape the message for JSON (replace backslashes, quotes, and newlines)
159 MESSAGE_ESCAPED=$(echo -n "$MESSAGE" | jq -Rs .)
Marc Kupietz34c5e412025-12-03 17:52:29 +0100160
Marc Kupietzb7c62c12026-07-16 11:56:36 +0200161 # 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"'}'
170fi
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).
176if [[ ${#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
231fi
Marc Kupietz34c5e412025-12-03 17:52:29 +0100232