diff --git a/docs/docs/cmd/teams/meeting/meeting-add.mdx b/docs/docs/cmd/teams/meeting/meeting-add.mdx new file mode 100644 index 00000000000..b42a5671038 --- /dev/null +++ b/docs/docs/cmd/teams/meeting/meeting-add.mdx @@ -0,0 +1,277 @@ +import Global from '/docs/cmd/_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# teams meeting add + +Create a new online meeting + +## Usage + +```sh +m365 teams meeting add [options] +``` + +## Options + +```md definition-list +`-s, --startTime [startTime]` +: The start time of the meeting. If not specified, the startTime will be set to the current time. + +`-e, --endTime [endTime]` +: The end time of the meeting. If not specified, the endTime will be set to one hour after the startTime. + +`--subject [subject]` +: The subject of the meeting. + +`-p, --participantUserNames [participantUserNames]` +: A comma-separated list of participant UPNs. + +`--organizerEmail [organizerEmail]` +: The organizer's email address. + +`-r, --recordAutomatically` +: When using this flag, the meeting will be recorded automatically. +``` + + + +## Remarks + +To create an online meeting for a specific organizer, use the **--organizerEmail** parameter along with app-only permissions. The registered app should have the **OnlineMeetings.ReadWrite.All** permissions, and a special policy should be assigned to the user specified in the **organizerEmail** option. You can find more information on how to assign this policy to a user [here](https://learn.microsoft.com/en-us/graph/cloud-communication-online-meeting-application-access-policy). + +## Examples + +Create a new online meeting for the currently logged-in user, starting immediately and ending after one hour. + +```sh +m365 teams meeting add +``` + +Create a new online meeting for the currently logged-in user, with a specified start date and a duration of one hour. + +```sh +m365 teams meeting add --startTime "2023-09-21T13:30:00Z" +``` + +Create a new online meeting for the currently logged-in user, with specified end date the current date as the start date. + +```sh +m365 teams meeting add --endTime "2023-09-21T23:55:00Z" +``` + +Create a new online meeting for the currently logged-in user, with specified start and end dates. + +```sh +m365 teams meeting add --startTime "2023-09-21T13:30:00Z" --endTime "2023-09-21T23:55:00Z" +``` + +Create a new online meeting for the currently logged-in user, with a specified subject. + +```sh +m365 teams meeting add --startTime "2023-09-21T13:30:00Z" --endTime "2023-09-21T23:55:00Z" --subject "Test Subject" +``` + +Create a new online meeting for the currently logged-in user, with a specified subject and a list of participantUserNames. + +```sh +m365 teams meeting add --subject "Test Subject" --participantUserNames "john.doe@contoso.com,olga.manager@contoso.com" +``` + +Create a new online meeting for the currently logged-in user, with a specified subject, a list of participantUserNames, and automatic meeting recording. + +```sh +m365 teams meeting add --subject "Test Subject" --participantUserNames "john.doe@contoso.com,olga.manager@contoso.com" --recordAutomatically +``` + +Create a new online meeting for a selected organizer, with a specified subject, a list of participantUserNames, and automatic meeting recording. This option is available for app-only permissions. + +```sh +m365 teams meeting add --organizerEmail "john.doe@contoso.com" --subject "Test Subject" --participantUserNames "olga.manager@contoso.com" --recordAutomatically +``` + +## Response + + + + + ```json + { + "id": "abc", + "creationDateTime": "2023-07-25T19:29:32.033109Z", + "startDateTime": "2023-07-17T03:00:00Z", + "endDateTime": "2023-07-17T04:00:00Z", + "joinUrl": "https://teams.microsoft.com/l/meetup-join/abc", + "joinWebUrl": "https://teams.microsoft.com/l/meetup-join/abc", + "meetingCode": "12345", + "subject": "Subject", + "isBroadcast": false, + "autoAdmittedUsers": "unknownFutureValue", + "outerMeetingAutoAdmittedUsers": null, + "isEntryExitAnnounced": false, + "allowedPresenters": "everyone", + "allowMeetingChat": "enabled", + "shareMeetingChatHistoryDefault": "none", + "allowTeamworkReactions": true, + "allowAttendeeToEnableMic": true, + "allowAttendeeToEnableCamera": true, + "recordAutomatically": false, + "anonymizeIdentityForRoles": [], + "capabilities": [], + "videoTeleconferenceId": null, + "externalId": null, + "iCalUid": null, + "meetingType": null, + "allowParticipantsToChangeName": false, + "allowRecording": true, + "allowTranscription": true, + "meetingMigrationMode": null, + "broadcastSettings": null, + "audioConferencing": null, + "meetingInfo": null, + "participants": { + "organizer": { + "upn": "john.doe@contoso.com", + "role": "presenter", + "identity": { + "application": null, + "device": null, + "user": { + "id": "12345678-1234-1234-1234-12345678", + "displayName": null, + "tenantId": "12345678-1234-1234-1234-12345678", + "identityProvider": "AAD" + } + } + }, + "attendees": [ + { + "upn": "adele.vance@contoso.com", + "role": "attendee", + "identity": { + "application": null, + "device": null, + "user": { + "id": "12345678-1234-1234-1234-12345678", + "displayName": null, + "tenantId": "12345678-1234-1234-1234-12345678, + "identityProvider": "AAD" + } + } + } + ] + }, + "lobbyBypassSettings": { + "scope": "unknownFutureValue", + "isDialInBypassEnabled": false + }, + "joinMeetingIdSettings": { + "isPasscodeRequired": true, + "joinMeetingId": "12345", + "passcode": "123456" + }, + "chatInfo": { + "threadId": "abc", + "messageId": "0", + "replyChainMessageId": null + }, + "joinInformation": { + "content": "textContent", + "contentType": "html" + }, + "watermarkProtection": { + "isEnabledForContentSharing": false, + "isEnabledForVideo": false + } + } + ``` + + + + + ```text + allowAttendeeToEnableCamera : true + allowAttendeeToEnableMic : true + allowMeetingChat : enabled + allowParticipantsToChangeName : false + allowRecording : true + allowTeamworkReactions : true + allowTranscription : true + allowedPresenters : everyone + anonymizeIdentityForRoles : [] + audioConferencing : null + autoAdmittedUsers : everyoneInCompany + broadcastSettings : null + capabilities : [] + chatInfo : {"threadId":"19:meeting_ID@thread.v2","messageId":"0","replyChainMessageId":null} + chatRestrictions : null + creationDateTime : 2023-11-13T16:02:12.5012352Z + endDateTime : 2023-11-13T17:02:11.9711697Z + externalId : null + iCalUid : null + id : meetingID + isBroadcast : false + isEntryExitAnnounced : true + joinInformation : {"content":"data:text/html,htmlMessageTemplate","contentType":"html"} + joinMeetingIdSettings : {"isPasscodeRequired":false,"joinMeetingId":"123456789012","passcode":null} + joinUrl : https://teams.microsoft.com/l/meetup-join/abc + joinWebUrl : https://teams.microsoft.com/l/meetup-join/abc + lobbyBypassSettings : {"scope":"organization","isDialInBypassEnabled":false} + meetingCode : 123456789012 + meetingInfo : null + meetingMigrationMode : null + meetingType : null + outerMeetingAutoAdmittedUsers : null + participants : {"organizer":{"upn":"user@contoso.com","role":"presenter","identity":{"application":null,"device":null,"user":{"id":"12345678-1234-1234-1234-12345678","displayName":null,"tenantId":"12345678-1234-1234-1234-12345678","identityProvider":"AAD"}}},"attendees":[]} + recordAutomatically : false + shareMeetingChatHistoryDefault: none + startDateTime : 2023-11-13T16:02:11.9711697Z + subject : null + videoTeleconferenceId : null + watermarkProtection : null + ``` + + + + + ```csv + id,creationDateTime,startDateTime,endDateTime,joinUrl,meetingCode,isBroadcast,autoAdmittedUsers,joinWebUrl,isEntryExitAnnounced,allowedPresenters,allowAttendeeToEnableMic,allowAttendeeToEnableCamera,allowMeetingChat,shareMeetingChatHistoryDefault,allowTeamworkReactions,recordAutomatically,allowParticipantsToChangeName,allowTranscription,allowRecording +meetingId,2023-11-13T16:03:03.5669316Z,2023-11-13T16:03:03.2213499Z,2023-11-13T17:03:03.2213499Z,https://teams.microsoft.com/l/meetup-join/abc,123456789012,,everyoneInCompany,https://teams.microsoft.com/l/meetup-join/abc,1,everyone,1,1,enabled,none,1,,,1,1 + ``` + + + + + ```md + # teams meeting add + + Date: 13/11/2023 + + ## meetingId + + Property | Value + ---------|------- + id | meetingId + creationDateTime | 2023-11-13T16:03:57.6531542Z + startDateTime | 2023-11-13T16:03:56.8464734Z + endDateTime | 2023-11-13T17:03:56.8464734Z + joinUrl | https://teams.microsoft.com/l/meetup-join/abc + meetingCode | 123456789012 + isBroadcast | false + autoAdmittedUsers | everyoneInCompany + joinWebUrl | https://teams.microsoft.com/l/meetup-join/abc + isEntryExitAnnounced | true + allowedPresenters | everyone + allowAttendeeToEnableMic | true + allowAttendeeToEnableCamera | true + allowMeetingChat | enabled + shareMeetingChatHistoryDefault | none + allowTeamworkReactions | true + recordAutomatically | false + allowParticipantsToChangeName | false + allowTranscription | true + allowRecording | true + ``` + + + \ No newline at end of file diff --git a/docs/docs/cmd/teams/meeting/meeting-create.mdx b/docs/docs/cmd/teams/meeting/meeting-create.mdx deleted file mode 100644 index 7169a3821cd..00000000000 --- a/docs/docs/cmd/teams/meeting/meeting-create.mdx +++ /dev/null @@ -1,241 +0,0 @@ -import Global from '/docs/cmd/_global.mdx'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# teams meeting create - -Create a new online meeting - -## Usage - -```sh -m365 teams meeting create [options] -``` - -## Options - -```md definition-list -`-s --startTime [startTime]` -: The start time of the meeting. If not specified, the startTime will be set to the current time. - -`-e --endTime [endTime]` -: The end time of the meeting. If not specified, the endTime will be set to one hour after the startTime. - -`--subject [subject]` -: The subject of the meeting. - -`-p, --participants [participants]` -: A comma-separated list of participant UPNs. - -`--organizerEmail [organizerEmail]` -: The organizer's email address. Requires application permissions. - -`-r, --recordAutomatically` -: When using this flag, the meeting will be recorded automatically. -``` - - - -## Examples - -Create a new online meeting for the currently logged-in user, starting immediately and ending after one hour. - -```sh -m365 teams meeting create -``` - -Create a new online meeting for the currently logged-in user, with a specified start date and a duration of one hour. - -```sh -m365 teams meeting create --startTime "2023-09-21T13:30:00Z" -``` - -Create a new online meeting for the currently logged-in user, with specified start and end dates. - -```sh -m365 teams meeting create --startTime "2023-09-21T13:30:00Z" --endTime "2023-09-21T23:55:00Z" -``` - -Create a new online meeting for the currently logged-in user, with a specified subject. - -```sh -m365 teams meeting create --startTime "2023-09-21T13:30:00Z" --endTime "2023-09-21T23:55:00Z" --subject "Test Subject" -``` - -Create a new online meeting for the currently logged-in user, with a specified subject and a list of participants. - -```sh -m365 teams meeting create --subject "Test Subject" --participantEmails "john.doe@contoso.com,olga.manager@contoso.com" -``` - -Create a new online meeting for the currently logged-in user, with a specified subject, a list of participants, and automatic meeting recording. - -```sh -m365 teams meeting create --subject "Test Subject" --participantEmails "john.doe@contoso.com,olga.manager@contoso.com" --recordAutomatically -``` - -Create a new online meeting for a selected organizer, with a specified subject, a list of participants, and automatic meeting recording. This option is available for app-only permissions. - -```sh -m365 teams meeting create --organizerEmail "john.doe@contoso.com" --subject "Test Subject" --participantEmails "olga.manager@contoso.com" --recordAutomatically -``` - -## Response - - - - - ```json - [ - { - "id": "abc", - "creationDateTime": "2023-07-25T19:29:32.033109Z", - "startDateTime": "2023-07-17T03:00:00Z", - "endDateTime": "2023-07-17T04:00:00Z", - "joinUrl": "https://teams.microsoft.com/l/meetup-join/abc", - "joinWebUrl": "https://teams.microsoft.com/l/meetup-join/abc", - "meetingCode": "12345", - "subject": "Subject", - "isBroadcast": false, - "autoAdmittedUsers": "unknownFutureValue", - "outerMeetingAutoAdmittedUsers": null, - "isEntryExitAnnounced": false, - "allowedPresenters": "everyone", - "allowMeetingChat": "enabled", - "shareMeetingChatHistoryDefault": "none", - "allowTeamworkReactions": true, - "allowAttendeeToEnableMic": true, - "allowAttendeeToEnableCamera": true, - "recordAutomatically": false, - "anonymizeIdentityForRoles": [], - "capabilities": [], - "videoTeleconferenceId": null, - "externalId": null, - "iCalUid": null, - "meetingType": null, - "allowParticipantsToChangeName": false, - "allowRecording": true, - "allowTranscription": true, - "meetingMigrationMode": null, - "broadcastSettings": null, - "audioConferencing": null, - "meetingInfo": null, - "participants": { - "organizer": { - "upn": "john.doe@contoso.com", - "role": "presenter", - "identity": { - "application": null, - "device": null, - "user": { - "id": "b2091e18-7882-4efe-b7d1-90703f5a5c65", - "displayName": null, - "tenantId": "ad4f158a-97c7-4914-a9bd-038ecde40ff3", - "identityProvider": "AAD" - } - } - }, - "attendees": [ - { - "upn": "adele.vance@contoso.com", - "role": "attendee", - "identity": { - "application": null, - "device": null, - "user": { - "id": "52bd2d9c-2d89-416f-96c4-ca94245e22c8", - "displayName": null, - "tenantId": "ad4f158a-97c7-4914-a9bd-038ecde40ff3", - "identityProvider": "AAD" - } - } - } - ] - }, - "lobbyBypassSettings": { - "scope": "unknownFutureValue", - "isDialInBypassEnabled": false - }, - "joinMeetingIdSettings": { - "isPasscodeRequired": true, - "joinMeetingId": "12345", - "passcode": "Z3GYtQ" - }, - "chatInfo": { - "threadId": "abc", - "messageId": "0", - "replyChainMessageId": null - }, - "joinInformation": { - "content": "textContent", - "contentType": "html" - }, - "watermarkProtection": { - "isEnabledForContentSharing": false, - "isEnabledForVideo": false - } - } - ] - ``` - - - - - ```text - endDateTime : 2023-10-05T13:57:09.1889486Z - joinUrl : https://teams.microsoft.com/l/meetup-join/abc - recordAutomatically: false - startDateTime : 2023-10-05T12:57:09.1889486Z - subject : null - ``` - - - - - ```csv - @odata.context,id,creationDateTime,startDateTime,endDateTime,joinUrl,joinWebUrl,meetingCode,isBroadcast,autoAdmittedUsers,isEntryExitAnnounced,allowedPresenters,allowMeetingChat,shareMeetingChatHistoryDefault,allowTeamworkReactions,allowAttendeeToEnableMic,allowAttendeeToEnableCamera,recordAutomatically,allowParticipantsToChangeName,allowRecording,allowTranscription -https://graph.microsoft.com/v1.0/$metadata#users('id')/onlineMeetings/$entity,abc,2023-10-05T12:56:20.9466495Z,2023-10-05T12:56:20.6594121Z,2023-10-05T13:56:20.6594121Z,https://teams.microsoft.com/l/meetup-join/abc,https://teams.microsoft.com/l/meetup-join/abc,388012423843,,everyoneInCompany,1,everyone,enabled,none,1,1,1,,,1,1 - ``` - - - - - ```md -# teams meeting create --organizerEmail "john.doe@contoso.com" - -Date: 05/10/2023 - -## abc - - Property | Value ----------|------- -@odata.context | https://graph.microsoft.com/v1.0/$metadata#users('id')/onlineMeetings/$entity -id | abc -creationDateTime | 2023-10-05T12:56:00.6719952Z -startDateTime | 2023-10-05T12:56:00.2303956Z -endDateTime | 2023-10-05T13:56:00.2303956Z -joinUrl | https://teams.microsoft.com/l/meetup-join/abc -joinWebUrl | https://teams.microsoft.com/l/meetup-join/abc -meetingCode | 12345 -isBroadcast | false -autoAdmittedUsers | everyoneInCompany -isEntryExitAnnounced | true -allowedPresenters | everyone -allowMeetingChat | enabled -shareMeetingChatHistoryDefault | none -allowTeamworkReactions | true -allowAttendeeToEnableMic | true -allowAttendeeToEnableCamera | true -recordAutomatically | false -allowParticipantsToChangeName | false -allowRecording | true -allowTranscription | true - ``` - - - - - -## Remarks - -To create an online meeting for a specific organizer, use the **--organizerEmail** parameter along with app-only permissions. The registered app should have the **OnlineMeetings.ReadWrite.All** permissions, and a special policy should be assigned to the user specified in the **organizerEmail** option. You can find more information on how to assign this policy to a user [here](https://learn.microsoft.com/en-us/graph/cloud-communication-online-meeting-application-access-policy). \ No newline at end of file diff --git a/docs/src/config/sidebars.js b/docs/src/config/sidebars.js index 4a2b86d79d5..542978cace2 100644 --- a/docs/src/config/sidebars.js +++ b/docs/src/config/sidebars.js @@ -3806,8 +3806,8 @@ const sidebars = { meeting: [ { type: 'doc', - label: 'meeting create', - id: 'cmd/teams/meeting/meeting-create' + label: 'meeting add', + id: 'cmd/teams/meeting/meeting-add' }, { type: 'doc', diff --git a/src/m365/teams/commands.ts b/src/m365/teams/commands.ts index 8f16b9abae4..5aff8ea3d7d 100644 --- a/src/m365/teams/commands.ts +++ b/src/m365/teams/commands.ts @@ -29,7 +29,7 @@ export default { GUESTSETTINGS_LIST: `${prefix} guestsettings list`, GUESTSETTINGS_SET: `${prefix} guestsettings set`, MEETING_ATTENDANCEREPORT_LIST: `${prefix} meeting attendancereport list`, - MEETING_CREATE: `${prefix} meeting create`, + MEETING_ADD: `${prefix} meeting add`, MEETING_GET: `${prefix} meeting get`, MEETING_LIST: `${prefix} meeting list`, MEETING_TRANSCRIPT_LIST: `${prefix} meeting transcript list`, diff --git a/src/m365/teams/commands/meeting/meeting-create.spec.ts b/src/m365/teams/commands/meeting/meeting-add.spec.ts similarity index 74% rename from src/m365/teams/commands/meeting/meeting-create.spec.ts rename to src/m365/teams/commands/meeting/meeting-add.spec.ts index 4f6ee32ae24..0270dc8e882 100644 --- a/src/m365/teams/commands/meeting/meeting-create.spec.ts +++ b/src/m365/teams/commands/meeting/meeting-add.spec.ts @@ -13,13 +13,13 @@ import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; -import command from './meeting-create.js'; +import command from './meeting-add.js'; -describe(commands.MEETING_CREATE, () => { +describe(commands.MEETING_ADD, () => { const startTime = '2022-04-04T03:00:00Z'; const endTime = '2022-04-04T04:00:00Z'; const subject = 'test subject'; - const participants = 'abc@email.com,abc2@email.com'; + const participantUserNames = 'abc@email.com,abc2@email.com'; const organizerEmail = 'organizer@email.com'; // #region responses @@ -78,90 +78,101 @@ describe(commands.MEETING_CREATE, () => { }); it('has correct name', () => { - assert.strictEqual(command.name, commands.MEETING_CREATE); + assert.strictEqual(command.name, commands.MEETING_ADD); }); it('has a description', () => { assert.notStrictEqual(command.description, null); }); - it('completes validation when no parameter is provided', async () => { - const actual = await command.validate({ options: { startTime: undefined, endTime: undefined, subject: undefined, participants: undefined, organizerEmail: undefined, recordAutomatically: undefined } }, commandInfo); + it('completes validation when no parameters are provided', async () => { + const actual = await command.validate({ options: { startTime: undefined, endTime: undefined, subject: undefined, participantUserNames: undefined, organizerEmail: undefined, recordAutomatically: undefined } }, commandInfo); assert.strictEqual(actual, true); }); - it('completes validation when only startTime is provided and it is a valid ISODateTime', async () => { + it('completes validation when only the startTime parameter is provided, and it is a valid ISODateTime', async () => { const actual = await command.validate({ options: { startTime: startTime } }, commandInfo); assert.strictEqual(actual, true); }); - it('completes validation when the startTime and endTime are provided and they are valid ISODateTime', async () => { + it('completes validation when both the startTime and endTime parameters are provided, and they are valid ISODateTimes', async () => { const actual = await command.validate({ options: { startTime: startTime, endTime: endTime } }, commandInfo); assert.strictEqual(actual, true); }); - it('completes validation when only subject is provided', async () => { + it('completes validation when only the subject parameter is provided', async () => { const actual = await command.validate({ options: { subject: subject } }, commandInfo); assert.strictEqual(actual, true); }); - it('completes validation when only organizerEmail is provided', async () => { + it('completes validation when only the organizerEmail parameter is provided', async () => { const actual = await command.validate({ options: { organizerEmail: organizerEmail } }, commandInfo); assert.strictEqual(actual, true); }); - it('completes validation when only participants parameter is provided', async () => { - const actual = await command.validate({ options: { participants: participants } }, commandInfo); + it('completes validation when only the participantUserNames parameter is provided', async () => { + const actual = await command.validate({ options: { participantUserNames: participantUserNames } }, commandInfo); assert.strictEqual(actual, true); }); + it('completes validation when the correct endTime is provided, and the startTime is not provided', async () => { + const fakeTimers = sinon.useFakeTimers(new Date('2020-01-01T12:00:00.000Z')); + + const actual = await command.validate({ options: { endTime: endTime } }, commandInfo); + assert.strictEqual(actual, true); + + fakeTimers.restore(); + }); + it('fails validation when the startTime is not a valid ISODateTime', async () => { const actual = await command.validate({ options: { startTime: 'foo' } }, commandInfo); assert.notStrictEqual(actual, true); }); - it('fails validation when the correct startTime is provided and the endTime is not a valid ISODateTime', async () => { + it('fails validation when the correct startTime is provided, and the endTime is not a valid ISODateTime', async () => { const actual = await command.validate({ options: { startTime: startTime, endTime: 'foo' } }, commandInfo); assert.notStrictEqual(actual, true); }); - it('fails validation when the correct endTime is provided and the startTime is not provided', async () => { - const actual = await command.validate({ options: { endTime: endTime } }, commandInfo); + it('fails validation when the endTime is before the startTime', async () => { + const actual = await command.validate({ options: { startTime: '2023-01-01', endTime: '2022-12-31' } }, commandInfo); assert.notStrictEqual(actual, true); }); - - it('fails validation when endTime is before startTime', async () => { - const actual = await command.validate({ options: { startTime: '2023-01-01', endTime: '2022-12-31' } }, commandInfo); + it('fails validation when only the endTime is provided and occurs before the current time.', async () => { + const fakeTimers = sinon.useFakeTimers(new Date('2020-01-01T12:00:00.000Z')); + const actual = await command.validate({ options: { endTime: '1990-12-31' } }, commandInfo); assert.notStrictEqual(actual, true); + + fakeTimers.restore(); }); - it('fails validation when the organizerEmail is not a valid', async () => { + it('fails validation when the organizerEmail parameter is not a valid email address', async () => { const actual = await command.validate({ options: { organizerEmail: 'foo' } }, commandInfo); assert.notStrictEqual(actual, true); }); - it('fails validation when the participants is not a valid', async () => { - const actual = await command.validate({ options: { participants: 'foo' } }, commandInfo); + it('fails validation when the participantUserNames is not a valid', async () => { + const actual = await command.validate({ options: { participantUserNames: 'foo' } }, commandInfo); assert.notStrictEqual(actual, true); }); - it('fails validation when the participants are not separated by comma', async () => { - const actual = await command.validate({ options: { participants: 'abc@email.com|abc2@email.com' } }, commandInfo); + it('fails validation when the participantUserNames are not separated by comma', async () => { + const actual = await command.validate({ options: { participantUserNames: 'abc@email.com|abc2@email.com' } }, commandInfo); assert.notStrictEqual(actual, true); }); - it('fails validation when the participants has incorrect email', async () => { - const actual = await command.validate({ options: { participants: 'abc@email.com,foo' } }, commandInfo); + it('fails validation when the participantUserNames has incorrect format', async () => { + const actual = await command.validate({ options: { participantUserNames: 'abc@email.com,foo' } }, commandInfo); assert.notStrictEqual(actual, true); }); - it('fails validation when startDateTime is behind endDateTime', async () => { - const actual = await command.validate({ options: { startDateTime: '2023-01-01', endDateTime: '2022-12-31' } }, commandInfo); + it('fails validation when the startDate is after the endDate', async () => { + const actual = await command.validate({ options: { startTime: '2023-01-01', endTime: '2022-12-31' } }, commandInfo); assert.notStrictEqual(actual, true); }); - it('throws an error when the organizerEmail is not filled in when signed in using app-only authentication', async () => { + it('throws an error when the organizerEmail is not provided while signed in using app-only authentication', async () => { sinonUtil.restore(accessToken.isAppOnlyAccessToken); sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true); @@ -169,6 +180,18 @@ describe(commands.MEETING_CREATE, () => { new CommandError(`The option 'organizerEmail' is required when creating a meeting using app only permissions`)); }); + it('throws an error when the organizerEmail parameter is set and delegated permissions are used', async () => { + sinonUtil.restore(accessToken.isAppOnlyAccessToken); + sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(false); + + await assert.rejects(command.action(logger, { + options: { + verbose: true, + organizerEmail: organizerEmail + } + }), new CommandError(`The option 'organizerEmail' is not supported when creating a meeting using delegated permissions`)); + }); + it('create a meeting for the currently logged in user', async () => { let calledUrl = ''; let calledData = ''; @@ -194,7 +217,37 @@ describe(commands.MEETING_CREATE, () => { assert(loggerLogSpy.calledWith(meeting)); }); - it('create a meeting with defined startDate for the currently logged in user', async () => { + it('create a meeting with a defined startDate for the currently logged-in user', async () => { + const fakeTimers = sinon.useFakeTimers(new Date('2020-01-01T12:00:00.000Z')); + + let calledUrl = ''; + let calledData = ''; + + sinon.stub(request, 'post').callsFake(async opts => { + if (opts.url === 'https://graph.microsoft.com/v1.0/me/onlineMeetings') { + calledUrl = opts.url; + calledData = opts.data; + return meeting; + } + + throw 'Invalid request: ' + opts.url; + }); + + await command.action(logger, { + options: { + verbose: true, + endTime: endTime + } + }); + + assert.strictEqual(calledUrl, 'https://graph.microsoft.com/v1.0/me/onlineMeetings'); + assert.deepEqual(calledData, { startDateTime: '2020-01-01T12:00:00.000Z', endDateTime: endTime }); + assert(loggerLogSpy.calledWith(meeting)); + + fakeTimers.restore(); + }); + + it('create a meeting with a defined endDate and current date as startDate for the currently logged-in user', async () => { let calledUrl = ''; let calledData = ''; @@ -220,7 +273,7 @@ describe(commands.MEETING_CREATE, () => { assert(loggerLogSpy.calledWith(meeting)); }); - it('create a meeting with defined startDate and endDate for the currently logged in user', async () => { + it('create a meeting with defined startDate and endDate for the currently logged-in user', async () => { let calledUrl = ''; let calledData = ''; @@ -247,7 +300,7 @@ describe(commands.MEETING_CREATE, () => { assert(loggerLogSpy.calledWith(meeting)); }); - it('create a meeting with defined startDate, endDate and subject for the currently logged in user', async () => { + it('create a meeting with defined startDate, endDate and subject for the currently logged-in user', async () => { let calledUrl = ''; let calledData = ''; @@ -275,7 +328,7 @@ describe(commands.MEETING_CREATE, () => { assert(loggerLogSpy.calledWith(meeting)); }); - it('create a meeting with defined startDate, endDate, subject, and participants for the currently logged in user', async () => { + it('create a meeting with defined startDate, endDate, subject, and participantUserNames for the currently logged-in user', async () => { let calledUrl = ''; let calledData = ''; @@ -295,7 +348,7 @@ describe(commands.MEETING_CREATE, () => { startTime: startTime, endTime: endTime, subject: subject, - participants: participants + participantUserNames: participantUserNames } }); @@ -315,7 +368,7 @@ describe(commands.MEETING_CREATE, () => { assert(loggerLogSpy.calledWith(meeting)); }); - it('create a meeting with defined startDate, endDate, subject, participants and recordAutomatically for the currently logged in user', async () => { + it('create a meeting with defined startDate, endDate, subject, participantUserNames and recordAutomatically for the currently logged-in user', async () => { let calledUrl = ''; let calledData = ''; @@ -335,7 +388,7 @@ describe(commands.MEETING_CREATE, () => { startTime: startTime, endTime: endTime, subject: subject, - participants: participants, + participantUserNames: participantUserNames, recordAutomatically: true } }); @@ -357,7 +410,7 @@ describe(commands.MEETING_CREATE, () => { }); - it('create a meeting with defined startDate, endDate, subject, participants and recordAutomatically for the specified organizerEmail when app only authorization', async () => { + it('create a meeting with defined startDate, endDate, subject, participantUserNames and recordAutomatically for the specified organizerEmail when app only authorization', async () => { let calledUrl = ''; let calledData = ''; const testOrganizerId = '12345678-7882-4efe-b7d1-90703f5a5c65'; @@ -388,7 +441,7 @@ describe(commands.MEETING_CREATE, () => { startTime: startTime, endTime: endTime, subject: subject, - participants: participants, + participantUserNames: participantUserNames, recordAutomatically: true, organizerEmail: organizerEmail } @@ -410,7 +463,7 @@ describe(commands.MEETING_CREATE, () => { assert(loggerLogSpy.calledWith(meeting)); }); - it('handles error correctly when organizer Id is not found', async () => { + it('handles error appropriately when organizer Id is not found', async () => { const testOrganizerId = '12345678-7882-4efe-b7d1-90703f5a5c65'; sinonUtil.restore(accessToken.isAppOnlyAccessToken); @@ -437,7 +490,7 @@ describe(commands.MEETING_CREATE, () => { startTime: startTime, endTime: endTime, subject: subject, - participants: participants, + participantUserNames: participantUserNames, recordAutomatically: true, organizerEmail: organizerEmail } @@ -445,7 +498,7 @@ describe(commands.MEETING_CREATE, () => { ); }); - it('handles error forbidden correctly', async () => { + it('handles the forbidden error correctly', async () => { sinon.stub(request, 'post').callsFake(async opts => { if (opts.url === 'https://graph.microsoft.com/v1.0/me/onlineMeetings') { @@ -453,7 +506,7 @@ describe(commands.MEETING_CREATE, () => { response: { status: 403 }, - message: 'Forbidden' + message: "Forbidden. You do not have permission to perform this action. Please verify the command's details for more information." }; } @@ -467,14 +520,14 @@ describe(commands.MEETING_CREATE, () => { startTime: startTime, endTime: endTime, subject: subject, - participants: participants, + participantUserNames: participantUserNames, recordAutomatically: true } }), new CommandError(`Forbidden. You do not have permission to perform this action. Please verify the command's details for more information.`) ); }); - it('handles error correctly', async () => { + it('handles error appropriately', async () => { sinon.stub(request, 'post').callsFake(async opts => { if (opts.url === 'https://graph.microsoft.com/v1.0/me/onlineMeetings') { @@ -497,7 +550,7 @@ describe(commands.MEETING_CREATE, () => { startTime: startTime, endTime: endTime, subject: subject, - participants: participants, + participantUserNames: participantUserNames, recordAutomatically: true } }), new CommandError('Error message') diff --git a/src/m365/teams/commands/meeting/meeting-create.ts b/src/m365/teams/commands/meeting/meeting-add.ts similarity index 64% rename from src/m365/teams/commands/meeting/meeting-create.ts rename to src/m365/teams/commands/meeting/meeting-add.ts index c49dbfbfbc7..fe0cf521318 100644 --- a/src/m365/teams/commands/meeting/meeting-create.ts +++ b/src/m365/teams/commands/meeting/meeting-add.ts @@ -17,22 +17,18 @@ interface Options extends GlobalOptions { startTime?: string; endTime?: string; subject?: string; - participants?: string; + participantUserNames?: string; organizerEmail?: string; recordAutomatically?: boolean; } -class TeamsMeetingCreateCommand extends GraphCommand { +class TeamsMeetingAddCommand extends GraphCommand { public get name(): string { - return commands.MEETING_CREATE; + return commands.MEETING_ADD; } public get description(): string { - return 'Create a new online meeting'; - } - - public defaultProperties(): string[] | undefined { - return ['subject', 'startDateTime', 'endDateTime', 'joinUrl', 'recordAutomatically']; + return 'Creates a new online meeting'; } constructor() { @@ -49,7 +45,7 @@ class TeamsMeetingCreateCommand extends GraphCommand { startTime: typeof args.options.startTime !== 'undefined', endTime: typeof args.options.endTime !== 'undefined', subject: typeof args.options.subject !== 'undefined', - participants: typeof args.options.participants !== 'undefined', + participantUserNames: typeof args.options.participantUserNames !== 'undefined', organizerEmail: typeof args.options.organizerEmail !== 'undefined', recordAutomatically: !!args.options.recordAutomatically }); @@ -59,22 +55,22 @@ class TeamsMeetingCreateCommand extends GraphCommand { #initOptions(): void { this.options.unshift( { - option: '-s --startTime [startTime]' + option: '-s, --startTime [startTime]' }, { - option: '-e --endTime [endTime]' + option: '-e, --endTime [endTime]' }, { - option: '-s --subject [subject]' + option: '--subject [subject]' }, { - option: '-p --participants [participants]' + option: '-p, --participantUserNames [participantUserNames]' }, { option: '--organizerEmail [organizerEmail]' }, { - option: '-r --recordAutomatically' + option: '-r, --recordAutomatically' } ); } @@ -82,30 +78,40 @@ class TeamsMeetingCreateCommand extends GraphCommand { #initValidators(): void { this.validators.push( async (args: CommandArgs) => { + if (args.options.startTime && !validation.isValidISODateTime(args.options.startTime)) { return `'${args.options.startTime}' is not a valid ISO date string for startTime.`; } + if (args.options.endTime && !validation.isValidISODateTime(args.options.endTime)) { return `'${args.options.endTime}' is not a valid ISO date string for endTime.`; } + if (args.options.startTime && args.options.endTime && new Date(args.options.startTime) >= new Date(args.options.endTime)) { - return 'startTime value must be before endTime.'; + return 'The startTime value must be before endTime.'; } - if (args.options.endTime && !args.options.startTime) { - return 'startTime should be specified when endTime is specified.'; + + if (args.options.endTime && !args.options.startTime && new Date() >= new Date(args.options.endTime)) { + return 'When only the endTime is specified, it needs to be after the current time.'; } - if (args.options.participants) { - if (args.options.participants.indexOf(',') === -1 && !validation.isValidUserPrincipalName(args.options.participants)) { - return `${args.options.participants} contains invalid UPN.`; + + if (args.options.participantUserNames) { + + if (args.options.participantUserNames.indexOf(',') === -1 && !validation.isValidUserPrincipalName(args.options.participantUserNames)) { + return `${args.options.participantUserNames} contains invalid UPN.`; } - const participants = args.options.participants.trim().toLowerCase().split(',').filter(e => e && e !== ''); + + const participants = args.options.participantUserNames.trim().toLowerCase().split(',').filter(e => e && e !== ''); + if (!participants || participants.length === 0 || participants.some(e => !validation.isValidUserPrincipalName(e))) { - return `${args.options.participants} contains one or more invalid UPN.`; + return `${args.options.participantUserNames} contains one or more invalid UPN.`; } } + if (args.options.organizerEmail && !validation.isValidUserPrincipalName(args.options.organizerEmail)) { return `'${args.options.organizerEmail}' is not a valid email for organizerEmail.`; } + return true; } ); @@ -119,10 +125,25 @@ class TeamsMeetingCreateCommand extends GraphCommand { public async commandAction(logger: Logger, args: CommandArgs): Promise { try { const isAppOnlyAccessToken = accessToken.isAppOnlyAccessToken(auth.service.accessTokens[this.resource].accessToken)!; + if (isAppOnlyAccessToken && !args.options.organizerEmail) { throw `The option 'organizerEmail' is required when creating a meeting using app only permissions`; } - const graphBaseUrl = await this.getGraphBaseUrl(args.options); + + if (!isAppOnlyAccessToken && args.options.organizerEmail) { + throw `The option 'organizerEmail' is not supported when creating a meeting using delegated permissions`; + } + + let graphBaseUrl = `${this.resource}/v1.0/`; + + if (args.options.organizerEmail) { + const organizerId = await aadUser.getUserIdByEmail(args.options.organizerEmail); + graphBaseUrl = `${graphBaseUrl}users/${organizerId}`; + } + else { + graphBaseUrl = `${graphBaseUrl}me`; + } + const meeting = await this.createMeeting(logger, graphBaseUrl, args.options); await logger.log(meeting); } @@ -130,24 +151,6 @@ class TeamsMeetingCreateCommand extends GraphCommand { this.handleRejectedODataJsonPromise(err); } } - - /** - * Gets the base MS Graph URL for the request - * @param options - * @returns correct MS Graph URL for the request - */ - private async getGraphBaseUrl(options: Options): Promise { - let requestUrl = `${this.resource}/v1.0/`; - if (options.organizerEmail) { - const organizerId = await aadUser.getUserIdByEmail(options.organizerEmail); - requestUrl += `users/${organizerId}`; - } - else { - requestUrl += 'me'; - } - return requestUrl; - } - /** * Creates a new online meeting * @param logger @@ -155,52 +158,59 @@ class TeamsMeetingCreateCommand extends GraphCommand { * @param options * @returns MS Graph online meeting response */ - private async createMeeting(logger: Logger, graphBaseUrl: string, options: Options): Promise { + private async createMeeting(logger: Logger, graphBaseUrl: string, options: Options): Promise { + if (this.verbose) { - logger.logToStderr(`Creation of a meeting...`); + await logger.logToStderr(`Creating the meeting...`); } + const requestData: any = {}; - if (options.participants) { - const attendees = options.participants.trim().toLowerCase().split(',').map(p => ({ + + if (options.participantUserNames) { + const attendees = options.participantUserNames.trim().toLowerCase().split(',').map(p => ({ upn: p.trim() })); requestData.participants = { attendees }; } + if (options.startTime) { requestData.startDateTime = options.startTime; } + if (options.endTime) { requestData.endDateTime = options.endTime; + + if (!options.startTime) { + requestData.startDateTime = new Date().toISOString(); + } } + if (options.subject) { requestData.subject = options.subject; } + if (options.recordAutomatically !== undefined) { - requestData.recordAutomatically = options.recordAutomatically; + requestData.recordAutomatically = true; } - const requestOption: CliRequestOptions = { + + const requestOptions: CliRequestOptions = { headers: { - accept: 'application/json', + accept: 'application/json;odata.metadata=none', 'content-type': 'application/json' }, responseType: 'json', - method: 'POST', url: `${graphBaseUrl}/onlineMeetings`, data: requestData }; try { - const requestResponse = await request.post(requestOption); + const requestResponse = await request.post(requestOptions); return requestResponse; } catch (error: any) { - if (error.response.status === 403) { - throw `Forbidden. You do not have permission to perform this action. Please verify the command's details for more information.`; - } - throw error.message; } } } -export default new TeamsMeetingCreateCommand(); \ No newline at end of file +export default new TeamsMeetingAddCommand(); \ No newline at end of file