feat: enhance webhook debugging and ensure bot participant registration
- Add `ncdiscordhook:add-bot-participant` console command to register talk-bot in all configured rooms - Extend debug endpoint with webhook URL validation and test post capabilities - Improve `getBaseUrl()` to prefer public trusted domains, fixing container deployment issues - Add `OCS-APIRequest` header and accept 2xx HTTP status codes for successful message posting - Auto-register talk-bot as a participant during configuration initialization
This commit is contained in:
@@ -13,9 +13,11 @@ use OCP\AppFramework\Http\Attribute\AdminRequired;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IConfig;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IRequest;
|
||||
use OCP\IUserSession;
|
||||
use OCA\Talk\ParticipantService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class WebhookController extends Controller {
|
||||
@@ -26,8 +28,9 @@ class WebhookController extends Controller {
|
||||
private IGroupManager $groupManager;
|
||||
private IConfig $config;
|
||||
private IAppConfig $appConfig;
|
||||
private IClientService $clientService;
|
||||
|
||||
public function __construct(IRequest $request, TalkService $talkService, LoggerInterface $logger, IAppManager $appManager, IUserSession $userSession, IGroupManager $groupManager, IConfig $config, IAppConfig $appConfig) {
|
||||
public function __construct(IRequest $request, TalkService $talkService, LoggerInterface $logger, IAppManager $appManager, IUserSession $userSession, IGroupManager $groupManager, IConfig $config, IAppConfig $appConfig, IClientService $clientService) {
|
||||
parent::__construct('ncdiscordhook', $request);
|
||||
$this->talkService = $talkService;
|
||||
$this->logger = $logger;
|
||||
@@ -36,6 +39,7 @@ class WebhookController extends Controller {
|
||||
$this->groupManager = $groupManager;
|
||||
$this->config = $config;
|
||||
$this->appConfig = $appConfig;
|
||||
$this->clientService = $clientService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,6 +205,9 @@ class WebhookController extends Controller {
|
||||
* Debug endpoint to verify the app is working.
|
||||
*
|
||||
* URL: GET /apps/ncdiscordhook/debug
|
||||
* Query params:
|
||||
* - webhook_url: Full webhook URL to diagnose (e.g. /apps/ncdiscordhook/bot-webhook/{room}/{token})
|
||||
* - test_post=1: Actually POST a test message to the room
|
||||
*/
|
||||
#[PublicPage]
|
||||
#[NoCSRFRequired]
|
||||
@@ -323,9 +330,338 @@ class WebhookController extends Controller {
|
||||
$info['talk_debug_error_trace'] = substr($e->getTraceAsString(), 0, 2000);
|
||||
}
|
||||
|
||||
// ── Webhook URL debug ─────────────────────────────────────────
|
||||
// Parse webhook URL if provided via query param
|
||||
$webhookUrl = $this->request->getParam('webhook_url', '');
|
||||
$doTestPost = (bool) $this->request->getParam('test_post', 0);
|
||||
if ($webhookUrl !== '') {
|
||||
$info['webhook_debug'] = $this->debugWebhookUrl($webhookUrl);
|
||||
if ($doTestPost) {
|
||||
$info['test_post'] = $this->runTestPost($webhookUrl);
|
||||
}
|
||||
}
|
||||
|
||||
return new DataResponse($info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug a webhook URL step by step.
|
||||
*/
|
||||
private function debugWebhookUrl(string $webhookUrl): array {
|
||||
$result = [];
|
||||
|
||||
// 1. Parse the URL
|
||||
$parsed = parse_url($webhookUrl);
|
||||
$result['parsed_url'] = $parsed;
|
||||
|
||||
$path = $parsed['path'] ?? '';
|
||||
$segments = explode('/', trim($path, '/'));
|
||||
|
||||
// Expected: apps/ncdiscordhook/bot-webhook/{roomToken}/{token}
|
||||
if (count($segments) >= 5 && $segments[0] === 'apps' && $segments[1] === 'ncdiscordhook' && $segments[2] === 'bot-webhook') {
|
||||
$roomToken = $segments[3];
|
||||
$token = $segments[4];
|
||||
$result['room_token'] = $roomToken;
|
||||
$result['token'] = $token;
|
||||
$result['url_valid'] = true;
|
||||
|
||||
// 2. Validate auth token
|
||||
$valid = $this->talkService->validateAuthToken($roomToken, $token);
|
||||
$result['auth_token_valid'] = $valid;
|
||||
|
||||
// 3. Check if room is configured
|
||||
$configured = $this->talkService->isBotEnabledForRoom($roomToken);
|
||||
$result['room_configured'] = $configured;
|
||||
|
||||
// 4. Check bot user
|
||||
try {
|
||||
$bot = $this->talkService->getBotUser();
|
||||
$result['bot_user_exists'] = $bot !== null;
|
||||
$result['bot_uid'] = $bot !== null ? $bot->getUID() : null;
|
||||
} catch (\Exception $e) {
|
||||
$result['bot_user_error'] = $e->getMessage();
|
||||
}
|
||||
|
||||
// 5. Check bot password
|
||||
$result['has_bot_password'] = $this->talkService->hasBotPassword();
|
||||
try {
|
||||
$pw = $this->talkService->getBotPassword();
|
||||
$result['bot_password_length'] = $pw !== null ? strlen($pw) : null;
|
||||
$result['bot_password_starts_with'] = $pw !== null ? substr($pw, 0, 4) . '...' : null;
|
||||
} catch (\Exception $e) {
|
||||
$result['bot_password_error'] = $e->getMessage();
|
||||
}
|
||||
|
||||
// 6. Check base URL
|
||||
$baseUrl = $this->talkService->getBaseUrl();
|
||||
$result['base_url'] = $baseUrl;
|
||||
$result['base_url_scheme'] = parse_url($baseUrl, PHP_URL_SCHEME) ?? null;
|
||||
$result['base_url_host'] = parse_url($baseUrl, PHP_URL_HOST) ?? null;
|
||||
|
||||
// 7. Check if room exists in DB
|
||||
try {
|
||||
$sysPrefix = $this->config->getSystemValueString('dbtableprefix', '');
|
||||
$talkPrefix = $this->config->getAppValue('spreed', 'databaseprefix', $sysPrefix);
|
||||
$roomTable = $this->talkService->detectTalkTableFromCatalog('talk_rooms', 'spreed_room');
|
||||
$result['room_table'] = $roomTable;
|
||||
|
||||
if ($roomTable !== null) {
|
||||
$stmt = $this->talkService->getDbConnection()->executeQuery(
|
||||
'SELECT token, type, name, object_type, object_id
|
||||
FROM "' . $roomTable . '" WHERE token = ?',
|
||||
[$roomToken],
|
||||
);
|
||||
$row = $stmt->fetch();
|
||||
$stmt->closeCursor();
|
||||
|
||||
$result['room_found_in_db'] = $row !== false;
|
||||
if ($row !== false) {
|
||||
$result['room_type'] = (int)$row['type'];
|
||||
// readable_name may not exist in all Talk versions
|
||||
$result['room_readable_name'] = isset($row['readable_name']) ? ($row['readable_name'] ?: null) : null;
|
||||
$result['room_name'] = $row['name'] ?: null;
|
||||
$result['room_object_type'] = $row['object_type'] ?: null;
|
||||
$result['room_object_id'] = $row['object_id'] ?: null;
|
||||
|
||||
// 8. Check room name doesn't look like a DM JSON
|
||||
if (is_string($row['name']) && str_starts_with($row['name'], '["')) {
|
||||
$result['room_name_is_json_dm'] = true;
|
||||
$result['room_name_is_json_dm_warning'] = 'Room name looks like a DM JSON array — this room type is normally filtered out but is explicitly configured.';
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$result['db_check_error'] = $e->getMessage();
|
||||
}
|
||||
|
||||
// 9. Construct Chat API endpoint
|
||||
$chatEndpoint = $baseUrl . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $roomToken;
|
||||
$result['chat_api_endpoint'] = $chatEndpoint;
|
||||
|
||||
// 10. Construct Basic auth header (without exposing full password)
|
||||
try {
|
||||
$pw = $this->talkService->getBotPassword();
|
||||
if ($pw !== null) {
|
||||
$b64 = base64_encode('talk-bot:' . $pw);
|
||||
$result['basic_auth_header_prefix'] = 'Basic ' . substr($b64, 0, 20) . '...';
|
||||
$result['basic_auth_header_length'] = strlen($b64);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$result['basic_auth_error'] = $e->getMessage();
|
||||
}
|
||||
|
||||
// 11. Test HTTP connectivity to the Chat API endpoint
|
||||
try {
|
||||
$client = $this->clientService->newClient();
|
||||
$response = $client->get($chatEndpoint, [
|
||||
'headers' => [
|
||||
'OCS-Expect-Formatted' => 'json',
|
||||
'Authorization' => 'Basic ' . base64_encode('talk-bot:' . ($this->talkService->getBotPassword() ?? 'ERROR')),
|
||||
],
|
||||
'nextcloud' => [
|
||||
'allow_local_address' => true,
|
||||
],
|
||||
]);
|
||||
$result['http_test_status'] = $response->getStatusCode();
|
||||
$result['http_test_body'] = $response->getBody();
|
||||
} catch (\Guzzle\Exception\ConnectException $e) {
|
||||
$result['http_test_error'] = 'Connection failed: ' . $e->getMessage();
|
||||
$result['http_test_error_type'] = 'connection';
|
||||
} catch (\Guzzle\Exception\ServerException $e) {
|
||||
$result['http_test_status'] = $e->getResponse() ? $e->getResponse()->getStatusCode() : null;
|
||||
$result['http_test_body'] = $e->getResponse() ? $e->getResponse()->getBody() : null;
|
||||
$result['http_test_error'] = 'Server error: ' . $e->getMessage();
|
||||
$result['http_test_error_type'] = 'server';
|
||||
} catch (\Guzzle\Exception\ClientException $e) {
|
||||
$result['http_test_status'] = $e->getResponse() ? $e->getResponse()->getStatusCode() : null;
|
||||
$result['http_test_body'] = $e->getResponse() ? $e->getResponse()->getBody() : null;
|
||||
$result['http_test_error'] = 'Client error: ' . $e->getMessage();
|
||||
$result['http_test_error_type'] = 'client';
|
||||
} catch (\Exception $e) {
|
||||
$result['http_test_error'] = $e->getMessage();
|
||||
$result['http_test_error_type'] = 'other';
|
||||
}
|
||||
|
||||
// 12. Summary of issues
|
||||
$issues = [];
|
||||
if (!$result['auth_token_valid'] ?? false === false) {
|
||||
$issues[] = 'Auth token is INVALID for this room';
|
||||
}
|
||||
if (!($result['room_configured'] ?? false)) {
|
||||
$issues[] = 'Room is NOT configured (bot not enabled)';
|
||||
}
|
||||
if (!($result['bot_user_exists'] ?? false)) {
|
||||
$issues[] = 'talk-bot user does NOT exist';
|
||||
}
|
||||
if (!($result['has_bot_password'] ?? false)) {
|
||||
$issues[] = 'Bot password is NOT configured';
|
||||
}
|
||||
if (($result['room_found_in_db'] ?? false) === false) {
|
||||
$issues[] = 'Room NOT found in Talk database';
|
||||
}
|
||||
if (($result['http_test_status'] ?? null) !== null && ($result['http_test_status'] ?? 0) >= 400) {
|
||||
$issues[] = 'Chat API returned HTTP ' . ($result['http_test_status'] ?? 'unknown');
|
||||
}
|
||||
if (isset($result['http_test_error'])) {
|
||||
$issues[] = 'HTTP test failed: ' . $result['http_test_error'];
|
||||
}
|
||||
$result['issues'] = $issues;
|
||||
$result['summary'] = empty($issues) ? 'All checks passed — if webhook still fails, check Talk server logs' : implode('; ', $issues);
|
||||
|
||||
} else {
|
||||
$result['url_valid'] = false;
|
||||
$result['parse_error'] = 'URL does not match expected pattern: /apps/ncdiscordhook/bot-webhook/{roomToken}/{token}';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a test post to simulate a full webhook delivery.
|
||||
*/
|
||||
private function runTestPost(string $webhookUrl): array {
|
||||
$result = [];
|
||||
|
||||
// Parse URL to get roomToken and token
|
||||
$parsed = parse_url($webhookUrl);
|
||||
$path = $parsed['path'] ?? '';
|
||||
$segments = explode('/', trim($path, '/'));
|
||||
|
||||
if (!(count($segments) >= 5 && $segments[0] === 'apps' && $segments[1] === 'ncdiscordhook' && $segments[2] === 'bot-webhook')) {
|
||||
return ['error' => 'Invalid webhook URL format'];
|
||||
}
|
||||
|
||||
$roomToken = $segments[3];
|
||||
$token = $segments[4];
|
||||
$result['room_token'] = $roomToken;
|
||||
$result['token'] = $token;
|
||||
|
||||
// Step 1: Validate auth token
|
||||
$authValid = $this->talkService->validateAuthToken($roomToken, $token);
|
||||
$result['step_1_auth_token'] = $authValid ? 'OK' : 'FAIL — token not found for this room';
|
||||
if (!$authValid) {
|
||||
$result['steps'] = $result;
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Step 2: Build a realistic Discord-style test payload
|
||||
$testPayload = [
|
||||
'content' => '🔧 **Test message from ncdiscordhook debug**\n\nThis is a test message sent via the debug endpoint. If you see this, the webhook pipeline is working!',
|
||||
'embeds' => [
|
||||
[
|
||||
'title' => 'Webhook Test',
|
||||
'description' => 'Sent at ' . date('Y-m-d H:i:s T'),
|
||||
'color' => 3066993,
|
||||
'footer' => [
|
||||
'text' => 'ncdiscordhook debug endpoint',
|
||||
],
|
||||
],
|
||||
],
|
||||
'username' => 'Debug Test',
|
||||
'avatar_url' => null,
|
||||
];
|
||||
|
||||
// Step 3: Map payload to message
|
||||
$message = $this->talkService->mapPayload($testPayload);
|
||||
$result['step_2_map_payload'] = $message !== '' ? 'OK — "' . substr($message, 0, 100) . '..." (' . strlen($message) . ' chars)' : 'FAIL — empty message';
|
||||
if ($message === '') {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Step 4: Get sender name
|
||||
$senderName = $this->talkService->getSenderName($testPayload);
|
||||
$result['step_3_sender_name'] = $senderName;
|
||||
|
||||
// Step 5: Check bot password
|
||||
$botPassword = $this->talkService->getBotPassword();
|
||||
$result['step_4_bot_password'] = $botPassword !== null ? 'OK (' . strlen($botPassword) . ' chars)' : 'FAIL — not configured';
|
||||
if ($botPassword === null) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Step 6: Check bot enabled for room
|
||||
$botEnabled = $this->talkService->isBotEnabledForRoom($roomToken);
|
||||
$result['step_5_bot_enabled'] = $botEnabled ? 'OK' : 'FAIL — room not enabled in config';
|
||||
if (!$botEnabled) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Step 7: Get base URL
|
||||
$baseUrl = $this->talkService->getBaseUrl();
|
||||
$result['step_6_base_url'] = $baseUrl !== '' ? $baseUrl : 'FAIL — not configured';
|
||||
if ($baseUrl === '') {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Step 8: Build the Chat API request (OCS path with CSRF bypass)
|
||||
$endpoint = $baseUrl . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $roomToken;
|
||||
$result['step_7_endpoint'] = $endpoint;
|
||||
|
||||
// Step 9: Try the actual Chat API POST
|
||||
try {
|
||||
$client = $this->clientService->newClient();
|
||||
$response = $client->post($endpoint, [
|
||||
'body' => json_encode([
|
||||
'message' => $message,
|
||||
'actorType' => 'users',
|
||||
'actorId' => 'talk-bot',
|
||||
'actorDisplayName' => $senderName,
|
||||
]),
|
||||
'headers' => [
|
||||
'OCS-Expect-Formatted' => 'json',
|
||||
'OCS-APIRequest' => '1',
|
||||
'Authorization' => 'Basic ' . base64_encode('talk-bot:' . $botPassword),
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'nextcloud' => [
|
||||
'allow_local_address' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$statusCode = $response->getStatusCode();
|
||||
$body = $response->getBody();
|
||||
$result['step_8_http_status'] = $statusCode;
|
||||
$result['step_8_http_body'] = $body;
|
||||
|
||||
if ($statusCode === 200) {
|
||||
$result['step_8_result'] = 'SUCCESS — message posted to Talk!';
|
||||
$result['overall'] = 'Webhook pipeline is WORKING. If messages still fail from Discord, check your Discord webhook configuration.';
|
||||
} else {
|
||||
$result['step_8_result'] = 'FAIL — HTTP ' . $statusCode;
|
||||
|
||||
// Parse OCS error if possible
|
||||
if (strpos($body, 'ocs') !== false) {
|
||||
$parsedBody = @json_decode($body, true);
|
||||
if (is_array($parsedBody) && isset($parsedBody['ocs']['meta']['message'])) {
|
||||
$result['step_8_error_message'] = $parsedBody['ocs']['meta']['message'];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Guzzle\Exception\ConnectException $e) {
|
||||
$result['step_8_result'] = 'FAIL — Connection error';
|
||||
$result['step_8_error'] = $e->getMessage();
|
||||
} catch (\Guzzle\Exception\ServerException $e) {
|
||||
$result['step_8_result'] = 'FAIL — Server error (5xx)';
|
||||
$result['step_8_error'] = $e->getMessage();
|
||||
if ($e->getResponse()) {
|
||||
$result['step_8_status'] = $e->getResponse()->getStatusCode();
|
||||
$result['step_8_body'] = $e->getResponse()->getBody();
|
||||
}
|
||||
} catch (\Guzzle\Exception\ClientException $e) {
|
||||
$result['step_8_result'] = 'FAIL — Client error (4xx)';
|
||||
$result['step_8_error'] = $e->getMessage();
|
||||
if ($e->getResponse()) {
|
||||
$result['step_8_status'] = $e->getResponse()->getStatusCode();
|
||||
$result['step_8_body'] = $e->getResponse()->getBody();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$result['step_8_result'] = 'FAIL — Unexpected error';
|
||||
$result['step_8_error'] = $e->getMessage();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available Talk rooms.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user