diff --git a/lib/Controller/CallController.php b/lib/Controller/CallController.php index d1530793082..854a8e5bbef 100644 --- a/lib/Controller/CallController.php +++ b/lib/Controller/CallController.php @@ -38,6 +38,7 @@ use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\Response; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; use OCP\IRequest; use OCP\IUserManager; @@ -55,6 +56,7 @@ public function __construct( private RoomService $roomService, private IUserManager $userManager, private ITimeFactory $timeFactory, + private IConfig $serverConfig, private Config $talkConfig, protected Authenticator $federationAuthenticator, private SIPDialOutService $dialOutService, @@ -127,6 +129,7 @@ public function getPeersForCall(): DataResponse { */ #[PublicPage] #[RequireModeratorParticipant] + #[Http\Attribute\NoCSRFRequired] public function downloadParticipantsForCall(string $format = 'csv'): DataDownloadResponse|Response { $timeout = $this->timeFactory->getTime() - Session::SESSION_TIMEOUT; $participants = $this->participantService->getParticipantsInCall($this->room, $timeout); @@ -158,7 +161,26 @@ public function downloadParticipantsForCall(string $format = 'csv'): DataDownloa fseek($output, 0); - return new DataDownloadResponse(stream_get_contents($output), 'participants.csv', 'text/csv'); + // Clean the room name + $cleanedRoomName = preg_replace('/[\/\\:*?"<>|\- ]+/', '-', $this->room->getName()); + // Limit to a reasonable length + $cleanedRoomName = substr($cleanedRoomName, 0, 100); + + $timezone = 'UTC'; + if ($this->participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { + $timezone = $this->serverConfig->getUserValue($this->participant->getAttendee()->getActorId(), 'core', 'timezone', 'UTC'); + } + + try { + $dateTimeZone = new \DateTimeZone($timezone); + } catch (\DateInvalidTimeZoneException) { + $dateTimeZone = null; + } + + $date = $this->timeFactory->getDateTime('now', $dateTimeZone)->format('Y-m-d'); + $fileName = $cleanedRoomName . ' ' . $date . '.csv'; + + return new DataDownloadResponse(stream_get_contents($output), $fileName, 'text/csv'); } /** diff --git a/src/components/TopBar/TopBarMenu.vue b/src/components/TopBar/TopBarMenu.vue index 18791370182..a5000e480b4 100644 --- a/src/components/TopBar/TopBarMenu.vue +++ b/src/components/TopBar/TopBarMenu.vue @@ -139,6 +139,14 @@ {{ t('spreed', 'Conversation settings') }} + + + {{ t('spreed', 'Download attendance list') }} + @@ -147,6 +155,7 @@ import Cog from 'vue-material-design-icons/Cog.vue' import DotsCircle from 'vue-material-design-icons/DotsCircle.vue' import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue' +import IconDownload from 'vue-material-design-icons/Download.vue' import File from 'vue-material-design-icons/File.vue' import Fullscreen from 'vue-material-design-icons/Fullscreen.vue' import FullscreenExit from 'vue-material-design-icons/FullscreenExit.vue' @@ -161,6 +170,7 @@ import GridView from 'vue-material-design-icons/ViewGrid.vue' import { showWarning } from '@nextcloud/dialogs' import { emit } from '@nextcloud/event-bus' import { t } from '@nextcloud/l10n' +import { generateOcsUrl } from '@nextcloud/router' import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js' @@ -208,6 +218,7 @@ export default { FullscreenExit, GridView, HandBackLeft, + IconDownload, MicrophoneOff, PromotedView, RecordCircle, @@ -369,6 +380,10 @@ export default { showCallLayoutSwitch() { return !this.callViewStore.isEmptyCallView }, + + downloadCallParticipantsLink() { + return generateOcsUrl('apps/spreed/api/v4/call/{token}/download', { token: this.token }) + }, }, watch: {