feat(webhook): handle Apprise file attachments and add request logging
working for Home Assistant, Changedetector.io, *arrs, Jellyfin... - Add multipart/form-data file attachment handling in WebhookController - Support raw data attachments in TalkService - Log raw request details for debugging
This commit is contained in:
@@ -175,6 +175,47 @@ class WebhookController extends Controller {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle Apprise file attachments — sent as multipart/form-data.
|
||||||
|
// Apprise API sends metadata in $data['attachments'] and file bytes
|
||||||
|
// in $_FILES['attachment[]'] (array of files).
|
||||||
|
if (!empty($_FILES['attachment[]']['name']) && is_array($_FILES['attachment[]']['name'])) {
|
||||||
|
$fileNames = $_FILES['attachment[]']['name'];
|
||||||
|
$fileTmps = $_FILES['attachment[]']['tmp_name'];
|
||||||
|
$fileTypes = $_FILES['attachment[]']['type'];
|
||||||
|
$fileErrors = $_FILES['attachment[]']['error'];
|
||||||
|
$attachmentMeta = $data['attachments'] ?? [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < count($fileNames); $i++) {
|
||||||
|
if ($fileErrors[$i] !== UPLOAD_ERR_OK) {
|
||||||
|
$this->logger->warning('NCbotwebhooks: file upload error for attachment ' . $i, [
|
||||||
|
'app' => 'nc_bot_webhooks',
|
||||||
|
'error_code' => $fileErrors[$i],
|
||||||
|
]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = basename($fileNames[$i]) ?: 'upload.' . pathinfo($fileTypes[$i] ?? '', PATHINFO_EXTENSION);
|
||||||
|
$mimeType = $fileTypes[$i] ?: 'application/octet-stream';
|
||||||
|
$fileData = file_get_contents($fileTmps[$i]);
|
||||||
|
|
||||||
|
if ($fileData === false || strlen($fileData) === 0) {
|
||||||
|
$this->logger->warning('NCbotwebhooks: empty or unreadable file attachment ' . $i, [
|
||||||
|
'app' => 'nc_bot_webhooks',
|
||||||
|
'tmp_name' => $fileTmps[$i],
|
||||||
|
]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filePath = $this->talkService->uploadImage($roomToken, $filename, $fileData, $mimeType);
|
||||||
|
if ($filePath !== null) {
|
||||||
|
$richObj = $this->talkService->buildRichObject($filePath, $mimeType, $roomToken);
|
||||||
|
if ($richObj !== null) {
|
||||||
|
$richObjects[] = $richObj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Post to Talk via Chat API
|
// Post to Talk via Chat API
|
||||||
$success = $this->talkService->postToRoom($roomToken, $message, $senderName, $richObjects);
|
$success = $this->talkService->postToRoom($roomToken, $message, $senderName, $richObjects);
|
||||||
|
|
||||||
@@ -247,6 +288,22 @@ class WebhookController extends Controller {
|
|||||||
|
|
||||||
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
|
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
|
||||||
|
|
||||||
|
// Log raw request details for debugging
|
||||||
|
$preview = substr($body, 0, 2000);
|
||||||
|
// Sanitize binary data from preview to avoid corrupting logs
|
||||||
|
$preview = preg_replace('/[^\x20-\x7E\x0A\x0D\x09]/', '.', $preview);
|
||||||
|
$this->logger->info('NCbotwebhooks: receiveApprise raw request', [
|
||||||
|
'app' => 'nc_bot_webhooks',
|
||||||
|
'room_token' => $roomToken,
|
||||||
|
'content_type' => $contentType,
|
||||||
|
'body_length' => strlen($body),
|
||||||
|
'body_preview' => $preview,
|
||||||
|
'files_count' => count($_FILES),
|
||||||
|
'files' => json_encode($_FILES),
|
||||||
|
'post_count' => count($_POST),
|
||||||
|
'get_count' => count($_GET),
|
||||||
|
]);
|
||||||
|
|
||||||
// Try JSON first
|
// Try JSON first
|
||||||
$data = @json_decode($body, true);
|
$data = @json_decode($body, true);
|
||||||
$jsonOk = json_last_error() === JSON_ERROR_NONE && is_array($data);
|
$jsonOk = json_last_error() === JSON_ERROR_NONE && is_array($data);
|
||||||
@@ -254,10 +311,30 @@ class WebhookController extends Controller {
|
|||||||
if (!$jsonOk) {
|
if (!$jsonOk) {
|
||||||
// Check if content-type indicates multipart form data
|
// Check if content-type indicates multipart form data
|
||||||
if (stripos($contentType, 'multipart/form-data') !== false) {
|
if (stripos($contentType, 'multipart/form-data') !== false) {
|
||||||
// PHP auto-parses multipart into $_POST
|
// PHP auto-parses multipart: files go to $_FILES, form fields to $_POST
|
||||||
if (!empty($_POST)) {
|
if (!empty($_POST)) {
|
||||||
$data = $_POST;
|
$data = $_POST;
|
||||||
}
|
}
|
||||||
|
// Apprise API sends the notification payload as form fields (type, title, body)
|
||||||
|
// and the file separately in $_FILES['file01'].
|
||||||
|
// Construct the attachments array from $_FILES so mapApprisePayload can process it.
|
||||||
|
if (!empty($_FILES['file01']) && $_FILES['file01']['error'] === UPLOAD_ERR_OK) {
|
||||||
|
$file = $_FILES['file01'];
|
||||||
|
if (!empty($data['type']) && $data['type'] === 'image') {
|
||||||
|
$data['attachments'] = [[
|
||||||
|
'path' => 'file://' . $file['tmp_name'],
|
||||||
|
'name' => $file['name'],
|
||||||
|
'mimeType' => $file['type'],
|
||||||
|
]];
|
||||||
|
} else {
|
||||||
|
// For non-image types, still make the file available
|
||||||
|
$data['attachments'] = [[
|
||||||
|
'path' => 'file://' . $file['tmp_name'],
|
||||||
|
'name' => $file['name'],
|
||||||
|
'mimeType' => $file['type'],
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
}
|
||||||
} elseif (!empty($_POST)) {
|
} elseif (!empty($_POST)) {
|
||||||
// Some content-types still get auto-parsed
|
// Some content-types still get auto-parsed
|
||||||
$data = $_POST;
|
$data = $_POST;
|
||||||
|
|||||||
@@ -866,6 +866,18 @@ class TalkService {
|
|||||||
$richObjects[] = $richObj;
|
$richObjects[] = $richObj;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} elseif (!empty($attachment['data'] ?? '')) {
|
||||||
|
// Raw data attachment (extracted from standalone multipart part)
|
||||||
|
$fileData = $attachment['data'];
|
||||||
|
$fileName = $attachment['filename'] ?? 'attachment';
|
||||||
|
$mimeType = $attachment['mimeType'] ?? 'application/octet-stream';
|
||||||
|
$uploadPath = $this->uploadImage($roomToken, $fileName, $fileData, $mimeType);
|
||||||
|
if ($uploadPath !== null) {
|
||||||
|
$richObj = $this->buildRichObject($uploadPath, $mimeType, $roomToken);
|
||||||
|
if ($richObj !== null) {
|
||||||
|
$richObjects[] = $richObj;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user