- Add Home Assistant integration guide with REST command examples (README.md) - Document Apprise `type` field mapping to status icons (✅, ⚠️, ❌) across agent.md and architecture.md - Update prerequisites to Nextcloud 33 (INSTALL.md) - Remove outdated link preview suppression note (agent.md) - Fix folder name casing in troubleshooting section (INSTALL.md)
1233 lines
47 KiB
Markdown
1233 lines
47 KiB
Markdown
# NCbotwebhooks — Maintenance Reference
|
|
|
|
Complete maintenance reference for the `nc_bot_webhooks` Nextcloud app. Covers every file, API interaction, data flow, design decision, and operational detail.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
- [Project Structure](#project-structure)
|
|
- [App Identity & Configuration](#app-identity--configuration)
|
|
- [Every File — Purpose & Key Details](#every-file--purpose--key-details)
|
|
- [API Interactions](#api-interactions)
|
|
- [Data Flow](#data-flow)
|
|
- [Configuration Model](#configuration-model)
|
|
- [Security Model](#security-model)
|
|
- [Known Issues & Limitations](#known-issues--limitations)
|
|
- [Operational Procedures](#operational-procedures)
|
|
- [Debugging Guide](#debugging-guide)
|
|
- [Migration Notes](#migration-notes)
|
|
- [Testing](#testing)
|
|
|
|
---
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
nc_bot_webhooks/
|
|
├── appinfo/
|
|
│ ├── routes.php # HTTP route definitions
|
|
│ ├── info.xml # App metadata (id, version, dependencies, classes)
|
|
│ └── cron.php # Background job registration
|
|
├── lib/
|
|
│ ├── Controller/
|
|
│ │ └── WebhookController.php # HTTP handlers (webhooks + admin endpoints)
|
|
│ ├── Service/
|
|
│ │ └── TalkService.php # Core business logic (1138 lines)
|
|
│ ├── Command/
|
|
│ │ └── DebugToggle.php # CLI command for debug endpoint
|
|
│ ├── Settings/
|
|
│ │ └── Admin.php # Admin settings form handler (ISettings)
|
|
│ ├── NavigationProvider.php # App navigation (INavigationProvider)
|
|
│ └── Cron/
|
|
│ └── ImageCleanup.php # Background job (IJob)
|
|
├── js/
|
|
│ └── settings.js # Admin settings UI (351 lines)
|
|
├── templates/
|
|
│ └── adminSettings.php # Settings form HTML
|
|
├── css/
|
|
│ └── adminSettings.css # Settings UI styles
|
|
├── img/
|
|
│ ├── app.svg # App icon (used in navigation + admin)
|
|
│ └── app (copy).svg # Copy of app.svg (purpose unclear)
|
|
├── composer.json # Package name + PSR-4 mapping
|
|
├── composer/autoload.php # Manual PSR-4 autoloader
|
|
├── composer/autoload_psr4.php # Composer-generated PSR-4 map
|
|
├── README.md # User-facing documentation
|
|
├── INSTALL.md # Installation guide
|
|
├── architecture.md # Architecture documentation
|
|
└── agent.md # This file
|
|
```
|
|
|
|
**Key paths at runtime:**
|
|
- Web URL base: `/apps/nc_bot_webhooks/`
|
|
- Admin settings path: `Settings → Admin → NCbotwebhooks`
|
|
- Bot user: `talk-bot` (must exist and be admin)
|
|
- Image storage: `/nc_bot_webhooks-images/<room-token>/` under `talk-bot`'s personal files
|
|
|
|
---
|
|
|
|
## App Identity & Configuration
|
|
|
|
### Constants
|
|
|
|
| Constant | Value | Location |
|
|
|---|---|---|
|
|
| `APP_ID` | `'nc_bot_webhooks'` | `TalkService.php:24`, `WebhookController.php:33`, `DebugToggle.php:48` |
|
|
| `IMAGES_DIR` | `'nc_bot_webhooks-images'` | `TalkService.php:25`, `ImageCleanup.php:12` |
|
|
| `DEBUG_KEY` | `'debug_enabled'` | `DebugToggle.php:49` |
|
|
|
|
### AppConfig keys (stored in `oc_appconfig` table, `appid = 'nc_bot_webhooks'`)
|
|
|
|
| Key | Type | Purpose |
|
|
|---|---|---|
|
|
| `bot_password` | encrypted string | Bot user's app password (encrypted via `ICrypto`) |
|
|
| `rooms` | JSON object | Room token → display name mapping for configured rooms |
|
|
| `retention_days` | string (int) | Image retention period in days |
|
|
| `sender_name` | string | Default sender display name |
|
|
| `auth_tokens` | JSON object | Room token → auth token array mapping |
|
|
| `debug_enabled` | bool | Whether the `/debug` endpoint is active |
|
|
|
|
**Critical:** After the rename from `ncdiscordhook` to `nc_bot_webhooks`, old config data under the old app ID is inaccessible. Users upgrading must run:
|
|
```sql
|
|
UPDATE oc_appconfig SET appid = 'nc_bot_webhooks' WHERE appid = 'ncdiscordhook';
|
|
```
|
|
|
|
### Namespace
|
|
|
|
```
|
|
OCA\Ncbotwebhooks\
|
|
├── Controller\WebhookController
|
|
├── Service\TalkService
|
|
├── Command\DebugToggle
|
|
├── Settings\Admin
|
|
├── NavigationProvider
|
|
└── Cron\ImageCleanup
|
|
```
|
|
|
|
---
|
|
|
|
## Every File — Purpose & Key Details
|
|
|
|
### lib/Service/TalkService.php (1138 lines)
|
|
|
|
**The single most important file.** Contains all business logic.
|
|
|
|
#### Constructor / Dependencies
|
|
|
|
```php
|
|
public function __construct(
|
|
IAppConfig $config, // Nextcloud app config (key-value store)
|
|
IClientService $clientService, // HTTP client factory
|
|
IShareManager $shareManager, // File share management
|
|
IRootFolder $rootFolder, // Filesystem root
|
|
IUserManager $userManager, // User lookup
|
|
IDBConnection $db, // Database connection
|
|
IAttendeeMapper $attendeeMapper, // Talk attendee queries
|
|
LoggerInterface $logger, // PSR-3 logger
|
|
)
|
|
```
|
|
|
|
#### Bot Password Methods
|
|
|
|
| Method | Lines | Purpose |
|
|
|---|---|---|
|
|
| `hasBotPassword()` | ~28-34 | Checks if `bot_password` exists in AppConfig |
|
|
| `getBotPassword()` | ~36-48 | Retrieves and decrypts bot password via `ICrypto`. Returns `null` if not set. |
|
|
| `setBotPassword(string)` | ~50-58 | Encrypts and stores bot password via `ICrypto` → `IAppConfig` |
|
|
| `validateBotPassword(string)` | ~60-80 | **Round-trip encryption test**: encrypt → decrypt → compare. Checks: non-empty, no crypto-breaking chars, crypto layer functional. **Does NOT validate against Nextcloud server.** |
|
|
|
|
**Why round-trip test?** The original code used `TalkManager::getRemoteServer()` which requires a valid remote URL. The round-trip test is faster and more reliable — it verifies the password works with the crypto layer without making a network call.
|
|
|
|
#### Room Methods
|
|
|
|
| Method | Lines | Purpose |
|
|
|---|---|---|
|
|
| `getRooms()` | ~82-90 | Returns JSON-decoded `rooms` from AppConfig (room token → display name) |
|
|
| `setRooms(array)` | ~92-98 | Stores room token → display name mapping as JSON in AppConfig |
|
|
| `getAvailableTalkRooms()` | ~200-300 | **Queries Talk DB directly** (bypasses TalkManager). Filters: type IN (1,2,3), excludes deleted/note-to-self/sample/file rooms. Returns token → display name. |
|
|
| `isBotEnabledForRoom(string)` | ~996-999 | Checks if room token exists in configured rooms |
|
|
|
|
**Why direct DB queries?** Talk 14+ removed `TalkManager::getRoomForToken`. Talk's OCS API requires user sessions, not app passwords. Direct queries bypass both issues.
|
|
|
|
**Room type filter:**
|
|
- Type 1: public channel
|
|
- Type 2: group direct message
|
|
- Type 3: public direct message
|
|
- Excluded: type 4 (deleted), type 6 (note-to-self), sample rooms (`object_type = 'sample'`), file share rooms (`object_type = 'file'`), private DM rooms (`name LIKE '["%'`)
|
|
|
|
**Room name resolution:** In NC33, room display name is in the `name` column. Falls back to `token` if `name` is empty.
|
|
|
|
#### Base URL Resolution
|
|
|
|
`getBaseUrl()` resolves the server URL in this priority order:
|
|
|
|
1. `overwritehost` (highest priority, explicit hostname override)
|
|
2. `overwritewebroot` (if full URL; if path, falls through)
|
|
3. **Non-loopback trusted domain** — iterates `trusted_domains`, skips `127.0.0.1` and private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, ::1, fc00::/7) for Docker compatibility
|
|
4. `overwrite.cli.url` (last resort)
|
|
|
|
**Why this order?** Docker/container deployments often have `trusted_domains[0] = 127.0.0.1` (unreachable from within the container) and `overwrite.cli.url` pointing to `localhost:port`. The non-loopback filter handles this.
|
|
|
|
#### Auth Token Methods
|
|
|
|
| Method | Lines | Purpose |
|
|
|---|---|---|
|
|
| `getAuthTokens()` | ~474-478 | Returns JSON-decoded `auth_tokens` from AppConfig |
|
|
| `setAuthTokens(array)` | ~483-485 | Stores auth tokens as JSON in AppConfig |
|
|
| `validateAuthToken(roomToken, authToken)` | ~490-496 | Checks if authToken exists in the array for roomToken |
|
|
| `generateAuthToken(roomToken)` | ~502-511 | **Server-side**: `bin2hex(random_bytes(24))` → 48-char hex token |
|
|
| `revokeAuthToken(roomToken, authToken)` | ~516-529 | Removes token from array, cleans up empty room entries |
|
|
|
|
**Note:** The settings UI generates tokens client-side (`btoa(Math.random() + Date.now())`) — this is a known security limitation. Use `generateAuthToken()` for server-side generation.
|
|
|
|
#### Payload Mapping
|
|
|
|
| Method | Lines | Purpose |
|
|
|---|---|---|
|
|
| `mapPayload(array)` | ~548-583 | Maps Discord webhook JSON to Talk message text. Extracts: `content`, embed `title` (bold), embed `description`, embed `fields` (name: value). Joins with `\n\n`. |
|
|
| `mapApprisePayload(array, roomToken)` | ~608-766 | Maps Apprise JSON to internal format. Handles: `body`, `title`/`subject` as message, `type` → icon (✅ success, ⚠️ warning, ❌ error; none for info/image), `attachments` (remote URL, local file, base64), `type=image` special case. Returns `{message, senderName, displayName, richObjects, typeIcon}`. |
|
|
| `getSenderName(array)` | ~588-593 | Returns `username` from Discord payload, or config default |
|
|
| `getSenderNameDefault()` | ~598-600 | Returns config `sender_name` default |
|
|
| `prependDisplayName(string, string, string)` | ~540-546 | Prepends `{typeIcon}🤖 **{name}**\n\n` to message. `typeIcon` is ✅/⚠️/❌ for success/warning/error types, empty for info/image. Since Talk doesn't support per-message avatars. |
|
|
|
|
**Apprise attachment formats handled:**
|
|
1. **Remote URL**: `attachment['url']` → download → upload → rich object
|
|
2. **Local file**: `attachment['path']` starting with `file://` → read file → upload → rich object
|
|
3. **Base64**: `attachment['base64']` → decode → upload → rich object
|
|
4. **type=image**: special case — uses `attachment` (string URL) or `attachments` (array) as image URLs directly
|
|
|
|
**Type icon mapping:**
|
|
- `success` → ✅
|
|
- `warning` → ⚠️
|
|
- `error` → ❌
|
|
- `info` / `image` / missing → no icon (empty string)
|
|
|
|
**Base64 attachment format** (from Apprise library JSON method):
|
|
```json
|
|
{
|
|
"base64": "<base64-encoded-data>",
|
|
"filename": "image.png",
|
|
"mimetype": "image/png"
|
|
}
|
|
```
|
|
|
|
**type=image handling** (lines 622-667): When Apprise sends `type: "image"`, the body may be empty. The code uses `title` as the message text if body is empty, and processes all image URLs from `attachment` or `attachments` fields.
|
|
|
|
#### Image Handling
|
|
|
|
| Method | Lines | Purpose |
|
|
|---|---|---|
|
|
| `downloadImage(string)` | ~781-803 | HTTP GET via Nextcloud `IClientService`. Timeout: 15s. `allow_local_address: true` (needed for Docker). Returns `['data' => binary, 'mimeType' => string]` or null. |
|
|
| `uploadImage(roomToken, filename, data, mimeType)` | ~809-829 | Writes to `nc_bot_webhooks-images/<roomToken>/<filename>` under `talk-bot`'s personal storage. Uses `basename()` for path traversal protection. Returns relative path or null. |
|
|
| `buildRichObject(filePath, mimeType, roomToken)` | ~835-878 | Creates public link share (`SHARE_TYPE_LINK`) for file. Returns rich object data for Talk Chat API with token, name, mimetype, thumbnailReady, fileTarget, path. |
|
|
|
|
**Image flow for Discord:**
|
|
1. Extract `embed[].image.url` or `embed[].thumbnail.url`
|
|
2. `downloadImage()` — HTTP GET
|
|
3. Derive filename from URL path (or use `webhook-image.<ext>`)
|
|
4. `uploadImage()` — write to bot user storage
|
|
5. `buildRichObject()` — create public link share
|
|
6. Pass rich object to `postToRoom()` as `richObjects`
|
|
|
|
**Image flow for Apprise:**
|
|
1. Parse `attachments` array (supports URL, local file, base64)
|
|
2. For each attachment: download/read → upload → build rich object
|
|
3. Collect all rich objects → pass to `postToRoom()`
|
|
|
|
#### Talk Posting
|
|
|
|
| Method | Lines | Purpose |
|
|
|---|---|---|
|
|
| `postToRoom(roomToken, message, senderName, richObjects)` | ~894-991 | Posts message to Talk Chat API v1. Basic auth: `talk-bot:botPassword`. Endpoint: `/ocs/v2.php/apps/spreed/api/v1/chat/{roomToken}`. |
|
|
|
|
**Chat API v1 body format:**
|
|
```json
|
|
{
|
|
"message": "🤖 **CI Bot**\n\nBuild #1234 passed",
|
|
"actorType": "users",
|
|
"actorId": "talk-bot",
|
|
"actorDisplayName": "<bot's display name from room participant>",
|
|
"richObjects": {
|
|
"file-0": { "rich_object": {...}, "source": "file" }
|
|
},
|
|
"richObjectsEnd": {
|
|
"file-0": true
|
|
}
|
|
}
|
|
```
|
|
|
|
**Critical: `actorDisplayName` must match the bot's display name in the room's participant record.** In Talk 14+, if it doesn't match, the message is silently dropped. Resolved via `getBotDisplayNameForRoom()` which queries AttendeeMapper.
|
|
|
|
**`getBotDisplayNameForRoom()`** (lines 1005-1035):
|
|
1. Queries `talk_rooms` table for room ID from token
|
|
2. Uses `AttendeeMapper::findByActor(roomId, ACTOR_USERS, 'talk-bot')` to get attendee record
|
|
3. Returns attendee's display name, or `'talk-bot'` as fallback
|
|
|
|
#### Config Save
|
|
|
|
`saveConfig(array $config)` (lines 1100-1137):
|
|
1. Validate bot password (if provided) via round-trip test
|
|
2. Set bot password (if provided)
|
|
3. Set retention days (if provided)
|
|
4. Merge rooms: use provided rooms, remove disabled rooms
|
|
5. Set rooms
|
|
6. Set auth tokens (if provided)
|
|
7. Set sender name (if provided)
|
|
8. **`ensureBotParticipants()`** — adds talk-bot as participant in all configured rooms
|
|
|
|
**`ensureBotParticipants()`** (called on every save):
|
|
- Queries configured rooms
|
|
- For each room, checks if talk-bot is already a participant
|
|
- If not, inserts an attendee record via `AttendeeMapper`
|
|
- Ensures bot is in all rooms for Chat API to accept messages
|
|
|
|
#### Image Cleanup
|
|
|
|
| Method | Lines | Purpose |
|
|
|---|---|---|
|
|
| `purgeOldImages()` | ~1042-1059 | Gets retention days, computes cutoff time, gets bot user folder, calls `purgeFolder()` |
|
|
| `purgeFolder(Folder, int)` | ~1064-1084 | **Recursively** deletes files/folders by mtime only. Cleans up empty subdirectories. |
|
|
|
|
**⚠️ SECURITY CONCERN:** `purgeFolder()` deletes by mtime only — no file ownership verification. It operates on the `nc_bot_webhooks-images/` directory under the bot user's personal storage. This is safe because:
|
|
1. It only operates within the bot user's personal folder
|
|
2. The bot user is isolated from other users
|
|
3. The directory is specifically for webhook images
|
|
|
|
**User requested per-file attributes for additional safety — NOT YET IMPLEMENTED.**
|
|
|
|
#### Debug Helpers
|
|
|
|
| Method | Purpose |
|
|
|---|---|
|
|
| `detectTalkTableFromCatalog(newName, oldName)` | Queries `information_schema.tables` for Talk table. Tries both `talk_rooms`/`spreed_room` with prefix candidates from `spreed.databaseprefix` and `dbtableprefix`. |
|
|
| `getTalkTableColumns(tableName)` | Returns column names + types for a table |
|
|
| `getTalkTableSample(tableName, limit)` | Returns sample rows from a table |
|
|
| `getAllTalkRoomsDebug(limit)` | Returns all rooms with id, token, type, readable_name, label, name, object_type, object_id |
|
|
| `getRoomTypeBreakdown()` | Returns count of rooms per type |
|
|
|
|
---
|
|
|
|
### lib/Controller/WebhookController.php
|
|
|
|
**HTTP request handlers.** Extends `Controller`, constructor takes `appName`, `request`, `TalkService`, `IAppConfig`, `LoggerInterface`.
|
|
|
|
#### Discord Webhook Handler: `receive()`
|
|
|
|
**Route:** `POST /discord-webhook/{roomToken}/{token}`
|
|
|
|
**Flow:**
|
|
1. Validate auth token via `TalkService::validateAuthToken()`
|
|
2. Parse JSON from `php://input`
|
|
3. `mapPayload()` — convert to Talk message text
|
|
4. If no message content → return 400 `no_content`
|
|
5. `getSenderName()` — resolve sender name
|
|
6. `prependDisplayName()` — embed sender name in message
|
|
7. Iterate embeds → extract image/thumbnail URLs → `downloadImage()` → `uploadImage()` → `buildRichObject()`
|
|
8. `postToRoom()` — post to Talk Chat API
|
|
9. Return 201 `ok` or 500 `error`
|
|
|
|
**Response headers:** `X-Webhook-Status: ok|unauthorized|bad_request|no_content|error`
|
|
|
|
**HTTP status codes:** 401 (unauthorized), 400 (bad_request/no_content), 201 (ok), 500 (error)
|
|
|
|
#### Apprise Webhook Handler: `receiveApprise()`
|
|
|
|
**Route:** `POST /apprise-webhook/{roomToken}/{token}`
|
|
|
|
**Flow:**
|
|
1. Validate auth token
|
|
2. Detect content type: JSON → `$_POST` → form-encoded
|
|
3. Parse payload (try JSON → `$_POST` → `parse_str`)
|
|
4. **Unwrap `notifications` array** if present (Apprise API wrapper)
|
|
5. **Propagate wrapper-level `subject`/`title`** to notification entry (preserves original data before extraction)
|
|
6. `mapApprisePayload()` — convert to internal format
|
|
7. Handle attachments (remote URL, local file, base64)
|
|
8. `prependDisplayName()` — embed sender name
|
|
9. `postToRoom()` — post to Talk
|
|
10. Return response with `X-Webhook-Status`
|
|
|
|
**Key difference from Discord handler:** Apprise sends a different payload structure with `notifications` array wrapper. The code handles:
|
|
- Single notification (no wrapper)
|
|
- Wrapped notification (extract from `notifications[0]`)
|
|
- `type=image` notifications (special image handling)
|
|
- Multiple attachment formats (URL, local file, base64)
|
|
|
|
**Wrapper-level subject/title propagation fix:** When the wrapper has `subject` or `title` but the inner notification doesn't, these values are propagated to the notification entry so the sender name is preserved.
|
|
|
|
#### Apprise Notify Handler: `receiveAppriseNotify()`
|
|
|
|
**Route:** `POST /apprise-webhook/{roomToken}/notify/{token}`
|
|
|
|
Simply delegates to `receiveApprise()`. Apprise's `apprises://` URL scheme inserts `notify` in the path automatically.
|
|
|
|
#### Admin Endpoints
|
|
|
|
| Method | Route | Auth | Purpose |
|
|
|---|---|---|---|
|
|
| `saveConfig()` | POST `/save-config` | Admin session | Bulk config save (rooms, auth tokens, retention, sender name, bot password). Validates bot password, saves all config, ensures bot participants. |
|
|
| `saveBotPassword()` | POST `/save-bot-password` | Admin session | Standalone bot password save with validation. |
|
|
| `getRooms()` | GET `/rooms` | Admin session | Returns available Talk rooms and configured rooms for JS. |
|
|
| `debug()` | GET `/debug` | Admin session | Exposes DB schema, bot credentials, config (disabled by default). |
|
|
|
|
**`saveConfig()` flow:**
|
|
1. `TalkService::saveConfig()` — validates, saves, ensures participants
|
|
2. Return JSON: `{'status': 'ok', 'auth_tokens': <server state>}`
|
|
3. If validation fails → throw Exception → catch → return JSON error
|
|
|
|
**`getRooms()` flow:**
|
|
1. `TalkService::getAvailableTalkRooms()` → available rooms
|
|
2. `TalkService::getRooms()` → configured rooms
|
|
3. Merge: for each available room, check if configured → `{'token', 'name', 'configured'}`
|
|
4. Return JSON array
|
|
|
|
**`debug()` endpoint** (when enabled):
|
|
1. Get bot password from AppConfig
|
|
2. Get rooms from AppConfig
|
|
3. Get auth tokens from AppConfig
|
|
4. Get Talk table columns via `detectTalkTableFromCatalog()` → `getTalkTableColumns()`
|
|
5. Get room type breakdown
|
|
6. Return all as JSON
|
|
|
|
---
|
|
|
|
### lib/Command/DebugToggle.php
|
|
|
|
CLI command for managing the debug endpoint. Uses `IAppConfig` with key `debug_enabled` under app ID `nc_bot_webhooks`.
|
|
|
|
**Commands:**
|
|
- `php occ nc_bot_webhooks:debug:enable` — enable
|
|
- `php occ nc_bot_webhooks:debug:disable` — disable
|
|
- `php occ nc_bot_webhooks:debug:toggle` — toggle
|
|
- `php occ nc_bot_webhooks:debug:status` — show status
|
|
|
|
**Default:** disabled. **Warning:** enables public access to sensitive data (DB schema, bot credentials, config).
|
|
|
|
---
|
|
|
|
### lib/Settings/Admin.php
|
|
|
|
Admin settings form handler. Implements `ISettings`.
|
|
|
|
**`getForm()`:**
|
|
1. Loads CSS (`adminSettings`) and JS (`settings`)
|
|
2. Injects config data into template params:
|
|
- `hasBotPassword` — whether bot password is configured
|
|
- `retentionDays` — current retention value
|
|
- `rooms` — configured rooms (for checkbox state)
|
|
- `authTokens` — current auth tokens
|
|
- `configuredRooms` — configured rooms (for JS state)
|
|
- `serverUrl` — resolved base URL
|
|
- `senderName` — current sender name
|
|
- `l10n` — translated strings
|
|
3. Returns `TemplateResponse('nc_bot_webhooks', 'adminSettings', $params)`
|
|
|
|
**`getPriority(): 10`** — higher number = displayed lower in settings list
|
|
**`getSection(): 'additional'`** — places under "Additional" section
|
|
**`getIcons(): [imagePath('nc_bot_webhooks', 'app.svg')]`** — app icon
|
|
|
|
---
|
|
|
|
### lib/NavigationProvider.php
|
|
|
|
Implements `INavigationProvider`. Adds "NCbotwebhooks" entry to admin settings navigation.
|
|
|
|
**Admin-only:** returns empty array if user is not admin.
|
|
|
|
**Navigation entry:**
|
|
```php
|
|
[
|
|
'id' => 'nc_bot_webhooks',
|
|
'app_id' => 'nc_bot_webhooks',
|
|
'type' => 'settings',
|
|
'name' => 'NCbotwebhooks',
|
|
'href' => linkToRoute('settings.AdminSettings#index'),
|
|
'icon' => imagePath('nc_bot_webhooks', 'app.svg'),
|
|
'order' => 0,
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
### lib/Cron/ImageCleanup.php
|
|
|
|
Background job. Implements `IJob`.
|
|
|
|
**`run($argument)`:**
|
|
1. Get `talk-bot` user
|
|
2. Get `nc_bot_webhooks-images` folder from bot's personal storage
|
|
3. Call `TalkService::purgeOldImages()`
|
|
4. Log purge count
|
|
|
|
**Registration:** via `appinfo/info.xml` `<background-jobs>` and `appinfo/cron.php`.
|
|
|
|
**⚠️ NOTE:** Only deletes by mtime — no per-file attribute check. User requested per-file attributes for safety (NOT YET IMPLEMENTED).
|
|
|
|
---
|
|
|
|
### js/settings.js (351 lines)
|
|
|
|
Admin settings UI. All logic in `DOMContentLoaded` event handler.
|
|
|
|
**State variables:**
|
|
- `configuredRoomsState` — current room configuration
|
|
- `authTokensState` — current auth tokens (client-side)
|
|
- `serverAuthTokens` — canonical auth tokens from server (synced after save)
|
|
|
|
**Data sources:** Read from DOM data attributes on `#nc-config-data`:
|
|
- `data-configured-rooms` — JSON of configured rooms
|
|
- `data-auth-tokens` — JSON of auth tokens
|
|
- `data-server-url` — server base URL
|
|
- `data-sender-name` — current sender name
|
|
- `data-has-bot-password` — 1 or 0
|
|
- `data-retention` — current retention days
|
|
- `data-l10n` — JSON of translated strings
|
|
|
|
**Key functions:**
|
|
|
|
| Function | Lines | Purpose |
|
|
|---|---|---|
|
|
| `parseData(el, key)` | 5-11 | Safely parse JSON from data attribute |
|
|
| `showStatus(msg, type)` | 31-35 | Show transient status message (success/error) |
|
|
| `fetchRooms()` | 59-122 | GET `/rooms`, render checkboxes + token divs |
|
|
| `renderTokens(roomToken, container)` | 131-252 | Render auth tokens with webhook URLs + generate/revoke |
|
|
| `savePasswordBtn handler` | 255-282 | POST `/save-bot-password` with validation |
|
|
| `saveBtn handler` | 285-349 | POST `/save-config` with rooms, tokens, retention, sender name |
|
|
|
|
**Token generation** (line 245-246):
|
|
```javascript
|
|
var raw = Math.random().toString(36).substring(2) + Date.now().toString(36);
|
|
var token = btoa(raw).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
```
|
|
Uses URL-safe base64 encoding of `Math.random() + Date.now()`. **Known security limitation** — server-side `random_bytes(32)` would be more secure.
|
|
|
|
**URL generation** (lines 137-140):
|
|
```javascript
|
|
var discordPath = OC.generateUrl('/apps/' + APP_ID + '/discord-webhook/') + roomToken + '/{token}';
|
|
var apprisePath = OC.generateUrl('/apps/' + APP_ID + '/apprise-webhook/') + roomToken + '/notify/{token}';
|
|
var discordUrl = window.location.protocol + '//' + window.location.host + discordPath;
|
|
var appriseUrl = window.location.protocol + '//' + window.location.host + apprisePath;
|
|
```
|
|
Uses `window.location` for protocol/host, `{token}` placeholder replaced when token is generated.
|
|
|
|
**Save flow** (bulk config):
|
|
1. Collect checked checkboxes → rooms
|
|
2. Track disabled rooms (previously checked, now unchecked)
|
|
3. Build payload: rooms, disabled_rooms, auth_tokens, retention_days, sender_name
|
|
4. Include bot_password only if input is non-empty
|
|
5. POST to `/save-config`
|
|
6. On success: sync `authTokensState` from server response, re-render token divs
|
|
|
|
**Auto-fetch:** If configured rooms exist on load, automatically fetches room list.
|
|
|
|
---
|
|
|
|
### templates/adminSettings.php
|
|
|
|
Settings form HTML. Single `.nc-app-frame` wrapper containing:
|
|
|
|
1. **Bot App Password** — password input + conditional save button (shown only if no password configured)
|
|
2. **Default Sender Name** — text input
|
|
3. **Image Retention** — number input (1-365)
|
|
4. **Room Selection** — Fetch Rooms button + checkbox list + Generate Auth Token + Save Configuration + status
|
|
|
|
**Config data passed to JS** via `#nc-config-data` data attributes (JSON-encoded).
|
|
|
|
---
|
|
|
|
### css/adminSettings.css
|
|
|
|
Styles for the settings UI:
|
|
- `.nc-app-frame` — bordered frame around entire settings
|
|
- `.nc-settings-section` — bordered sections for each config group
|
|
- `.nc-room-item` — checkbox row layout
|
|
- `.nc-room-tokens` — token container with dark background
|
|
- `.nc-token-row` — token input + button row
|
|
- `.nc-token-url-input` — monospace URL display
|
|
- `.nc-token-copy` / `.nc-token-revoke` — action buttons
|
|
- `.nc-generate-token` — generate button
|
|
- `.nc-status-success` / `.nc-status-error` — status colors
|
|
- `.nc-hint` — helper text
|
|
|
|
---
|
|
|
|
### appinfo/routes.php
|
|
|
|
Route definitions. All webhook routes use `{roomToken}` and `{token}` path parameters.
|
|
|
|
```php
|
|
[
|
|
'webhook#receive', // POST /discord-webhook/{roomToken}/{token}
|
|
'webhook#saveConfig', // POST /save-config
|
|
'webhook#getRooms', // GET /rooms
|
|
'webhook#saveBotPassword', // POST /save-bot-password
|
|
'webhook#debug', // GET /debug (noCsrf)
|
|
'webhook#receiveApprise', // POST /apprise-webhook/{roomToken}/{token}
|
|
'webhook#receiveAppriseNotify', // POST /apprise-webhook/{roomToken}/notify/{token}
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
### appinfo/info.xml
|
|
|
|
```xml
|
|
<id>nc_bot_webhooks</id>
|
|
<name>NCbotwebhooks</name>
|
|
<summary>Discord webhook bridge for Nextcloud Talk with image support</summary>
|
|
<description>Accepts Discord webhook-style JSON payloads and posts them into Nextcloud Talk rooms, preserving image attachments.</description>
|
|
<version>1.1.0</version>
|
|
<licence>AGPL-3.0-or-later</licence>
|
|
<namespace>Ncbotwebhooks</namespace>
|
|
<dependencies>
|
|
<nextcloud min-version="33" max-version="33"/>
|
|
<app>spreed</app>
|
|
</dependencies>
|
|
```
|
|
|
|
**Dependencies:**
|
|
- Nextcloud 33 only (explicit version range)
|
|
- `spreed` app (Talk) must be enabled
|
|
|
|
**Class registrations:**
|
|
- Background job: `OCA\Ncbotwebhooks\Cron\ImageCleanup`
|
|
- Admin settings: `OCA\Ncbotwebhooks\Settings\Admin`
|
|
- CLI command: `OCA\Ncbotwebhooks\Command\DebugToggle`
|
|
|
|
---
|
|
|
|
### composer.json
|
|
|
|
```json
|
|
{
|
|
"name": "nc_bot_webhooks/app",
|
|
"autoload": {
|
|
"psr-4": {
|
|
"OCA\\Ncbotwebhooks\\": "../lib/"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Note: The `../lib/` path assumes this is in a `nc_bot_webhooks/` subdirectory of the project root. Nextcloud's app loading may not use this directly — the manual `composer/autoload.php` is the primary autoloader.
|
|
|
|
---
|
|
|
|
## API Interactions
|
|
|
|
### Nextcloud Talk Chat API v1
|
|
|
|
**Endpoint:** `POST /ocs/v2.php/apps/spreed/api/v1/chat/{roomToken}`
|
|
|
|
**Authentication:** Basic auth with `talk-bot:bot_password`
|
|
|
|
**Headers:**
|
|
```
|
|
OCS-Expect-Formatted: json
|
|
OCS-APIRequest: 1
|
|
Authorization: Basic <base64('talk-bot:bot_password')>
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request body:**
|
|
```json
|
|
{
|
|
"message": "Message text (supports rich text formatting)",
|
|
"actorType": "users",
|
|
"actorId": "talk-bot",
|
|
"actorDisplayName": "Display name from room participant",
|
|
"richObjects": {
|
|
"file-0": {
|
|
"rich_object": {
|
|
"id": "",
|
|
"elements": [{
|
|
"type": "file",
|
|
"id": "<share token>",
|
|
"name": "filename.png",
|
|
"mimetype": "image/png",
|
|
"thumbnailReady": true,
|
|
"fileTarget": "/nc_bot_webhooks-images/roomToken/filename.png",
|
|
"path": "filename.png"
|
|
}]
|
|
},
|
|
"source": "file"
|
|
}
|
|
},
|
|
"richObjectsEnd": {
|
|
"file-0": true
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response:** 200 on success, error on failure. The app checks `statusCode >= 200 && statusCode < 300`.
|
|
|
|
**Critical constraint:** `actorDisplayName` MUST match the bot's display name in the room's participant record. Mismatch → message silently dropped.
|
|
|
|
### Apprise API
|
|
|
|
Apprise sends webhook payloads in several formats depending on the transport:
|
|
|
|
**JSON format (primary):**
|
|
```json
|
|
{
|
|
"version": 0,
|
|
"type": "info|success|warning|error|image",
|
|
"title": "Title",
|
|
"subject": "Subject",
|
|
"body": "Message body",
|
|
"notifications": [
|
|
{
|
|
"subject": "Subject",
|
|
"title": "Title",
|
|
"body": "Body",
|
|
"type": "type",
|
|
"attachments": [...]
|
|
}
|
|
],
|
|
"attachments": [...]
|
|
}
|
|
```
|
|
|
|
**Wrapper behavior:** Apprise wraps individual notifications in a `notifications` array. The code handles both wrapped (extract from array) and unwrapped (use directly) formats.
|
|
|
|
**Wrapper-level subject/title propagation:** When the wrapper has `subject` or `title` but the inner notification doesn't, these are propagated down to preserve sender name context.
|
|
|
|
**Content types handled:**
|
|
1. `application/json` — direct JSON parse
|
|
2. `multipart/form-data` — `$_POST` (files in `$_FILES['file01']`)
|
|
3. `application/x-www-form-urlencoded` — `parse_str()`
|
|
|
|
**Apprise URL schemes:**
|
|
- `discord://` → uses Discord webhook format
|
|
- `apprises://` → uses Apprise format with `notify` in path: `/apprise-webhook/{room}/notify/{token}`
|
|
|
|
**Type field:** The `type` field maps to an icon displayed before the bot name:
|
|
- `success` → ✅
|
|
- `warning` → ⚠️
|
|
- `error` → ❌
|
|
- `info` / `image` / missing → no icon
|
|
|
|
### Discord Webhook API
|
|
|
|
Discord webhooks send JSON with these fields:
|
|
```json
|
|
{
|
|
"content": "Message text",
|
|
"embeds": [
|
|
{
|
|
"title": "Embed title",
|
|
"description": "Embed description",
|
|
"color": 3066993,
|
|
"fields": [{"name": "Field", "value": "Value"}],
|
|
"image": {"url": "https://..."},
|
|
"thumbnail": {"url": "https://..."}
|
|
}
|
|
],
|
|
"username": "Sender name",
|
|
"avatar_url": "https://..."
|
|
}
|
|
```
|
|
|
|
**Mapping to Talk:**
|
|
- `content` → message text
|
|
- `embeds[].title` → bold line `**Title**`
|
|
- `embeds[].description` → message body
|
|
- `embeds[].fields` → `name: value` lines
|
|
- `embeds[].image.url` / `thumbnail.url` → download → upload → inline image
|
|
- `username` → sender display name (prepended as `🤖 **username**`)
|
|
- `avatar_url` → ignored (Talk doesn't support per-message avatars)
|
|
|
|
---
|
|
|
|
## Data Flow
|
|
|
|
### Webhook Ingestion (Discord)
|
|
|
|
```
|
|
POST /discord-webhook/{roomToken}/{token}
|
|
│
|
|
├─► WebhookController::receive()
|
|
│ │
|
|
│ ├─ 1. validateAuthToken(roomToken, token)
|
|
│ ├─ 2. json_decode(file_get_contents('php://input'))
|
|
│ ├─ 3. mapPayload(data) → message text
|
|
│ ├─ 4. getSenderName(data) → sender name
|
|
│ ├─ 5. prependDisplayName(senderName, message)
|
|
│ ├─ 6. For each embed image/thumbnail:
|
|
│ │ ├─ downloadImage(url)
|
|
│ │ ├─ uploadImage(roomToken, filename, data, mimeType)
|
|
│ │ └─ buildRichObject(filePath, mimeType, roomToken)
|
|
│ ├─ 7. postToRoom(roomToken, message, senderName, richObjects)
|
|
│ │ ├─ getBotPassword()
|
|
│ │ ├─ isBotEnabledForRoom(roomToken)
|
|
│ │ ├─ getBaseUrl()
|
|
│ │ ├─ getBotDisplayNameForRoom(roomToken)
|
|
│ │ └─ HTTP POST with Basic auth
|
|
│ └─► DataResponse with X-Webhook-Status
|
|
│
|
|
└─► HTTP 201/400/401/500 + X-Webhook-Status header
|
|
```
|
|
|
|
### Webhook Ingestion (Apprise)
|
|
|
|
```
|
|
POST /apprise-webhook/{roomToken}/{token}
|
|
│
|
|
├─► WebhookController::receiveApprise()
|
|
│ │
|
|
│ ├─ 1. validateAuthToken(roomToken, token)
|
|
│ ├─ 2. Detect content type (JSON → $_POST → parse_str)
|
|
│ ├─ 3. Parse payload
|
|
│ ├─ 4. If 'notifications' in data: extract first entry, propagate wrapper subject/title
|
|
│ ├─ 5. mapApprisePayload(data, roomToken)
|
|
│ │ ├─ Resolve displayName from title/subject
|
|
│ │ ├─ Build message from body (or title if body empty)
|
|
│ │ ├─ Add type prefix ([Info], [Warning], etc.)
|
|
│ │ └─ Process attachments (URL → download, file:// → read, base64 → decode)
|
|
│ ├─ 6. prependDisplayName(displayName, message)
|
|
│ ├─ 7. postToRoom(roomToken, message, senderName, richObjects)
|
|
│ └─► DataResponse with X-Webhook-Status
|
|
│
|
|
└─► HTTP 201/400/401/500 + X-Webhook-Status header
|
|
```
|
|
|
|
### Configuration Save
|
|
|
|
```
|
|
POST /save-config (admin)
|
|
│
|
|
├─► WebhookController::saveConfig()
|
|
│ │
|
|
│ ├─► TalkService::saveConfig(config)
|
|
│ │ ├─ validateBotPassword() if provided
|
|
│ │ ├─ setBotPassword() if provided
|
|
│ │ ├─ setRetentionDays() if provided
|
|
│ │ ├─ setRooms() with disabled rooms removed
|
|
│ │ ├─ setAuthTokens() if provided
|
|
│ │ ├─ setSenderName() if provided
|
|
│ │ └─ ensureBotParticipants()
|
|
│ └─► JSON {status: 'ok', auth_tokens: ...}
|
|
│
|
|
└─► JSON response
|
|
```
|
|
|
|
### Room Listing
|
|
|
|
```
|
|
GET /rooms (admin)
|
|
│
|
|
├─► WebhookController::getRooms()
|
|
│ │
|
|
│ ├─► TalkService::getAvailableTalkRooms()
|
|
│ │ ├─ detectTalkTableFromCatalog('talk_rooms', 'spreed_room')
|
|
│ │ ├─ Query: SELECT token, COALESCE(NULLIF(name,''),token) FROM talk_rooms
|
|
│ │ │ WHERE type IN (1,2,3) AND object_type NOT IN ('sample','note_to_self','file')
|
|
│ │ └─ Return token → display name
|
|
│ │
|
|
│ ├─► TalkService::getRooms() (configured rooms)
|
|
│ │
|
|
│ └─► Merge: for each available room, add 'configured' flag
|
|
│
|
|
└─► JSON array of {token, name, configured}
|
|
```
|
|
|
|
---
|
|
|
|
## Configuration Model
|
|
|
|
### AppConfig (oc_appconfig table)
|
|
|
|
All config is stored in the `oc_appconfig` table with `appid = 'nc_bot_webhooks'`.
|
|
|
|
| appid | config_key | config_value |
|
|
|---|---|---|
|
|
| nc_bot_webhooks | bot_password | `<encrypted>` |
|
|
| nc_bot_webhooks | rooms | `{"roomToken1": "Room Name", ...}` |
|
|
| nc_bot_webhooks | retention_days | `"90"` |
|
|
| nc_bot_webhooks | sender_name | `"Webhook Bot"` |
|
|
| nc_bot_webhooks | auth_tokens | `{"roomToken1": ["token1", "token2"], ...}` |
|
|
| nc_bot_webhooks | debug_enabled | `0` or `1` |
|
|
|
|
### Bot User Storage
|
|
|
|
```
|
|
talk-bot/
|
|
└── nc_bot_webhooks-images/
|
|
├── <room-token-1>/
|
|
│ ├── screenshot.png
|
|
│ └── diagram.jpg
|
|
└── <room-token-2>/
|
|
└── photo.png
|
|
```
|
|
|
|
Each image has a public link share (`SHARE_TYPE_LINK`) created via `ShareManager`. The share token is embedded in the Talk message's `rich_object` so Talk resolves and displays the inline image.
|
|
|
|
### Base URL Resolution Priority
|
|
|
|
1. `overwritehost` — explicit hostname override
|
|
2. `overwritewebroot` — if full URL
|
|
3. Non-loopback `trusted_domains` — skips 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, ::1, fc00::/7
|
|
4. `overwrite.cli.url` — last resort
|
|
|
|
### Talk Table Detection Priority
|
|
|
|
1. `spreed.databaseprefix` (app config) + `talk_rooms`
|
|
2. `dbtableprefix` (system config) + `talk_rooms`
|
|
3. `spreed.databaseprefix` + `spreed_room`
|
|
4. `dbtableprefix` + `spreed_room`
|
|
5. `talk_rooms` (no prefix)
|
|
6. `spreed_room` (no prefix)
|
|
|
|
Each candidate is verified via `information_schema.tables` + test query.
|
|
|
|
---
|
|
|
|
## Security Model
|
|
|
|
### Authentication Layers
|
|
|
|
| Layer | Mechanism |
|
|
|---|---|
|
|
| Webhook access | Auth token in URL path (`/discord-webhook/{room}/{token}`) |
|
|
| Admin settings | Nextcloud session auth + `#[AdminRequired]` attribute |
|
|
| Bot → Talk | Basic auth (`talk-bot:bot_password`) |
|
|
|
|
### Authorization
|
|
|
|
- Admin-only endpoints: `/save-config`, `/save-bot-password`, `/rooms` require admin session
|
|
- Webhook endpoints: `#[PublicPage]` + `#[NoCSRFRequired]` — auth token is sole access control
|
|
- Bot must be participant in target room (enforced by `isBotEnabledForRoom()` + Talk's own check)
|
|
|
|
### Data Isolation
|
|
|
|
- Bot password: encrypted at rest via `ICrypto`
|
|
- Images: stored in `talk-bot`'s personal storage under `nc_bot_webhooks-images/<room-token>/`
|
|
- Image cleanup: only operates within `nc_bot_webhooks-images/` under bot user's folder
|
|
- Per-file attributes: **NOT YET IMPLEMENTED** (user requested for extra safety)
|
|
|
|
### Image Download Safety
|
|
|
|
- Uses Nextcloud `IClientService` with built-in local address blocking
|
|
- `allow_local_address: true` — needed for Docker (container-internal addresses)
|
|
- Filenames sanitized via `basename()` — prevents path traversal
|
|
|
|
### Debug Endpoint
|
|
|
|
- `/apps/nc_bot_webhooks/debug` exposes DB schema, bot credentials, config
|
|
- **Disabled by default**
|
|
- Enabled only via CLI (`php occ nc_bot_webhooks:debug:enable`)
|
|
- Stored in `appconfig` (not hardcoded)
|
|
- **Should be disabled after troubleshooting**
|
|
|
|
---
|
|
|
|
## Known Issues & Limitations
|
|
|
|
### Critical
|
|
|
|
1. **Apprise 500 error after rename:** Config data still under old app ID `ncdiscordhook`. Fix:
|
|
```sql
|
|
UPDATE oc_appconfig SET appid = 'nc_bot_webhooks' WHERE appid = 'ncdiscordhook';
|
|
```
|
|
|
|
2. **Version pinning to NC33:** `<nextcloud min-version="33" max-version="33"/>` — app only works on NC33. If upgrading Nextcloud, remove max-version or update it.
|
|
|
|
### Security
|
|
|
|
3. **Client-side token generation:** Settings UI uses `btoa(Math.random() + Date.now())`. Server-side `random_bytes(32)` would be more secure. Use `TalkService::generateAuthToken()` for server-side generation.
|
|
|
|
4. **No rate limiting:** Webhook endpoints have no rate limiting. Handle at web server / reverse proxy level.
|
|
|
|
5. **Image cleanup by mtime only:** No per-file ownership verification. Safe because scoped to bot user's folder, but user requested per-file attributes for extra safety (NOT YET IMPLEMENTED).
|
|
|
|
### Functional
|
|
|
|
3. **Link preview suppression removed:** Nextcloud Talk generates link previews server-side. URL suppression via angle brackets (`<url>`) does not work. This feature was removed.
|
|
|
|
### Functional (continued)
|
|
|
|
6. **avatar_url ignored:** NCTalk doesn't support per-message avatars. `username` is embedded as sender name instead.
|
|
|
|
7. **actorDisplayName mismatch:** If bot's display name changes in the room, messages will be silently dropped. `getBotDisplayNameForRoom()` resolves this on each message.
|
|
|
|
8. **Table name detection failure:** If `information_schema` is unavailable or Talk table uses an unusual name, `detectTalkTableFromCatalog()` returns null and room listing fails.
|
|
|
|
9. **Bot not in room:** If talk-bot is removed from a configured room, `postToRoom()` will fail. `ensureBotParticipants()` runs on every save to prevent this.
|
|
|
|
---
|
|
|
|
## Operational Procedures
|
|
|
|
### Enabling the App
|
|
|
|
```bash
|
|
# 1. Create bot user
|
|
php occ user:add --password-from-env --display-name="Webhook Bot" talk-bot
|
|
|
|
# 2. Make bot admin (via web UI: Settings → Users → talk-bot → Admin)
|
|
|
|
# 3. Generate app password (via web UI: Settings → talk-bot → Devices & sessions → Add device)
|
|
|
|
# 4. Copy app to Nextcloud apps directory
|
|
cp -r nc_bot_webhooks /path/to/nextcloud/apps/
|
|
|
|
# 5. Enable app
|
|
php occ app:enable nc_bot_webhooks
|
|
|
|
# 6. Configure via web UI (Settings → Admin → NCbotwebhooks)
|
|
```
|
|
|
|
### Configuring Rooms
|
|
|
|
1. Go to Settings → Admin → NCbotwebhooks
|
|
2. Enter bot app password
|
|
3. Click "Fetch Rooms" to list available Talk rooms
|
|
4. Check rooms to enable webhooks for
|
|
5. Generate auth tokens for each room
|
|
6. Click "Save Configuration"
|
|
|
|
### Managing Auth Tokens
|
|
|
|
**Via UI:**
|
|
- Click "+ Generate Auth Token" for a room
|
|
- Click "Revoke All" to remove all tokens for a room
|
|
- Click "Copy" to copy webhook URLs
|
|
|
|
**Via CLI / direct DB:**
|
|
- Tokens stored as JSON in `oc_appconfig` under `auth_tokens`
|
|
- Format: `{"roomToken": ["token1", "token2"], ...}`
|
|
|
|
### Debugging Webhook Issues
|
|
|
|
1. Check `X-Webhook-Status` header in response
|
|
2. Enable debug endpoint: `php occ nc_bot_webhooks:debug:enable`
|
|
3. Visit `/apps/nc_bot_webhooks/debug` to inspect config
|
|
4. Check Nextcloud log: `data/nextcloud.log` or Settings → Admin → Logging
|
|
5. Disable debug: `php occ nc_bot_webhooks:debug:disable`
|
|
|
|
### Migrating from ncdiscordhook
|
|
|
|
```sql
|
|
UPDATE oc_appconfig SET appid = 'nc_bot_webhooks' WHERE appid = 'ncdiscordhook';
|
|
```
|
|
|
|
### Image Cleanup
|
|
|
|
**Manual trigger:**
|
|
```bash
|
|
# The cron job runs automatically. To trigger manually:
|
|
php occ background:cron
|
|
```
|
|
|
|
**Adjust retention:** Settings → Admin → NCbotwebhooks → Image Retention (1-365 days)
|
|
|
|
**Check images directory:**
|
|
```bash
|
|
ls /path/to/nextcloud/data/talk-bot/nc_bot_webhooks-images/
|
|
```
|
|
|
|
---
|
|
|
|
## Debugging Guide
|
|
|
|
### Common Error Patterns
|
|
|
|
| Symptom | Cause | Fix |
|
|
|---|---|---|
|
|
| Apprise 500 error | Config under old app ID | Run migration SQL above |
|
|
| "talk-bot user not found" | Bot user doesn't exist | Re-create bot user |
|
|
| "Bot password not configured" | No bot password in settings | Enter bot password |
|
|
| Messages not appearing | Bot not in room / actorDisplayName mismatch | Check admin settings, re-save |
|
|
| Image upload fails | Bot storage quota exceeded | Check bot user storage |
|
|
| "No Talk rooms found" | Talk not enabled / table detection failed | Verify Talk app, check debug endpoint |
|
|
| Room listing empty | Bot not admin / Talk version incompatible | Grant admin, check NC version |
|
|
|
|
### Debug Endpoints
|
|
|
|
**CLI:**
|
|
```bash
|
|
php occ nc_bot_webhooks:debug:enable # Enable
|
|
php occ nc_bot_webhooks:debug:disable # Disable
|
|
php occ nc_bot_webhooks:debug:status # Check
|
|
php occ nc_bot_webhooks:debug:toggle # Toggle
|
|
```
|
|
|
|
**Web (when enabled):** `GET /apps/nc_bot_webhooks/debug` returns JSON with:
|
|
- Bot password (plaintext)
|
|
- Configured rooms
|
|
- Auth tokens
|
|
- Talk table columns
|
|
- Room type breakdown
|
|
|
|
### Talk Service Debug Helpers
|
|
|
|
These public methods on `TalkService` are available for programmatic debugging:
|
|
|
|
| Method | Returns |
|
|
|---|---|
|
|
| `detectTalkTableFromCatalog('talk_rooms', 'spreed_room')` | Table name or null |
|
|
| `getTalkTableColumns('table_name')` | Array of {name, type, nullable} |
|
|
| `getTalkTableSample('table_name', 10)` | Array of sample rows |
|
|
| `getAllTalkRoomsDebug(100)` | Array of {id, token, type, readable_name, label, name, object_type, object_id} |
|
|
| `getRoomTypeBreakdown()` | {type: count} array |
|
|
|
|
### Log Tags
|
|
|
|
All log entries use:
|
|
- Logger tag: `'NCbotwebhooks:'`
|
|
- App tag: `'app' => 'nc_bot_webhooks'`
|
|
|
|
Example log entries:
|
|
```
|
|
NCbotwebhooks: webhook processed successfully
|
|
NCbotwebhooks: failed to post webhook message to Talk
|
|
NCbotwebhooks: message posted to room ABC123
|
|
NCbotwebhooks: bot password not configured
|
|
NCbotwebhooks: bot not enabled for room
|
|
NCbotwebhooks: base URL not configured
|
|
NCbotwebhooks: getAvailableTalkRooms
|
|
NCbotwebhooks: found 5 rooms
|
|
NCbotwebhooks: purgeOldImages
|
|
NCbotwebhooks: purged 3 old image files
|
|
```
|
|
|
|
---
|
|
|
|
## Migration Notes
|
|
|
|
### From ncdiscordhook to nc_bot_webhooks (v1.1.0)
|
|
|
|
**Breaking change:** New app ID = new installation. Existing config data is under old app ID.
|
|
|
|
**Migration steps:**
|
|
1. Copy new app files to Nextcloud apps directory
|
|
2. Enable app: `php occ app:enable nc_bot_webhooks`
|
|
3. Run config migration SQL
|
|
4. Re-enter bot password in settings (encrypted values don't migrate)
|
|
5. Re-configure rooms and auth tokens (or migrate via SQL)
|
|
|
|
**Config migration SQL:**
|
|
```sql
|
|
UPDATE oc_appconfig SET appid = 'nc_bot_webhooks' WHERE appid = 'ncdiscordhook';
|
|
```
|
|
|
|
**What migrates:**
|
|
- `rooms` — room configuration
|
|
- `auth_tokens` — auth tokens
|
|
- `retention_days` — retention period
|
|
- `sender_name` — sender name
|
|
|
|
**What does NOT migrate:**
|
|
- `bot_password` — must be re-entered (encrypted values are tied to the instance, not the app ID)
|
|
|
|
### Nextcloud Version Compatibility
|
|
|
|
Currently pinned to Nextcloud 33 (`min-version="33" max-version="33"`). If upgrading Nextcloud:
|
|
|
|
1. Update `max-version` in `info.xml`
|
|
2. Test Talk API compatibility (Chat API v1 may change)
|
|
3. Test `information_schema` queries on new NC version
|
|
4. Test AttendeeMapper API compatibility
|
|
|
|
### Talk API Version Notes
|
|
|
|
- Uses **Chat API v1** only (`/ocs/v2.php/apps/spreed/api/v1/chat/{roomToken}`)
|
|
- Talk 14+ removed `TalkManager::getRoomForToken` — app uses direct DB queries
|
|
- Talk 14+ requires `actorDisplayName` to match participant record exactly
|
|
- Talk 19 / NC33 only supports v1 of the Chat API (older versions deprecated)
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
### Manual Testing Checklist
|
|
|
|
1. **Discord webhook** — POST valid payload → message appears in Talk room
|
|
2. **Discord webhook with images** — embed with image URL → image appears inline
|
|
3. **Discord webhook with embeds** — embed with title/description/fields → formatted message
|
|
4. **Discord webhook invalid token** → 401 unauthorized
|
|
5. **Discord webhook invalid JSON** → 400 bad_request
|
|
6. **Discord webhook empty content** → 200 no_content
|
|
7. **Apprise webhook** — POST valid notification → message appears in Talk room
|
|
8. **Apprise webhook with attachments** — URL attachments → images appear inline
|
|
9. **Apprise webhook type=image** — image notification → inline image
|
|
10. **Apprise webhook base64 attachment** — base64 data → inline image
|
|
11. **Apprise webhook form-encoded** — form data → message appears
|
|
12. **Admin settings** — save config → rooms configured, bot participants added
|
|
13. **Token generation** — generate token → appears in room token list
|
|
14. **Token revoke** — revoke all → tokens cleared
|
|
15. **Image cleanup** — wait for cron → old images purged
|
|
16. **Debug endpoint** — enable → inspect config → disable
|
|
|
|
### Curl Test Commands
|
|
|
|
**Discord webhook:**
|
|
```bash
|
|
curl -X POST \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"content":"Test message","username":"CI Bot"}' \
|
|
https://your-server/apps/nc_bot_webhooks/discord-webhook/<room-token>/<auth-token>
|
|
```
|
|
|
|
**Apprise webhook:**
|
|
```bash
|
|
curl -X POST \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"title":"Test","body":"Test message","type":"info"}' \
|
|
https://your-server/apps/nc_bot_webhooks/apprise-webhook/<room-token>/<auth-token>
|
|
```
|
|
|
|
**Check debug endpoint (when enabled):**
|
|
```bash
|
|
curl -u admin:password https://your-server/apps/nc_bot_webhooks/debug | jq .
|
|
```
|
|
|
|
---
|
|
|
|
## Quick Reference
|
|
|
|
### File Sizes (approximate)
|
|
|
|
| File | Lines | Size |
|
|
|---|---|---|
|
|
| `TalkService.php` | 1138 | ~35 KB |
|
|
| `WebhookController.php` | ~400 | ~15 KB |
|
|
| `settings.js` | 351 | ~12 KB |
|
|
| `Admin.php` | 74 | ~3 KB |
|
|
| `ImageCleanup.php` | 54 | ~1.5 KB |
|
|
| `DebugToggle.php` | 102 | ~3 KB |
|
|
| `NavigationProvider.php` | 38 | ~1 KB |
|
|
| `adminSettings.php` | 54 | ~2 KB |
|
|
| `adminSettings.css` | 106 | ~2 KB |
|
|
| `routes.php` | 43 | ~1 KB |
|
|
| `info.xml` | 33 | ~1 KB |
|
|
|
|
### Key Dependencies
|
|
|
|
| Dependency | Purpose |
|
|
|---|---|
|
|
| `IAppConfig` | App-level key-value config storage |
|
|
| `IClientService` | HTTP client (with local address blocking) |
|
|
| `IShareManager` | File share management (public link shares) |
|
|
| `IRootFolder` | Filesystem root (user folder access) |
|
|
| `IUserManager` | User lookup (`get('talk-bot')`) |
|
|
| `IDBConnection` | Database queries |
|
|
| `IAttendeeMapper` | Talk attendee queries |
|
|
| `ICrypto` | Password encryption/decryption |
|
|
| `IShareManager` | Public link share creation |
|
|
| `IL10N` | Internationalization |
|
|
| `LoggerInterface` | PSR-3 logging |
|
|
|
|
### Nextcloud APIs Used
|
|
|
|
| API | Version | Endpoint / Interface |
|
|
|---|---|---|
|
|
| Talk Chat API | v1 | `/ocs/v2.php/apps/spreed/api/v1/chat/{roomToken}` |
|
|
| Talk AttendeeMapper | — | `findByActor(roomId, actorType, actorId)` |
|
|
| Talk Room catalog | — | `information_schema.tables` + table name detection |
|
|
| Nextcloud Share API | — | `newShare()`, `createShare()`, `getShareById()` |
|
|
| Nextcloud File API | — | `getUserFolder()`, `getFolder()`, `newFile()` |
|
|
| Nextcloud AppConfig | — | `getAppValue()`, `setAppValue()`, `getValueBool()` |
|
|
| Nextcloud Crypto | — | `encrypt()`, `decrypt()` via `ICrypto` |
|
|
| Nextcloud OCS API | — | OCS-Expect-Formatted / OCS-APIRequest headers |
|