From 6453715bf0bdcb91a6617cd15ca2c0af4221ce07 Mon Sep 17 00:00:00 2001 From: kyle Date: Fri, 12 Jun 2026 14:32:52 -0700 Subject: [PATCH] fix: update TalkService for Talk 14 compatibility and add CLI copy helper - **TalkService**: - Refactored `ensureBotParticipants` to use `AttendeeMapper` directly, removing the dependency on `ParticipantService`. - Added `getBotDisplayNameForRoom` to resolve the bot's display name via direct DB query. - Updated `sendMessage` to use Chat API v1 and include `richObjectsEnd`. - Improved `getBaseUrl` logic to handle `overwritehost` and `overwritewebroot`. - **WebhookController**: - Fixed `getAllValues` to iterate known keys manually, preventing `RuntimeException` in Nextcloud 33. - Added error handling for `saveConfig`. - Removed unused `ParticipantService` import. - **js/settings.js**: - Added functionality to copy CLI commands to the clipboard. - **Deletions**: - Removed `lib/Command/AddBotParticipant.php` as its logic is now integrated into `TalkService`. - **Templates**: - Updated admin settings labels and comments. --- js/settings.js | 16 +++ lib/Command/AddBotParticipant.php | 123 -------------------- lib/Controller/WebhookController.php | 29 +++-- lib/Service/TalkService.php | 166 +++++++++++++++++++++------ lib/Settings/Admin.php | 1 + templates/adminSettings.php | 4 +- 6 files changed, 172 insertions(+), 167 deletions(-) delete mode 100644 lib/Command/AddBotParticipant.php diff --git a/js/settings.js b/js/settings.js index 546bc1b..5911fdc 100644 --- a/js/settings.js +++ b/js/settings.js @@ -39,6 +39,22 @@ document.addEventListener('DOMContentLoaded', function () { savePasswordBtn.style.display = this.value.trim() ? 'inline-block' : 'none'; }); + // Copy CLI commands to clipboard + document.querySelectorAll('.nc-copy-btn').forEach(function (btn) { + btn.addEventListener('click', function () { + var text = this.dataset.copy || ''; + navigator.clipboard.writeText(text).then(function () { + var orig = this.textContent; + this.textContent = '✓'; + this.style.color = '#28a745'; + setTimeout(function () { + btn.textContent = orig; + btn.style.color = ''; + }, 2000); + }.bind(this)); + }); + }); + // Fetch available Talk rooms function fetchRooms() { fetchRoomsBtn.disabled = true; diff --git a/lib/Command/AddBotParticipant.php b/lib/Command/AddBotParticipant.php deleted file mode 100644 index f482782..0000000 --- a/lib/Command/AddBotParticipant.php +++ /dev/null @@ -1,123 +0,0 @@ -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('No configured rooms found.'); - 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('talk-bot user not found. Creating it...'); - // talk-bot might not exist yet - $output->writeln('Run: php occ user:add --password-from-env talk-bot'); - 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(' Room ' . $token . ': talk-bot already an attendee (id=' . $roomId . ')'); - $skipped++; - continue; - } catch (DoesNotExistException $e) { - // Not found - will create - } catch (\Exception $e) { - $output->writeln(' Room ' . $token . ': query error - ' . $e->getMessage() . ''); - $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(' Room ' . $token . ': added talk-bot as attendee (room_id=' . $roomId . ')'); - $added++; - } catch (Exception $e) { - $output->writeln(' Room ' . $token . ': insert failed - ' . $e->getMessage() . ''); - } - } catch (\Exception $e) { - $output->writeln(' Room ' . $token . ': ' . $e->getMessage() . ''); - } - } - - $output->writeln(''); - $output->writeln('Done: ' . $added . ' added, ' . $skipped . ' skipped.'); - return 0; - } -} diff --git a/lib/Controller/WebhookController.php b/lib/Controller/WebhookController.php index da09967..2ca508e 100644 --- a/lib/Controller/WebhookController.php +++ b/lib/Controller/WebhookController.php @@ -17,7 +17,6 @@ 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 { @@ -193,7 +192,18 @@ class WebhookController extends Controller { ); } - $this->talkService->saveConfig($config); + try { + $this->talkService->saveConfig($config); + } catch (\Exception $e) { + $this->logger->error('NCdiscordhook: saveConfig failed: ' . $e->getMessage(), [ + 'app' => 'ncdiscordhook', + 'exception' => (string)$e, + ]); + return new DataResponse( + ['error' => 'Save failed: ' . $e->getMessage()], + Http::STATUS_INTERNAL_SERVER_ERROR, + ); + } return new DataResponse([ 'status' => 'ok', @@ -245,10 +255,15 @@ class WebhookController extends Controller { $info['system_config_error'] = $e->getMessage(); } - // AppConfig values + // AppConfig values (iterate known keys to avoid lazy AppConfig RuntimeException in NC33) try { - $appConfig = $this->appConfig->getAllValues('ncdiscordhook'); - $info['app_config_keys'] = $appConfig; + $knownKeys = ['bot_password', 'rooms', 'auth_tokens', 'retention_days', 'sender_name']; + $appConfigKeys = []; + foreach ($knownKeys as $key) { + $val = $this->appConfig->getValueString('ncdiscordhook', $key, ''); + $appConfigKeys[$key] = $val !== '' ? '[set]' : '[empty]'; + } + $info['app_config_keys'] = $appConfigKeys; $info['app_config_room_count'] = count($this->talkService->getRooms()); $info['app_config_auth_token_count'] = count($this->talkService->getAuthTokens()); $info['app_config_bot_password_set'] = $this->talkService->hasBotPassword(); @@ -434,7 +449,7 @@ class WebhookController extends Controller { $result['db_check_error'] = $e->getMessage(); } - // 9. Construct Chat API endpoint + // 9. Construct Chat API endpoint (v1 for Talk 19 / NC33) $chatEndpoint = $baseUrl . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $roomToken; $result['chat_api_endpoint'] = $chatEndpoint; @@ -593,7 +608,7 @@ class WebhookController extends Controller { return $result; } - // Step 8: Build the Chat API request (OCS path with CSRF bypass) + // Step 8: Build the Chat API request (v1 for Talk 19 / NC33) $endpoint = $baseUrl . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $roomToken; $result['step_7_endpoint'] = $endpoint; diff --git a/lib/Service/TalkService.php b/lib/Service/TalkService.php index 8421b7e..75a9e99 100644 --- a/lib/Service/TalkService.php +++ b/lib/Service/TalkService.php @@ -15,6 +15,9 @@ use OCP\IURLGenerator; use OCP\IUserManager; use OCP\Share\IManager; use OCA\Talk\Manager as TalkManager; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Model\AttendeeMapper; +use OCP\AppFramework\Db\DoesNotExistException; use Psr\Log\LoggerInterface; class TalkService { @@ -32,6 +35,7 @@ class TalkService { private TalkManager $talkManager; private ICrypto $crypto; private IConfig $config; + private AttendeeMapper $attendeeMapper; public function __construct( IClientService $clientService, @@ -45,6 +49,7 @@ class TalkService { LoggerInterface $logger, TalkManager $talkManager, ICrypto $crypto, + AttendeeMapper $attendeeMapper, ) { $this->clientService = $clientService; $this->config = $config; @@ -57,6 +62,7 @@ class TalkService { $this->logger = $logger; $this->talkManager = $talkManager; $this->crypto = $crypto; + $this->attendeeMapper = $attendeeMapper; } // ── Bot password ────────────────────────────────────────────── @@ -111,9 +117,20 @@ class TalkService { * often points to a localhost:port that is unreachable from within the container. */ public function getBaseUrl(): string { + // Prefer overwritehost (hostname override) over trusted_domains + $overwriteHost = $this->config->getSystemValueString('overwritehost', ''); + if ($overwriteHost !== '') { + $proto = $this->config->getSystemValueString('overwriteproto', 'https'); + return rtrim($proto . '://' . $overwriteHost, '/'); + } + $overwritten = $this->config->getSystemValueString('overwritewebroot', ''); if ($overwritten !== '') { - return rtrim($overwritten, '/'); + // overwritewebroot may be a path — prepend current host/proto if needed + if (preg_match('#^https?://#', $overwritten)) { + return rtrim($overwritten, '/'); + } + // It's a path — fall through to trusted_domains } // Prefer a public (non-loopback, non-private) trusted domain. @@ -671,7 +688,7 @@ class TalkService { * * @param string $roomToken Talk room token * @param string $message Message text - * @param string $senderName Sender display name + * @param string $senderName Discord sender name (embedded in message text) * @param array $richObjects Rich object data (optional) * @return bool Success */ @@ -702,22 +719,32 @@ class TalkService { return false; } + // Use Chat API v1 (Talk 19 / NC33 only supports v1) $endpoint = $baseUrl . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $roomToken; - // Build message body for Chat API + // Get bot's actual display name from the room participant record. + // In Talk 14+, actorDisplayName MUST match the participant's display name + // or the message is silently dropped. + $botDisplayName = $this->getBotDisplayNameForRoom($roomToken); + + // Build message body for Chat API v4 $body = [ 'message' => $message, 'actorType' => 'users', 'actorId' => 'talk-bot', - 'actorDisplayName' => $senderName, + 'actorDisplayName' => $botDisplayName, ]; if (!empty($richObjects)) { $indexed = []; + $indexedEnd = []; foreach ($richObjects as $index => $richObj) { - $indexed['file-' . $index] = $richObj; + $key = 'file-' . $index; + $indexed[$key] = $richObj; + $indexedEnd[$key] = true; } $body['richObjects'] = $indexed; + $body['richObjectsEnd'] = $indexedEnd; } $jsonBody = json_encode($body); @@ -772,6 +799,42 @@ class TalkService { return isset($rooms[$roomToken]); } + /** + * Get the bot's display name as registered in a Talk room. + * Falls back to 'talk-bot' if not found. + */ + public function getBotDisplayNameForRoom(string $roomToken): string { + // Resolve token → room_id via direct DB query (TalkManager::getRoomForToken removed in Talk 14+) + $roomTable = $this->detectTalkTableFromCatalog('talk_rooms', 'spreed_room'); + if ($roomTable === null) { + return 'talk-bot'; + } + + try { + $stmt = $this->db->executeQuery( + 'SELECT id FROM "' . $roomTable . '" WHERE token = ?', + [$roomToken], + ); + $roomId = (int)$stmt->fetchOne(); + $stmt->closeCursor(); + + if ($roomId === 0) { + return 'talk-bot'; + } + + $attendee = $this->attendeeMapper->findByActor($roomId, Attendee::ACTOR_USERS, 'talk-bot'); + return $attendee->getDisplayName() ?: 'talk-bot'; + } catch (DoesNotExistException $e) { + return 'talk-bot'; + } catch (\Exception $e) { + $this->logger->warning('NCdiscordhook: failed to get bot display name for room ' . $roomToken, [ + 'app' => self::APP_ID, + 'error' => $e->getMessage(), + ]); + return 'talk-bot'; + } + } + // ── Image cleanup ───────────────────────────────────────────── /** @@ -845,17 +908,18 @@ class TalkService { } if (isset($config['rooms'])) { - $this->setRooms($config['rooms']); + $rooms = $config['rooms']; + } else { + $rooms = $this->getRooms(); } - // Remove explicitly disabled rooms from config + // Remove explicitly disabled rooms from the configured set if (isset($config['disabled_rooms']) && is_array($config['disabled_rooms'])) { - $rooms = $this->getRooms(); foreach ($config['disabled_rooms'] as $token) { unset($rooms[$token]); } - $this->setRooms($rooms); } + $this->setRooms($rooms); if (isset($config['auth_tokens'])) { $this->setAuthTokens($config['auth_tokens']); @@ -872,13 +936,9 @@ class TalkService { /** * Add talk-bot user as a participant in all configured rooms. * Required so the Chat API accepts requests authenticated as talk-bot. + * Uses AttendeeMapper directly to avoid injecting ParticipantService (17 deps). */ 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; @@ -891,33 +951,69 @@ class TalkService { return; } + // Get Talk DB table prefix + $sysPrefix = $this->config->getSystemValueString('dbtableprefix', ''); + $talkPrefix = $this->config->getAppValue('spreed', 'databaseprefix', $sysPrefix); + $attendeeTable = $talkPrefix . 'talk_attendee'; + + // Detect Talk rooms table name + $roomTable = $this->detectTalkTableFromCatalog('talk_rooms', 'spreed_room'); + if ($roomTable === null) { + $this->logger->warning('NCdiscordhook: Talk rooms table not found, skipping participant setup', [ + '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(); + // Resolve token → room_id via direct DB query (TalkManager::getRoomForToken removed in Talk 14+) + $stmt = $this->db->executeQuery( + 'SELECT id FROM "' . $roomTable . '" WHERE token = ?', + [$token], + ); + $roomId = (int)$stmt->fetchOne(); + $stmt->closeCursor(); + + if ($roomId === 0) { + $this->logger->warning('NCdiscordhook: room token not found in database', [ + 'app' => self::APP_ID, + 'token' => $token, + ]); + continue; } - $hasBot = false; - foreach ($actors as $actor) { - if (isset($actor['type']) && $actor['type'] === 'users' && isset($actor['actorId']) && $actor['actorId'] === 'talk-bot') { - $hasBot = true; - break; - } + + // Check if talk-bot is already an attendee + try { + $this->attendeeMapper->findByActor($roomId, Attendee::ACTOR_USERS, 'talk-bot'); + // Already exists — nothing to do + continue; + } catch (DoesNotExistException $e) { + // Not found — will create } - if (!$hasBot) { - $this->talkParticipantService->addParticipant( - $room, - $botUser, - \OCA\Talk\Participant::TYPE_USER, - \OCA\Talk\Participant::PERMISSIONS_DEFAULT, - ); + + // Create attendee record directly + $newAttendee = new Attendee(); + $newAttendee->setRoomId($roomId); + $newAttendee->setActorType(Attendee::ACTOR_USERS); + $newAttendee->setActorId('talk-bot'); + $newAttendee->setDisplayName('talk-bot'); + $newAttendee->setParticipantType(\OCA\Talk\Participant::PERMISSIONS_DEFAULT); + $newAttendee->setPermissions(\OCA\Talk\Participant::PERMISSIONS_MAX_DEFAULT); + $newAttendee->setNotificationLevel(3); // Full notification level + $newAttendee->setFavorite(false); + $newAttendee->setArchived(false); + + try { + $this->attendeeMapper->insert($newAttendee); $this->logger->info('NCdiscordhook: added talk-bot as participant in room ' . $token, [ 'app' => self::APP_ID, + 'room_id' => $roomId, + ]); + } catch (\Exception $e) { + $this->logger->warning('NCdiscordhook: failed to insert attendee for room ' . $token, [ + 'app' => self::APP_ID, + 'error' => $e->getMessage(), ]); } } catch (\Exception $e) { diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index 0b48f16..11dbe21 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -28,6 +28,7 @@ class Admin implements ISettings { 'configuredRooms' => $this->talkService->getRooms(), 'serverUrl' => $this->talkService->getBaseUrl(), 'senderName' => $this->talkService->getSenderNameDefault(), + // CLI commands for setup (user copies these to their server) 'l10n' => [ 'fetch_rooms' => $this->l10n->t('Fetch Rooms'), 'error_fetching' => $this->l10n->t('Error fetching rooms'), diff --git a/templates/adminSettings.php b/templates/adminSettings.php index cbc955c..0551301 100644 --- a/templates/adminSettings.php +++ b/templates/adminSettings.php @@ -1,6 +1,6 @@
-

Bot Configuration

-
+

Bot App Password

+