Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/components/Editor/AddTalkModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -124,6 +124,11 @@ export default {
type: Object,
required: true,
},

delegatorUserId: {
type: String,
default: null,
},
},

setup() {
Expand Down Expand Up @@ -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')
Expand Down
31 changes: 16 additions & 15 deletions src/mixins/EditorMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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
*/
Expand All @@ -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,
})
}
},
Expand Down
53 changes: 45 additions & 8 deletions src/services/talkService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
try {
await transceivePost<TalkRoomAddParticipantRequest, { type: number }>(`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<undefined, TalkRoomParticipant[]>(`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
*
Expand Down Expand Up @@ -348,14 +384,14 @@ async function transceiveGet<TRequest extends object | undefined, TResponse>(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<TResponse>
// 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') {
Expand All @@ -374,7 +410,7 @@ async function transceiveGet<TRequest extends object | undefined, TResponse>(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}`)
}
}
Expand Down Expand Up @@ -408,14 +444,14 @@ async function transceivePost<TRequest extends object, TResponse>(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<TResponse>
// 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') {
Expand All @@ -434,7 +470,7 @@ async function transceivePost<TRequest extends object, TResponse>(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}`)
}
}
Expand All @@ -448,6 +484,7 @@ export default {
listRooms,
createRoom,
createRoomFromProposal,
addParticipantAsModerator,
updateRoomParticipantsFromEvent,
generateRoomUrl,
containsRoomUrl,
Expand Down
1 change: 1 addition & 0 deletions src/views/EditFull.vue
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@
<AddTalkModal
v-if="isTalkModalOpen"
:calendarObjectInstance="calendarObjectInstance"
:delegatorUserId="delegatorUserId"
@close="isTalkModalOpen = false"
@updateLocation="updateLocation"
@updateDescription="updateDescription" />
Expand Down
1 change: 1 addition & 0 deletions src/views/EditSimple.vue
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@
<AddTalkModal
v-if="isTalkModalOpen"
:calendarObjectInstance="calendarObjectInstance"
:delegatorUserId="delegatorUserId"
@close="isTalkModalOpen = false"
@updateLocation="updateLocation"
@updateDescription="updateDescription" />
Expand Down
Loading