refactor(TalkService): update sharing logic and use ChatManager for system messages

Mostly working. Image actually being embedded now.
- Inject `ChatManager` dependency into `TalkService`.
- Refactor `uploadImage` to generate timestamped filenames (`talk-bot YYYY-MM-DD hh.mm.ss.microsec.ext`) and fix directory creation logic for Nextcloud 33 compatibility.
- Update file sharing logic to use `TYPE_ROOM` shares resolved by `fileId` and create a separate `TYPE_LINK` share for public previews.
- Switch message posting to use `ChatManager::addSystemMessage` for `file_shared` events.
- Send text message via HTTP API as a fallback to ensure participant records and moderation checks.
- Update logging to include debug information for rich objects and share IDs.
This commit is contained in:
kyle
2026-06-13 22:40:55 -07:00
parent 77040ae006
commit dec9b5267e
+200 -127
View File
@@ -15,6 +15,7 @@ use OCP\IURLGenerator;
use OCP\IUserManager; use OCP\IUserManager;
use OCP\Share\IManager as IShareManager; use OCP\Share\IManager as IShareManager;
use OCP\Share\IShare; use OCP\Share\IShare;
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Manager as TalkManager; use OCA\Talk\Manager as TalkManager;
use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\AttendeeMapper; use OCA\Talk\Model\AttendeeMapper;
@@ -39,6 +40,7 @@ class TalkService {
private AttendeeMapper $attendeeMapper; private AttendeeMapper $attendeeMapper;
private IShareManager $shareManager; private IShareManager $shareManager;
private ParticipantService $participantService; private ParticipantService $participantService;
private ChatManager $chatManager;
public function __construct( public function __construct(
IClientService $clientService, IClientService $clientService,
@@ -54,6 +56,7 @@ class TalkService {
AttendeeMapper $attendeeMapper, AttendeeMapper $attendeeMapper,
IShareManager $shareManager, IShareManager $shareManager,
ParticipantService $participantService, ParticipantService $participantService,
ChatManager $chatManager,
) { ) {
$this->clientService = $clientService; $this->clientService = $clientService;
$this->config = $config; $this->config = $config;
@@ -68,6 +71,7 @@ class TalkService {
$this->attendeeMapper = $attendeeMapper; $this->attendeeMapper = $attendeeMapper;
$this->shareManager = $shareManager; $this->shareManager = $shareManager;
$this->participantService = $participantService; $this->participantService = $participantService;
$this->chatManager = $chatManager;
} }
/** /**
@@ -882,8 +886,9 @@ class TalkService {
} }
/** /**
* Upload an image to the bot user's files. * Upload an image to the bot user's files under IMAGES_DIR/<roomToken>/.
* Returns the file path (e.g. nc_bot_webhooks-images/roomToken/filename.png) or null on failure. * Filename format: talk-bot YYYY-MM-DD hh.mm.ss.microsec.ext
* Returns the filecache path (e.g. nc_bot_webhooks-images/abc123/talk-bot 2026-06-13 01.35.19.123456_filename.png) or null on failure.
*/ */
public function uploadImage(string $roomToken, string $filename, string $data, string $mimeType): ?string { public function uploadImage(string $roomToken, string $filename, string $data, string $mimeType): ?string {
$bot = $this->userManager->get('talk-bot'); $bot = $this->userManager->get('talk-bot');
@@ -906,8 +911,7 @@ class TalkService {
'userFolder_type' => get_class($userFolder), 'userFolder_type' => get_class($userFolder),
]); ]);
// LazyFolder in Nextcloud 33 doesn't delegate getFolder() properly // Ensure IMAGES_DIR exists (e.g. nc_bot_webhooks-images/)
// Use get() + manual creation as a compatible fallback
$imagesDir = null; $imagesDir = null;
try { try {
$imagesDir = $userFolder->get(self::IMAGES_DIR); $imagesDir = $userFolder->get(self::IMAGES_DIR);
@@ -920,9 +924,7 @@ class TalkService {
'app' => self::APP_ID, 'app' => self::APP_ID,
'path' => self::IMAGES_DIR, 'path' => self::IMAGES_DIR,
]); ]);
// Create directory manually $userFolder->newFolder(self::IMAGES_DIR);
$parentDir = $userFolder->get('');
$parentDir->newFolder(self::IMAGES_DIR);
$imagesDir = $userFolder->get(self::IMAGES_DIR); $imagesDir = $userFolder->get(self::IMAGES_DIR);
} catch (\Error $e) { } catch (\Error $e) {
$this->logger->error('NCbotwebhooks: get threw Error on images dir: ' . $e->getMessage(), [ $this->logger->error('NCbotwebhooks: get threw Error on images dir: ' . $e->getMessage(), [
@@ -933,6 +935,7 @@ class TalkService {
throw $e; throw $e;
} }
// Ensure per-room subdirectory exists
$roomDir = null; $roomDir = null;
try { try {
$roomDir = $imagesDir->get($roomToken); $roomDir = $imagesDir->get($roomToken);
@@ -958,19 +961,22 @@ class TalkService {
throw $e; throw $e;
} }
// Avoid path traversal + use timestamped filename to prevent overwrites. // Build timestamped filename: talk-bot YYYY-MM-DD hh.mm.ss.microsec.ext
$safeFilename = basename($filename); $safeFilename = $this->sanitizeFilename($filename);
$ext = pathinfo($safeFilename, PATHINFO_EXTENSION); $ext = pathinfo($safeFilename, PATHINFO_EXTENSION);
$timestampedFilename = $this->generateTimestampFilename($safeFilename, $ext); $base = pathinfo($safeFilename, PATHINFO_FILENAME);
$timestamp = date('Y-m-d H.i.s') . '.' . sprintf('%06d', (int)(microtime(true) * 1000) % 1000000);
$relativePath = 'talk-bot ' . $timestamp . '_' . $base . '.' . $ext;
$this->logger->info('NCbotwebhooks: writing file', [ $this->logger->info('NCbotwebhooks: writing file', [
'app' => self::APP_ID, 'app' => self::APP_ID,
'original' => $safeFilename, 'original' => $safeFilename,
'path' => $timestampedFilename, 'path' => $relativePath,
]); ]);
$roomDir->newFile($timestampedFilename, $data); $roomDir->newFile($relativePath, $data);
$this->logger->info('NCbotwebhooks: file written successfully', [ $this->logger->info('NCbotwebhooks: file written successfully', [
'app' => self::APP_ID, 'app' => self::APP_ID,
'path' => self::IMAGES_DIR . '/' . $roomToken . '/' . $timestampedFilename, 'path' => self::IMAGES_DIR . '/' . $roomToken . '/' . $relativePath,
]); ]);
// Verify filecache entry was created by newFile. // Verify filecache entry was created by newFile.
@@ -984,7 +990,7 @@ class TalkService {
$stmtFc = $pdo2->prepare( $stmtFc = $pdo2->prepare(
'SELECT "fileid","storage","path","path_hash","name","mimetype","size" FROM "' . $fcTable . '" WHERE "path" = ?', 'SELECT "fileid","storage","path","path_hash","name","mimetype","size" FROM "' . $fcTable . '" WHERE "path" = ?',
); );
$stmtFc->execute([self::IMAGES_DIR . '/' . $roomToken . '/' . $timestampedFilename]); $stmtFc->execute([self::IMAGES_DIR . '/' . $roomToken . '/' . $relativePath]);
$fcRow = $stmtFc->fetchAll(\PDO::FETCH_ASSOC); $fcRow = $stmtFc->fetchAll(\PDO::FETCH_ASSOC);
$stmtFc->closeCursor(); $stmtFc->closeCursor();
$this->logger->info('NCbotwebhooks: uploadImage filecache check', [ $this->logger->info('NCbotwebhooks: uploadImage filecache check', [
@@ -996,7 +1002,7 @@ class TalkService {
$this->logger->warning('NCbotwebhooks: uploadImage filecache check failed: ' . $e->getMessage(), ['app' => self::APP_ID]); $this->logger->warning('NCbotwebhooks: uploadImage filecache check failed: ' . $e->getMessage(), ['app' => self::APP_ID]);
} }
return self::IMAGES_DIR . '/' . $roomToken . '/' . $timestampedFilename; return self::IMAGES_DIR . '/' . $roomToken . '/' . $relativePath;
} catch (\Error $e) { } catch (\Error $e) {
$this->logger->error('NCbotwebhooks: uploadImage Error: ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine(), ['app' => self::APP_ID]); $this->logger->error('NCbotwebhooks: uploadImage Error: ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine(), ['app' => self::APP_ID]);
return null; return null;
@@ -1007,20 +1013,13 @@ class TalkService {
} }
/** /**
* Generate a unique timestamp-based filename to prevent overwrites. * Sanitize a filename to prevent path traversal.
* Format: YYYY-MM-DD hh.mm.ss.microsec.ext (e.g. 2026-06-13 01.35.19.123456.jpg)
*/ */
private function generateTimestampFilename(string $originalFilename, string $extension): string { private function sanitizeFilename(string $filename): string {
$timestamp = date('Y-m-d_H-i-s') . '.' . sprintf('%06d', (int)(microtime(true) * 1000) % 1000000); return basename($filename);
$base = pathinfo($originalFilename, PATHINFO_FILENAME);
// Sanitize base name — only keep safe chars
$base = preg_replace('/[^A-Za-z0-9_\-\x80-\xFF]/', '_', $base);
if ($extension !== '') {
return $base . '_' . $timestamp . '.' . $extension;
}
return $base . '_' . $timestamp;
} }
/** /**
* Build rich object data for a Talk message from an uploaded file path. * 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 public link share so Talk can resolve the rich object.
@@ -1037,10 +1036,8 @@ class TalkService {
$safeFilename = basename($filePath); $safeFilename = basename($filePath);
$this->logger->info('NCbotwebhooks: richObject step=basename', ['app' => self::APP_ID]); $this->logger->info('NCbotwebhooks: richObject step=basename', ['app' => self::APP_ID]);
// filecache 'path' stores relative paths WITHOUT leading slash. // filePath is the full filecache path returned by uploadImage (e.g. nc_bot_webhooks-images/abc123/talk-bot 2026-06-13 01.35.19.123456_filename.png).
// Using the wrong format breaks path_hash lookups and inserts $fileCachePath = $filePath;
// duplicate entries with wrong parent references.
$fileCachePath = self::IMAGES_DIR . '/' . $roomToken . '/' . $safeFilename;
$this->logger->info('NCbotwebhooks: richObject step=buildPath', ['app' => self::APP_ID, 'path' => $fileCachePath]); $this->logger->info('NCbotwebhooks: richObject step=buildPath', ['app' => self::APP_ID, 'path' => $fileCachePath]);
// Use PDO directly to avoid any DBConnection method that could trigger lazy init. // Use PDO directly to avoid any DBConnection method that could trigger lazy init.
@@ -1202,32 +1199,31 @@ class TalkService {
$this->logger->warning('NCbotwebhooks: richObject step=nodeResolveFail', ['app' => self::APP_ID, 'error' => $e->getMessage()]); $this->logger->warning('NCbotwebhooks: richObject step=nodeResolveFail', ['app' => self::APP_ID, 'error' => $e->getMessage()]);
} }
// Create a public link share using the IManager API. // Create a room share so Talk's SystemMessage parser can resolve the
// createShare() generates its own token for TYPE_LINK shares. // file via getFileFromShare(). TYPE_ROOM shares are stored in the
// share_external table (as external shares) and resolved by fileId
// lookup in the original owner's folder — matching how
// RecordingService::shareToChat() works.
$share = $this->shareManager->newShare(); $share = $this->shareManager->newShare();
if ($node !== null) { if ($node !== null) {
$share->setNode($node) $share->setNode($node)
->setShareType(IShare::TYPE_LINK) ->setShareType(IShare::TYPE_ROOM)
->setSharedBy($bot->getUID()) ->setSharedBy($bot->getUID())
->setShareOwner($bot->getUID()) ->setSharedWith($roomToken)
->setPermissions(\OCP\Constants::PERMISSION_READ); ->setPermissions(\OCP\Constants::PERMISSION_READ);
} else { } else {
$share->setNodeId($fileId) $share->setNodeId($fileId)
->setShareType(IShare::TYPE_LINK) ->setShareType(IShare::TYPE_ROOM)
->setSharedBy($bot->getUID()) ->setSharedBy($bot->getUID())
->setShareOwner($bot->getUID()) ->setSharedWith($roomToken)
->setPermissions(\OCP\Constants::PERMISSION_READ); ->setPermissions(\OCP\Constants::PERMISSION_READ);
} }
$shareToken = null;
$shareId = null; $shareId = null;
try { try {
$created = $this->shareManager->createShare($share); $created = $this->shareManager->createShare($share);
$shareId = (int)$created->getId(); $shareId = (int)$created->getId();
// createShare() generates its own token for TYPE_LINK shares, $this->logger->info('NCbotwebhooks: richObject step=shareCreated', ['app' => self::APP_ID, 'shareId' => $shareId]);
// overriding the one we set via setToken(). Use the generated token.
$shareToken = $created->getToken();
$this->logger->info('NCbotwebhooks: richObject step=shareCreated', ['app' => self::APP_ID, 'shareId' => $shareId, 'token' => $shareToken]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->logger->warning('NCbotwebhooks: richObject step=shareCreateFail', ['app' => self::APP_ID, 'error' => $e->getMessage()]); $this->logger->warning('NCbotwebhooks: richObject step=shareCreateFail', ['app' => self::APP_ID, 'error' => $e->getMessage()]);
} }
@@ -1260,47 +1256,47 @@ class TalkService {
// Final fallback: 0 // Final fallback: 0
} }
// Clear the password so the share URL works for inline image embedding. // Create a public link share so Talk's linkifier can scrape the
// Server default policy applies a password to all new link shares, but // /s/{token} page for og:image meta tags and render rich preview
// browsers cannot authenticate when Talk loads the image inline — they // cards. This compensates for Talk 23.0.6's lack of a fallback
// hit the password entry page instead. Clearing the password makes the // when the file_shared system message can't resolve the file in
// /s/{token} URL publicly fetchable while keeping the clickable link // the strict path (chat history for regular users).
// as a verification URL the user can open in a browser. $linkShareId = null;
$passwordCleared = false; $linkToken = null;
if ($shareId !== null) { $linkPublicUrl = '';
try { try {
$rows = $this->db->executeUpdate( $linkShare = $this->shareManager->newShare();
'UPDATE "' . $shareTable . '" SET "password" = NULL WHERE "id" = ? AND "share_type" = ' . IShare::TYPE_LINK, if ($node !== null) {
[$shareId], $linkShare->setNode($node)
); ->setShareType(IShare::TYPE_LINK)
if ($rows > 0) { ->setSharedBy($bot->getUID())
$passwordCleared = true; ->setPermissions(\OCP\Constants::PERMISSION_READ);
$this->logger->info('NCbotwebhooks: richObject step=passwordCleared', ['app' => self::APP_ID, 'shareId' => $shareId]); } else {
} else { $linkShare->setNodeId($fileId)
$this->logger->warning('NCbotwebhooks: richObject step=passwordClearedNoMatch', ['app' => self::APP_ID, 'shareId' => $shareId]); ->setShareType(IShare::TYPE_LINK)
} ->setSharedBy($bot->getUID())
} catch (\Throwable $e) { ->setPermissions(\OCP\Constants::PERMISSION_READ);
$this->logger->warning('NCbotwebhooks: richObject step=passwordClearFail', ['app' => self::APP_ID, 'error' => $e->getMessage()]);
} }
$createdLink = $this->shareManager->createShare($linkShare);
$linkToken = $createdLink->getToken();
$linkShareId = (int)$createdLink->getId();
$linkPublicUrl = $this->urlGen->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', ['token' => $linkToken]);
$this->logger->info('NCbotwebhooks: richObject step=linkShareCreated', ['app' => self::APP_ID, 'linkShareId' => $linkShareId, 'token' => $linkToken]);
} catch (\Throwable $e) {
$this->logger->warning('NCbotwebhooks: richObject step=linkShareFail', ['app' => self::APP_ID, 'error' => $e->getMessage()]);
} }
// Build the public share URL and download URL. $fullPublicUrl = $linkPublicUrl;
// Always construct them regardless of share INSERT success so $fullDownloadUrl = '';
// postToRoom can use them for user verification and inline embedding.
$fullPublicUrl = $shareToken !== null
? $this->urlGenerator->getBaseUrl() . '/s/' . $shareToken
: '';
$fullDownloadUrl = $shareToken !== null
? rtrim($this->urlGenerator->getBaseUrl(), '/') . '/s/' . $shareToken . '/download/' . rawurlencode($safeFilename)
: '';
$this->logger->info('NCbotwebhooks: richObject step=publicShareUrl', ['app' => self::APP_ID, 'url' => $fullPublicUrl, 'downloadUrl' => $fullDownloadUrl]);
return [ return [
'fileId' => $fileId, 'fileId' => $fileId,
'mimeType' => $mimeType, 'mimeType' => $mimeType,
'publicUrl' => $fullPublicUrl, 'publicUrl' => $fullPublicUrl,
'downloadUrl' => $fullDownloadUrl, 'downloadUrl' => $fullDownloadUrl,
'shareToken' => $shareToken, 'shareToken' => $linkToken,
'shareId' => $shareId ?? null, 'shareId' => $shareId ?? null,
'filename' => $safeFilename, 'filename' => $safeFilename,
'fileCachePath' => $fileCachePath, 'fileCachePath' => $fileCachePath,
@@ -1368,18 +1364,10 @@ class TalkService {
// Basic auth: base64('talk-bot:' . bot_password) // Basic auth: base64('talk-bot:' . bot_password)
$credentials = base64_encode('talk-bot:' . $botPassword); $credentials = base64_encode('talk-bot:' . $botPassword);
// Append public share URLs to the message text so Talk's linkifier can // Append public share URLs to the message text as a fallback so Talk's
// scrape OG data and render rich link preview cards with image thumbnails. // linkifier can scrape OG data and render rich link preview cards with
// // image thumbnails, in case the file_shared system message fails to
// We intentionally do NOT use file_shared system messages here. The // produce an inline image preview.
// SystemMessage parser resolves fileIds by looking in the viewer's own
// storage mount points — it never finds files uploaded by the bot user.
//
// Instead, we send a normal chat message with the public share URLs
// (https://nextcloud.example.com/s/{token}) in the text. Talk's
// linkifier extracts these URLs, fetches the public share page, and
// scrapes the og:image meta tag to produce a rich link preview card
// with the image thumbnail. This works for both logged-in and guest users.
$shareUrls = []; $shareUrls = [];
if (!empty($richObjects)) { if (!empty($richObjects)) {
foreach ($richObjects as $richObject) { foreach ($richObjects as $richObject) {
@@ -1399,60 +1387,145 @@ class TalkService {
$message .= "\n\n" . implode("\n", $shareUrls); $message .= "\n\n" . implode("\n", $shareUrls);
} }
// Send the text message via the chat API. // Build a file_shared system message with the share ID and metaData
// POST /api/v1/chat/{token}?message={text}&actorDisplayName={name} // so Talk's SystemMessage parser can resolve the file via the room
// Actor type and ID come from the basic auth credentials (talk-bot user). // share. RecordingService::shareToChat() uses the same pattern.
// The message text includes public share URLs appended above so Talk's $richObject = $richObjects[0] ?? null;
// linkifier can scrape og:image and render rich link preview cards. $shareId = (string)($richObject['shareId'] ?? '');
$textEndpoint = $baseUrl . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $roomToken . '?message=' . urlencode($message) . ($botDisplayName ? '&actorDisplayName=' . urlencode($botDisplayName) : ''); $mimeType = $richObject['mimeType'] ?? 'application/octet-stream';
$messageType = match (true) {
$this->logger->info('NCbotwebhooks: sending text message to Talk', [ str_starts_with($mimeType, 'image/') => 'comment',
'app' => self::APP_ID, str_starts_with($mimeType, 'video/') => 'comment',
'room_token' => $roomToken, str_starts_with($mimeType, 'audio/') => 'comment',
'message' => $message, default => 'comment',
};
$systemMessage = json_encode([
'message' => 'file_shared',
'parameters' => [
'share' => $shareId !== '' ? $shareId : null,
'metaData' => [
'mimeType' => $mimeType,
'messageType' => $messageType,
],
],
]); ]);
// Get the Room object — required for ChatManager::addSystemMessage().
// We use getRoomByToken() which works without authentication.
$room = null;
try { try {
$client = $this->clientService->newClient(); $room = $this->talkManager->getRoomByToken($roomToken);
$response = $client->post($textEndpoint, [ } catch (\Exception $e) {
'headers' => [ $this->logger->error('NCbotwebhooks: failed to get room for token ' . $roomToken, [
'OCS-Expect-Formatted' => 'json',
'OCS-APIRequest' => '1',
'Authorization' => 'Basic ' . $credentials,
],
'nextcloud' => [
'allow_local_address' => true,
],
]);
$statusCode = $response->getStatusCode();
$responseBody = $response->getBody()->getContents();
$this->logger->info('NCbotwebhooks: chat API response', [
'app' => self::APP_ID, 'app' => self::APP_ID,
'room_token' => $roomToken, 'error' => $e->getMessage(),
'status' => $statusCode,
'body' => $responseBody,
]);
if ($statusCode >= 200 && $statusCode < 300) {
$this->logger->info('NCbotwebhooks: message posted to room ' . $roomToken, [
'app' => self::APP_ID,
]);
return true;
}
$this->logger->error('NCbotwebhooks: chat API returned ' . $statusCode, [
'app' => self::APP_ID,
'room_token' => $roomToken,
'body' => $responseBody,
]); ]);
return false; return false;
}
$this->logger->info('NCbotwebhooks: sending file_shared message to Talk', [
'app' => self::APP_ID,
'room_token' => $roomToken,
'system_message' => $systemMessage,
'message_text' => $message,
'debug' => [
'richObject_keys' => array_keys($richObject ?? []),
'richObject_fileId' => $richObject['fileId'] ?? null,
'richObject_shareId' => $richObject['shareId'] ?? null,
'richObject_fileCachePath' => $richObject['fileCachePath'] ?? null,
'richObject_downloadUrl' => $richObject['downloadUrl'] ?? null,
'richObject_publicUrl' => $richObject['publicUrl'] ?? null,
'richObject_shareToken' => $richObject['shareToken'] ?? null,
'richObject_filename' => $richObject['filename'] ?? null,
'richObject_mimeType' => $richObject['mimeType'] ?? null,
'shareId_used' => $shareId,
],
]);
// Use ChatManager::addSystemMessage() directly (PHP API) so the
// system message is created with the correct verb ('object_shared')
// and the attachment entry is created. The HTTP sendMessage() endpoint
// only creates regular 'comment' messages — it cannot create system
// messages.
try {
$bot = $this->userManager->get('talk-bot');
$actorType = Attendee::ACTOR_USERS;
$actorId = 'talk-bot';
$this->chatManager->addSystemMessage(
$room,
null, // participant — not needed for webhook bot; mention perms check uses null-safe operator
$actorType,
$actorId,
$systemMessage,
new \DateTime('now', new \DateTimeZone('UTC')),
true, // sendNotifications
);
$this->logger->info('NCbotwebhooks: system message posted to room ' . $roomToken, [
'app' => self::APP_ID,
]);
// Verify the system message was created with correct fileId/shareId
$this->logger->info('NCbotwebhooks: system message verification', [
'app' => self::APP_ID,
'fileId' => $richObject['fileId'] ?? null,
'shareId' => $shareId,
'fileCachePath' => $richObject['fileCachePath'] ?? null,
'botUserExists' => $bot !== null,
'botUserId' => $bot ? $bot->getUID() : 'null',
]);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->error('NCbotwebhooks: chat API request failed: ' . $e->getMessage(), [ $this->logger->error('NCbotwebhooks: addSystemMessage failed: ' . $e->getMessage(), [
'app' => self::APP_ID, 'app' => self::APP_ID,
'room_token' => $roomToken, 'room_token' => $roomToken,
]); ]);
return false; return false;
} }
// Send the text message (with appended share URLs) via HTTP API so
// Talk's @RequireParticipant middleware creates/finds the bot
// participant record, and the message gets proper moderation checks.
// This provides the human-readable text that the system message
// does not include.
if (trim($message) !== '') {
$textEndpoint = $baseUrl . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $roomToken
. '?message=' . urlencode($message)
. ($botDisplayName ? '&actorDisplayName=' . urlencode($botDisplayName) : '');
try {
$client = $this->clientService->newClient();
$textResponse = $client->post($textEndpoint, [
'headers' => [
'OCS-Expect-Formatted' => 'json',
'OCS-APIRequest' => '1',
'Content-Type' => 'application/json',
'Authorization' => 'Basic ' . $credentials,
],
'body' => '{}',
'nextcloud' => [
'allow_local_address' => true,
],
]);
$textStatus = $textResponse->getStatusCode();
if ($textStatus >= 200 && $textStatus < 300) {
$this->logger->info('NCbotwebhooks: text message posted to room ' . $roomToken, [
'app' => self::APP_ID,
]);
} else {
$this->logger->warning('NCbotwebhooks: text message POST returned ' . $textStatus, [
'app' => self::APP_ID,
'room_token' => $roomToken,
]);
}
} catch (\Exception $e) {
$this->logger->warning('NCbotwebhooks: text message POST failed: ' . $e->getMessage(), [
'app' => self::APP_ID,
'room_token' => $roomToken,
]);
}
}
return true;
} }
/** /**