Add attachment support to talk.sh
The Talk bot API cannot upload files, so attachments (-a/--attachment,
repeatable) are uploaded via WebDAV with a regular user account
(NC_TALK_USER/NC_TALK_APP_PASSWORD or -U/-P) and then shared into the
conversation via the OCS share API (shareType 10). Files land in the
user's attachment folder (NC_TALK_ATTACHMENT_FOLDER, default /Talk)
with a timestamp prefix to avoid overwrite and re-share conflicts.
The message is now optional when attachments are given, and stdin is
not consumed in that case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Change-Id: Id01704e6d0788661c166f612e4c1a3300e5a60b1
diff --git a/bin/talk.sh b/bin/talk.sh
index b00109e..3c2d3e7 100755
--- a/bin/talk.sh
+++ b/bin/talk.sh
@@ -4,6 +4,10 @@
NC_URL="${NC_TALK_URL:-https://cloud.ids-mannheim.de}"
CONVERSATION="${NC_TALK_CONVERSATION:-o6toyqx7}"
SECRET="${NC_TALK_SECRET}"
+NC_USER="${NC_TALK_USER}"
+NC_APP_PASSWORD="${NC_TALK_APP_PASSWORD}"
+ATTACHMENT_FOLDER="${NC_TALK_ATTACHMENT_FOLDER:-/Talk}"
+ATTACHMENTS=()
MESSAGE=""
DEBUG=false
SILENT=false
@@ -18,15 +22,26 @@
Options:
-h, --help Show this help message
-d, --debug Output extra debugging
- -c, --conversation <token> Send message to conversation (default: o6toyqx7)
+ -c, --conversation <token> Send message to conversation (default: $CONVERSATION)
-s, --secret <secret> Bot secret
- -u, --url <url> Nextcloud URL (default: https://cloud.ids-mannheim.de)
+ -u, --url <url> Nextcloud URL (default: $NC_URL)
+ -a, --attachment <file> Attach a file (PDF, image, ...); may be given
+ multiple times. Requires a Nextcloud user
+ account (see -U/-P), since the bot API cannot
+ upload files. Attachments are uploaded to the
+ user's attachment folder and shared into the
+ conversation as that user (not as the bot).
+ -U, --user <user> Nextcloud user for attachment upload
+ -P, --password <password> App password for attachment upload
--silent Send message silently (no notification)
Environment variables:
- NC_TALK_URL Nextcloud URL
- NC_TALK_CONVERSATION Conversation token
- NC_TALK_SECRET Bot secret
+ NC_TALK_URL Nextcloud URL
+ NC_TALK_CONVERSATION Conversation token
+ NC_TALK_SECRET Bot secret
+ NC_TALK_USER Nextcloud user for attachment upload
+ NC_TALK_APP_PASSWORD App password for attachment upload
+ NC_TALK_ATTACHMENT_FOLDER Remote folder for uploads (default: /Talk)
EOF
exit 0
@@ -54,6 +69,18 @@
NC_URL="$2"
shift 2
;;
+ -a|--attachment)
+ ATTACHMENTS+=("$2")
+ shift 2
+ ;;
+ -U|--user)
+ NC_USER="$2"
+ shift 2
+ ;;
+ -P|--password)
+ NC_APP_PASSWORD="$2"
+ shift 2
+ ;;
--silent)
SILENT=true
shift
@@ -69,8 +96,9 @@
esac
done
-# Check if we should read from stdin (before consuming stdin)
-if [[ -z "$MESSAGE" ]] || [[ "$MESSAGE" == "-" ]]; then
+# Check if we should read from stdin (before consuming stdin).
+# With attachments, an empty message is allowed and does not trigger stdin reading.
+if [[ "$MESSAGE" == "-" ]] || { [[ -z "$MESSAGE" ]] && [[ ${#ATTACHMENTS[@]} -eq 0 ]]; }; then
if [[ "$DEBUG" == true ]]; then
echo "Reading from stdin" >&2
fi
@@ -89,33 +117,116 @@
exit 1
fi
-if [[ -z "$MESSAGE" ]]; then
+if [[ -z "$MESSAGE" ]] && [[ ${#ATTACHMENTS[@]} -eq 0 ]]; then
echo "Error: Message is required." >&2
exit 1
fi
+# Check attachment prerequisites
+if [[ ${#ATTACHMENTS[@]} -gt 0 ]]; then
+ if [[ -z "$NC_USER" ]] || [[ -z "$NC_APP_PASSWORD" ]]; then
+ echo "Error: Attachments require a Nextcloud user and app password (the bot API cannot upload files)." >&2
+ echo "Use -U/--user and -P/--password or set NC_TALK_USER and NC_TALK_APP_PASSWORD." >&2
+ exit 1
+ fi
+ for FILE in "${ATTACHMENTS[@]}"; do
+ if [[ ! -f "$FILE" ]]; then
+ echo "Error: Attachment not found: $FILE" >&2
+ exit 1
+ fi
+ done
+fi
+
if [[ "$DEBUG" == true ]]; then
echo "URL: $NC_URL" >&2
echo "Conversation: $CONVERSATION" >&2
echo "Message: $MESSAGE" >&2
echo "Silent: $SILENT" >&2
+ if [[ ${#ATTACHMENTS[@]} -gt 0 ]]; then
+ echo "Attachments: ${ATTACHMENTS[*]}" >&2
+ echo "Upload user: $NC_USER" >&2
+ echo "Attachment folder: $ATTACHMENT_FOLDER" >&2
+ fi
fi
-# Generate a random header and signature
-RANDOM_HEADER=$(openssl rand -hex 32)
-MESSAGE_TO_SIGN="${RANDOM_HEADER}${MESSAGE}"
-SIGNATURE=$(echo -n "${MESSAGE_TO_SIGN}" | openssl dgst -sha256 -hmac "${SECRET}" | cut -d' ' -f2)
+if [[ -n "$MESSAGE" ]]; then
+ # Generate a random header and signature
+ RANDOM_HEADER=$(openssl rand -hex 32)
+ MESSAGE_TO_SIGN="${RANDOM_HEADER}${MESSAGE}"
+ SIGNATURE=$(echo -n "${MESSAGE_TO_SIGN}" | openssl dgst -sha256 -hmac "${SECRET}" | cut -d' ' -f2)
-# Escape the message for JSON (replace backslashes, quotes, and newlines)
-MESSAGE_ESCAPED=$(echo -n "$MESSAGE" | jq -Rs .)
+ # Escape the message for JSON (replace backslashes, quotes, and newlines)
+ MESSAGE_ESCAPED=$(echo -n "$MESSAGE" | jq -Rs .)
-# Send the message
-curl -X POST \
-"${NC_URL}/ocs/v2.php/apps/spreed/api/v1/bot/${CONVERSATION}/message" \
- -H "Content-Type: application/json" \
- -H "Accept: application/json" \
- -H "OCS-APIRequest: true" \
- -H "X-Nextcloud-Talk-Bot-Random: ${RANDOM_HEADER}" \
- -H "X-Nextcloud-Talk-Bot-Signature: ${SIGNATURE}" \
- -d '{"message":'"${MESSAGE_ESCAPED}"',"silent":'"$SILENT"'}'
+ # Send the message
+ curl -X POST \
+ "${NC_URL}/ocs/v2.php/apps/spreed/api/v1/bot/${CONVERSATION}/message" \
+ -H "Content-Type: application/json" \
+ -H "Accept: application/json" \
+ -H "OCS-APIRequest: true" \
+ -H "X-Nextcloud-Talk-Bot-Random: ${RANDOM_HEADER}" \
+ -H "X-Nextcloud-Talk-Bot-Signature: ${SIGNATURE}" \
+ -d '{"message":'"${MESSAGE_ESCAPED}"',"silent":'"$SILENT"'}'
+fi
+
+# Upload and share attachments. The bot API cannot upload files, so this uses
+# the WebDAV and OCS share APIs with a regular user account: upload the file
+# to the user's attachment folder, then share it into the conversation
+# (shareType 10 = Talk room).
+if [[ ${#ATTACHMENTS[@]} -gt 0 ]]; then
+ # The WebDAV path needs the internal user ID, which can differ from the
+ # login name (e.g. a UUID for LDAP accounts), so resolve it via OCS.
+ DAV_USER=$(curl -s -u "${NC_USER}:${NC_APP_PASSWORD}" \
+ -H "OCS-APIRequest: true" \
+ "${NC_URL}/ocs/v2.php/cloud/user?format=json" | jq -r '.ocs.data.id // empty')
+ if [[ -z "$DAV_USER" ]]; then
+ echo "Error: Could not resolve user ID for $NC_USER. Check user name and app password." >&2
+ exit 1
+ fi
+ if [[ "$DEBUG" == true ]]; then
+ echo "DAV user ID: $DAV_USER" >&2
+ fi
+
+ # Make sure the attachment folder exists (405 = already exists, ignored)
+ curl -s -o /dev/null -X MKCOL -u "${NC_USER}:${NC_APP_PASSWORD}" \
+ "${NC_URL}/remote.php/dav/files/${DAV_USER}${ATTACHMENT_FOLDER}"
+
+ for FILE in "${ATTACHMENTS[@]}"; do
+ BASENAME=$(basename "$FILE")
+ # Prefix with a timestamp to avoid overwriting or re-share conflicts
+ REMOTE_NAME="$(date +%Y%m%d-%H%M%S)_${BASENAME}"
+ REMOTE_NAME_ENCODED=$(jq -rn --arg v "$REMOTE_NAME" '$v|@uri')
+
+ if [[ "$DEBUG" == true ]]; then
+ echo "Uploading $FILE as ${ATTACHMENT_FOLDER}/${REMOTE_NAME}" >&2
+ fi
+
+ HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
+ -u "${NC_USER}:${NC_APP_PASSWORD}" \
+ -T "$FILE" \
+ "${NC_URL}/remote.php/dav/files/${DAV_USER}${ATTACHMENT_FOLDER}/${REMOTE_NAME_ENCODED}")
+ if [[ "$HTTP_CODE" != 2* ]]; then
+ echo "Error: Upload of $FILE failed (HTTP $HTTP_CODE)." >&2
+ exit 1
+ fi
+
+ SHARE_RESPONSE=$(curl -s -X POST \
+ -u "${NC_USER}:${NC_APP_PASSWORD}" \
+ -H "OCS-APIRequest: true" \
+ -H "Accept: application/json" \
+ "${NC_URL}/ocs/v2.php/apps/files_sharing/api/v1/shares" \
+ -d "shareType=10" \
+ -d "shareWith=${CONVERSATION}" \
+ --data-urlencode "path=${ATTACHMENT_FOLDER}/${REMOTE_NAME}")
+ SHARE_STATUS=$(echo "$SHARE_RESPONSE" | jq -r '.ocs.meta.statuscode // empty')
+ if [[ "$SHARE_STATUS" != "200" ]]; then
+ echo "Error: Sharing $FILE into conversation failed:" >&2
+ echo "$SHARE_RESPONSE" | jq -r '.ocs.meta.message // .' >&2
+ exit 1
+ fi
+ if [[ "$DEBUG" == true ]]; then
+ echo "Shared ${REMOTE_NAME} into conversation ${CONVERSATION}" >&2
+ fi
+ done
+fi