diff --git a/docs/docs/cmd/teams/meeting/meeting-list.mdx b/docs/docs/cmd/teams/meeting/meeting-list.mdx index 872790add1c..f2e0a7b8a31 100644 --- a/docs/docs/cmd/teams/meeting/meeting-list.mdx +++ b/docs/docs/cmd/teams/meeting/meeting-list.mdx @@ -15,39 +15,39 @@ m365 teams meeting list [options] ## Options ```md definition-list -`-u, --userId [userId]` -: The id of the user or shared mailbox, omit to retrieve meetings for current signed in user. Use either `id`, `userName` or `email`, but not multiple. - -`-n, --userName [userName]` -: The name of the user or shared mailbox, omit to retrieve meetings for current signed in user. Use either `id`, `userName` or `email`, but not multiple. - -`--email [email]` -: The email of the user or shared mailbox, omit to retrieve meetings for current signed in user. Use either `id`, `userName` or `email`, but not multiple. - `--startDateTime ` -: The startdate used to query for meetings. +: Time indicating the **inclusive** start of a time range of content to return. `--endDateTime [endDateTime]` -: The enddate used to query for meetings. +: Time indicating the **exclusive** end of a time range of content to return. `--isOrganizer` : Set to retrieve only meetings the user is organizer for. + +`-u, --userId [userId]` +: The id of the user or shared mailbox, omit to retrieve meetings for current signed in user. Use either `id`, `userName` or `email`, but not multiple. Use only when using application permissions. + +`-n, --userName [userName]` +: The name of the user or shared mailbox, omit to retrieve meetings for current signed in user. Use either `id`, `userName` or `email`, but not multiple. Use only when using application permissions. + +`--email [email]` +: The email of the user or shared mailbox, omit to retrieve meetings for current signed in user. Use either `id`, `userName` or `email`, but not multiple. Use only when using application permissions. ``` ## Examples -Lists all meetings for a specific user retrieved by email of which the user is organizer of started after a specific datetime +List all meetings of the currently logged in user between two specific dates ```sh -m365 teams meeting list --startDateTime '2022-01-01T10:00:00Z' --email user@tenant.com --isOrganizer +m365 teams meeting list --startDateTime '2022-01-01T10:00:00Z' --endDateTime '2022-03-31T23:59:59Z' ``` -List all meeting of the currently logged in user between two specific dates +List all meetings for a specific user retrieved by email of which the user is an organizer using application permissions ```sh -m365 teams meeting list --startDateTime '2022-01-01T10:00:00Z' --endDateTime '2022-03-31T23:59:59Z' +m365 teams meeting list --startDateTime '2022-01-01T10:00:00Z' --email user@tenant.com --isOrganizer ``` ## Response @@ -58,83 +58,91 @@ m365 teams meeting list --startDateTime '2022-01-01T10:00:00Z' --endDateTime '20 ```json [ { - "id": "AAMkADgzN2Q1NThiLTI0NjYtNGIxYS05MDdjLTg1OWQxNzgwZGM2ZgBGAAAAAAC6jQfUzacTSIHqMw2yacnUBwBiOC8xvYmdT6G2E_hLMK5kAAAAAAENAABiOC8xvYmdT6G2E_hLMK5kAAIw3TQIAAA=", - "createdDateTime": "2022-06-26T12:39:34.224055Z", - "lastModifiedDateTime": "2022-06-26T12:41:36.4357085Z", - "changeKey": "YjgvMb2JnU+hthPoSzCuZAACMHITIQ==", - "categories": [], - "transactionId": null, - "originalStartTimeZone": "W. Europe Standard Time", - "originalEndTimeZone": "W. Europe Standard Time", - "iCalUId": "040000008200E00074C5B7101A82E008000000001AF70ACA5989D801000000000000000010000000048716A892ACAE4DB6CC16097796C401", - "reminderMinutesBeforeStart": 15, - "isReminderOn": true, - "hasAttachments": false, - "subject": "Test meeting", - "bodyPreview": "________________________________________________________________________________\r\\\nMicrosoft Teams meeting\r\\\nJoin on your computer or mobile app\r\\\nClick here to join the meeting\r\\\nLearn More | Meeting options\r\\\n___________________________________________", - "importance": "normal", - "sensitivity": "normal", - "isAllDay": false, - "isCancelled": false, - "isOrganizer": true, - "responseRequested": true, - "seriesMasterId": null, - "showAs": "busy", - "type": "singleInstance", - "webLink": "https://outlook.office365.com/owa/?itemid=AAMkADgzN2Q1NThiLTI0NjYtNGIxYS05MDdjLTg1OWQxNzgwZGM2ZgBGAAAAAAC6jQfUzacTSIHqMw2yacnUBwBiOC8xvYmdT6G2E%2BhLMK5kAAAAAAENAABiOC8xvYmdT6G2E%2BhLMK5kAAIw3TQIAAA%3D&exvsurl=1&path=/calendar/item", - "onlineMeetingUrl": null, - "isOnlineMeeting": true, - "onlineMeetingProvider": "teamsForBusiness", - "allowNewTimeProposals": true, - "occurrenceId": null, - "isDraft": false, - "hideAttendees": false, - "responseStatus": { - "response": "organizer", - "time": "0001-01-01T00:00:00Z" - }, - "body": { - "contentType": "html", - "content": "\r\\\n\r\\\n\r\\\n\r\\\n\r\\\n

\r\\\n
\r\\\n
\r\\\n
________________________________________________________________________________\r\\\n
\r\\\n
\r\\\n
Microsoft Teams meeting\r\\\n
\r\\\n
\r\\\n
Join on your computer or mobile app\r\\\n
\r\\\nClick\r\\\n here to join the meeting
\r\\\n
Learn More\r\\\n | \r\\\nMeeting options
\r\\\n
\r\\\n
\r\\\n
\r\\\n
\r\\\n
\r\\\n
________________________________________________________________________________\r\\\n
\r\\\n
\r\\\n\r\\\n\r\\\n" + "id": "MSpiMjA5MWUxOC03ODgyLTRlZmUtYjdkMS05MDcwM2Y1YTVjNjUqMCoqMTk6bWVldGluZ19NakEyWkRrNU5tSXRZak15TVMwMFpURTVMVGxqWW1ZdE9ERmpaVGhrTkRVd016ZGlAdGhyZWFkLnYy", + "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/19%3ameeting_MjA2ZDk5NmItYjMyMS00ZTE5LTljYmYtODFjZThkNDUwMzdi%40thread.v2/0?context=%7b%22Tid%22%3a%22ad4f158a-97c7-4914-a9bd-038ecde40ff3%22%2c%22Oid%22%3a%22b2091e18-7882-4efe-b7d1-90703f5a5c65%22%7d", + "joinWebUrl": "https://teams.microsoft.com/l/meetup-join/19%3ameeting_MjA2ZDk5NmItYjMyMS00ZTE5LTljYmYtODFjZThkNDUwMzdi%40thread.v2/0?context=%7b%22Tid%22%3a%22ad4f158a-97c7-4914-a9bd-038ecde40ff3%22%2c%22Oid%22%3a%22b2091e18-7882-4efe-b7d1-90703f5a5c65%22%7d", + "meetingCode": "396464591835", + "subject": "Team meeting", + "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" + } + } + } + ] }, - "start": { - "dateTime": "2022-06-26T12:30:00.0000000", - "timeZone": "UTC" + "lobbyBypassSettings": { + "scope": "unknownFutureValue", + "isDialInBypassEnabled": false }, - "end": { - "dateTime": "2022-06-26T13:00:00.0000000", - "timeZone": "UTC" + "joinMeetingIdSettings": { + "isPasscodeRequired": true, + "joinMeetingId": "396464591835", + "passcode": "Z3GYtQ" }, - "location": { - "displayName": "", - "locationType": "default", - "uniqueIdType": "unknown", - "address": {}, - "coordinates": {} + "chatInfo": { + "threadId": "19:meeting_MjA2ZDk5NmItYjMyMS00ZTE5LTljYmYtODFjZThkNDUwMzdi@thread.v2", + "messageId": "0", + "replyChainMessageId": null }, - "locations": [], - "recurrence": null, - "attendees": [ - { - "type": "required", - "status": { - "response": "none", - "time": "0001-01-01T00:00:00Z" - }, - "emailAddress": { - "name": "User D", - "address": "userD@outlook.com" - } - } - ], - "organizer": { - "emailAddress": { - "name": "User", - "address": "user@tenant.com" - } + "joinInformation": { + "content": "data:text/html,%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%0d%0a+%0d%0a+%3cdiv+class%3d%22me-email-text%22+style%3d%22color%3a%23252424%3bfont-family%3a%27Segoe+UI%27%2c%27Helvetica+Neue%27%2cHelvetica%2cArial%2csans-serif%3b%22+lang%3d%22en-US%22%3e%0d%0a++++%3cdiv+style%3d%22margin-top%3a+24px%3b+margin-bottom%3a+20px%3b%22%3e%0d%0a++++++++%3cspan+style%3d%22font-size%3a+24px%3b+color%3a%23252424%22%3eMicrosoft+Teams+meeting%3c%2fspan%3e%0d%0a++++%3c%2fdiv%3e%0d%0a++++%3cdiv+style%3d%22margin-bottom%3a+20px%3b%22%3e%0d%0a++++++++%3cdiv+style%3d%22margin-top%3a+0px%3b+margin-bottom%3a+0px%3b+font-weight%3a+bold%22%3e%0d%0a++++++++++%3cspan+style%3d%22font-size%3a+14px%3b+color%3a%23252424%22%3eJoin+on+your+computer%2c+mobile+app+or+room+device%3c%2fspan%3e%0d%0a++++++++%3c%2fdiv%3e%0d%0a++++++++%3ca+class%3d%22me-email-headline%22+style%3d%22font-size%3a+14px%3bfont-family%3a%27Segoe+UI+Semibold%27%2c%27Segoe+UI%27%2c%27Helvetica+Neue%27%2cHelvetica%2cArial%2csans-serif%3btext-decoration%3a+underline%3bcolor%3a+%236264a7%3b%22+href%3d%22https%3a%2f%2fteams.microsoft.com%2fl%2fmeetup-join%2f19%253ameeting_MjA2ZDk5NmItYjMyMS00ZTE5LTljYmYtODFjZThkNDUwMzdi%2540thread.v2%2f0%3fcontext%3d%257b%2522Tid%2522%253a%2522ad4f158a-97c7-4914-a9bd-038ecde40ff3%2522%252c%2522Oid%2522%253a%2522b2091e18-7882-4efe-b7d1-90703f5a5c65%2522%257d%22+target%3d%22_blank%22+rel%3d%22noreferrer+noopener%22%3eClick+here+to+join+the+meeting%3c%2fa%3e%0d%0a++++%3c%2fdiv%3e%0d%0a++++%3cdiv+style%3d%22margin-bottom%3a20px%3b+margin-top%3a20px%22%3e%0d%0a++++%3cdiv+style%3d%22margin-bottom%3a4px%22%3e%0d%0a++++++++%3cspan+data-tid%3d%22meeting-code%22+style%3d%22font-size%3a+14px%3b+color%3a%23252424%3b%22%3e%0d%0a++++++++++++Meeting+ID%3a+%3cspan+style%3d%22font-size%3a16px%3b+color%3a%23252424%3b%22%3e396+464+591+835%3c%2fspan%3e%0d%0a+++++++%3c%2fspan%3e%0d%0a+++++++++++%3cbr+%2f%3e%3cspan+style%3d%22font-size%3a+14px%3b+color%3a%23252424%3b%22%3e+Passcode%3a+%3c%2fspan%3e+%3cspan+style%3d%22font-size%3a+16px%3b+color%3a%23252424%3b%22%3e+Z3GYtQ+%3c%2fspan%3e%0d%0a++++++++%3cdiv+style%3d%22font-size%3a+14px%3b%22%3e%3ca+class%3d%22me-email-link%22+style%3d%22font-size%3a+14px%3btext-decoration%3a+underline%3bcolor%3a+%236264a7%3bfont-family%3a%27Segoe+UI%27%2c%27Helvetica+Neue%27%2cHelvetica%2cArial%2csans-serif%3b%22+target%3d%22_blank%22+href%3d%22https%3a%2f%2fwww.microsoft.com%2fen-us%2fmicrosoft-teams%2fdownload-app%22+rel%3d%22noreferrer+noopener%22%3e%0d%0a++++++++Download+Teams%3c%2fa%3e+%7c+%3ca+class%3d%22me-email-link%22+style%3d%22font-size%3a+14px%3btext-decoration%3a+underline%3bcolor%3a+%236264a7%3bfont-family%3a%27Segoe+UI%27%2c%27Helvetica+Neue%27%2cHelvetica%2cArial%2csans-serif%3b%22+target%3d%22_blank%22+href%3d%22https%3a%2f%2fwww.microsoft.com%2fmicrosoft-teams%2fjoin-a-meeting%22+rel%3d%22noreferrer+noopener%22%3eJoin+on+the+web%3c%2fa%3e%3c%2fdiv%3e%0d%0a++++%3c%2fdiv%3e%0d%0a+%3c%2fdiv%3e%0d%0a++++%0d%0a++++++%0d%0a++++%0d%0a++++%0d%0a++++%0d%0a++++%3cdiv+style%3d%22margin-bottom%3a+24px%3bmargin-top%3a+20px%3b%22%3e%0d%0a++++++++%3ca+class%3d%22me-email-link%22+style%3d%22font-size%3a+14px%3btext-decoration%3a+underline%3bcolor%3a+%236264a7%3bfont-family%3a%27Segoe+UI%27%2c%27Helvetica+Neue%27%2cHelvetica%2cArial%2csans-serif%3b%22+target%3d%22_blank%22+href%3d%22https%3a%2f%2faka.ms%2fJoinTeamsMeeting%22+rel%3d%22noreferrer+noopener%22%3eLearn+More%3c%2fa%3e++%7c+%3ca+class%3d%22me-email-link%22+style%3d%22font-size%3a+14px%3btext-decoration%3a+underline%3bcolor%3a+%236264a7%3bfont-family%3a%27Segoe+UI%27%2c%27Helvetica+Neue%27%2cHelvetica%2cArial%2csans-serif%3b%22+target%3d%22_blank%22+href%3d%22https%3a%2f%2fteams.microsoft.com%2fmeetingOptions%2f%3forganizerId%3db2091e18-7882-4efe-b7d1-90703f5a5c65%26tenantId%3dad4f158a-97c7-4914-a9bd-038ecde40ff3%26threadId%3d19_meeting_MjA2ZDk5NmItYjMyMS00ZTE5LTljYmYtODFjZThkNDUwMzdi%40thread.v2%26messageId%3d0%26language%3den-US%22+rel%3d%22noreferrer+noopener%22%3eMeeting+options%3c%2fa%3e+%0d%0a++++++%3c%2fdiv%3e%0d%0a%3c%2fdiv%3e%0d%0a%3cdiv+style%3d%22font-size%3a+14px%3b+margin-bottom%3a+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" }, - "onlineMeeting": { - "joinUrl": "https://teams.microsoft.com/l/meetup-join/19%3ameeting_OWIwM2MzNmQtZmY1My00MzM0LWIxMGQtYzkyNzI3OWU5ODMx%40thread.v2/0?context=%7b%22Tid%22%3a%22e1dd4023-a656-480a-8a0e-c1b1eec51e1d%22%2c%22Oid%22%3a%22fe36f75e-c103-410b-a18a-2bf6df06ac3a%22%7d" + "watermarkProtection": { + "isEnabledForContentSharing": false, + "isEnabledForVideo": false } } ] @@ -144,17 +152,52 @@ m365 teams meeting list --startDateTime '2022-01-01T10:00:00Z' --endDateTime '20 ```text - subject start end - ------------------- -------------------- -------------------- - Meeting title 26/06/2022, 12:30:00 26/06/2022, 13:00:00 + subject startDateTime endDateTime + ------------ -------------------- -------------------- + Team meeting 2023-07-17T03:00:00Z 2023-07-17T04:00:00Z ``` ```csv - subject,start,end - Meeting title,"26/06/2022, 12:30:00","26/06/2022, 13:00:00" + id,creationDateTime,startDateTime,endDateTime,joinUrl,joinWebUrl,meetingCode,subject,isBroadcast,autoAdmittedUsers,isEntryExitAnnounced,allowedPresenters,allowMeetingChat,shareMeetingChatHistoryDefault,allowTeamworkReactions,allowAttendeeToEnableMic,allowAttendeeToEnableCamera,recordAutomatically,allowParticipantsToChangeName,allowRecording,allowTranscription + MSpiMjA5MWUxOC03ODgyLTRlZmUtYjdkMS05MDcwM2Y1YTVjNjUqMCoqMTk6bWVldGluZ19NakEyWkRrNU5tSXRZak15TVMwMFpURTVMVGxqWW1ZdE9ERmpaVGhrTkRVd016ZGlAdGhyZWFkLnYy,2023-07-25T19:29:32.033109Z,2023-07-17T03:00:00Z,2023-07-17T04:00:00Z,https://teams.microsoft.com/l/meetup-join/19%3ameeting_MjA2ZDk5NmItYjMyMS00ZTE5LTljYmYtODFjZThkNDUwMzdi%40thread.v2/0?context=%7b%22Tid%22%3a%22ad4f158a-97c7-4914-a9bd-038ecde40ff3%22%2c%22Oid%22%3a%22b2091e18-7882-4efe-b7d1-90703f5a5c65%22%7d,https://teams.microsoft.com/l/meetup-join/19%3ameeting_MjA2ZDk5NmItYjMyMS00ZTE5LTljYmYtODFjZThkNDUwMzdi%40thread.v2/0?context=%7b%22Tid%22%3a%22ad4f158a-97c7-4914-a9bd-038ecde40ff3%22%2c%22Oid%22%3a%22b2091e18-7882-4efe-b7d1-90703f5a5c65%22%7d,396464591835,Team meeting,,unknownFutureValue,,everyone,enabled,none,1,1,1,,,1,1 + ``` + + + + + ```md + # teams meeting list --startDateTime "2023-07-17" + + Date: 25/7/2023 + + ## MSpiMjA5MWUxOC03ODgyLTRlZmUtYjdkMS05MDcwM2Y1YTVjNjUqMCoqMTk6bWVldGluZ19NakEyWkRrNU5tSXRZak15TVMwMFpURTVMVGxqWW1ZdE9ERmpaVGhrTkRVd016ZGlAdGhyZWFkLnYy + + Property | Value + ---------|------- + id | MSpiMjA5MWUxOC03ODgyLTRlZmUtYjdkMS05MDcwM2Y1YTVjNjUqMCoqMTk6bWVldGluZ19NakEyWkRrNU5tSXRZak15TVMwMFpURTVMVGxqWW1ZdE9ERmpaVGhrTkRVd016ZGlAdGhyZWFkLnYy + 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/19%3ameeting\_MjA2ZDk5NmItYjMyMS00ZTE5LTljYmYtODFjZThkNDUwMzdi%40thread.v2/0?context=%7b%22Tid%22%3a%22ad4f158a-97c7-4914-a9bd-038ecde40ff3%22%2c%22Oid%22%3a%22b2091e18-7882-4efe-b7d1-90703f5a5c65%22%7d + joinWebUrl | https://teams.microsoft.com/l/meetup-join/19%3ameeting\_MjA2ZDk5NmItYjMyMS00ZTE5LTljYmYtODFjZThkNDUwMzdi%40thread.v2/0?context=%7b%22Tid%22%3a%22ad4f158a-97c7-4914-a9bd-038ecde40ff3%22%2c%22Oid%22%3a%22b2091e18-7882-4efe-b7d1-90703f5a5c65%22%7d + meetingCode | 396464591835 + subject | Team meeting + isBroadcast | false + autoAdmittedUsers | unknownFutureValue + isEntryExitAnnounced | false + allowedPresenters | everyone + allowMeetingChat | enabled + shareMeetingChatHistoryDefault | none + allowTeamworkReactions | true + allowAttendeeToEnableMic | true + allowAttendeeToEnableCamera | true + recordAutomatically | false + allowParticipantsToChangeName | false + allowRecording | true + allowTranscription | true ``` diff --git a/docs/docs/v7-upgrade-guidance.mdx b/docs/docs/v7-upgrade-guidance.mdx index 5cc804f9835..96b57216af3 100644 --- a/docs/docs/v7-upgrade-guidance.mdx +++ b/docs/docs/v7-upgrade-guidance.mdx @@ -10,6 +10,196 @@ When doing destructive things like deleting a site, CLI for Microsoft 365 asks y Update your scripts to use the new option `force` instead of `confirm`. Verify that your scripts work as intended. +## Updated `teams meeting list` command output + +In the past versions, the command `teams meeting list` returned calendar objects as a result. Because of this, the IDs displayed in the output were not the same as the IDs used in the `teams meeting get` command and other teams meeting commands. In version 7 of the CLI for Microsoft 365, we have updated the output of the `teams meeting list` command to return meeting objects instead of calendar objects. This means that the IDs displayed in the output are now the same as the IDs used in the other `teams meeting` commands. + +**v6 JSON command output:** + +```json +[ + { + "id": "AAMkADgzN2Q1NThiLTI0NjYtNGIxYS05MDdjLTg1OWQxNzgwZGM2ZgBGAAAAAAC6jQfUzacTSIHqMw2yacnUBwBiOC8xvYmdT6G2E_hLMK5kAAAAAAENAABiOC8xvYmdT6G2E_hLMK5kAAIw3TQIAAA=", + "createdDateTime": "2022-06-26T12:39:34.224055Z", + "lastModifiedDateTime": "2022-06-26T12:41:36.4357085Z", + "changeKey": "YjgvMb2JnU+hthPoSzCuZAACMHITIQ==", + "categories": [], + "transactionId": null, + "originalStartTimeZone": "W. Europe Standard Time", + "originalEndTimeZone": "W. Europe Standard Time", + "iCalUId": "040000008200E00074C5B7101A82E008000000001AF70ACA5989D801000000000000000010000000048716A892ACAE4DB6CC16097796C401", + "reminderMinutesBeforeStart": 15, + "isReminderOn": true, + "hasAttachments": false, + "subject": "Test meeting", + "bodyPreview": "________________________________________________________________________________\r\\\nMicrosoft Teams meeting\r\\\nJoin on your computer or mobile app\r\\\nClick here to join the meeting\r\\\nLearn More | Meeting options\r\\\n___________________________________________", + "importance": "normal", + "sensitivity": "normal", + "isAllDay": false, + "isCancelled": false, + "isOrganizer": true, + "responseRequested": true, + "seriesMasterId": null, + "showAs": "busy", + "type": "singleInstance", + "webLink": "https://outlook.office365.com/owa/?itemid=AAMkADgzN2Q1NThiLTI0NjYtNGIxYS05MDdjLTg1OWQxNzgwZGM2ZgBGAAAAAAC6jQfUzacTSIHqMw2yacnUBwBiOC8xvYmdT6G2E%2BhLMK5kAAAAAAENAABiOC8xvYmdT6G2E%2BhLMK5kAAIw3TQIAAA%3D&exvsurl=1&path=/calendar/item", + "onlineMeetingUrl": null, + "isOnlineMeeting": true, + "onlineMeetingProvider": "teamsForBusiness", + "allowNewTimeProposals": true, + "occurrenceId": null, + "isDraft": false, + "hideAttendees": false, + "responseStatus": { + "response": "organizer", + "time": "0001-01-01T00:00:00Z" + }, + "body": { + "contentType": "html", + "content": "\r\\\n\r\\\n\r\\\n\r\\\n\r\\\n

\r\\\n
\r\\\n
\r\\\n
________________________________________________________________________________\r\\\n
\r\\\n
\r\\\n
Microsoft Teams meeting\r\\\n
\r\\\n
\r\\\n
Join on your computer or mobile app\r\\\n
\r\\\nClick\r\\\n here to join the meeting
\r\\\n
Learn More\r\\\n | \r\\\nMeeting options
\r\\\n
\r\\\n
\r\\\n
\r\\\n
\r\\\n
\r\\\n
________________________________________________________________________________\r\\\n
\r\\\n
\r\\\n\r\\\n\r\\\n" + }, + "start": { + "dateTime": "2022-06-26T12:30:00.0000000", + "timeZone": "UTC" + }, + "end": { + "dateTime": "2022-06-26T13:00:00.0000000", + "timeZone": "UTC" + }, + "location": { + "displayName": "", + "locationType": "default", + "uniqueIdType": "unknown", + "address": {}, + "coordinates": {} + }, + "locations": [], + "recurrence": null, + "attendees": [ + { + "type": "required", + "status": { + "response": "none", + "time": "0001-01-01T00:00:00Z" + }, + "emailAddress": { + "name": "User D", + "address": "userD@outlook.com" + } + } + ], + "organizer": { + "emailAddress": { + "name": "User", + "address": "user@tenant.com" + } + }, + "onlineMeeting": { + "joinUrl": "https://teams.microsoft.com/l/meetup-join/19%3ameeting_OWIwM2MzNmQtZmY1My00MzM0LWIxMGQtYzkyNzI3OWU5ODMx%40thread.v2/0?context=%7b%22Tid%22%3a%22e1dd4023-a656-480a-8a0e-c1b1eec51e1d%22%2c%22Oid%22%3a%22fe36f75e-c103-410b-a18a-2bf6df06ac3a%22%7d" + } + } +] +``` + +**v7 JSON command output:** + +```json +[ + { + "id": "MSpiMjA5MWUxOC03ODgyLTRlZmUtYjdkMS05MDcwM2Y1YTVjNjUqMCoqMTk6bWVldGluZ19NakEyWkRrNU5tSXRZak15TVMwMFpURTVMVGxqWW1ZdE9ERmpaVGhrTkRVd016ZGlAdGhyZWFkLnYy", + "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/19%3ameeting_MjA2ZDk5NmItYjMyMS00ZTE5LTljYmYtODFjZThkNDUwMzdi%40thread.v2/0?context=%7b%22Tid%22%3a%22ad4f158a-97c7-4914-a9bd-038ecde40ff3%22%2c%22Oid%22%3a%22b2091e18-7882-4efe-b7d1-90703f5a5c65%22%7d", + "joinWebUrl": "https://teams.microsoft.com/l/meetup-join/19%3ameeting_MjA2ZDk5NmItYjMyMS00ZTE5LTljYmYtODFjZThkNDUwMzdi%40thread.v2/0?context=%7b%22Tid%22%3a%22ad4f158a-97c7-4914-a9bd-038ecde40ff3%22%2c%22Oid%22%3a%22b2091e18-7882-4efe-b7d1-90703f5a5c65%22%7d", + "meetingCode": "396464591835", + "subject": "Team meeting", + "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": "396464591835", + "passcode": "Z3GYtQ" + }, + "chatInfo": { + "threadId": "19:meeting_MjA2ZDk5NmItYjMyMS00ZTE5LTljYmYtODFjZThkNDUwMzdi@thread.v2", + "messageId": "0", + "replyChainMessageId": null + }, + "joinInformation": { + "content": "data:text/html,%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%0d%0a+%0d%0a+%3cdiv+class%3d%22me-email-text%22+style%3d%22color%3a%23252424%3bfont-family%3a%27Segoe+UI%27%2c%27Helvetica+Neue%27%2cHelvetica%2cArial%2csans-serif%3b%22+lang%3d%22en-US%22%3e%0d%0a++++%3cdiv+style%3d%22margin-top%3a+24px%3b+margin-bottom%3a+20px%3b%22%3e%0d%0a++++++++%3cspan+style%3d%22font-size%3a+24px%3b+color%3a%23252424%22%3eMicrosoft+Teams+meeting%3c%2fspan%3e%0d%0a++++%3c%2fdiv%3e%0d%0a++++%3cdiv+style%3d%22margin-bottom%3a+20px%3b%22%3e%0d%0a++++++++%3cdiv+style%3d%22margin-top%3a+0px%3b+margin-bottom%3a+0px%3b+font-weight%3a+bold%22%3e%0d%0a++++++++++%3cspan+style%3d%22font-size%3a+14px%3b+color%3a%23252424%22%3eJoin+on+your+computer%2c+mobile+app+or+room+device%3c%2fspan%3e%0d%0a++++++++%3c%2fdiv%3e%0d%0a++++++++%3ca+class%3d%22me-email-headline%22+style%3d%22font-size%3a+14px%3bfont-family%3a%27Segoe+UI+Semibold%27%2c%27Segoe+UI%27%2c%27Helvetica+Neue%27%2cHelvetica%2cArial%2csans-serif%3btext-decoration%3a+underline%3bcolor%3a+%236264a7%3b%22+href%3d%22https%3a%2f%2fteams.microsoft.com%2fl%2fmeetup-join%2f19%253ameeting_MjA2ZDk5NmItYjMyMS00ZTE5LTljYmYtODFjZThkNDUwMzdi%2540thread.v2%2f0%3fcontext%3d%257b%2522Tid%2522%253a%2522ad4f158a-97c7-4914-a9bd-038ecde40ff3%2522%252c%2522Oid%2522%253a%2522b2091e18-7882-4efe-b7d1-90703f5a5c65%2522%257d%22+target%3d%22_blank%22+rel%3d%22noreferrer+noopener%22%3eClick+here+to+join+the+meeting%3c%2fa%3e%0d%0a++++%3c%2fdiv%3e%0d%0a++++%3cdiv+style%3d%22margin-bottom%3a20px%3b+margin-top%3a20px%22%3e%0d%0a++++%3cdiv+style%3d%22margin-bottom%3a4px%22%3e%0d%0a++++++++%3cspan+data-tid%3d%22meeting-code%22+style%3d%22font-size%3a+14px%3b+color%3a%23252424%3b%22%3e%0d%0a++++++++++++Meeting+ID%3a+%3cspan+style%3d%22font-size%3a16px%3b+color%3a%23252424%3b%22%3e396+464+591+835%3c%2fspan%3e%0d%0a+++++++%3c%2fspan%3e%0d%0a+++++++++++%3cbr+%2f%3e%3cspan+style%3d%22font-size%3a+14px%3b+color%3a%23252424%3b%22%3e+Passcode%3a+%3c%2fspan%3e+%3cspan+style%3d%22font-size%3a+16px%3b+color%3a%23252424%3b%22%3e+Z3GYtQ+%3c%2fspan%3e%0d%0a++++++++%3cdiv+style%3d%22font-size%3a+14px%3b%22%3e%3ca+class%3d%22me-email-link%22+style%3d%22font-size%3a+14px%3btext-decoration%3a+underline%3bcolor%3a+%236264a7%3bfont-family%3a%27Segoe+UI%27%2c%27Helvetica+Neue%27%2cHelvetica%2cArial%2csans-serif%3b%22+target%3d%22_blank%22+href%3d%22https%3a%2f%2fwww.microsoft.com%2fen-us%2fmicrosoft-teams%2fdownload-app%22+rel%3d%22noreferrer+noopener%22%3e%0d%0a++++++++Download+Teams%3c%2fa%3e+%7c+%3ca+class%3d%22me-email-link%22+style%3d%22font-size%3a+14px%3btext-decoration%3a+underline%3bcolor%3a+%236264a7%3bfont-family%3a%27Segoe+UI%27%2c%27Helvetica+Neue%27%2cHelvetica%2cArial%2csans-serif%3b%22+target%3d%22_blank%22+href%3d%22https%3a%2f%2fwww.microsoft.com%2fmicrosoft-teams%2fjoin-a-meeting%22+rel%3d%22noreferrer+noopener%22%3eJoin+on+the+web%3c%2fa%3e%3c%2fdiv%3e%0d%0a++++%3c%2fdiv%3e%0d%0a+%3c%2fdiv%3e%0d%0a++++%0d%0a++++++%0d%0a++++%0d%0a++++%0d%0a++++%0d%0a++++%3cdiv+style%3d%22margin-bottom%3a+24px%3bmargin-top%3a+20px%3b%22%3e%0d%0a++++++++%3ca+class%3d%22me-email-link%22+style%3d%22font-size%3a+14px%3btext-decoration%3a+underline%3bcolor%3a+%236264a7%3bfont-family%3a%27Segoe+UI%27%2c%27Helvetica+Neue%27%2cHelvetica%2cArial%2csans-serif%3b%22+target%3d%22_blank%22+href%3d%22https%3a%2f%2faka.ms%2fJoinTeamsMeeting%22+rel%3d%22noreferrer+noopener%22%3eLearn+More%3c%2fa%3e++%7c+%3ca+class%3d%22me-email-link%22+style%3d%22font-size%3a+14px%3btext-decoration%3a+underline%3bcolor%3a+%236264a7%3bfont-family%3a%27Segoe+UI%27%2c%27Helvetica+Neue%27%2cHelvetica%2cArial%2csans-serif%3b%22+target%3d%22_blank%22+href%3d%22https%3a%2f%2fteams.microsoft.com%2fmeetingOptions%2f%3forganizerId%3db2091e18-7882-4efe-b7d1-90703f5a5c65%26tenantId%3dad4f158a-97c7-4914-a9bd-038ecde40ff3%26threadId%3d19_meeting_MjA2ZDk5NmItYjMyMS00ZTE5LTljYmYtODFjZThkNDUwMzdi%40thread.v2%26messageId%3d0%26language%3den-US%22+rel%3d%22noreferrer+noopener%22%3eMeeting+options%3c%2fa%3e+%0d%0a++++++%3c%2fdiv%3e%0d%0a%3c%2fdiv%3e%0d%0a%3cdiv+style%3d%22font-size%3a+14px%3b+margin-bottom%3a+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" + }, + "watermarkProtection": { + "isEnabledForContentSharing": false, + "isEnabledForVideo": false + } + } +] +``` + +### What action do I need to take? + +Update your scripts to expect the new output format displayed above. + ## Removed option `schemaXml` for `spo list` commands In version 7 of the CLI for Microsoft 365, we have removed the option `schemaXml` for the `spo list add` and `spo list set` commands. This option was used to specify the list schema XML when creating a list. We have removed this option as it is no longer supported by Microsoft 365. diff --git a/src/m365/teams/commands/meeting/meeting-list.spec.ts b/src/m365/teams/commands/meeting/meeting-list.spec.ts index aa8e16cc178..48f5c53e1df 100644 --- a/src/m365/teams/commands/meeting/meeting-list.spec.ts +++ b/src/m365/teams/commands/meeting/meeting-list.spec.ts @@ -12,6 +12,7 @@ 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 { formatting } from '../../../../utils/formatting.js'; import commands from '../../commands.js'; import command from './meeting-list.js'; @@ -20,267 +21,123 @@ describe(commands.MEETING_LIST, () => { const startDateTime = '2022-01-01'; const endDateTime = '2022-12-31'; const userName = 'user@tenant.com'; - const meetingResponse = { - value: [ - { - "id": "AAMkADgzN2Q1NThiLTI0NjYtNGIxYS05MDdjLTg1OWQxNzgwZGM2ZgBGAAAAAAC6jQfUzacTSIHqMw2yacnUBwBiOC8xvYmdT6G2E_hLMK5kAAAAAAENAABiOC8xvYmdT6G2E_hLMK5kAAIw3TQIAAA=", - "createdDateTime": "2022-06-26T12:39:34.224055Z", - "lastModifiedDateTime": "2022-06-26T12:41:36.4357085Z", - "changeKey": "YjgvMb2JnU+hthPoSzCuZAACMHITIQ==", - "categories": [], - "transactionId": null, - "originalStartTimeZone": "W. Europe Standard Time", - "originalEndTimeZone": "W. Europe Standard Time", - "iCalUId": "040000008200E00074C5B7101A82E008000000001AF70ACA5989D801000000000000000010000000048716A892ACAE4DB6CC16097796C401", - "reminderMinutesBeforeStart": 15, - "isReminderOn": true, - "hasAttachments": false, - "subject": "Test", - "bodyPreview": "________________________________________________________________________________\r\\\nMicrosoft Teams meeting\r\\\nJoin on your computer or mobile app\r\\\nClick here to join the meeting\r\\\nLearn More | Meeting options\r\\\n___________________________________________", - "importance": "normal", - "sensitivity": "normal", - "isAllDay": false, - "isCancelled": false, - "isOrganizer": true, - "responseRequested": true, - "seriesMasterId": null, - "showAs": "busy", - "type": "singleInstance", - "webLink": "https://outlook.office365.com/owa/?itemid=AAMkADgzN2Q1NThiLTI0NjYtNGIxYS05MDdjLTg1OWQxNzgwZGM2ZgBGAAAAAAC6jQfUzacTSIHqMw2yacnUBwBiOC8xvYmdT6G2E%2BhLMK5kAAAAAAENAABiOC8xvYmdT6G2E%2BhLMK5kAAIw3TQIAAA%3D&exvsurl=1&path=/calendar/item", - "onlineMeetingUrl": null, - "isOnlineMeeting": true, - "onlineMeetingProvider": "teamsForBusiness", - "allowNewTimeProposals": true, - "occurrenceId": null, - "isDraft": false, - "hideAttendees": false, - "responseStatus": { - "response": "organizer", - "time": "0001-01-01T00:00:00Z" - }, - "body": { - "contentType": "html", - "content": "\r\\\n\r\\\n\r\\\n\r\\\n\r\\\n

\r\\\n
\r\\\n
\r\\\n
________________________________________________________________________________\r\\\n
\r\\\n
\r\\\n
Microsoft Teams meeting\r\\\n
\r\\\n
\r\\\n
Join on your computer or mobile app\r\\\n
\r\\\nClick\r\\\n here to join the meeting
\r\\\n
Learn More\r\\\n | \r\\\nMeeting options
\r\\\n
\r\\\n
\r\\\n
\r\\\n
\r\\\n
\r\\\n
________________________________________________________________________________\r\\\n
\r\\\n
\r\\\n\r\\\n\r\\\n" - }, - "start": { - "dateTime": "2022-06-26T12:30:00.0000000", - "timeZone": "UTC" - }, - "end": { - "dateTime": "2022-06-26T13:00:00.0000000", - "timeZone": "UTC" - }, - "location": { - "displayName": "", - "locationType": "default", - "uniqueIdType": "unknown", - "address": {}, - "coordinates": {} - }, - "locations": [], - "recurrence": null, - "attendees": [ - { - "type": "required", - "status": { - "response": "none", - "time": "0001-01-01T00:00:00Z" - }, - "emailAddress": { - "name": "User D", - "address": "userD@outlook.com" - } - } - ], + + // #region responses + const meetings = [ + { + "id": "MSpiMjA5MWUxOC03ODgyLTRlZmUtYjdkMS05MDcwM2Y1YTVjNjUqMCoqMTk6bWVldGluZ19NakEyWkRrNU5tSXRZak15TVMwMFpURTVMVGxqWW1ZdE9ERmpaVGhrTkRVd016ZGlAdGhyZWFkLnYy", + "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/19%3ameeting_MjA2ZDk5NmItYjMyMS00ZTE5LTljYmYtODFjZThkNDUwMzdi%40thread.v2/0?context=%7b%22Tid%22%3a%22ad4f158a-97c7-4914-a9bd-038ecde40ff3%22%2c%22Oid%22%3a%22b2091e18-7882-4efe-b7d1-90703f5a5c65%22%7d", + "joinWebUrl": "https://teams.microsoft.com/l/meetup-join/19%3ameeting_MjA2ZDk5NmItYjMyMS00ZTE5LTljYmYtODFjZThkNDUwMzdi%40thread.v2/0?context=%7b%22Tid%22%3a%22ad4f158a-97c7-4914-a9bd-038ecde40ff3%22%2c%22Oid%22%3a%22b2091e18-7882-4efe-b7d1-90703f5a5c65%22%7d", + "meetingCode": "396464591835", + "subject": "Team meeting", + "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": { - "emailAddress": { - "name": "User", - "address": "user@tenant.com" + "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" + } } }, - "onlineMeeting": { - "joinUrl": "https://teams.microsoft.com/l/meetup-join/19%3ameeting_OWIwM2MzNmQtZmY1My00MzM0LWIxMGQtYzkyNzI3OWU5ODMx%40thread.v2/0?context=%7b%22Tid%22%3a%22e1dd4023-a656-480a-8a0e-c1b1eec51e1d%22%2c%22Oid%22%3a%22fe36f75e-c103-410b-a18a-2bf6df06ac3a%22%7d" - } - }, - { - "id": "AAMkADgzN2Q1NThiLTI0NjYtNGIxYS05MDdjLTg1OWQxNzgwZGM2ZgBGAAAAAAC6jQfUzacTSIHqMw2yacnUBwBiOC8xvYmdT6G2E_hLMK5kAAAAAAENAABiOC8xvYmdT6G2E_hLMK5kAAH8dhmuAAA=", - "createdDateTime": "2022-04-08T11:48:22.2527866Z", - "lastModifiedDateTime": "2022-04-08T11:50:24.1356845Z", - "changeKey": "YjgvMb2JnU+hthPoSzCuZAAB/B2ICg==", - "categories": [], - "transactionId": null, - "originalStartTimeZone": "Romance Standard Time", - "originalEndTimeZone": "Romance Standard Time", - "iCalUId": "040000008200E00074C5B7101A82E00800000000A87B618C3E4BD8010000000000000000100000006D28750A6361354E9076FFD0D4C5452E", - "reminderMinutesBeforeStart": 15, - "isReminderOn": true, - "hasAttachments": false, - "subject": "Test", - "bodyPreview": "________________________________________________________________________________\r\\\nMicrosoft Teams-vergadering\r\\\nDeelnemen op uw computer of via de mobiele app\r\\\nKlik hier om deel te nemen aan de vergadering\r\\\nMeer informatie | Opties voor vergadering", - "importance": "normal", - "sensitivity": "normal", - "isAllDay": false, - "isCancelled": false, - "isOrganizer": true, - "responseRequested": true, - "seriesMasterId": null, - "showAs": "busy", - "type": "singleInstance", - "webLink": "https://outlook.office365.com/owa/?itemid=AAMkADgzN2Q1NThiLTI0NjYtNGIxYS05MDdjLTg1OWQxNzgwZGM2ZgBGAAAAAAC6jQfUzacTSIHqMw2yacnUBwBiOC8xvYmdT6G2E%2BhLMK5kAAAAAAENAABiOC8xvYmdT6G2E%2BhLMK5kAAH8dhmuAAA%3D&exvsurl=1&path=/calendar/item", - "onlineMeetingUrl": null, - "isOnlineMeeting": true, - "onlineMeetingProvider": "teamsForBusiness", - "allowNewTimeProposals": true, - "occurrenceId": null, - "isDraft": false, - "hideAttendees": false, - "responseStatus": { - "response": "organizer", - "time": "0001-01-01T00:00:00Z" - }, - "body": { - "contentType": "html", - "content": "\r\\\n\r\\\n\r\\\n\r\\\n\r\\\n

\r\\\n
\r\\\n
\r\\\n
________________________________________________________________________________\r\\\n
\r\\\n
\r\\\n
Microsoft Teams-vergadering\r\\\n
\r\\\n
\r\\\n
Deelnemen op uw computer of via de mobiele app\r\\\n
\r\\\nKlik\r\\\n hier om deel te nemen aan de vergadering
\r\\\n
Meer informatie\r\\\n | \r\\\nOpties voor vergadering
\r\\\n
\r\\\n
\r\\\n
\r\\\n
\r\\\n
\r\\\n
________________________________________________________________________________\r\\\n
\r\\\n
\r\\\n\r\\\n\r\\\n" - }, - "start": { - "dateTime": "2022-04-08T11:30:00.0000000", - "timeZone": "UTC" - }, - "end": { - "dateTime": "2022-04-08T12:00:00.0000000", - "timeZone": "UTC" - }, - "location": { - "displayName": "", - "locationType": "default", - "uniqueIdType": "unknown", - "address": {}, - "coordinates": {} - }, - "locations": [], - "recurrence": null, "attendees": [ { - "type": "required", - "status": { - "response": "none", - "time": "0001-01-01T00:00:00Z" - }, - "emailAddress": { - "name": "User A", - "address": "userA@tenant.com" + "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" + } } } - ], - "organizer": { - "emailAddress": { - "name": "User B", - "address": "user@tenant.com" - } - }, - "onlineMeeting": { - "joinUrl": "https://teams.microsoft.com/l/meetup-join/19%3ameeting_MjM1ZDM1ZjYtNTgwOC00MWM4LThiYWItNmZhNmM3MTJjZGZm%40thread.v2/0?context=%7b%22Tid%22%3a%22e1dd4023-a656-480a-8a0e-c1b1eec51e1d%22%2c%22Oid%22%3a%22fe36f75e-c103-410b-a18a-2bf6df06ac3a%22%7d" - } + ] + }, + "lobbyBypassSettings": { + "scope": "unknownFutureValue", + "isDialInBypassEnabled": false + }, + "joinMeetingIdSettings": { + "isPasscodeRequired": true, + "joinMeetingId": "396464591835", + "passcode": "Z3GYtQ" }, + "chatInfo": { + "threadId": "19:meeting_MjA2ZDk5NmItYjMyMS00ZTE5LTljYmYtODFjZThkNDUwMzdi@thread.v2", + "messageId": "0", + "replyChainMessageId": null + }, + "joinInformation": { + "content": "data:text/html,%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%0d%0a+%0d%0a+%3cdiv+class%3d%22me-email-text%22+style%3d%22color%3a%23252424%3bfont-family%3a%27Segoe+UI%27%2c%27Helvetica+Neue%27%2cHelvetica%2cArial%2csans-serif%3b%22+lang%3d%22en-US%22%3e%0d%0a++++%3cdiv+style%3d%22margin-top%3a+24px%3b+margin-bottom%3a+20px%3b%22%3e%0d%0a++++++++%3cspan+style%3d%22font-size%3a+24px%3b+color%3a%23252424%22%3eMicrosoft+Teams+meeting%3c%2fspan%3e%0d%0a++++%3c%2fdiv%3e%0d%0a++++%3cdiv+style%3d%22margin-bottom%3a+20px%3b%22%3e%0d%0a++++++++%3cdiv+style%3d%22margin-top%3a+0px%3b+margin-bottom%3a+0px%3b+font-weight%3a+bold%22%3e%0d%0a++++++++++%3cspan+style%3d%22font-size%3a+14px%3b+color%3a%23252424%22%3eJoin+on+your+computer%2c+mobile+app+or+room+device%3c%2fspan%3e%0d%0a++++++++%3c%2fdiv%3e%0d%0a++++++++%3ca+class%3d%22me-email-headline%22+style%3d%22font-size%3a+14px%3bfont-family%3a%27Segoe+UI+Semibold%27%2c%27Segoe+UI%27%2c%27Helvetica+Neue%27%2cHelvetica%2cArial%2csans-serif%3btext-decoration%3a+underline%3bcolor%3a+%236264a7%3b%22+href%3d%22https%3a%2f%2fteams.microsoft.com%2fl%2fmeetup-join%2f19%253ameeting_MjA2ZDk5NmItYjMyMS00ZTE5LTljYmYtODFjZThkNDUwMzdi%2540thread.v2%2f0%3fcontext%3d%257b%2522Tid%2522%253a%2522ad4f158a-97c7-4914-a9bd-038ecde40ff3%2522%252c%2522Oid%2522%253a%2522b2091e18-7882-4efe-b7d1-90703f5a5c65%2522%257d%22+target%3d%22_blank%22+rel%3d%22noreferrer+noopener%22%3eClick+here+to+join+the+meeting%3c%2fa%3e%0d%0a++++%3c%2fdiv%3e%0d%0a++++%3cdiv+style%3d%22margin-bottom%3a20px%3b+margin-top%3a20px%22%3e%0d%0a++++%3cdiv+style%3d%22margin-bottom%3a4px%22%3e%0d%0a++++++++%3cspan+data-tid%3d%22meeting-code%22+style%3d%22font-size%3a+14px%3b+color%3a%23252424%3b%22%3e%0d%0a++++++++++++Meeting+ID%3a+%3cspan+style%3d%22font-size%3a16px%3b+color%3a%23252424%3b%22%3e396+464+591+835%3c%2fspan%3e%0d%0a+++++++%3c%2fspan%3e%0d%0a+++++++++++%3cbr+%2f%3e%3cspan+style%3d%22font-size%3a+14px%3b+color%3a%23252424%3b%22%3e+Passcode%3a+%3c%2fspan%3e+%3cspan+style%3d%22font-size%3a+16px%3b+color%3a%23252424%3b%22%3e+Z3GYtQ+%3c%2fspan%3e%0d%0a++++++++%3cdiv+style%3d%22font-size%3a+14px%3b%22%3e%3ca+class%3d%22me-email-link%22+style%3d%22font-size%3a+14px%3btext-decoration%3a+underline%3bcolor%3a+%236264a7%3bfont-family%3a%27Segoe+UI%27%2c%27Helvetica+Neue%27%2cHelvetica%2cArial%2csans-serif%3b%22+target%3d%22_blank%22+href%3d%22https%3a%2f%2fwww.microsoft.com%2fen-us%2fmicrosoft-teams%2fdownload-app%22+rel%3d%22noreferrer+noopener%22%3e%0d%0a++++++++Download+Teams%3c%2fa%3e+%7c+%3ca+class%3d%22me-email-link%22+style%3d%22font-size%3a+14px%3btext-decoration%3a+underline%3bcolor%3a+%236264a7%3bfont-family%3a%27Segoe+UI%27%2c%27Helvetica+Neue%27%2cHelvetica%2cArial%2csans-serif%3b%22+target%3d%22_blank%22+href%3d%22https%3a%2f%2fwww.microsoft.com%2fmicrosoft-teams%2fjoin-a-meeting%22+rel%3d%22noreferrer+noopener%22%3eJoin+on+the+web%3c%2fa%3e%3c%2fdiv%3e%0d%0a++++%3c%2fdiv%3e%0d%0a+%3c%2fdiv%3e%0d%0a++++%0d%0a++++++%0d%0a++++%0d%0a++++%0d%0a++++%0d%0a++++%3cdiv+style%3d%22margin-bottom%3a+24px%3bmargin-top%3a+20px%3b%22%3e%0d%0a++++++++%3ca+class%3d%22me-email-link%22+style%3d%22font-size%3a+14px%3btext-decoration%3a+underline%3bcolor%3a+%236264a7%3bfont-family%3a%27Segoe+UI%27%2c%27Helvetica+Neue%27%2cHelvetica%2cArial%2csans-serif%3b%22+target%3d%22_blank%22+href%3d%22https%3a%2f%2faka.ms%2fJoinTeamsMeeting%22+rel%3d%22noreferrer+noopener%22%3eLearn+More%3c%2fa%3e++%7c+%3ca+class%3d%22me-email-link%22+style%3d%22font-size%3a+14px%3btext-decoration%3a+underline%3bcolor%3a+%236264a7%3bfont-family%3a%27Segoe+UI%27%2c%27Helvetica+Neue%27%2cHelvetica%2cArial%2csans-serif%3b%22+target%3d%22_blank%22+href%3d%22https%3a%2f%2fteams.microsoft.com%2fmeetingOptions%2f%3forganizerId%3db2091e18-7882-4efe-b7d1-90703f5a5c65%26tenantId%3dad4f158a-97c7-4914-a9bd-038ecde40ff3%26threadId%3d19_meeting_MjA2ZDk5NmItYjMyMS00ZTE5LTljYmYtODFjZThkNDUwMzdi%40thread.v2%26messageId%3d0%26language%3den-US%22+rel%3d%22noreferrer+noopener%22%3eMeeting+options%3c%2fa%3e+%0d%0a++++++%3c%2fdiv%3e%0d%0a%3c%2fdiv%3e%0d%0a%3cdiv+style%3d%22font-size%3a+14px%3b+margin-bottom%3a+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" + }, + "watermarkProtection": { + "isEnabledForContentSharing": false, + "isEnabledForVideo": false + } + } + ]; + + const calendarMeetingsResponse = { + value: [ { - "id": "AAMkADgzN2Q1NThiLTI0NjYtNGIxYS05MDdjLTg1OWQxNzgwZGM2ZgBGAAAAAAC6jQfUzacTSIHqMw2yacnUBwBiOC8xvYmdT6G2E_hLMK5kAAAAAAENAABiOC8xvYmdT6G2E_hLMK5kAAHxtR_EAAA=", - "createdDateTime": "2022-03-23T14:41:00.1950925Z", - "lastModifiedDateTime": "2022-03-23T14:43:02.1403526Z", - "changeKey": "YjgvMb2JnU+hthPoSzCuZAAB8WHbHA==", - "categories": [], - "transactionId": "2f831e09-5507-24ba-2352-bc29160933ef", - "originalStartTimeZone": "Aleutian Standard Time", - "originalEndTimeZone": "Aleutian Standard Time", - "iCalUId": "040000008200E00074C5B7101A82E0080000000095AA9303C43ED801000000000000000010000000EDB19B20BAF3C548841220C2102492CB", - "reminderMinutesBeforeStart": 15, - "isReminderOn": true, - "hasAttachments": false, - "subject": "Online meeting test", - "bodyPreview": "________________________________________________________________________________\r\\\nMicrosoft Teams meeting\r\\\nJoin on your computer or mobile app\r\\\nClick here to join the meeting\r\\\nLearn More | Meeting options\r\\\n_______________________________________________", - "importance": "normal", - "sensitivity": "normal", - "isAllDay": false, - "isCancelled": false, - "isOrganizer": true, - "responseRequested": true, - "seriesMasterId": null, - "showAs": "busy", - "type": "singleInstance", - "webLink": "https://outlook.office365.com/owa/?itemid=AAMkADgzN2Q1NThiLTI0NjYtNGIxYS05MDdjLTg1OWQxNzgwZGM2ZgBGAAAAAAC6jQfUzacTSIHqMw2yacnUBwBiOC8xvYmdT6G2E%2BhLMK5kAAAAAAENAABiOC8xvYmdT6G2E%2BhLMK5kAAHxtR%2BEAAA%3D&exvsurl=1&path=/calendar/item", - "onlineMeetingUrl": null, - "isOnlineMeeting": true, - "onlineMeetingProvider": "teamsForBusiness", - "allowNewTimeProposals": true, - "occurrenceId": null, - "isDraft": false, - "hideAttendees": false, - "responseStatus": { - "response": "organizer", - "time": "0001-01-01T00:00:00Z" - }, - "body": { - "contentType": "html", - "content": "\r\\\n\r\\\n\r\\\n\r\\\n\r\\\n
\r\\\n
________________________________________________________________________________\r\\\n
\r\\\n
\r\\\n
Microsoft Teams meeting\r\\\n
\r\\\n
\r\\\n
Join on your computer or mobile app\r\\\n
\r\\\nClick\r\\\n here to join the meeting
\r\\\n
Learn More\r\\\n | \r\\\nMeeting options
\r\\\n
\r\\\n
\r\\\n
\r\\\n
\r\\\n
\r\\\n
________________________________________________________________________________\r\\\n
\r\\\n\r\\\n\r\\\n" - }, - "start": { - "dateTime": "2022-03-15T05:00:00.0000000", - "timeZone": "UTC" - }, - "end": { - "dateTime": "2022-03-15T05:30:00.0000000", - "timeZone": "UTC" - }, - "location": { - "displayName": "", - "locationType": "default", - "uniqueIdType": "unknown", - "address": {}, - "coordinates": {} - }, - "locations": [], - "recurrence": null, - "attendees": [ - { - "type": "required", - "status": { - "response": "none", - "time": "0001-01-01T00:00:00Z" - }, - "emailAddress": { - "name": "Joni Sherman", - "address": "JoniS@tenant.com" - } - } - ], - "organizer": { - "emailAddress": { - "name": "User B", - "address": "user@tenant.com" - } - }, - "onlineMeeting": { - "joinUrl": "https://teams.microsoft.com/l/meetup-join/19%3ameeting_ZmIxNmI2MzItMGE0MC00NmYwLWIzNGItYzcwMWJiMmQ3MTY0%40thread.v2/0?context=%7b%22Tid%22%3a%22e1dd4023-a656-480a-8a0e-c1b1eec51e1d%22%2c%22Oid%22%3a%22fe36f75e-c103-410b-a18a-2bf6df06ac3a%22%7d" + onlineMeeting: { + joinUrl: meetings[0].joinWebUrl } } ] }; - const meetingResponseText: any = [ - { - "subject": "Test", - "start": "2022-06-26T12:30:00.0000000", - "end": "2022-06-26T13:00:00.0000000" - }, - { - "subject": "Test", - "start": "2022-04-08T11:30:00.0000000", - "end": "2022-04-08T12:00:00.0000000" - }, - { - "subject": "Online meeting test", - "start": "2022-03-15T05:00:00.0000000", - "end": "2022-03-15T05:30:00.0000000" - } - ]; + + const graphBatchResponse = { + responses: [ + { + id: '1', + status: 200, + body: { + value: meetings + } + } + ] + }; + + // #endregion + let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; @@ -297,6 +154,7 @@ describe(commands.MEETING_LIST, () => { accessToken: 'abc' }; commandInfo = Cli.getCommandInfo(command); + sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(false); }); beforeEach(() => { @@ -319,6 +177,7 @@ describe(commands.MEETING_LIST, () => { sinonUtil.restore([ accessToken.isAppOnlyAccessToken, request.get, + request.post, aadUser.getUserIdByEmail ]); }); @@ -338,117 +197,409 @@ describe(commands.MEETING_LIST, () => { }); it('defines correct properties for the default output', () => { - assert.deepStrictEqual(command.defaultProperties(), ['subject', 'start', 'end']); + assert.deepStrictEqual(command.defaultProperties(), ['subject', 'startDateTime', 'endDateTime']); + }); + + it('completes validation when the startDateTime is a valid ISODateTime, endDateTime is a valid ISODateTime and userId is a valid Guid', async () => { + const actual = await command.validate({ options: { startDateTime: startDateTime, endDateTime: endDateTime, userId: userId } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('fails validation when the startDateTime is not a valid ISODateTime', async () => { + const actual = await command.validate({ options: { startDateTime: 'foo', userId: userId } }, commandInfo); + assert.notStrictEqual(actual, true); }); - it('lists messages using application permissions for a specific userName and specifying only startDateTime', async () => { + it('fails validation when the userId is not a valid guid', async () => { + const actual = await command.validate({ options: { startDateTime: startDateTime, userId: 'foo' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation when the userName is not a valid UPN', async () => { + const actual = await command.validate({ options: { startDateTime: startDateTime, userName: 'invalid' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation when the email is not a valid UPN', async () => { + const actual = await command.validate({ options: { startDateTime: startDateTime, email: 'invalid' } }, 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('fails validation when the endDateTime is not a valid ISODateTime', async () => { + const actual = await command.validate({ options: { startDateTime: startDateTime, endDateTime: 'foo', userId: userId } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('throws an error when the userName, userId or email is not filled in when signed in using app-only authentication', async () => { + 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/${userName}/events?$filter=start/dateTime ge '${startDateTime}'`) { - return meetingResponse; + await assert.rejects(command.action(logger, { options: { startDateTime: '2022-04-04' } } as any), + new CommandError(`The option 'userId', 'userName' or 'email' is required when retrieving meetings using app only permissions`)); + }); + + it('throws an error when the userName is filled in when signed in using delegated authentication', async () => { + await assert.rejects(command.action(logger, { options: { startDateTime: '2022-04-04', email: userName } } as any), + new CommandError(`The options 'userId', 'userName' and 'email' cannot be used when retrieving meetings using delegated permissions`)); + }); + + it('logs meetings for the currently logged in user', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/me/events?$filter=start/dateTime ge '${startDateTime}' and end/dateTime lt '${endDateTime}' and isOrganizer eq true&$select=onlineMeeting`) { + return calendarMeetingsResponse; } - throw 'Invalid request'; + + throw 'Invalid request: ' + opts.url; }); - await command.action(logger, { options: { userName: userName, startDateTime: startDateTime } }); - assert(loggerLogSpy.calledWith(meetingResponse.value)); + sinon.stub(request, 'post').callsFake(async opts => { + if (opts.url === 'https://graph.microsoft.com/v1.0/$batch') { + return graphBatchResponse; + } + + throw 'Invalid request: ' + opts.url; + }); + + await command.action(logger, { + options: { + verbose: true, + startDateTime: startDateTime, + endDateTime: endDateTime, + isOrganizer: true + } + }); + + assert(loggerLogSpy.calledWith(meetings)); }); - it('lists messages using application permissions for a specific userId with a pretty output and specifying both startDateTime and endDateTime', async () => { + it('logs meetings for a user specified by userId', async () => { + 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/${userId}/events?$filter=start/dateTime ge '${startDateTime}' and end/dateTime le '${endDateTime}'`) { - return meetingResponse; + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/users/${userId}/events?$filter=start/dateTime ge '${startDateTime}'&$select=onlineMeeting`) { + return calendarMeetingsResponse; + } + + throw 'Invalid request: ' + opts.url; + }); + + sinon.stub(request, 'post').callsFake(async opts => { + if (opts.url === 'https://graph.microsoft.com/v1.0/$batch') { + return graphBatchResponse; + } + + throw 'Invalid request: ' + opts.url; + }); + + await command.action(logger, { + options: { + verbose: true, + startDateTime: startDateTime, + userId: userId } - throw 'Invalid request'; }); - await command.action(logger, { options: { userId: userId, startDateTime: startDateTime, endDateTime: endDateTime, output: 'text' } }); - assert(loggerLogSpy.calledWith(meetingResponseText)); + assert(loggerLogSpy.calledWith(meetings)); }); - it('lists messages using application permissions for a specific user retrieved by email and specifying all other possible options', async () => { + it('logs meetings for a user specified by userName', async () => { + sinonUtil.restore(accessToken.isAppOnlyAccessToken); sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true); - sinon.stub(aadUser, 'getUserIdByEmail').resolves(userId); + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/users/${userName}/events?$filter=start/dateTime ge '${startDateTime}'&$select=onlineMeeting`) { + return calendarMeetingsResponse; + } + + throw 'Invalid request: ' + opts.url; + }); + + sinon.stub(request, 'post').callsFake(async opts => { + if (opts.url === 'https://graph.microsoft.com/v1.0/$batch') { + return graphBatchResponse; + } + + throw 'Invalid request: ' + opts.url; + }); - sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/users/${userId}/events?$filter=start/dateTime ge '${startDateTime}' and end/dateTime le '${endDateTime}'`) { - return meetingResponse; + await command.action(logger, { + options: { + verbose: true, + startDateTime: startDateTime, + userName: userName } - throw 'Invalid request'; }); - await command.action(logger, { options: { email: userName, startDateTime: startDateTime, endDateTime: endDateTime, output: 'text' } }); - assert(loggerLogSpy.calledWith(meetingResponseText)); + assert(loggerLogSpy.calledWith(meetings)); }); - it('lists messages using delegated permissions specifying both startDateTime and only retrieving the events that the user is organizer from', async () => { - sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(false); + it('logs meetings for a user specified by email', async () => { + sinonUtil.restore(accessToken.isAppOnlyAccessToken); + sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true); + sinon.stub(aadUser, 'getUserIdByEmail').resolves(userId); + + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/users/${userId}/events?$filter=start/dateTime ge '${startDateTime}'&$select=onlineMeeting`) { + return calendarMeetingsResponse; + } + + throw 'Invalid request: ' + opts.url; + }); - sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/me/events?$filter=start/dateTime ge '${startDateTime}' and isOrganizer eq true`) { - return meetingResponse; + sinon.stub(request, 'post').callsFake(async opts => { + if (opts.url === 'https://graph.microsoft.com/v1.0/$batch') { + return graphBatchResponse; } - throw 'Invalid request'; + + throw 'Invalid request: ' + opts.url; }); - await command.action(logger, { options: { verbose: true, output: 'json', startDateTime: startDateTime, isOrganizer: true } }); - assert(loggerLogSpy.calledWith(meetingResponse.value)); + await command.action(logger, { + options: { + verbose: true, + startDateTime: startDateTime, + email: userName + } + }); + + assert(loggerLogSpy.calledWith(meetings)); }); - it('correctly handles error when listing events', async () => { - const error = { - "error": { - "code": "UnknownError", - "message": "An error has occurred", - "innerError": { - "date": "2022-02-14T13:27:37", - "request-id": "77e0ed26-8b57-48d6-a502-aca6211d6e7c", - "client-request-id": "77e0ed26-8b57-48d6-a502-aca6211d6e7c" - } + it('filters out non meeting events when retrieving calendar events', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/me/events?$filter=start/dateTime ge '${startDateTime}'&$select=onlineMeeting`) { + return { + value: [ + calendarMeetingsResponse.value[0], + { + onlineMeeting: null + } + ] + }; } - }; - sinon.stub(request, 'get').rejects(error); + throw 'Invalid request: ' + opts.url; + }); - await assert.rejects(command.action(logger, { options: { verbose: true } } as any), - new CommandError('An error has occurred')); - }); + const postStub = sinon.stub(request, 'post').callsFake(async opts => { + if (opts.url === 'https://graph.microsoft.com/v1.0/$batch') { + return graphBatchResponse; + } - it('completes validation when the startDateTime is a valid ISODateTime, endDateTime is a valid ISODateTime and userId is a valid Guid', async () => { - const actual = await command.validate({ options: { verbose: true, startDateTime: startDateTime, endDateTime: endDateTime, userId: userId } }, commandInfo); - assert.strictEqual(actual, true); + throw 'Invalid request: ' + opts.url; + }); + + await command.action(logger, { + options: { + verbose: true, + startDateTime: startDateTime + } + }); + + assert.deepStrictEqual(postStub.lastCall.args[0].data, { + requests: [ + { + id: 0, + method: 'GET', + url: `me/onlineMeetings?$filter=joinWebUrl eq '${formatting.encodeQueryParameter(calendarMeetingsResponse.value[0].onlineMeeting.joinUrl)}'` + } + ] + }); }); - it('fails validation when the startDateTime is not a valid ISODateTime', async () => { - const actual = await command.validate({ options: { verbose: true, startDateTime: 'foo', userId: userId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('retrieves meetings correctly when specifying userId', async () => { + 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/${userId}/events?$filter=start/dateTime ge '${startDateTime}'&$select=onlineMeeting`) { + return calendarMeetingsResponse; + } + + throw 'Invalid request: ' + opts.url; + }); + + const postStub = sinon.stub(request, 'post').callsFake(async opts => { + if (opts.url === 'https://graph.microsoft.com/v1.0/$batch') { + return graphBatchResponse; + } + + throw 'Invalid request: ' + opts.url; + }); + + await command.action(logger, { + options: { + verbose: true, + startDateTime: startDateTime, + userId: userId + } + }); + + assert.deepStrictEqual(postStub.firstCall.args[0].data, { + requests: [ + { + id: 0, + method: 'GET', + url: `users/${userId}/onlineMeetings?$filter=joinWebUrl eq '${formatting.encodeQueryParameter(calendarMeetingsResponse.value[0].onlineMeeting.joinUrl)}'` + } + ] + }); }); - it('fails validation when the userId is not a valid guid', async () => { - const actual = await command.validate({ options: { verbose: true, startDateTime: startDateTime, userId: 'foo' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('retrieves meetings correctly when specifying userName', async () => { + 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/${userName}/events?$filter=start/dateTime ge '${startDateTime}'&$select=onlineMeeting`) { + return calendarMeetingsResponse; + } + + throw 'Invalid request: ' + opts.url; + }); + + const postStub = sinon.stub(request, 'post').callsFake(async opts => { + if (opts.url === 'https://graph.microsoft.com/v1.0/$batch') { + return graphBatchResponse; + } + + throw 'Invalid request: ' + opts.url; + }); + + await command.action(logger, { + options: { + verbose: true, + startDateTime: startDateTime, + userName: userName + } + }); + + assert.deepStrictEqual(postStub.firstCall.args[0].data, { + requests: [ + { + id: 0, + method: 'GET', + url: `users/${userName}/onlineMeetings?$filter=joinWebUrl eq '${formatting.encodeQueryParameter(calendarMeetingsResponse.value[0].onlineMeeting.joinUrl)}'` + } + ] + }); }); - it('fails validation when the endDateTime is not a valid ISODateTime', async () => { - const actual = await command.validate({ options: { verbose: true, startDateTime: startDateTime, endDateTime: 'foo', userId: userId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('retrieves meetings correctly when specifying email', async () => { + sinonUtil.restore(accessToken.isAppOnlyAccessToken); + sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true); + sinon.stub(aadUser, 'getUserIdByEmail').resolves(userId); + + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/users/${userId}/events?$filter=start/dateTime ge '${startDateTime}'&$select=onlineMeeting`) { + return calendarMeetingsResponse; + } + + throw 'Invalid request: ' + opts.url; + }); + + const postStub = sinon.stub(request, 'post').callsFake(async opts => { + if (opts.url === 'https://graph.microsoft.com/v1.0/$batch') { + return graphBatchResponse; + } + + throw 'Invalid request: ' + opts.url; + }); + + await command.action(logger, { + options: { + verbose: true, + startDateTime: startDateTime, + email: userName + } + }); + + assert.deepStrictEqual(postStub.firstCall.args[0].data, { + requests: [ + { + id: 0, + method: 'GET', + url: `users/${userId}/onlineMeetings?$filter=joinWebUrl eq '${formatting.encodeQueryParameter(calendarMeetingsResponse.value[0].onlineMeeting.joinUrl)}'` + } + ] + }); }); - it('throws an error when the userName, userId or email is not filled in when signed in using app-only authentication', async () => { + it('handles error correctly when retrieving calendar events', async () => { + sinonUtil.restore(accessToken.isAppOnlyAccessToken); sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true); - await assert.rejects(command.action(logger, { options: { verbose: true, startDateTime: '2022-04-04' } } as any), - new CommandError(`The option 'userId', 'userName' or 'email' is required when retrieving meetings using app only permissions`)); + sinon.stub(request, 'get').rejects({ + error: { + error: { + message: 'User could not be found.' + } + } + }); + + await assert.rejects(command.action(logger, { + options: { + verbose: true, + startDateTime: startDateTime, + userId: userId + } + }), new CommandError('User could not be found.')); }); - it('throws an error when the userName is filled in when signed in using delegated authentication', async () => { - sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(false); + it('handles error correctly when retrieving meetings', async () => { + sinon.stub(request, 'get').resolves(calendarMeetingsResponse); + + sinon.stub(request, 'post').resolves({ + responses: [ + { + id: '0', + status: 404, + body: { + error: { + message: 'Something went wrong.' + } + } + } + ] + }); - await assert.rejects(command.action(logger, { options: { verbose: true, startDateTime: '2022-04-04', userName: userName } } as any), - new CommandError(`The options 'userId', 'userName' and 'email' cannot be used when retrieving meetings using delegated permissions`)); + await assert.rejects(command.action(logger, { + options: { + verbose: true, + startDateTime: startDateTime + } + }), new CommandError('Something went wrong.')); + }); + + it('handles error without message correctly when retrieving meetings', async () => { + sinon.stub(request, 'get').resolves(calendarMeetingsResponse); + + sinon.stub(request, 'post').resolves({ + responses: [ + { + id: '0', + status: 404, + body: { + error: { + message: '', + code: 'Forbidden' + } + } + } + ] + }); + + await assert.rejects(command.action(logger, { + options: { + verbose: true, + startDateTime: startDateTime + } + }), new CommandError('Forbidden')); }); }); diff --git a/src/m365/teams/commands/meeting/meeting-list.ts b/src/m365/teams/commands/meeting/meeting-list.ts index f7ed0c775a2..c126491501f 100644 --- a/src/m365/teams/commands/meeting/meeting-list.ts +++ b/src/m365/teams/commands/meeting/meeting-list.ts @@ -1,7 +1,6 @@ import { Event } from '@microsoft/microsoft-graph-types'; import auth from '../../../../Auth.js'; import GlobalOptions from '../../../../GlobalOptions.js'; -import { Cli } from '../../../../cli/Cli.js'; import { Logger } from '../../../../cli/Logger.js'; import { aadUser } from '../../../../utils/aadUser.js'; import { accessToken } from '../../../../utils/accessToken.js'; @@ -9,6 +8,8 @@ import { odata } from '../../../../utils/odata.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 { formatting } from '../../../../utils/formatting.js'; interface CommandArgs { options: Options; @@ -33,7 +34,7 @@ class TeamsMeetingListCommand extends GraphCommand { } public defaultProperties(): string[] | undefined { - return ['subject', 'start', 'end']; + return ['subject', 'startDateTime', 'endDateTime']; } constructor() { @@ -83,15 +84,27 @@ class TeamsMeetingListCommand extends GraphCommand { this.validators.push( async (args: CommandArgs) => { if (!validation.isValidISODateTime(args.options.startDateTime)) { - return `'${args.options.startDateTime}' is not a valid ISO date string`; + return `'${args.options.startDateTime}' is not a valid ISO date string for startDateTime.`; + } + + if (args.options.endDateTime && !validation.isValidISODateTime(args.options.endDateTime)) { + return `'${args.options.startDateTime}' is not a valid ISO date string for endDateTime.`; + } + + if (args.options.startDateTime && args.options.endDateTime && args.options.startDateTime > args.options.endDateTime) { + return 'startDateTime value must be before endDateTime.'; } if (args.options.userId && !validation.isValidGuid(args.options.userId)) { - return `${args.options.userId} is not a valid Guid`; + return `${args.options.userId} is not a valid GUID for userId.`; } - if (args.options.endDateTime && !validation.isValidISODateTime(args.options.endDateTime)) { - return `'${args.options.startDateTime}' is not a valid ISO date string`; + if (args.options.userName && !validation.isValidUserPrincipalName(args.options.userName)) { + return `'${args.options.userName}' is not a valid UPN for userName.`; + } + + if (args.options.email && !validation.isValidUserPrincipalName(args.options.email)) { + return `'${args.options.email}' is not a valid UPN for email.`; } return true; @@ -101,66 +114,113 @@ class TeamsMeetingListCommand extends GraphCommand { public async commandAction(logger: Logger, args: CommandArgs): Promise { try { - const isAppOnlyAccessToken: boolean | undefined = accessToken.isAppOnlyAccessToken(auth.service.accessTokens[this.resource].accessToken); + const isAppOnlyAccessToken = accessToken.isAppOnlyAccessToken(auth.service.accessTokens[this.resource].accessToken)!; + if (isAppOnlyAccessToken && !args.options.userId && !args.options.userName && !args.options.email) { + throw `The option 'userId', 'userName' or 'email' is required when retrieving meetings using app only permissions`; + } + else if (!isAppOnlyAccessToken && (args.options.userId || args.options.userName || args.options.email)) { + throw `The options 'userId', 'userName' and 'email' cannot be used when retrieving meetings using delegated permissions`; + } if (this.verbose) { - await logger.logToStderr(`Retrieving meetings for ${isAppOnlyAccessToken ? 'specific user' : 'currently logged in user'}`); + await logger.logToStderr(`Retrieving meetings for user: ${args.options.userId || args.options.userName || args.options.email || accessToken.getUserNameFromAccessToken(auth.service.accessTokens[this.resource].accessToken)}...`); } - let requestUrl = `${this.resource}/v1.0/`; - if (isAppOnlyAccessToken) { - if (!args.options.userId && !args.options.userName && !args.options.email) { - throw `The option 'userId', 'userName' or 'email' is required when retrieving meetings using app only permissions`; - } + const graphBaseUrl = await this.getGraphBaseUrl(args.options); + const meetingUrls = await this.getMeetingJoinUrls(graphBaseUrl, args.options); + const meetings = await this.getTeamsMeetings(logger, graphBaseUrl, meetingUrls); - requestUrl += 'users/'; - if (args.options.userId) { - requestUrl += args.options.userId; - } - else if (args.options.userName) { - requestUrl += args.options.userName; - } - else if (args.options.email) { - const userId = await aadUser.getUserIdByEmail(args.options.email); - requestUrl += userId; - } - } - else { - if (args.options.userId || args.options.userName || args.options.email) { - throw `The options 'userId', 'userName' and 'email' cannot be used when retrieving meetings using delegated permissions`; - } + await logger.log(meetings); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } - requestUrl += `me`; - } + /** + * Get the first part of the Graph API URL that contains the user information. + */ + private async getGraphBaseUrl(options: Options): Promise { + let requestUrl = `${this.resource}/v1.0/`; - requestUrl += `/events?$filter=start/dateTime ge '${args.options.startDateTime}'`; + if (options.userId || options.userName) { + requestUrl += `users/${options.userId || options.userName}`; + } + else if (options.email) { + const userId = await aadUser.getUserIdByEmail(options.email); + requestUrl += `users/${userId}`; + } + else { + requestUrl += 'me'; + } - if (args.options.endDateTime) { - requestUrl += ` and end/dateTime le '${args.options.endDateTime}'`; - } + return requestUrl; + } - if (args.options.isOrganizer) { - requestUrl += ' and isOrganizer eq true'; - } + /** + * Gets the meeting join urls for the specified user using calendar events. + */ + private async getMeetingJoinUrls(graphBaseUrl: string, options: Options): Promise { + let requestUrl = graphBaseUrl; + + requestUrl += `/events?$filter=start/dateTime ge '${options.startDateTime}'`; + if (options.endDateTime) { + requestUrl += ` and end/dateTime lt '${options.endDateTime}'`; + } + if (options.isOrganizer) { + requestUrl += ' and isOrganizer eq true'; + } + requestUrl += '&$select=onlineMeeting'; + + const items = await odata.getAllItems(requestUrl); + const result = items.filter(i => i.onlineMeeting).map(i => i.onlineMeeting!.joinUrl!); + + return result; + } - const res = await odata.getAllItems(requestUrl); - const resFiltered = res.filter(y => y.isOnlineMeeting); - if (!args.options.output || !Cli.shouldTrimOutput(args.options.output)) { - await logger.log(resFiltered); + private async getTeamsMeetings(logger: Logger, graphBaseUrl: string, meetingUrls: string[]): Promise { + const graphRelativeUrl = graphBaseUrl.replace(`${this.resource}/v1.0/`, ''); + let result: any[] = []; + + for (let i = 0; i < meetingUrls.length; i += 20) { + if (this.verbose) { + logger.logToStderr(`Retrieving meetings ${i + 1}-${Math.min(i + 20, meetingUrls.length)}...`); } - else { - //converted to text friendly output - await logger.log(resFiltered.map(i => { - return { - subject: i.subject, - start: i.start!.dateTime, - end: i.end!.dateTime - }; - })); + const batch = meetingUrls.slice(i, i + 20); + const requestOptions: CliRequestOptions = { + url: `${this.resource}/v1.0/$batch`, + headers: { + accept: 'application/json', + 'content-type': 'application/json' + }, + responseType: 'json', + data: { + requests: batch.map((url, index) => ({ + id: i + index, + method: 'GET', + url: `${graphRelativeUrl}/onlineMeetings?$filter=joinWebUrl eq '${formatting.encodeQueryParameter(url)}'` + })) + } + }; + + const requestResponse = await request.post<{ responses: { id: string; status: number; headers: any; body: any; }[] }>(requestOptions); + + for (const response of requestResponse.responses) { + if (response.status === 200) { + result.push(response.body.value[0]); + } + else { + // Encountered errors where message was empty resulting in [object Object] error messages + if (!response.body.error.message) { + throw response.body.error.code; + } + throw response.body; + } } } - catch (err: any) { - this.handleRejectedODataJsonPromise(err); - } + + // Sort all meetings by start date + result = result.sort((a, b) => a.startDateTime < b.startDateTime ? -1 : a.startDateTime > b.startDateTime ? 1 : 0); + return result; } }