feat(webhook): add Apprise webhook support and rename bot-webhook to discord-webhook

- Rename the Discord webhook endpoint from `/bot-webhook/` to `/discord-webhook/` for better clarity.
- Add new routes and controller methods (`receiveApprise`, `receiveAppriseNotify`) to support Apprise integrations.
- Implement `mapApprisePayload` in TalkService to map Apprise JSON format (title, body, type, attachments) to Talk message format.
- Update the settings UI to generate and display URLs for both Discord and Apprise webhooks.
- Update `INSTALL.md` and `README.md` documentation to reflect the new webhook paths and Apprise support.
- Refactor token revocation logic to revoke all tokens for a room at once.
This commit is contained in:
kyle
2026-06-12 16:40:53 -07:00
parent 6453715bf0
commit 42fa70491d
6 changed files with 411 additions and 52 deletions
+138
View File
@@ -566,6 +566,144 @@ class TalkService {
return $this->config->getAppValue(self::APP_ID, 'sender_name', 'Webhook Bot');
}
/**
* Map Apprise JSON payload to our internal format.
*
* Apprise sends: { version, type, title, body, attachments }
* Returns: { message, senderName, richObjects }
*/
public function mapApprisePayload(array $data, string $roomToken = ''): array {
$parts = [];
// Title (if present)
if (!empty($data['title'])) {
$parts[] = '**' . $data['title'] . '**';
}
// Body
if (!empty($data['body'])) {
$parts[] = $data['body'];
}
// For image-type notifications, use the attachment URL as the message
if (!empty($data['type']) && $data['type'] === 'image') {
// Apprise sends image notifications with type=image and attachment (string URL)
$imageUrls = [];
if (!empty($data['attachment']) && is_string($data['attachment'])) {
$imageUrls[] = $data['attachment'];
}
if (!empty($data['attachments']) && is_array($data['attachments'])) {
foreach ($data['attachments'] as $a) {
if (is_string($a)) {
$imageUrls[] = $a;
} elseif (is_array($a) && !empty($a['url'])) {
$imageUrls[] = $a['url'];
}
}
}
if (!empty($imageUrls)) {
// Use title as message if body is empty
if (empty($data['body'])) {
$message = !empty($data['title']) ? '**' . $data['title'] . '**' : '';
} else {
$message = implode("\n\n", $parts);
}
$senderName = $this->config->getAppValue(self::APP_ID, 'sender_name', 'Webhook Bot');
$richObjects = [];
foreach ($imageUrls as $imageUrl) {
$imageData = $this->downloadImage($imageUrl);
if ($imageData === null) {
continue;
}
$fileName = basename(parse_url($imageUrl, PHP_URL_PATH)) ?: 'attachment';
$uploadPath = $this->uploadImage($roomToken, $fileName, $imageData['data'], $imageData['mimeType']);
if ($uploadPath !== null) {
$richObj = $this->buildRichObject($uploadPath, $imageData['mimeType'], $roomToken);
if ($richObj !== null) {
$richObjects[] = $richObj;
}
}
}
return [
'message' => $message,
'senderName' => $senderName,
'richObjects' => $richObjects,
];
}
}
// Type prefix for context (e.g. "[Warning]")
if (!empty($data['type'])) {
$typeLabels = [
'info' => '[Info]',
'success' => '[Success]',
'warning' => '[Warning]',
'error' => '[Error]',
];
if (isset($typeLabels[$data['type']])) {
array_unshift($parts, $typeLabels[$data['type']]);
}
}
$message = implode("\n\n", $parts);
// Sender name: use app name if available, otherwise default
$senderName = $this->config->getAppValue(self::APP_ID, 'sender_name', 'Webhook Bot');
// Handle attachments: download files and upload to Talk
$richObjects = [];
if (!empty($data['attachments']) && is_array($data['attachments'])) {
foreach ($data['attachments'] as $attachment) {
if (!is_array($attachment)) {
continue;
}
$filePath = $attachment['path'] ?? '';
if (str_starts_with($filePath, 'file://')) {
$localPath = substr($filePath, 7); // Strip 'file://' prefix
if (is_file($localPath) && is_readable($localPath)) {
$fileData = file_get_contents($localPath);
if ($fileData === false) {
continue;
}
$fileName = $attachment['name'] ?? basename($localPath);
$mimeType = $attachment['mimeType'] ?? mime_content_type($localPath);
$uploadPath = $this->uploadImage($roomToken, $fileName, $fileData, $mimeType);
if ($uploadPath !== null) {
$richObj = $this->buildRichObject($uploadPath, $mimeType, $roomToken);
if ($richObj !== null) {
$richObjects[] = $richObj;
}
}
}
} elseif (!empty($attachment['url'] ?? '')) {
// Remote URL attachment
$imageData = $this->downloadImage($attachment['url']);
if ($imageData === null) {
continue;
}
$fileName = $attachment['name'] ?? 'attachment';
$uploadPath = $this->uploadImage($roomToken, $fileName, $imageData['data'], $imageData['mimeType']);
if ($uploadPath !== null) {
$richObj = $this->buildRichObject($uploadPath, $imageData['mimeType'], $roomToken);
if ($richObj !== null) {
$richObjects[] = $richObj;
}
}
}
}
}
return [
'message' => $message,
'senderName' => $senderName,
'richObjects' => $richObjects,
];
}
/**
* Set sender name default.
*/