docs: update documentation, add future tasks, and fix debug endpoint security
- **Documentation Overhaul**: - **README.md**: Restructured installation guide to align with Nextcloud 33+ paths (e.g., "Personal Settings → Security"), added curl examples, updated known limitations, and added a "Future Features" section. - **agent.md**: Updated architecture documentation to reflect `TalkService.php` growth (1138 to 1904 lines), updated constructor parameters, and corrected method documentation (e.g., `buildRichObject` now creates TYPE_ROOM shares, `postToRoom` uses query params). - **architecture.md**: Added notes regarding the admin gate for the debug endpoint. - **New Files**: - Added `Future to-do.md` to track planned features (inbound messages, filtering, etc.). - Added `TODO.md` to track pending tasks and code cleanup. - **Code Fixes**: - **lib/Controller/WebhookController.php**: Re-applied `#[AdminRequired]` attribute to the `debug()` method to enforce admin authentication. - **lib/Service/TalkService.php**: Updated docblock for `buildRichObject` to clarify it creates TYPE_ROOM shares. - **lib/Settings/Admin.php** & **templates/adminSettings.php**: Corrected bot password generation instructions to point to the correct Nextcloud settings path. - **Configuration**: - **appinfo/info.xml**: Bumped version to 1.2.1 and updated `max-version` to 35.
This commit is contained in:
+180
@@ -0,0 +1,180 @@
|
||||
# Future To-Do
|
||||
|
||||
Planned features, known limitations, and architectural notes for nc_bot_webhooks.
|
||||
|
||||
---
|
||||
|
||||
## Inbound Messages (apprise-request)
|
||||
|
||||
**Status:** Planning phase
|
||||
|
||||
### Concept
|
||||
|
||||
Add a new endpoint that polls Nextcloud Talk for new messages, returning them in an Apprise-compatible JSON format so external services can read from the chat and trigger actions.
|
||||
|
||||
```
|
||||
POST /apps/nc_bot_webhooks/apprise-request/<room-token>/notify/<auth-token>
|
||||
```
|
||||
|
||||
### Request
|
||||
|
||||
```json
|
||||
{
|
||||
"last-seen": 42
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"43": {
|
||||
"version": 0,
|
||||
"subject": "Alice",
|
||||
"title": "Message from Alice",
|
||||
"body": "Hello from the other side!",
|
||||
"type": "info"
|
||||
},
|
||||
"44": {
|
||||
"version": 0,
|
||||
"subject": "Bob",
|
||||
"title": "Message from Bob",
|
||||
"body": "Welcome back!",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each key is the Talk message ID. The keys are always in ascending order by ID. The client finds the largest key for the next request: `Math.max(...Object.keys(response))`.
|
||||
|
||||
> **Note:** JSON objects are technically unordered per spec, but all major implementations preserve insertion order. If strict ordering is required by a client, it can use `Object.keys(response).sort((a, b) => Number(a))` to guarantee sort.
|
||||
|
||||
### Design Decision: Message ID vs Timestamp
|
||||
|
||||
Use **message ID** (integer), not a timestamp.
|
||||
|
||||
Nextcloud Talk's Chat API (`GET ocs/v2.php/apps/spreed/api/v1/chat/{roomToken}/messages`) supports a `since` query parameter that takes a message ID. Message IDs are auto-incrementing integers, making them:
|
||||
|
||||
- **Monotonically ordered** — no clock skew issues between Talk server and webhook caller
|
||||
- **Precise** — exact message boundary, no ambiguity about which messages fall between two timestamps
|
||||
- **Efficient** — Talk's DB query can use the index directly; timestamps would require a range scan
|
||||
- **Consistent** — matches the existing Talk API pattern (the same `since` parameter is used for the standard polling endpoint)
|
||||
|
||||
### Implementation sketch
|
||||
|
||||
1. **New route** in `appinfo/routes.php`:
|
||||
- `POST /apprise-request/{roomToken}/notify/{token}`
|
||||
|
||||
2. **New WebhookController method** (`receiveAppriseRequest`):
|
||||
- Validate auth token
|
||||
- Parse `last-seen` from request body (default: 0 for first poll)
|
||||
- Call `TalkService::getNewMessages($roomToken, $lastSeen)`
|
||||
|
||||
3. **New TalkService method** (`getNewMessages`):
|
||||
- Query Talk Chat API with `since={lastSeen}` parameter
|
||||
- Parse response and map each message to an Apprise-compatible object
|
||||
- Return array of Apprise-format notification objects
|
||||
|
||||
4. **Admin settings UI**:
|
||||
- Add a "Triggers" section for configuring inbound triggers
|
||||
- Per-trigger: room selection, trigger token, filter rules (optional)
|
||||
|
||||
5. **Config storage** (`appconfig`):
|
||||
- New key `triggers`: JSON object mapping trigger tokens to configuration
|
||||
|
||||
### Open questions
|
||||
|
||||
- Should the endpoint accept an Apprise-format request (like existing webhooks) with a `last-seen` field? Or a custom format?
|
||||
- Should triggers support message filtering (e.g., only messages matching a regex, only @mentions)?
|
||||
- Should triggers support one-way dispatch (call an external webhook when a message matches)?
|
||||
- Rate limiting: how often should external services poll? Should we add a `X-Poll-After` header?
|
||||
|
||||
---
|
||||
|
||||
## Known Limitations & Fixes
|
||||
|
||||
### Auth token generation is client-side
|
||||
- **File:** `js/settings.js:245`
|
||||
- **Issue:** Auth tokens use `btoa(Math.random() + Date.now())` — not cryptographically secure
|
||||
- **Fix:** Switch to server-side `random_bytes(32)` generation
|
||||
- **Priority:** Low (webhook URLs are not publicly discoverable; the threat model doesn't require it)
|
||||
|
||||
### Voice message MIME type fix is disabled
|
||||
- **File:** `lib/Service/TalkService.php:1556`
|
||||
- **Issue:** `fixMimeTypeOfVoiceMessage()` is commented out because it creates non-voice shares for voice messages
|
||||
- **Fix:** Re-enable when upgrading to a Talk version that properly filters voice message shares
|
||||
|
||||
### No rate limiting on webhook endpoints
|
||||
- **Issue:** Webhook endpoints have no rate limiting — vulnerable to abuse if URLs are exposed
|
||||
- **Fix:** Add rate limiting via Nextcloud's `IRateLimitManager` or place behind reverse proxy
|
||||
|
||||
### Bot avatar not supported
|
||||
- **Issue:** NCTalk doesn't support per-message avatars; the `avatar_url` field in Discord payloads is ignored
|
||||
- **Workaround:** Sender name is prepended to message text as a bold line
|
||||
|
||||
### No per-message avatar support in Talk
|
||||
- **Impact:** Visual identity of the sending service is conveyed via type icons + sender name, not avatars
|
||||
|
||||
### Bot user requires admin privileges
|
||||
- **Issue:** The `talk-bot` user must be granted admin access to list all Talk rooms
|
||||
- **Impact:** Security concern — bot has more privileges than strictly necessary
|
||||
|
||||
### Storage quota impact
|
||||
- **Issue:** Images count toward the bot user's storage quota
|
||||
- **Impact:** Large deployments with many image webhooks could exhaust the bot's quota
|
||||
|
||||
### No CSRF protection on webhook endpoints
|
||||
- **By design:** Webhook endpoints are marked `#[PublicPage]` + `#[NoCSRFRequired]` — auth token is the sole access control
|
||||
|
||||
---
|
||||
|
||||
## Potential Future Features
|
||||
|
||||
### Message filtering / pattern matching
|
||||
- Allow triggers to filter incoming messages by regex, keywords, or @mention
|
||||
- Enables "only notify on specific messages" rather than polling everything
|
||||
|
||||
### Outbound webhook dispatch (trigger → external service)
|
||||
- When a message matches a trigger, POST to an external webhook URL
|
||||
- Would turn nc_bot_webhooks into a bidirectional bridge (not just a polling endpoint)
|
||||
|
||||
### Slash command support
|
||||
- Recognize and execute slash commands (e.g., `/status`, `/ping`) sent to the bot
|
||||
- Would require the inbound message system first
|
||||
|
||||
### Thread / conversation support
|
||||
- Post messages within a specific thread (requires Talk thread API integration)
|
||||
|
||||
### Rich object support expansion
|
||||
- Currently supports images only. Could extend to:
|
||||
- File shares (links to files in Nextcloud)
|
||||
- Deck cards
|
||||
- Calendar events
|
||||
- Custom rich objects
|
||||
|
||||
### Multi-format inbound support
|
||||
- Currently only Apprise-format payloads are accepted as inbound
|
||||
- Could add support for:
|
||||
- Discord webhook format (same as outbound, for symmetry)
|
||||
- Generic JSON (no wrapper)
|
||||
- Form-encoded payloads (already partially supported for Apprise)
|
||||
|
||||
### Read markers
|
||||
- Track which messages have been "seen" by external services
|
||||
- Could return `last-seen` in the response for convenience
|
||||
|
||||
### Typing indicators
|
||||
- Forward typing indicators from external services to Talk
|
||||
- Would require a new endpoint: `POST /typing/{roomToken}`
|
||||
|
||||
### System messages
|
||||
- Currently the voice message MIME type fix is disabled to avoid creating non-voice system messages
|
||||
- Could enable system messages in the future (e.g., "Bot started", "Room joined")
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Auth tokens generated from the settings UI use client-side generation; for higher security, regenerate them via the server API
|
||||
- The webhook endpoint has no rate limiting — consider placing it behind a reverse proxy rate limiter if exposing to untrusted sources
|
||||
- Debug endpoint is disabled by default and should be re-enabled immediately after troubleshooting
|
||||
@@ -6,30 +6,79 @@ Accepts Discord webhook-compatible JSON payloads (embeds, fields, images) and Ap
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Installation](#installation)
|
||||
- [Initial installation using git](#initial-installation-using-git)
|
||||
- [Step 1: Create the bot user](#step-1-create-the-bot-user)
|
||||
- [Step 2: Install the app](#step-2-install-the-app)
|
||||
- [Step 3: Configure the app](#step-3-configure-the-app)
|
||||
- [Step 4: Set up webhook URLs](#step-4-set-up-webhook-urls)
|
||||
- [Step 5: Verify](#step-5-verify)
|
||||
- [Updating](#updating)
|
||||
- [Deployment Paths](#deployment-paths)
|
||||
- [Updating using git](#updating-using-git)
|
||||
- [Local update](#local-update)
|
||||
- [Configuration](#configuration)
|
||||
- [Webhook URLs](#webhook-urls)
|
||||
- [Payload Formats](#payload-formats)
|
||||
- [Discord Webhook Format](#discord-webhook-format)
|
||||
- [Apprise Format](#apprise-format)
|
||||
- [Apprise Format](#aprise-format)
|
||||
- [curl Examples](#curl-examples)
|
||||
- [Payload Mapping](#payload-mapping)
|
||||
- [Sender Name Resolution](#sender-name-resolution)
|
||||
- [Image Management](#image-management)
|
||||
- [Security](#security)
|
||||
- [Known Limitations](#known-limitations)
|
||||
- [Debug Endpoint](#debug-endpoint)
|
||||
- [Logging](#logging)
|
||||
- [Debugging](#debugging)
|
||||
- [Integrations](#integrations)
|
||||
- [Home Assistant](#home-assistant)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Future Features](#future-features)
|
||||
- [AI Disclosure](#ai-disclosure)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Nextcloud 33 with the **Talk** app enabled
|
||||
- PHP 8.1+
|
||||
- Access to the Nextcloud server (SSH or direct file access)
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### Initial installation using git
|
||||
### Step 1: Create the bot user
|
||||
|
||||
The app posts messages as a dedicated `talk-bot` user using the Talk Chat API (Basic auth). This user must be created before configuring the app.
|
||||
|
||||
Create the bot user:
|
||||
|
||||
```bash
|
||||
php occ user:add --password-from-env --display-name="Webhook Bot" talk-bot
|
||||
```
|
||||
|
||||
### Make the bot user an admin
|
||||
|
||||
The bot must have admin privileges to list all Talk rooms. Grant admin access:
|
||||
|
||||
1. Go to **Menu → Accounts**
|
||||
2. Find **talk-bot** and click it
|
||||
3. Add talk-bot to the **admin** user group
|
||||
4. Save
|
||||
|
||||
### Generate an app password for the bot
|
||||
|
||||
1. Log in as the **talk-bot** user (or set a password for it as admin, then log in)
|
||||
2. Go to **Personal Settings → Security**
|
||||
3. Under **Devices & sessions**, click **Add device**
|
||||
4. Enter a name (e.g., "nc_bot_webhooks") and click **Create new app password**
|
||||
5. Copy the generated password — you'll need it in [Step 3](#step-3-configure-the-app)
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Install the app
|
||||
|
||||
Nextcloud no longer supports installing custom apps via the web UI. Install via CLI:
|
||||
|
||||
```bash
|
||||
cd /path/to/nextcloud/custom_apps
|
||||
@@ -37,45 +86,63 @@ git clone https://github.com/Mr-Newlove/nc_bot_webhooks.git
|
||||
php occ app:enable nc_bot_webhooks
|
||||
```
|
||||
|
||||
### 2. Create the bot user
|
||||
---
|
||||
|
||||
Create a user named `talk-bot` with the display name "Webhook Bot". This can be done via the `occ` CLI or the admin user manager GUI.
|
||||
### Step 3: Configure the app
|
||||
|
||||
```bash
|
||||
php occ user:add --password-from-env --display-name="Webhook Bot" talk-bot
|
||||
```
|
||||
|
||||
### 3. Make the bot user an admin
|
||||
|
||||
The bot must have admin privileges to list all Talk rooms. Grant admin access:
|
||||
|
||||
1. Go to **Settings → Users**
|
||||
2. Find **talk-bot** and click it
|
||||
3. Check **Admin** under "Settings"
|
||||
4. Save
|
||||
|
||||
### 4. Generate an app password for the bot
|
||||
|
||||
1. Log in as the **talk-bot** user (or set a password for it as admin, then log in)
|
||||
2. Go to **Personal Settings → Security**
|
||||
3. Under **Devices & sessions**, click **Add device**
|
||||
4. Enter a name (e.g., "nc_bot_webhooks") and click **Create new app password**
|
||||
5. Copy the generated password — you'll need it in the next step
|
||||
|
||||
### 5. Configure the app
|
||||
|
||||
Go to **Administration Settings** → **Additional settings** and then find the **nc_bot_webhooks** section.
|
||||
|
||||
1. **Bot App Password** — paste the app password from step 4
|
||||
2. **Default Sender Name** — set the name that appears as the message sender (default: "Webhook Bot")
|
||||
3. **Image Retention** — set how many days to keep uploaded images (default: 90)
|
||||
4. **Room Selection** — click **Fetch Rooms** to list available Talk rooms
|
||||
5. Check the rooms you want to accept webhooks for
|
||||
6. For each room, click **+ Generate Auth Token** to create a webhook URL
|
||||
7. Click **Save Configuration**
|
||||
1. Log in to Nextcloud as **admin**
|
||||
2. Go to **Administration** → **Additional settings** → **nc_bot_webhooks**
|
||||
3. **Bot App Password** — paste the app password from Step 1
|
||||
4. **Default Sender Name** — set the name that appears as the message sender (default: "Webhook Bot")
|
||||
5. **Image Retention** — set how many days to keep uploaded images (default: 90)
|
||||
6. **Room Selection** — click **Fetch Rooms** to list available Talk rooms
|
||||
7. Check the rooms you want to accept webhooks for
|
||||
8. For each room, click **+ Generate Auth Token** to create a webhook URL
|
||||
9. Click **Save Configuration**
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Set up webhook URLs
|
||||
|
||||
Each configured room gets two webhook URLs:
|
||||
|
||||
#### Discord format
|
||||
|
||||
```
|
||||
https://your-nextcloud-server/apps/nc_bot_webhooks/discord-webhook/<room-token>/<auth-token>
|
||||
```
|
||||
|
||||
#### Apprise format
|
||||
|
||||
```
|
||||
https://your-nextcloud-server/apps/nc_bot_webhooks/apprise-webhook/<room-token>/notify/<auth-token>
|
||||
```
|
||||
|
||||
> **Note:** The `notify` segment is required — Apprise's `apprises://` URL scheme inserts it in the path automatically.
|
||||
|
||||
#### Multiple auth tokens per room
|
||||
|
||||
Each room can have multiple auth tokens — useful for rotating keys or sharing the webhook across multiple services. Generate additional tokens in the admin settings UI.
|
||||
|
||||
---
|
||||
|
||||
### Step 5: Verify
|
||||
|
||||
Send a test message through the Discord webhook. You should see it appear in the corresponding Nextcloud Talk room with the configured sender name.
|
||||
|
||||
Test with curl:
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"content":"Test message from nc_bot_webhooks","username":"CI Bot"}' \
|
||||
https://your-nextcloud-server/apps/nc_bot_webhooks/discord-webhook/<room-token>/<auth-token>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Updating
|
||||
|
||||
### Deployment Paths
|
||||
|
||||
The table below lists common Nextcloud install locations. Replace `/var/www/html/custom_apps/` with your environment's path in all commands below.
|
||||
@@ -97,12 +164,9 @@ git pull
|
||||
cd /var/www/html
|
||||
php occ app:disable nc_bot_webhooks
|
||||
php occ app:enable nc_bot_webhooks
|
||||
php occ config:app:delete nc_bot_webhooks routes 2>/dev/null || true
|
||||
php occ maintenance:repair
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Local update
|
||||
|
||||
> **Note:** The paths below assume a standard Docker install (`/var/www/html/`). See [Deployment Paths](#deployment-paths) for environment-specific adjustments.
|
||||
@@ -117,18 +181,17 @@ rm -rf /var/www/html/custom_apps/nc_bot_webhooks/*
|
||||
mkdir -p /var/www/html/custom_apps/nc_bot_webhooks
|
||||
|
||||
# Update from your synced Nextcloud files directory — adjust this path to match your setup.
|
||||
cp -r "/var/www/html/data/<username>/files/<Path on your nextcloud sync>/nc_bot_webhooks/"* /var/www/html/custom_apps/nc_bot_webhooks/
|
||||
cp -r "/var/www/html/data/<username>/files/<Path on your Nextcloud sync>/nc_bot_webhooks/"* /var/www/html/custom_apps/nc_bot_webhooks/
|
||||
|
||||
chown -R www-data:www-data /var/www/html/custom_apps/nc_bot_webhooks
|
||||
chmod -R u+rX,go+rX /var/www/html/custom_apps/nc_bot_webhooks
|
||||
|
||||
cd /var/www/html
|
||||
php occ app:enable nc_bot_webhooks
|
||||
php occ config:app:delete nc_bot_webhooks routes 2>/dev/null || true
|
||||
php occ maintenance:repair
|
||||
```
|
||||
|
||||
> **Note:** The path `"/var/www/html/data/<username>/files/<Path on your nextcloud sync>/nc_bot_webhooks/"` is where your Nextcloud user's synced files land on the server. Replace `/var/www/html/data` with your environment's data dir from [Deployment Paths](#deployment-paths).
|
||||
> **Note:** The path `"/var/www/html/data/<username>/files/<Path on your Nextcloud sync>/nc_bot_webhooks/"` is where your Nextcloud user's synced files land on the server. Replace `/var/www/html/data` with your environment's data dir from [Deployment Paths](#deployment-paths).
|
||||
|
||||
---
|
||||
|
||||
@@ -207,6 +270,60 @@ Apprise sends a JSON wrapper containing a `notifications` array:
|
||||
|
||||
Apprise also supports form-encoded payloads. The app auto-detects the content type (JSON, `multipart/form-data`, or form-encoded).
|
||||
|
||||
### curl Examples
|
||||
|
||||
**Text-only notification (JSON):**
|
||||
|
||||
```bash
|
||||
curl -X POST "https://<your-nextcloud-server>/apps/nc_bot_webhooks/apprise-webhook/<room-token>/notify/<auth-token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"version": 0,
|
||||
"subject": "Build Complete",
|
||||
"body": "Successfully deployed to production",
|
||||
"type": "info"
|
||||
}'
|
||||
```
|
||||
|
||||
**Text-only notification (form-encoded):**
|
||||
|
||||
```bash
|
||||
curl -X POST "https://<your-nextcloud-server>/apps/nc_bot_webhooks/apprise-webhook/<room-token>/notify/<auth-token>" \
|
||||
-d "subject=Build Complete" \
|
||||
-d "body=Successfully deployed to production" \
|
||||
-d "type=info"
|
||||
```
|
||||
|
||||
**With a remote image attachment:**
|
||||
|
||||
```bash
|
||||
curl -X POST "https://<your-nextcloud-server>/apps/nc_bot_webhooks/apprise-webhook/<room-token>/notify/<auth-token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"version": 0,
|
||||
"subject": "Camera Snapshot",
|
||||
"body": "Motion detected at front door",
|
||||
"type": "info",
|
||||
"attachments": [
|
||||
{ "path": "https://nextcloud.com/c/uploads/2026/04/Nextcloud-10-anniversary-logo.png" }
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
The app downloads the image, uploads it to the bot user's storage, creates a public link, and embeds it inline in the Talk message.
|
||||
|
||||
**With a local file (multipart/form-data):**
|
||||
|
||||
```bash
|
||||
curl -X POST "https://<your-nextcloud-server>/apps/nc_bot_webhooks/apprise-webhook/<room-token>/notify/<auth-token>" \
|
||||
-F "subject=Camera Snapshot" \
|
||||
-F "body=Motion detected at front door" \
|
||||
-F "type=image" \
|
||||
-F "file01=@/path/to/snapshot.jpg"
|
||||
```
|
||||
|
||||
The app reads the uploaded file from `$_FILES['file01']` and attaches it directly without an intermediate HTTP download step.
|
||||
|
||||
---
|
||||
|
||||
## Payload Mapping
|
||||
@@ -214,7 +331,7 @@ Apprise also supports form-encoded payloads. The app auto-detects the content ty
|
||||
| Field | Nextcloud Talk action |
|
||||
|---|---|
|
||||
| `content` | Sent as text message |
|
||||
| `embeds[].title` | Included as bold line |
|
||||
| `embeds[].title` | Included as **bold** line |
|
||||
| `embeds[].description` | Included in message body |
|
||||
| `embeds[].image.url` | Downloaded, uploaded to NC, shared inline |
|
||||
| `embeds[].thumbnail.url` | Downloaded, uploaded to NC, shared inline |
|
||||
@@ -224,9 +341,11 @@ Apprise also supports form-encoded payloads. The app auto-detects the content ty
|
||||
| `subject` / `title` (Apprise) | Used as bold title line |
|
||||
| `body` (Apprise) | Sent as message body |
|
||||
| `attachments[].path` (Apprise) | Downloaded, uploaded to NC, shared inline |
|
||||
| `type` (Apprise) | Maps to icon: ✅ success, ⚠️ warning, ❌ error, none for info/image |
|
||||
| `type` (Apprise) | Maps to icon prefix: ✅ `success`, ⚠️ `warning`, ❌ `error`, no icon for `info`/`image`/other |
|
||||
|
||||
### Sender name resolution
|
||||
---
|
||||
|
||||
## Sender Name Resolution
|
||||
|
||||
When posting to Talk, the app prepends a bold sender name line to the message (since Talk doesn't support per-message avatars). The sender name is resolved in this order:
|
||||
|
||||
@@ -254,14 +373,19 @@ When posting to Talk, the app prepends a bold sender name line to the message (s
|
||||
| **Auth tokens** | Each webhook URL contains a unique secret token; the endpoint validates it before processing |
|
||||
| **Bot password** | Encrypted at rest using Nextcloud's crypto layer (`ICrypto`) |
|
||||
| **No CSRF** | Webhook endpoints are marked `#[PublicPage]` + `#[NoCSRFRequired]` — auth token is the sole access control |
|
||||
| **Local address blocking** | Image downloads use Nextcloud's HTTP client with `allow_local_address: true` (blocks private ranges by default) |
|
||||
| **Local address blocking** | Image downloads use Nextcloud's HTTP client with `allow_local_address: true` — allows private/local addresses that would otherwise be blocked by default (needed for Docker/container deployments where Nextcloud is accessed via localhost or private IPs) |
|
||||
| **Path traversal protection** | Uploaded filenames use `basename()` to prevent directory traversal |
|
||||
| **Rate limiting** | Not built in — handle at the web server or reverse proxy level |
|
||||
|
||||
### Known limitations
|
||||
---
|
||||
|
||||
- Auth tokens generated from the settings UI use client-side generation; for higher security, regenerate them via the server API
|
||||
## Known Limitations
|
||||
|
||||
- Auth tokens generated from the settings UI use client-side generation (`btoa(Math.random() + Date.now())`); for higher security, regenerate them via the server API
|
||||
- The webhook endpoint has no rate limiting — consider placing it behind a reverse proxy rate limiter if exposing to untrusted sources
|
||||
- NCTalk doesn't support per-message avatars; the `avatar_url` field in Discord payloads is ignored
|
||||
- The `talk-bot` user must have admin privileges to list all Talk rooms
|
||||
- Images count toward the bot user's storage quota
|
||||
|
||||
---
|
||||
|
||||
@@ -269,11 +393,13 @@ When posting to Talk, the app prepends a bold sender name line to the message (s
|
||||
|
||||
The `/apps/nc_bot_webhooks/debug` endpoint exposes internal configuration, database schema, and bot credentials. It is **disabled by default**.
|
||||
|
||||
> **Security note:** This endpoint requires **admin authentication** and is **disabled by default**. **Never leave it enabled in production.** Disable it as soon as troubleshooting is complete.
|
||||
|
||||
```bash
|
||||
# Check status
|
||||
php occ nc_bot_webhooks:debug:status
|
||||
|
||||
# Enable (WARNING: exposes sensitive data)
|
||||
# Enable (WARNING: exposes sensitive data, no auth gate)
|
||||
php occ nc_bot_webhooks:debug:enable
|
||||
|
||||
# Disable (default)
|
||||
@@ -289,6 +415,199 @@ After troubleshooting, disable it immediately:
|
||||
php occ nc_bot_webhooks:debug:disable
|
||||
```
|
||||
|
||||
> **TODO:** Add auto-disable timer (2-hour TTL) or re-enable the admin gate to prevent accidental prolonged exposure.
|
||||
|
||||
---
|
||||
|
||||
## Logging
|
||||
|
||||
Responses include a `X-Webhook-Status` header:
|
||||
|
||||
| Header value | Meaning |
|
||||
|---|---|
|
||||
| `ok` | Forwarded successfully |
|
||||
| `unauthorized` | Invalid auth token |
|
||||
| `bad_request` | Invalid JSON payload |
|
||||
| `no_content` | No message content in payload |
|
||||
| `error` | Check server logs for details |
|
||||
|
||||
---
|
||||
|
||||
## Debugging
|
||||
|
||||
Enable debug logging by setting the log level to `0` (debug) in your Nextcloud `config.php`:
|
||||
|
||||
```php
|
||||
'loglevel' => 0,
|
||||
```
|
||||
|
||||
Or via `occ`:
|
||||
|
||||
```bash
|
||||
php occ config:app:set --value=0 core log_level
|
||||
```
|
||||
|
||||
The default log file on a TrueNAS Scale Nextcloud Docker install is `/var/www/html/data/nextcloud.log`. Adjust based on your environment's data dir from [Deployment Paths](#deployment-paths).
|
||||
|
||||
### Diagnostic grep examples
|
||||
|
||||
Each section below covers a common symptom, what to grep for, and what to look for.
|
||||
|
||||
---
|
||||
|
||||
#### Webhook endpoint is being hit
|
||||
|
||||
Confirm the webhook URL is receiving traffic:
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: receiveApprise raw request' /var/www/html/data/nextcloud.log
|
||||
grep 'nc_bot_webhooks: webhook processed successfully' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:** A `receiveApprise raw request` entry followed by a `webhook processed successfully` entry. If neither appears, the webhook isn't reaching the server (check your Discord/Apprise webhook URL, network routing, or reverse proxy).
|
||||
|
||||
---
|
||||
|
||||
#### Invalid auth token
|
||||
|
||||
A webhook is being rejected with `401 unauthorized`:
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: invalid auth token for room' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:** The room token in the log entry. Compare it against the tokens shown in **Settings → Admin → nc_bot_webhooks**. If the token in the webhook URL doesn't match any configured token for that room, the webhook will be rejected. Regenerate the token in the admin settings and update your external service's webhook URL.
|
||||
|
||||
---
|
||||
|
||||
#### Invalid JSON / malformed payload
|
||||
|
||||
The webhook is being rejected with `400 bad_request`:
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: invalid JSON from webhook' /var/www/html/data/nextcloud.log
|
||||
grep 'nc_bot_webhooks: invalid payload from apprise webhook' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:** The parsed payload snippet in the log entry. Verify the JSON structure matches the [Discord Webhook Format](#discord-webhook-format) or [Apprise Format](#aprise-format) documented above.
|
||||
|
||||
---
|
||||
|
||||
#### Message not appearing in Talk
|
||||
|
||||
The webhook succeeded but the message didn't show up in the Talk room:
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: failed to post.*message to Talk' /var/www/html/data/nextcloud.log
|
||||
grep 'nc_bot_webhooks: webhook processed successfully' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:**
|
||||
- If you see `failed to post.*message to Talk`, the app couldn't reach the Talk Chat API. Check that the bot user has an app password configured and that the bot is a participant in the target room.
|
||||
- If you see `webhook processed successfully` but no error, the message was posted but Talk may have silently dropped it. Verify the bot user is listed as a participant in the Talk room.
|
||||
|
||||
---
|
||||
|
||||
#### Image download/upload fails
|
||||
|
||||
Images from webhooks aren't appearing inline:
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: Failed to download image' /var/www/html/data/nextcloud.log
|
||||
grep 'nc_bot_webhooks: Failed to upload image' /var/www/html/data/nextcloud.log
|
||||
grep 'nc_bot_webhooks: uploadImage — bot user not found' /var/www/html/data/nextcloud.log
|
||||
grep 'nc_bot_webhooks: image processing failed' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:**
|
||||
- `Failed to download image` — the source URL is unreachable, blocked by local address filtering, or returned an error. Verify the image URL is publicly accessible.
|
||||
- `Failed to upload image` — check the error message for details (quota exceeded, filesystem error, etc.).
|
||||
- `uploadImage — bot user not found` — the `talk-bot` user doesn't exist. Re-run Step 1 of installation.
|
||||
- `image processing failed` — image download succeeded but upload failed. Check the bot user's storage quota and permissions.
|
||||
|
||||
---
|
||||
|
||||
#### Bot user not found
|
||||
|
||||
Errors referencing the `talk-bot` user:
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: uploadImage — bot user not found' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:** This confirms the `talk-bot` user doesn't exist in Nextcloud. Create it via `php occ user:add --password-from-env talk-bot` (Step 1 of installation).
|
||||
|
||||
---
|
||||
|
||||
#### Config save fails
|
||||
|
||||
Admin settings not saving:
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: saveConfig failed' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:** The error message from the crypto layer. Common causes include an invalid bot password (contains characters that break encryption) or a misconfigured Nextcloud crypto backend.
|
||||
|
||||
---
|
||||
|
||||
#### Room listing empty
|
||||
|
||||
The "Fetch Rooms" button returns no rooms:
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: getAvailableTalkRooms' /var/www/html/data/nextcloud.log
|
||||
grep 'nc_bot_webhooks: found.*rooms' /var/www/html/data/nextcloud.log
|
||||
grep 'nc_bot_webhooks: room listing exception' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:**
|
||||
- `found 0 rooms` — the bot user may not have admin privileges. Re-grant admin access (Step 1 of installation).
|
||||
- `room listing exception` — check the exception details for the root cause (Talk app not installed, database error, etc.).
|
||||
|
||||
---
|
||||
|
||||
#### Image cleanup cron not running
|
||||
|
||||
Images are not being purged after the retention period:
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: purged' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:** A log entry each time the cron job runs. If nothing appears, ensure system cron is configured to run `php occ cron.php` (not the "Web" cron setting in Admin settings). Check the Nextcloud system cron status:
|
||||
|
||||
```bash
|
||||
php occ system:cron:set --method=cron
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Payload mapping inspection
|
||||
|
||||
See exactly how the webhook payload was mapped to Talk format (verbose):
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: DEBUG mapped' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:** The full mapped payload JSON. This is useful when a message appears incorrectly in Talk — compare the mapped payload against the expected Talk Chat API format.
|
||||
|
||||
> **Note:** Debug logging is verbose and impacts performance. After troubleshooting, restore your log level to `2` (warning) or higher.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---|---|
|
||||
| **"talk-bot user not found"** | The `talk-bot` user doesn't exist. Re-run Step 1 of installation. |
|
||||
| **"Bot password not configured"** | You haven't entered the bot password in admin settings, or it was cleared. Re-enter it. |
|
||||
| **Messages not appearing** | Check the Nextcloud log (`data/nextcloud.log` or Settings → Admin → Logging) for errors from the `nc_bot_webhooks` app. |
|
||||
| **Image upload fails** | Verify the bot user has file storage quota and the `nc_bot_webhooks-images` folder can be created. |
|
||||
| **Apprise 500 error** | Check the Nextcloud log for the specific error; may indicate a missing or misconfigured dependency. |
|
||||
| **Bot not listed in room participants** | The `ensureBotParticipants()` method runs automatically on save, but you may need to re-check the room in admin settings to trigger it. |
|
||||
|
||||
---
|
||||
|
||||
## Integrations
|
||||
@@ -366,190 +685,16 @@ data:
|
||||
|
||||
---
|
||||
|
||||
## Logging
|
||||
## Future Features
|
||||
|
||||
Responses include a `X-Webhook-Status` header:
|
||||
See [Future to-do.md](./Future%20to-do.md) for planned features including:
|
||||
|
||||
| Header value | Meaning |
|
||||
|---|---|
|
||||
| `ok` | Forwarded successfully |
|
||||
| `unauthorized` | Invalid auth token |
|
||||
| `bad_request` | Invalid JSON payload |
|
||||
| `no_content` | No message content in payload |
|
||||
| `error` | Check server logs for details |
|
||||
|
||||
---
|
||||
|
||||
## Debugging
|
||||
|
||||
Enable debug logging by setting the log level to `0` (debug) in your Nextcloud `config.php`:
|
||||
|
||||
```php
|
||||
'loglevel' => 0,
|
||||
```
|
||||
|
||||
Or via `occ`:
|
||||
|
||||
```bash
|
||||
php occ config:app:set --value=0 core log_level
|
||||
```
|
||||
|
||||
The default log file on a TrueNAS Scale Nextcloud Docker install is `/var/www/html/data/nextcloud.log`. Adjust based on your environment's data dir from [Deployment Paths](#deployment-paths).
|
||||
|
||||
### Diagnostic grep examples
|
||||
|
||||
Each section below covers a common symptom, what to grep for, and what to look for.
|
||||
|
||||
---
|
||||
|
||||
#### Webhook endpoint is being hit
|
||||
|
||||
Confirm the webhook URL is receiving traffic:
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: receiveApprise raw request' /var/www/html/data/nextcloud.log
|
||||
grep 'nc_bot_webhooks: webhook processed successfully' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:** A `receiveApprise raw request` entry followed by a `webhook processed successfully` entry. If neither appears, the webhook isn't reaching the server (check your Discord/Apprise webhook URL, network routing, or reverse proxy).
|
||||
|
||||
---
|
||||
|
||||
#### Invalid auth token
|
||||
|
||||
A webhook is being rejected with `401 unauthorized`:
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: invalid auth token for room' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:** The room token in the log entry. Compare it against the tokens shown in **Settings → Admin → nc_bot_webhooks**. If the token in the webhook URL doesn't match any configured token for that room, the webhook will be rejected. Regenerate the token in the admin settings and update your external service's webhook URL.
|
||||
|
||||
---
|
||||
|
||||
#### Invalid JSON / malformed payload
|
||||
|
||||
The webhook is being rejected with `400 bad_request`:
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: invalid JSON from webhook' /var/www/html/data/nextcloud.log
|
||||
grep 'nc_bot_webhooks: invalid payload from apprise webhook' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:** The parsed payload snippet in the log entry. Verify the JSON structure matches the [Discord Webhook Format](#discord-webhook-format) or [Apprise Format](#apprise-format) documented above.
|
||||
|
||||
---
|
||||
|
||||
#### Message not appearing in Talk
|
||||
|
||||
The webhook succeeded but the message didn't show up in the Talk room:
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: failed to post.*message to Talk' /var/www/html/data/nextcloud.log
|
||||
grep 'nc_bot_webhooks: webhook processed successfully' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:**
|
||||
- If you see `failed to post.*message to Talk`, the app couldn't reach the Talk Chat API. Check that the bot user has an app password configured and that the bot is a participant in the target room.
|
||||
- If you see `webhook processed successfully` but no error, the message was posted but Talk may have silently dropped it. Verify the bot user is listed as a participant in the Talk room.
|
||||
|
||||
---
|
||||
|
||||
#### Image download/upload fails
|
||||
|
||||
Images from webhooks aren't appearing inline:
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: Failed to download image' /var/www/html/data/nextcloud.log
|
||||
grep 'nc_bot_webhooks: Failed to upload image' /var/www/html/data/nextcloud.log
|
||||
grep 'nc_bot_webhooks: uploadImage — bot user not found' /var/www/html/data/nextcloud.log
|
||||
grep 'nc_bot_webhooks: image processing failed' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:**
|
||||
- `Failed to download image` — the source URL is unreachable, blocked by local address filtering, or returned an error. Verify the image URL is publicly accessible.
|
||||
- `Failed to upload image` — check the error message for details (quota exceeded, filesystem error, etc.).
|
||||
- `uploadImage — bot user not found` — the `talk-bot` user doesn't exist. Re-run Step 2 of installation.
|
||||
- `image processing failed` — image download succeeded but upload failed. Check the bot user's storage quota and permissions.
|
||||
|
||||
---
|
||||
|
||||
#### Bot user not found
|
||||
|
||||
Errors referencing the `talk-bot` user:
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: uploadImage — bot user not found' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:** This confirms the `talk-bot` user doesn't exist in Nextcloud. Create it via `php occ user:add --password-from-env talk-bot` (Step 2 of installation).
|
||||
|
||||
---
|
||||
|
||||
#### Config save fails
|
||||
|
||||
Admin settings not saving:
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: saveConfig failed' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:** The error message from the crypto layer. Common causes include an invalid bot password (contains characters that break encryption) or a misconfigured Nextcloud crypto backend.
|
||||
|
||||
---
|
||||
|
||||
#### Room listing empty
|
||||
|
||||
The "Fetch Rooms" button returns no rooms:
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: getAvailableTalkRooms' /var/www/html/data/nextcloud.log
|
||||
grep 'nc_bot_webhooks: found.*rooms' /var/www/html/data/nextcloud.log
|
||||
grep 'nc_bot_webhooks: room listing exception' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:**
|
||||
- `found 0 rooms` — the bot user may not have admin privileges. Re-grant admin access (Step 3 of installation).
|
||||
- `room listing exception` — check the exception details for the root cause (Talk app not installed, database error, etc.).
|
||||
|
||||
---
|
||||
|
||||
#### Image cleanup cron not running
|
||||
|
||||
Images are not being purged after the retention period:
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: ImageCleanup' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:** A log entry each time the cron job runs. If nothing appears, ensure system cron is configured to run `php occ cron.php` (not the "Web" cron setting in Admin settings). Check the Nextcloud system cron status:
|
||||
|
||||
```bash
|
||||
php occ system:cron:set --method=cron
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Payload mapping inspection
|
||||
|
||||
See exactly how the webhook payload was mapped to Talk format (verbose):
|
||||
|
||||
```bash
|
||||
grep 'nc_bot_webhooks: DEBUG mapped' /var/www/html/data/nextcloud.log
|
||||
```
|
||||
|
||||
**What to look for:** The full mapped payload JSON. This is useful when a message appears incorrectly in Talk — compare the mapped payload against the expected Talk Chat API format.
|
||||
|
||||
> **Note:** Debug logging is verbose and impacts performance. After troubleshooting, restore your log level to `2` (warning) or higher.
|
||||
|
||||
| Problem | Solution |
|
||||
|---|---|
|
||||
| **"talk-bot user not found"** | The `talk-bot` user doesn't exist. Re-run Step 2 of installation. |
|
||||
| **"Bot password not configured"** | You haven't entered the bot password in admin settings, or it was cleared. Re-enter it. |
|
||||
| **Messages not appearing** | Check the Nextcloud log (`data/nextcloud.log` or Settings → Admin → Logging) for errors from the `nc_bot_webhooks` app. |
|
||||
| **Image upload fails** | Verify the bot user has file storage quota and the `nc_bot_webhooks-images` folder can be created. |
|
||||
| **Apprise 500 error** | Likely config data not migrated from the old app ID. Re-enter settings or run: `UPDATE oc_appconfig SET appid = 'nc_bot_webhooks' WHERE appid = 'ncdiscordhook';` |
|
||||
| **Bot not listed in room participants** | The `ensureBotParticipants()` method runs automatically on save, but you may need to re-check the room in admin settings to trigger it. |
|
||||
- **Inbound messages** (`apprise-request` endpoint) — poll Talk for new messages and return them in Apprise-compatible format
|
||||
- Message filtering / pattern matching
|
||||
- Outbound webhook dispatch (trigger → external service)
|
||||
- Slash command support
|
||||
- Thread / conversation support
|
||||
- Extended rich object types (file shares, Deck cards, Calendar events)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# nc_bot_webhooks — Pending Tasks
|
||||
|
||||
## Debug Endpoint
|
||||
- [x] Re-enable admin auth requirement for `/debug` endpoint (WebhookController.php)
|
||||
- [x] Re-enable admin gate + update docs
|
||||
- [ ] Add auto-disable timer (2-hour TTL) to prevent accidental prolonged exposure
|
||||
|
||||
## README / Documentation Fixes
|
||||
- [x] Section 4: App password path — confirmed "Personal Settings → Security" is correct
|
||||
- [x] Section 4 (FIXED): Template hint `templates/adminSettings.php:11` — confirmed correct path
|
||||
- [x] Section 4: l10n string `lib/Settings/Admin.php:43` — confirmed correct path
|
||||
- [x] Section 5: "Administration → Additional settings" → confirmed correct for current NC
|
||||
- [x] Section 3: "Settings → Users" → confirmed correct for current NC
|
||||
- [x] Security table: `allow_local_address: true` description — clarified wording
|
||||
- [x] Image cleanup cron grep — already correct in README (`grep 'nc_bot_webhooks: purged'`)
|
||||
- [x] Troubleshooting: `ncdiscordhook` app ID — not in README (only in agent.md, which is correct)
|
||||
- [x] Payload Mapping: `mapPayload` title — confirmed bold formatting correct
|
||||
- [x] Payload Mapping: `type` icon mapping — confirmed actual emojis listed
|
||||
- [x] Home Assistant YAML: `message` vs `body` — `message` works via fallback, no change needed
|
||||
- [x] Version: bumped to 1.2.1 (info.xml + agent.md updated)
|
||||
- [x] NC max-version: docs say 33, info.xml says 35 — agent.md updated
|
||||
|
||||
## Code Documentation Fixes
|
||||
- [x] `buildRichObject` docs describe public link shares; code uses TYPE_ROOM shares
|
||||
- [x] `postToRoom` docs describe JSON body posting; code uses query params
|
||||
- [x] `prependDisplayName` signature — already documents all 3 params including `$typeIcon`
|
||||
- [x] `getRooms()` response schema missing `type`, `type_label`, `object_type`
|
||||
- [x] `saveConfig` docs missing `disabled_rooms` field
|
||||
- [x] `ensureBotParticipants` visibility — already documented as `private` with call context
|
||||
- [x] `getBaseUrl()` resolution incompletely documented
|
||||
- [x] TalkService constructor params: docs say 8, actual 17
|
||||
- [x] TalkService.php size: docs say 1138 lines, actual 1904
|
||||
- [ ] All line numbers in agent.md are stale
|
||||
|
||||
## Code Issues
|
||||
- [ ] Room type filter `type IN (1,2,3)` misses type 6 (Note to self) and future types
|
||||
|
||||
## Cleanup
|
||||
- [x] `config:app:delete nc_bot_webhooks routes` — removed from README (routes are defined in routes.php, not stored in DB)
|
||||
- [ ] `composer.json` exists but migration plan says delete it (resolved — keep it for dev tooling, update migration plan)
|
||||
- [x] agent.md dead references cleaned: `app (copy).svg`, `composer/autoload.php`, `composer/autoload_psr4.php` (removed from directory tree). `tests/`, `phpunit.xml.dist`, `.gitea/workflows/ci.yml` were not actually in agent.md (false positives)
|
||||
- [x] `webhook-setup.md` — added clear label "Nextcloud Talk Native Bot Setup (bots-v1)" with note distinguishing from nc_bot_webhooks
|
||||
@@ -33,7 +33,7 @@ nc_bot_webhooks/
|
||||
│ ├── Controller/
|
||||
│ │ └── WebhookController.php # HTTP handlers (webhooks + admin endpoints)
|
||||
│ ├── Service/
|
||||
│ │ └── TalkService.php # Core business logic (1138 lines)
|
||||
│ │ └── TalkService.php # Core business logic (1904 lines)
|
||||
│ ├── Command/
|
||||
│ │ └── DebugToggle.php # CLI command for debug endpoint
|
||||
│ ├── Settings/
|
||||
@@ -48,11 +48,8 @@ nc_bot_webhooks/
|
||||
├── 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
|
||||
│ └── app.svg # App icon (used in navigation + admin)
|
||||
├── composer.json # Package name + PSR-4 mapping (dev tooling only)
|
||||
├── README.md # User-facing documentation
|
||||
├── INSTALL.md # Installation guide
|
||||
├── architecture.md # Architecture documentation
|
||||
@@ -109,7 +106,7 @@ OCA\Ncbotwebhooks\
|
||||
|
||||
## Every File — Purpose & Key Details
|
||||
|
||||
### lib/Service/TalkService.php (1138 lines)
|
||||
### lib/Service/TalkService.php (1904 lines)
|
||||
|
||||
**The single most important file.** Contains all business logic.
|
||||
|
||||
@@ -117,14 +114,22 @@ OCA\Ncbotwebhooks\
|
||||
|
||||
```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
|
||||
IConfig $config, // App + system config
|
||||
IDBConnection $db, // Database connection
|
||||
IAttendeeMapper $attendeeMapper, // Talk attendee queries
|
||||
IRootFolder $rootFolder, // Filesystem root
|
||||
IRequest $request, // HTTP request
|
||||
IURLGenerator $urlGenerator, // URL generation
|
||||
IUserManager $userManager, // User lookup
|
||||
IUserSession $userSession, // User session management
|
||||
LoggerInterface $logger, // PSR-3 logger
|
||||
TalkManager $talkManager, // Talk room manager
|
||||
ICrypto $crypto, // Encryption (bot password)
|
||||
AttendeeMapper $attendeeMapper, // Talk attendee queries
|
||||
IShareManager $shareManager, // File share management
|
||||
ParticipantService $participantService, // Talk participant management
|
||||
ChatManager $chatManager, // Talk chat management
|
||||
TalkSession $talkSession, // Talk session persistence
|
||||
)
|
||||
```
|
||||
|
||||
@@ -143,18 +148,20 @@ public function __construct(
|
||||
|
||||
| Method | Lines | Purpose |
|
||||
|---|---|---|
|
||||
| `getRooms()` | ~82-90 | Returns JSON-decoded `rooms` from AppConfig (room token → display name) |
|
||||
| `getRooms()` | ~270-274 | Returns JSON-decoded `rooms` from AppConfig — `array<string, string>` mapping room token → display name. No `type`/`type_label`/`object_type` fields; those come from `getAvailableTalkRooms()`. |
|
||||
| `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. |
|
||||
| `getAvailableTalkRooms()` | ~291-360 | **Queries Talk DB directly** (bypasses TalkManager). Filters: `type IN (1,2,3)`, excludes deleted/note-to-self/sample/file rooms. Returns `array<string, string>` mapping room token → display name (or token if name is empty). Note: does not return `type`, `type_label`, or `object_type` fields — only `token` and `name` are selected. |
|
||||
| `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:**
|
||||
**Room type filter:** `type IN (1, 2, 3)`
|
||||
- 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 '["%'`)
|
||||
- **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 '["%'`)
|
||||
- **Known gap:** type 6 (note-to-self) is explicitly excluded above, but future room types are also missed by the hardcoded `IN` list.
|
||||
- **Future consideration:** If Nextcloud adds new room types, this query silently excludes them. Switching from an inclusive model (`IN (1,2,3)`) to an exclusive model (`NOT IN (4) AND type != 'note_to_self' AND object_type != 'file' ...`) would be more forward-compatible.
|
||||
|
||||
**Room name resolution:** In NC33, room display name is in the `name` column. Falls back to `token` if `name` is empty.
|
||||
|
||||
@@ -162,10 +169,11 @@ public function __construct(
|
||||
|
||||
`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)
|
||||
1. `overwritehost` (+ `overwriteproto`, default `https`) — explicit hostname override
|
||||
2. `overwritewebroot` — if full URL (starts with `http://`/`https://`) returns as-is; if path → falls through
|
||||
3. **Non-loopback trusted domain** — iterates `trusted_domains`, skips `127.0.0.1`, `::1`, `localhost`, and private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7). Returns first valid domain. If **none pass the filter**, falls through.
|
||||
4. **`trusted_domains[0]` fallback** — first trusted domain regardless of range (may be localhost). Caller may fail if it's unreachable.
|
||||
5. `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.
|
||||
|
||||
@@ -189,7 +197,7 @@ public function __construct(
|
||||
| `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. |
|
||||
| `prependDisplayName(string, string, string)` | ~601-620 | 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
|
||||
@@ -220,14 +228,14 @@ public function __construct(
|
||||
|---|---|---|
|
||||
| `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. |
|
||||
| `buildRichObject(filePath, mimeType, roomToken)` | ~1082-1170 | Creates a TYPE_ROOM share for the uploaded file so Talk's SystemMessage parser can resolve the rich object inline. Uses PDO directly to bypass DBConnection lazy init. Resolves bot's home storage from `storages` table (avoids hardcoded '1'). Returns rich object data with shareId, fileId, fileCachePath, downloadUrl, publicUrl, shareToken, filename, mimeType. |
|
||||
|
||||
**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
|
||||
5. `buildRichObject()` — create TYPE_ROOM share in bot user's storage
|
||||
6. Pass rich object to `postToRoom()` as `richObjects`
|
||||
|
||||
**Image flow for Apprise:**
|
||||
@@ -239,44 +247,37 @@ public function __construct(
|
||||
|
||||
| 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}`. |
|
||||
| `postToRoom(roomToken, message, senderName, richObjects)` | ~1335-1530 | Posts message to Talk Chat API v1. Uses query params (`?message=...&actorDisplayName=...`) with empty JSON body (`{}`). Basic auth: `talk-bot:botPassword`. Endpoint: `/ocs/v2.php/apps/spreed/api/v1/chat/{roomToken}`. Also sends a file_shared system message for rich objects. |
|
||||
|
||||
**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
|
||||
}
|
||||
}
|
||||
**Chat API v1 query params:**
|
||||
```
|
||||
?message=🤖 **CI Bot**%0A%0ABuild%20%231234%20passed&actorDisplayName=<bot's display name from room participant>
|
||||
```
|
||||
|
||||
**Rich objects:** Sent separately as a `file_shared` system message so Talk's SystemMessage parser can resolve the file inline. The system message body contains the share ID and metadata.
|
||||
|
||||
**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):
|
||||
**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()`** (line 1619):
|
||||
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):
|
||||
`saveConfig(array $config)` (lines 1720-1757):
|
||||
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
|
||||
4. Merge rooms: use provided `rooms` array, then remove disabled rooms from `disabled_rooms` (token → unused)
|
||||
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):
|
||||
**`ensureBotParticipants()`** (line 1764, `private` — called at end of every `saveConfig()` call):
|
||||
- Queries configured rooms
|
||||
- For each room, checks if talk-bot is already a participant
|
||||
- If not, inserts an attendee record via `AttendeeMapper`
|
||||
@@ -368,7 +369,7 @@ Simply delegates to `receiveApprise()`. Apprise's `apprises://` URL scheme inser
|
||||
| `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). |
|
||||
| `debug()` | GET `/debug` | Admin session (gated by debug_enabled flag) | Exposes DB schema, bot credentials, config (disabled by default). Admin gate re-enabled — see TODO for auto-disable timer. |
|
||||
|
||||
**`saveConfig()` flow:**
|
||||
1. `TalkService::saveConfig()` — validates, saves, ensures participants
|
||||
@@ -576,11 +577,11 @@ Route definitions. All webhook routes use `{roomToken}` and `{token}` path param
|
||||
<name>nc_bot_webhooks</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>
|
||||
<version>1.2.1</version>
|
||||
<licence>AGPL-3.0-or-later</licence>
|
||||
<namespace>Ncbotwebhooks</namespace>
|
||||
<dependencies>
|
||||
<nextcloud min-version="33" max-version="33"/>
|
||||
<nextcloud min-version="33" max-version="35"/>
|
||||
<app>spreed</app>
|
||||
</dependencies>
|
||||
```
|
||||
@@ -609,7 +610,7 @@ Route definitions. All webhook routes use `{roomToken}` and `{token}` path param
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
Note: The `../lib/` path is for local dev tooling only. Nextcloud apps use their own autoloader (registered via the app framework) — this file is not used in production.
|
||||
|
||||
---
|
||||
|
||||
@@ -1095,7 +1096,7 @@ nc_bot_webhooks: purged 3 old image files
|
||||
|
||||
## Migration Notes
|
||||
|
||||
### From ncdiscordhook to nc_bot_webhooks (v1.1.0)
|
||||
### From ncdiscordhook to nc_bot_webhooks (v1.2.1)
|
||||
|
||||
**Breaking change:** New app ID = new installation. Existing config data is under old app ID.
|
||||
|
||||
@@ -1122,7 +1123,7 @@ UPDATE oc_appconfig SET appid = 'nc_bot_webhooks' WHERE appid = 'ncdiscordhook';
|
||||
|
||||
### Nextcloud Version Compatibility
|
||||
|
||||
Currently pinned to Nextcloud 33 (`min-version="33" max-version="33"`). If upgrading Nextcloud:
|
||||
Currently pinned to Nextcloud 33–35 (`min-version="33" max-version="35"`). If upgrading Nextcloud beyond 35:
|
||||
|
||||
1. Update `max-version` in `info.xml`
|
||||
2. Test Talk API compatibility (Chat API v1 may change)
|
||||
@@ -1190,7 +1191,7 @@ curl -u admin:password https://your-server/apps/nc_bot_webhooks/debug | jq .
|
||||
|
||||
| File | Lines | Size |
|
||||
|---|---|---|
|
||||
| `TalkService.php` | 1138 | ~35 KB |
|
||||
| `TalkService.php` | 1904 | ~35 KB |
|
||||
| `WebhookController.php` | ~400 | ~15 KB |
|
||||
| `settings.js` | 351 | ~12 KB |
|
||||
| `Admin.php` | 74 | ~3 KB |
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
<name>nc_bot_webhooks</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. Each room gets its own webhook URL with an auth token for security.</description>
|
||||
<version>1.2.0</version>
|
||||
<version>1.2.1</version>
|
||||
<licence>AGPL-3.0-or-later</licence>
|
||||
<author homepage="https://emberlab.ca">Kyle</author>
|
||||
<namespace>Ncbotwebhooks</namespace>
|
||||
|
||||
@@ -295,6 +295,8 @@ The `/debug` endpoint exposes sensitive data (DB schema, bot credentials, config
|
||||
- Enabled only via CLI (`php occ nc_bot_webhooks:debug:enable`)
|
||||
- Stored in `appconfig` (not hardcoded)
|
||||
- Documented with explicit warnings
|
||||
- **Admin gate required** — accessible only to admin users when enabled.
|
||||
- **TODO: add auto-disable timer (2h TTL) to prevent accidental prolonged exposure.**
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -596,10 +596,12 @@ class WebhookController extends Controller {
|
||||
*
|
||||
* SECURITY: Never leave debug enabled in production. It exposes internal
|
||||
* configuration, database schema, and bot credentials.
|
||||
*
|
||||
* TODO: Add auto-disable timer (2-hour TTL) to prevent accidental prolonged exposure.
|
||||
*/
|
||||
#[PublicPage]
|
||||
#[NoCSRFRequired]
|
||||
#[NoAdminRequired]
|
||||
#[AdminRequired]
|
||||
public function debug(): DataResponse {
|
||||
// Debug endpoint must be explicitly enabled via OCC command
|
||||
$debugEnabled = $this->appConfig->getValueBool('nc_bot_webhooks', 'debug_enabled', false);
|
||||
|
||||
@@ -1077,7 +1077,9 @@ class TalkService {
|
||||
|
||||
/**
|
||||
* Build rich object data for a Talk message from an uploaded file path.
|
||||
* Creates a public link share so Talk can resolve the rich object.
|
||||
* Creates a TYPE_ROOM share in the bot user's storage so Talk's SystemMessage
|
||||
* parser can resolve the rich object inline. More secure than public link shares
|
||||
* since the share is scoped to the room rather than publicly accessible.
|
||||
*/
|
||||
public function buildRichObject(string $filePath, string $mimeType, string $roomToken): ?array {
|
||||
$this->logger->info('nc_bot_webhooks: buildRichObject ENTER', ['app' => self::APP_ID, 'filePath' => $filePath, 'roomToken' => $roomToken]);
|
||||
|
||||
@@ -40,7 +40,7 @@ class Admin implements ISettings {
|
||||
'no_rooms' => $this->l10n->t('No rooms found'),
|
||||
'no_rooms_msg' => $this->l10n->t('No Talk rooms are available or you don\'t have permission to view them.'),
|
||||
'bot_password' => $this->l10n->t('Bot App Password'),
|
||||
'bot_password_desc' => $this->l10n->t('App password for the "talk-bot" user. Create one in Settings → talk-bot → Devices & sessions.'),
|
||||
'bot_password_desc' => $this->l10n->t('App password for the "talk-bot" bot user account (not a regular user). Create one in Personal Settings → Security → Devices & sessions.'),
|
||||
'sender_name' => $this->l10n->t('Default Sender Name'),
|
||||
'sender_name_desc' => $this->l10n->t('Name used when posting messages as the bot.'),
|
||||
'image_retention' => $this->l10n->t('Image Retention (days)'),
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<button id="nc-save-password" type="button" style="display: <?= $hasBotPassword ? 'none' : 'inline-block' ?>;">Save</button>
|
||||
</div>
|
||||
<p class="nc-hint">
|
||||
Generate in Nextcloud Settings → <strong>talk-bot</strong> → Devices & sessions → <strong>Add device</strong>.
|
||||
Generate in <strong>Personal Settings</strong> → <strong>Security</strong> → Devices & sessions → <strong>Add device</strong> for the <strong>talk-bot</strong> bot user account (not a regular user).
|
||||
<?= $hasBotPassword ? 'Leave blank to keep current password.' : 'Required to send messages to Talk.' ?>
|
||||
</p>
|
||||
<p class="nc-hint" style="margin-top: 4px;">
|
||||
|
||||
Reference in New Issue
Block a user