diff --git a/src/components/Editor/AddTalkModal.vue b/src/components/Editor/AddTalkModal.vue index eddab3d210..5503b1cb09 100644 --- a/src/components/Editor/AddTalkModal.vue +++ b/src/components/Editor/AddTalkModal.vue @@ -93,7 +93,7 @@ import md5 from 'md5' import { mapStores } from 'pinia' import IconAdd from 'vue-material-design-icons/Plus.vue' import useCalendarObjectInstanceStore from '../../store/calendarObjectInstance.js' -import { createRoom, generateRoomUrl, listRooms } from '@/services/talkService' +import { addParticipantAsModerator, createRoom, generateRoomUrl, listRooms } from '@/services/talkService' // Ref https://github.com/nextcloud/spreed/blob/main/docs/constants.md const CONVERSATION_TYPE_GROUP = 2 @@ -124,6 +124,11 @@ export default { type: Object, required: true, }, + + delegatorUserId: { + type: String, + default: null, + }, }, setup() { @@ -239,6 +244,14 @@ export default { throw new Error('No token returned from createRoom') } + if (this.delegatorUserId) { + try { + await addParticipantAsModerator(room.token, this.delegatorUserId) + } catch (error) { + console.error('Failed to add delegator as moderator:', error) + } + } + const url = generateRoomUrl(room) if (!url) { throw new Error('Failed to generate URL from token') diff --git a/src/mixins/EditorMixin.js b/src/mixins/EditorMixin.js index b9a72caf15..287bb24d61 100644 --- a/src/mixins/EditorMixin.js +++ b/src/mixins/EditorMixin.js @@ -282,6 +282,17 @@ export default { selectedCalendar() { return this.calendarsStore.getCalendarById(this.calendarId) }, + /** + * Returns the userId of the delegator when the selected calendar is delegated, or null otherwise + * + * @return {string|null} + */ + delegatorUserId() { + if (!this.selectedCalendar?.isDelegated || !this.selectedCalendar.delegatorUrl) { + return null + } + return this.principalsStore.getPrincipalByUrl(this.selectedCalendar.delegatorUrl)?.userId ?? null + }, /** * Returns whether or not the user is allowed to delete this event * @@ -522,8 +533,8 @@ export default { }, /** - * When creating an event on a delegated calendar, automatically adds the - * delegator as an attendee so they are aware of and invited to the event. + * When creating an event on a delegated calendar, sets the delegator as the organizer. + * The assistant (current user) should not be listed as organizer or attendee. * * @param {object|null} calendar The calendar object to check */ @@ -541,21 +552,11 @@ export default { return } - const alreadyAttendee = this.calendarObjectInstance.attendees.some((attendee) => removeMailtoPrefix(attendee.uri) === delegatorPrincipal.emailAddress) - - if (!alreadyAttendee) { - this.calendarObjectInstanceStore.addAttendee({ + if (!this.calendarObjectInstance.organizer) { + this.calendarObjectInstanceStore.setOrganizer({ calendarObjectInstance: this.calendarObjectInstance, commonName: delegatorPrincipal.displayname, - uri: delegatorPrincipal.emailAddress, - calendarUserType: 'INDIVIDUAL', - participationStatus: 'NEEDS-ACTION', - role: 'REQ-PARTICIPANT', - rsvp: true, - language: null, - timezoneId: null, - organizer: this.principalsStore.getCurrentUserPrincipal, - member: null, + email: delegatorPrincipal.emailAddress, }) } }, diff --git a/src/services/talkService.ts b/src/services/talkService.ts index 4ca68e53d3..3acfc40037 100644 --- a/src/services/talkService.ts +++ b/src/services/talkService.ts @@ -178,6 +178,42 @@ export async function createRoomFromProposal(proposal: ProposalInterface): Promi return createRoom(payload) } +/** + * Add a user to a Talk room and promote them to moderator. + * Fetches the participant list to resolve the attendeeId required by the moderator endpoint. + * + * @param token The Talk room token + * @param userId The Nextcloud user ID to add and promote + * + * @throws Error if adding the participant or promoting fails + */ +export async function addParticipantAsModerator(token: string, userId: string): Promise { + try { + await transceivePost(`room/${token}/participants`, { + newParticipant: userId, + source: 'users', + }) + } catch (error) { + // Ignore if the user is already a participant + logger.debug('Participant may already exist in room', { userId, error }) + } + + const participants = await transceiveGet(`room/${token}/participants`, undefined) + const participant = participants.find((p) => p.actorId === userId && p.actorType === 'users') + if (!participant) { + throw new Error(`Could not find participant ${userId} in room ${token} after adding`) + } + + try { + await transceivePost<{ attendeeId: number }, object>(`room/${token}/moderators`, { + attendeeId: participant.attendeeId, + }) + } catch (error) { + console.error('Failed to promote user to moderator:', error) + throw new Error('Failed to promote user to moderator', { cause: error as Error }) + } +} + /** * Updates Talk room/conversation participants based on calendar event attendees * @@ -348,14 +384,14 @@ async function transceiveGet(pat // Check if response has OCS envelope structure if (response.data && typeof response.data === 'object' && 'ocs' in response.data) { const ocsResponse = response.data as OcsEnvelope - // Response sanity checks - if (!ocsResponse.ocs || !ocsResponse.ocs.meta || !ocsResponse.ocs.data || !ocsResponse.ocs.meta.status) { + // Response sanity checks (data may legitimately be null for endpoints that return no payload) + if (!ocsResponse.ocs || !ocsResponse.ocs.meta || !ocsResponse.ocs.meta.status) { throw new Error('Talk service error: malformed response') } if (ocsResponse.ocs.meta.status !== 'ok' && ocsResponse.ocs.meta.message) { throw new Error(`Talk service error: ${ocsResponse.ocs.meta.message}`) } - if (ocsResponse.ocs.meta.status !== 'ok' && typeof ocsResponse.ocs.data === 'object' && 'error' in ocsResponse.ocs.data) { + if (ocsResponse.ocs.meta.status !== 'ok' && ocsResponse.ocs.data !== null && typeof ocsResponse.ocs.data === 'object' && 'error' in ocsResponse.ocs.data) { throw new Error(`Talk service error: ${ocsResponse.ocs.data.error}`) } if (ocsResponse.ocs.meta.status !== 'ok') { @@ -374,7 +410,7 @@ async function transceiveGet(pat if (ocsError.ocs.meta.message) { throw new Error(`Talk service error: ${ocsError.ocs.meta.message}`) } - if (typeof ocsError.ocs.data === 'object' && 'error' in ocsError.ocs.data) { + if (ocsError.ocs.data !== null && typeof ocsError.ocs.data === 'object' && 'error' in ocsError.ocs.data) { throw new Error(`Talk service error: ${ocsError.ocs.data.error}`) } } @@ -408,14 +444,14 @@ async function transceivePost(path: string, // Check if response has OCS envelope structure if (response.data && typeof response.data === 'object' && 'ocs' in response.data) { const ocsResponse = response.data as OcsEnvelope - // Response sanity checks - if (!ocsResponse.ocs || !ocsResponse.ocs.meta || !ocsResponse.ocs.data || !ocsResponse.ocs.meta.status) { + // Response sanity checks (data may legitimately be null for endpoints that return no payload) + if (!ocsResponse.ocs || !ocsResponse.ocs.meta || !ocsResponse.ocs.meta.status) { throw new Error('Talk service error: malformed response') } if (ocsResponse.ocs.meta.status !== 'ok' && ocsResponse.ocs.meta.message) { throw new Error(`Talk service error: ${ocsResponse.ocs.meta.message}`) } - if (ocsResponse.ocs.meta.status !== 'ok' && typeof ocsResponse.ocs.data === 'object' && 'error' in ocsResponse.ocs.data) { + if (ocsResponse.ocs.meta.status !== 'ok' && ocsResponse.ocs.data !== null && typeof ocsResponse.ocs.data === 'object' && 'error' in ocsResponse.ocs.data) { throw new Error(`Talk service error: ${ocsResponse.ocs.data.error}`) } if (ocsResponse.ocs.meta.status !== 'ok') { @@ -434,7 +470,7 @@ async function transceivePost(path: string, if (ocsError.ocs.meta.message) { throw new Error(`Talk service error: ${ocsError.ocs.meta.message}`) } - if (typeof ocsError.ocs.data === 'object' && 'error' in ocsError.ocs.data) { + if (ocsError.ocs.data !== null && typeof ocsError.ocs.data === 'object' && 'error' in ocsError.ocs.data) { throw new Error(`Talk service error: ${ocsError.ocs.data.error}`) } } @@ -448,6 +484,7 @@ export default { listRooms, createRoom, createRoomFromProposal, + addParticipantAsModerator, updateRoomParticipantsFromEvent, generateRoomUrl, containsRoomUrl, diff --git a/src/views/EditFull.vue b/src/views/EditFull.vue index 2a08cc0e91..76ebcf19a0 100644 --- a/src/views/EditFull.vue +++ b/src/views/EditFull.vue @@ -208,6 +208,7 @@ diff --git a/src/views/EditSimple.vue b/src/views/EditSimple.vue index 61102d50f2..13c65603d4 100644 --- a/src/views/EditSimple.vue +++ b/src/views/EditSimple.vue @@ -190,6 +190,7 @@