diff --git a/docs/docs/cmd/teams/meeting/meeting-create.mdx b/docs/docs/cmd/teams/meeting/meeting-create.mdx
new file mode 100644
index 00000000000..7169a3821cd
--- /dev/null
+++ b/docs/docs/cmd/teams/meeting/meeting-create.mdx
@@ -0,0 +1,241 @@
+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 f404bd25a4e..4a2b86d79d5 100644
--- a/docs/src/config/sidebars.js
+++ b/docs/src/config/sidebars.js
@@ -3804,6 +3804,11 @@ const sidebars = {
},
{
meeting: [
+ {
+ type: 'doc',
+ label: 'meeting create',
+ id: 'cmd/teams/meeting/meeting-create'
+ },
{
type: 'doc',
label: 'meeting get',
diff --git a/src/m365/teams/commands.ts b/src/m365/teams/commands.ts
index 7ff1d3c7e5d..8f16b9abae4 100644
--- a/src/m365/teams/commands.ts
+++ b/src/m365/teams/commands.ts
@@ -29,6 +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_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-create.spec.ts
new file mode 100644
index 00000000000..4f6ee32ae24
--- /dev/null
+++ b/src/m365/teams/commands/meeting/meeting-create.spec.ts
@@ -0,0 +1,506 @@
+import assert from 'assert';
+import sinon from 'sinon';
+import auth from '../../../../Auth.js';
+import { CommandError } from '../../../../Command.js';
+import { Cli } from '../../../../cli/Cli.js';
+import { CommandInfo } from '../../../../cli/CommandInfo.js';
+import { Logger } from '../../../../cli/Logger.js';
+import request from '../../../../request.js';
+import { telemetry } from '../../../../telemetry.js';
+import { aadUser } from '../../../../utils/aadUser.js';
+import { accessToken } from '../../../../utils/accessToken.js';
+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';
+
+describe(commands.MEETING_CREATE, () => {
+ 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 organizerEmail = 'organizer@email.com';
+
+ // #region responses
+ const meeting = `{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#users('1af1bc3a-eb29-4e90-b38a-7ff729d4ac00')/onlineMeetings/$entity","id":"MSoxYWYxYmMzYS1lYjI5LTRlOTAtYjM4YS03ZmY3MjlkNGFjMDAqMCoqMTk6bWVldGluZ19NekkyTnpoak4yTXRaVEk1WmkwMFlUaGpMVGd6WTJNdFl6RTFNamRpTUdSbFpUQTNAdGhyZWFkLnYy","creationDateTime":"2023-10-03T19:13:29.9677485Z","startDateTime":"2023-10-03T19:13:29.596964Z","endDateTime":"2023-10-03T20:13:29.596964Z","joinUrl":"https://teams.microsoft.com/l/meetup-join/19%3ameeting_MzI2N…+4px%3bfont-family%3a%27Segoe+UI%27%2c%27Helvetica+Neue%27%2cHelvetica%2cArial%2csans-serif%3b%22%3e%0d%0a%0d%0a%3c%2fdiv%3e%0d%0a%3cdiv+style%3d%22font-size%3a+12px%3b%22%3e%0d%0a%0d%0a%3c%2fdiv%3e%0d%0a%0d%0a%3c%2fdiv%3e%0d%0a%3cdiv+style%3d%22width%3a100%25%3b%22%3e%0d%0a++++%3cspan+style%3d%22white-space%3anowrap%3bcolor%3a%235F5F5F%3bopacity%3a.36%3b%22%3e________________________________________________________________________________%3c%2fspan%3e%0d%0a%3c%2fdiv%3e","contentType":"html"}}`;
+
+ // #endregion
+
+ let log: string[];
+ let logger: Logger;
+ let loggerLogSpy: sinon.SinonSpy;
+ let commandInfo: CommandInfo;
+
+ before(() => {
+ sinon.stub(auth, 'restoreAuth').resolves();
+ sinon.stub(telemetry, 'trackEvent').returns();
+ sinon.stub(pid, 'getProcessName').returns('');
+ sinon.stub(session, 'getId').returns('');
+ auth.service.connected = true;
+ auth.service.accessTokens[auth.defaultResource] = {
+ expiresOn: 'abc',
+ accessToken: 'abc'
+ };
+ commandInfo = Cli.getCommandInfo(command);
+ sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(false);
+ });
+
+ beforeEach(() => {
+ log = [];
+ logger = {
+ log: async (msg: string) => {
+ log.push(msg);
+ },
+ logRaw: async (msg: string) => {
+ log.push(msg);
+ },
+ logToStderr: async (msg: string) => {
+ log.push(msg);
+ }
+ };
+ loggerLogSpy = sinon.spy(logger, 'log');
+ });
+
+ afterEach(() => {
+ sinonUtil.restore([
+ accessToken.isAppOnlyAccessToken,
+ request.get,
+ request.post,
+ aadUser.getUserIdByEmail
+ ]);
+ });
+
+ after(() => {
+ sinon.restore();
+ auth.service.connected = false;
+ auth.service.accessTokens = {};
+ });
+
+ it('has correct name', () => {
+ assert.strictEqual(command.name, commands.MEETING_CREATE);
+ });
+
+ 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);
+ assert.strictEqual(actual, true);
+ });
+
+ it('completes validation when only startTime 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 () => {
+ const actual = await command.validate({ options: { startTime: startTime, endTime: endTime } }, commandInfo);
+ assert.strictEqual(actual, true);
+ });
+
+ it('completes validation when only subject 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 () => {
+ 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);
+ assert.strictEqual(actual, true);
+ });
+
+ 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 () => {
+ 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);
+ 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);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('fails validation when the organizerEmail is not a valid', 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);
+ 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);
+ 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);
+ 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);
+ assert.notStrictEqual(actual, true);
+ });
+
+ it('throws an error when the organizerEmail is not filled in when signed in using app-only authentication', async () => {
+ sinonUtil.restore(accessToken.isAppOnlyAccessToken);
+ sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true);
+
+ await assert.rejects(command.action(logger, { options: { verbose: true } }),
+ new CommandError(`The option 'organizerEmail' is required when creating a meeting using app only permissions`));
+ });
+
+ it('create a meeting for the currently logged in user', async () => {
+ 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
+ }
+ });
+
+ assert.strictEqual(calledUrl, 'https://graph.microsoft.com/v1.0/me/onlineMeetings');
+ assert.deepEqual(calledData, {});
+ assert(loggerLogSpy.calledWith(meeting));
+ });
+
+ it('create a meeting with defined startDate for the currently logged in user', async () => {
+ 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,
+ startTime: startTime
+ }
+ });
+
+ assert.strictEqual(calledUrl, 'https://graph.microsoft.com/v1.0/me/onlineMeetings');
+ assert.deepEqual(calledData, { startDateTime: startTime });
+ assert(loggerLogSpy.calledWith(meeting));
+ });
+
+ it('create a meeting with defined startDate and endDate for the currently logged in user', async () => {
+ 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,
+ startTime: startTime,
+ endTime: endTime
+ }
+ });
+
+ assert.strictEqual(calledUrl, 'https://graph.microsoft.com/v1.0/me/onlineMeetings');
+ assert.deepEqual(calledData, { startDateTime: startTime, endDateTime: endTime });
+ assert(loggerLogSpy.calledWith(meeting));
+ });
+
+ it('create a meeting with defined startDate, endDate and subject for the currently logged in user', async () => {
+ 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,
+ startTime: startTime,
+ endTime: endTime,
+ subject: subject
+ }
+ });
+
+ assert.strictEqual(calledUrl, 'https://graph.microsoft.com/v1.0/me/onlineMeetings');
+ assert.deepEqual(calledData, { startDateTime: startTime, endDateTime: endTime, subject: subject });
+ assert(loggerLogSpy.calledWith(meeting));
+ });
+
+ it('create a meeting with defined startDate, endDate, subject, and participants for the currently logged in user', async () => {
+ 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,
+ startTime: startTime,
+ endTime: endTime,
+ subject: subject,
+ participants: participants
+ }
+ });
+
+ assert.strictEqual(calledUrl, 'https://graph.microsoft.com/v1.0/me/onlineMeetings');
+ assert.deepEqual(calledData, {
+ startDateTime: startTime, endDateTime: endTime, subject: subject, participants: {
+ attendees: [
+ {
+ "upn": "abc@email.com"
+ },
+ {
+ "upn": "abc2@email.com"
+ }
+ ]
+ }
+ });
+ assert(loggerLogSpy.calledWith(meeting));
+ });
+
+ it('create a meeting with defined startDate, endDate, subject, participants and recordAutomatically for the currently logged in user', async () => {
+ 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,
+ startTime: startTime,
+ endTime: endTime,
+ subject: subject,
+ participants: participants,
+ recordAutomatically: true
+ }
+ });
+
+ assert.strictEqual(calledUrl, 'https://graph.microsoft.com/v1.0/me/onlineMeetings');
+ assert.deepEqual(calledData, {
+ startDateTime: startTime, endDateTime: endTime, subject: subject, participants: {
+ attendees: [
+ {
+ "upn": "abc@email.com"
+ },
+ {
+ "upn": "abc2@email.com"
+ }
+ ]
+ }, recordAutomatically: true
+ });
+ assert(loggerLogSpy.calledWith(meeting));
+ });
+
+
+ it('create a meeting with defined startDate, endDate, subject, participants and recordAutomatically for the specified organizerEmail when app only authorization', async () => {
+ let calledUrl = '';
+ let calledData = '';
+ const testOrganizerId = '12345678-7882-4efe-b7d1-90703f5a5c65';
+
+ sinonUtil.restore(accessToken.isAppOnlyAccessToken);
+ sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true);
+
+ sinon.stub(request, 'get').callsFake(async opts => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/users?$filter=mail eq 'organizer%40email.com'&$select=id`) {
+ return { value: [{ id: testOrganizerId }] };
+ }
+ throw 'Invalid request: ' + opts.url;
+ });
+
+ sinon.stub(request, 'post').callsFake(async opts => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/users/${testOrganizerId}/onlineMeetings`) {
+ calledUrl = opts.url;
+ calledData = opts.data;
+ return meeting;
+ }
+
+ throw 'Invalid request: ' + opts.url;
+ });
+
+ await command.action(logger, {
+ options: {
+ verbose: true,
+ startTime: startTime,
+ endTime: endTime,
+ subject: subject,
+ participants: participants,
+ recordAutomatically: true,
+ organizerEmail: organizerEmail
+ }
+ });
+
+ assert.strictEqual(calledUrl, `https://graph.microsoft.com/v1.0/users/${testOrganizerId}/onlineMeetings`);
+ assert.deepEqual(calledData, {
+ startDateTime: startTime, endDateTime: endTime, subject: subject, participants: {
+ attendees: [
+ {
+ "upn": "abc@email.com"
+ },
+ {
+ "upn": "abc2@email.com"
+ }
+ ]
+ }, recordAutomatically: true
+ });
+ assert(loggerLogSpy.calledWith(meeting));
+ });
+
+ it('handles error correctly when organizer Id is not found', async () => {
+ const testOrganizerId = '12345678-7882-4efe-b7d1-90703f5a5c65';
+
+ sinonUtil.restore(accessToken.isAppOnlyAccessToken);
+ sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true);
+
+ sinon.stub(request, 'get').callsFake(async opts => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/users?$filter=mail eq 'organizer%40email.com'&$select=id`) {
+ return { value: [] };
+ }
+ throw 'Invalid request: ' + opts.url;
+ });
+
+ sinon.stub(request, 'post').callsFake(async opts => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/users/${testOrganizerId}/onlineMeetings`) {
+ return meeting;
+ }
+
+ throw 'Invalid request: ' + opts.url;
+ });
+
+ await assert.rejects(command.action(logger, {
+ options: {
+ verbose: true,
+ startTime: startTime,
+ endTime: endTime,
+ subject: subject,
+ participants: participants,
+ recordAutomatically: true,
+ organizerEmail: organizerEmail
+ }
+ }), new CommandError(`The specified user with email organizer@email.com does not exist`)
+ );
+ });
+
+ it('handles error forbidden correctly', async () => {
+
+ sinon.stub(request, 'post').callsFake(async opts => {
+ if (opts.url === 'https://graph.microsoft.com/v1.0/me/onlineMeetings') {
+ throw {
+ response: {
+ status: 403
+ },
+ message: 'Forbidden'
+ };
+ }
+
+ throw 'Invalid request: ' + opts.url;
+ });
+
+
+ await assert.rejects(command.action(logger, {
+ options: {
+ verbose: true,
+ startTime: startTime,
+ endTime: endTime,
+ subject: subject,
+ participants: participants,
+ 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 () => {
+
+ sinon.stub(request, 'post').callsFake(async opts => {
+ if (opts.url === 'https://graph.microsoft.com/v1.0/me/onlineMeetings') {
+
+ throw {
+ response: {
+ status: 404
+ },
+ message: 'Error message'
+ };
+ }
+
+ throw 'Invalid request: ' + opts.url;
+ });
+
+
+ await assert.rejects(command.action(logger, {
+ options: {
+ verbose: true,
+ startTime: startTime,
+ endTime: endTime,
+ subject: subject,
+ participants: participants,
+ recordAutomatically: true
+ }
+ }), new CommandError('Error message')
+ );
+ });
+});
\ No newline at end of file
diff --git a/src/m365/teams/commands/meeting/meeting-create.ts b/src/m365/teams/commands/meeting/meeting-create.ts
new file mode 100644
index 00000000000..c49dbfbfbc7
--- /dev/null
+++ b/src/m365/teams/commands/meeting/meeting-create.ts
@@ -0,0 +1,206 @@
+import auth from '../../../../Auth.js';
+import GlobalOptions from '../../../../GlobalOptions.js';
+import { Logger } from '../../../../cli/Logger.js';
+import { aadUser } from '../../../../utils/aadUser.js';
+import { accessToken } from '../../../../utils/accessToken.js';
+import { validation } from '../../../../utils/validation.js';
+import GraphCommand from "../../../base/GraphCommand.js";
+import commands from '../../commands.js';
+import request, { CliRequestOptions } from '../../../../request.js';
+import { OnlineMeeting } from '@microsoft/microsoft-graph-types';
+
+interface CommandArgs {
+ options: Options;
+}
+
+interface Options extends GlobalOptions {
+ startTime?: string;
+ endTime?: string;
+ subject?: string;
+ participants?: string;
+ organizerEmail?: string;
+ recordAutomatically?: boolean;
+}
+
+class TeamsMeetingCreateCommand extends GraphCommand {
+ public get name(): string {
+ return commands.MEETING_CREATE;
+ }
+
+ public get description(): string {
+ return 'Create a new online meeting';
+ }
+
+ public defaultProperties(): string[] | undefined {
+ return ['subject', 'startDateTime', 'endDateTime', 'joinUrl', 'recordAutomatically'];
+ }
+
+ constructor() {
+ super();
+
+ this.#initTelemetry();
+ this.#initOptions();
+ this.#initValidators();
+ }
+
+ #initTelemetry(): void {
+ this.telemetry.push((args: CommandArgs) => {
+ Object.assign(this.telemetryProperties, {
+ startTime: typeof args.options.startTime !== 'undefined',
+ endTime: typeof args.options.endTime !== 'undefined',
+ subject: typeof args.options.subject !== 'undefined',
+ participants: typeof args.options.participants !== 'undefined',
+ organizerEmail: typeof args.options.organizerEmail !== 'undefined',
+ recordAutomatically: !!args.options.recordAutomatically
+ });
+ });
+ }
+
+ #initOptions(): void {
+ this.options.unshift(
+ {
+ option: '-s --startTime [startTime]'
+ },
+ {
+ option: '-e --endTime [endTime]'
+ },
+ {
+ option: '-s --subject [subject]'
+ },
+ {
+ option: '-p --participants [participants]'
+ },
+ {
+ option: '--organizerEmail [organizerEmail]'
+ },
+ {
+ option: '-r --recordAutomatically'
+ }
+ );
+ }
+
+ #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.';
+ }
+ if (args.options.endTime && !args.options.startTime) {
+ return 'startTime should be specified when endTime is specified.';
+ }
+ if (args.options.participants) {
+ if (args.options.participants.indexOf(',') === -1 && !validation.isValidUserPrincipalName(args.options.participants)) {
+ return `${args.options.participants} contains invalid UPN.`;
+ }
+ const participants = args.options.participants.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.`;
+ }
+ }
+ if (args.options.organizerEmail && !validation.isValidUserPrincipalName(args.options.organizerEmail)) {
+ return `'${args.options.organizerEmail}' is not a valid email for organizerEmail.`;
+ }
+ return true;
+ }
+ );
+ }
+
+ /**
+ * Executes the command
+ * @param logger Logger instance
+ * @param args Command arguments
+ */
+ 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);
+ const meeting = await this.createMeeting(logger, graphBaseUrl, args.options);
+ await logger.log(meeting);
+ }
+ catch (err: any) {
+ 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
+ * @param graphBaseUrl
+ * @param options
+ * @returns MS Graph online meeting response
+ */
+ private async createMeeting(logger: Logger, graphBaseUrl: string, options: Options): Promise {
+ if (this.verbose) {
+ logger.logToStderr(`Creation of a meeting...`);
+ }
+ const requestData: any = {};
+ if (options.participants) {
+ const attendees = options.participants.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.subject) {
+ requestData.subject = options.subject;
+ }
+ if (options.recordAutomatically !== undefined) {
+ requestData.recordAutomatically = options.recordAutomatically;
+ }
+ const requestOption: CliRequestOptions = {
+ headers: {
+ accept: 'application/json',
+ 'content-type': 'application/json'
+ },
+ responseType: 'json',
+ method: 'POST',
+ url: `${graphBaseUrl}/onlineMeetings`,
+ data: requestData
+ };
+
+ try {
+ const requestResponse = await request.post(requestOption);
+ 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