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:
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\NCdiscordhook\Command;
|
||||||
|
|
||||||
|
use OCA\Talk\Manager as TalkManager;
|
||||||
|
use OCA\Talk\Participant;
|
||||||
|
use OCA\Talk\Model\Attendee;
|
||||||
|
use OCA\Talk\Model\AttendeeMapper;
|
||||||
|
use OCP\AppFramework\Db\DoesNotExistException;
|
||||||
|
use OCP\DB\Exception;
|
||||||
|
use OCP\IConfig;
|
||||||
|
use OCP\IDBConnection;
|
||||||
|
use OCP\IUserManager;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
class AddBotParticipant extends Command {
|
||||||
|
public static defaultName = 'ncdiscordhook:add-bot-participant';
|
||||||
|
public static defaultDescription = 'Add talk-bot as a participant in all configured rooms (fixes Chat API 404)';
|
||||||
|
|
||||||
|
private IConfig $config;
|
||||||
|
private IDBConnection $db;
|
||||||
|
private TalkManager $talkManager;
|
||||||
|
private IUserManager $userManager;
|
||||||
|
private AttendeeMapper $attendeeMapper;
|
||||||
|
|
||||||
|
private const APP_ID = 'ncdiscordhook';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
IConfig $config,
|
||||||
|
IDBConnection $db,
|
||||||
|
TalkManager $talkManager,
|
||||||
|
IUserManager $userManager,
|
||||||
|
AttendeeMapper $attendeeMapper,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
$this->config = $config;
|
||||||
|
$this->db = $db;
|
||||||
|
$this->talkManager = $talkManager;
|
||||||
|
$this->userManager = $userManager;
|
||||||
|
$this->attendeeMapper = $attendeeMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||||
|
$output->writeln('Reading configured rooms from app config...');
|
||||||
|
|
||||||
|
// Get configured rooms from app config (stored as JSON)
|
||||||
|
$roomsJson = $this->config->getAppValue(self::APP_ID, 'rooms', '[]');
|
||||||
|
$rooms = @json_decode($roomsJson, true);
|
||||||
|
if (!is_array($rooms)) {
|
||||||
|
$output->writeln('<error>No configured rooms found.</error>');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$roomTokens = array_keys($rooms);
|
||||||
|
$output->writeln('Found ' . count($roomTokens) . ' configured room(s).');
|
||||||
|
|
||||||
|
// Get talk-bot user
|
||||||
|
$botUser = $this->userManager->get('talk-bot');
|
||||||
|
if ($botUser === null) {
|
||||||
|
$output->writeln('<error>talk-bot user not found. Creating it...</error>');
|
||||||
|
// talk-bot might not exist yet
|
||||||
|
$output->writeln('<comment>Run: php occ user:add --password-from-env talk-bot</comment>');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get table prefix
|
||||||
|
$sysPrefix = $this->config->getSystemValueString('dbtableprefix', '');
|
||||||
|
$talkPrefix = $this->config->getAppValue('spreed', 'databaseprefix', $sysPrefix);
|
||||||
|
$attendeeTable = $talkPrefix . 'talk_attendee';
|
||||||
|
|
||||||
|
$added = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
|
||||||
|
foreach ($roomTokens as $token) {
|
||||||
|
try {
|
||||||
|
$room = $this->talkManager->getRoomForToken($token);
|
||||||
|
$roomId = $room->getId();
|
||||||
|
|
||||||
|
// Check if talk-bot is already an attendee
|
||||||
|
try {
|
||||||
|
$attendee = $this->attendeeMapper->findByActor($roomId, 'users', 'talk-bot');
|
||||||
|
$output->writeln(' <info>Room ' . $token . ': talk-bot already an attendee (id=' . $roomId . ')</info>');
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
} catch (DoesNotExistException $e) {
|
||||||
|
// Not found - will create
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$output->writeln(' <warning>Room ' . $token . ': query error - ' . $e->getMessage() . '</warning>');
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create attendee record
|
||||||
|
$newAttendee = new Attendee();
|
||||||
|
$newAttendee->setRoomId($roomId);
|
||||||
|
$newAttendee->setActorType('users');
|
||||||
|
$newAttendee->setActorId('talk-bot');
|
||||||
|
$newAttendee->setDisplayName('talk-bot');
|
||||||
|
$newAttendee->setParticipantType(Participant::PERMISSIONS_DEFAULT);
|
||||||
|
$newAttendee->setPermissions(Participant::PERMISSIONS_MAX_DEFAULT);
|
||||||
|
$newAttendee->setNotificationLevel(3); // Full level
|
||||||
|
$newAttendee->setFavorite(false);
|
||||||
|
$newAttendee->setArchived(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->attendeeMapper->insert($newAttendee);
|
||||||
|
$output->writeln(' <info>Room ' . $token . ': added talk-bot as attendee (room_id=' . $roomId . ')</info>');
|
||||||
|
$added++;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$output->writeln(' <warning>Room ' . $token . ': insert failed - ' . $e->getMessage() . '</warning>');
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$output->writeln(' <error>Room ' . $token . ': ' . $e->getMessage() . '</error>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->writeln('');
|
||||||
|
$output->writeln('<info>Done: ' . $added . ' added, ' . $skipped . ' skipped.</info>');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,9 +13,11 @@ use OCP\AppFramework\Http\Attribute\AdminRequired;
|
|||||||
use OCP\App\IAppManager;
|
use OCP\App\IAppManager;
|
||||||
use OCP\IAppConfig;
|
use OCP\IAppConfig;
|
||||||
use OCP\IConfig;
|
use OCP\IConfig;
|
||||||
|
use OCP\Http\Client\IClientService;
|
||||||
use OCP\IGroupManager;
|
use OCP\IGroupManager;
|
||||||
use OCP\IRequest;
|
use OCP\IRequest;
|
||||||
use OCP\IUserSession;
|
use OCP\IUserSession;
|
||||||
|
use OCA\Talk\ParticipantService;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
class WebhookController extends Controller {
|
class WebhookController extends Controller {
|
||||||
@@ -26,8 +28,9 @@ class WebhookController extends Controller {
|
|||||||
private IGroupManager $groupManager;
|
private IGroupManager $groupManager;
|
||||||
private IConfig $config;
|
private IConfig $config;
|
||||||
private IAppConfig $appConfig;
|
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);
|
parent::__construct('ncdiscordhook', $request);
|
||||||
$this->talkService = $talkService;
|
$this->talkService = $talkService;
|
||||||
$this->logger = $logger;
|
$this->logger = $logger;
|
||||||
@@ -36,6 +39,7 @@ class WebhookController extends Controller {
|
|||||||
$this->groupManager = $groupManager;
|
$this->groupManager = $groupManager;
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
$this->appConfig = $appConfig;
|
$this->appConfig = $appConfig;
|
||||||
|
$this->clientService = $clientService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -201,6 +205,9 @@ class WebhookController extends Controller {
|
|||||||
* Debug endpoint to verify the app is working.
|
* Debug endpoint to verify the app is working.
|
||||||
*
|
*
|
||||||
* URL: GET /apps/ncdiscordhook/debug
|
* 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]
|
#[PublicPage]
|
||||||
#[NoCSRFRequired]
|
#[NoCSRFRequired]
|
||||||
@@ -323,9 +330,338 @@ class WebhookController extends Controller {
|
|||||||
$info['talk_debug_error_trace'] = substr($e->getTraceAsString(), 0, 2000);
|
$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);
|
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.
|
* Get available Talk rooms.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -116,16 +116,41 @@ class TalkService {
|
|||||||
return rtrim($overwritten, '/');
|
return rtrim($overwritten, '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the first trusted domain as the base URL.
|
// Prefer a public (non-loopback, non-private) trusted domain.
|
||||||
// This is the only reliable method when the HTTP client blocks localhost
|
// In Docker/container deployments, trusted_domains[0] is often 127.0.0.1
|
||||||
// and overwrite.cli.url points to an internal Docker address.
|
// which is unreachable from within the container.
|
||||||
$trusted = $this->config->getSystemValue('trusted_domains', []);
|
$trusted = $this->config->getSystemValue('trusted_domains', []);
|
||||||
|
foreach ($trusted as $domain) {
|
||||||
|
if ($domain === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Already has a scheme — trust it as-is
|
||||||
|
if (preg_match('#^https?://#', $domain)) {
|
||||||
|
return rtrim($domain, '/');
|
||||||
|
}
|
||||||
|
// Skip loopback and localhost
|
||||||
|
$lower = strtolower($domain);
|
||||||
|
if ($lower === 'localhost'
|
||||||
|
|| $lower === '127.0.0.1'
|
||||||
|
|| $lower === '::1'
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Skip private IP ranges (10.x, 172.16-31.x, 192.168.x)
|
||||||
|
if (preg_match(
|
||||||
|
'/^(10\.\d{1,3}|172\.(1[6-9]|2\d|3[01])|192\.168)\./i',
|
||||||
|
$domain
|
||||||
|
)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return 'https://' . $domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to first trusted domain (may be localhost; caller may fail)
|
||||||
if (!empty($trusted[0])) {
|
if (!empty($trusted[0])) {
|
||||||
// Check if trusted_domains[0] already has a scheme
|
|
||||||
if (preg_match('#^https?://#', $trusted[0])) {
|
if (preg_match('#^https?://#', $trusted[0])) {
|
||||||
return rtrim($trusted[0], '/');
|
return rtrim($trusted[0], '/');
|
||||||
}
|
}
|
||||||
// Use https by default for trusted domains
|
|
||||||
return 'https://' . $trusted[0];
|
return 'https://' . $trusted[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,6 +731,7 @@ class TalkService {
|
|||||||
'body' => $jsonBody,
|
'body' => $jsonBody,
|
||||||
'headers' => [
|
'headers' => [
|
||||||
'OCS-Expect-Formatted' => 'json',
|
'OCS-Expect-Formatted' => 'json',
|
||||||
|
'OCS-APIRequest' => '1',
|
||||||
'Authorization' => 'Basic ' . $credentials,
|
'Authorization' => 'Basic ' . $credentials,
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
],
|
],
|
||||||
@@ -715,7 +741,7 @@ class TalkService {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$statusCode = $response->getStatusCode();
|
$statusCode = $response->getStatusCode();
|
||||||
if ($statusCode === Http::STATUS_OK) {
|
if ($statusCode >= 200 && $statusCode < 300) {
|
||||||
$this->logger->info('NCdiscordhook: message posted to room ' . $roomToken, [
|
$this->logger->info('NCdiscordhook: message posted to room ' . $roomToken, [
|
||||||
'app' => self::APP_ID,
|
'app' => self::APP_ID,
|
||||||
]);
|
]);
|
||||||
@@ -838,5 +864,68 @@ class TalkService {
|
|||||||
if (isset($config['sender_name'])) {
|
if (isset($config['sender_name'])) {
|
||||||
$this->setSenderName($config['sender_name']);
|
$this->setSenderName($config['sender_name']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure talk-bot is a participant in all configured rooms
|
||||||
|
$this->ensureBotParticipants();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add talk-bot user as a participant in all configured rooms.
|
||||||
|
* Required so the Chat API accepts requests authenticated as talk-bot.
|
||||||
|
*/
|
||||||
|
private function ensureBotParticipants(): void {
|
||||||
|
if ($this->talkParticipantService === null) {
|
||||||
|
$this->logger->info('NCdiscordhook: ParticipantService not available, skipping auto-join for talk-bot', ['app' => self::APP_ID]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rooms = $this->getRooms();
|
||||||
|
if (empty($rooms)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the talk-bot user
|
||||||
|
$botUser = $this->userManager->get('talk-bot');
|
||||||
|
if ($botUser === null) {
|
||||||
|
$this->logger->warning('NCdiscordhook: talk-bot user not found', ['app' => self::APP_ID]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (array_keys($rooms) as $token) {
|
||||||
|
try {
|
||||||
|
$room = $this->talkManager->getRoomForToken($token);
|
||||||
|
// Check if talk-bot is already a participant
|
||||||
|
$participants = $room->getParticipants();
|
||||||
|
$actors = [];
|
||||||
|
if (is_array($participants)) {
|
||||||
|
$actors = $participants['actors'] ?? [];
|
||||||
|
} elseif (is_object($participants) && method_exists($participants, 'getActors')) {
|
||||||
|
$actors = $participants->getActors();
|
||||||
|
}
|
||||||
|
$hasBot = false;
|
||||||
|
foreach ($actors as $actor) {
|
||||||
|
if (isset($actor['type']) && $actor['type'] === 'users' && isset($actor['actorId']) && $actor['actorId'] === 'talk-bot') {
|
||||||
|
$hasBot = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$hasBot) {
|
||||||
|
$this->talkParticipantService->addParticipant(
|
||||||
|
$room,
|
||||||
|
$botUser,
|
||||||
|
\OCA\Talk\Participant::TYPE_USER,
|
||||||
|
\OCA\Talk\Participant::PERMISSIONS_DEFAULT,
|
||||||
|
);
|
||||||
|
$this->logger->info('NCdiscordhook: added talk-bot as participant in room ' . $token, [
|
||||||
|
'app' => self::APP_ID,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->logger->warning('NCdiscordhook: failed to add talk-bot to room ' . $token, [
|
||||||
|
'app' => self::APP_ID,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user