diff --git a/scripts/Add-EngageGroupAdmins/Add-EngageGroupAdmins.ps1 b/scripts/Add-EngageGroupAdmins/Add-EngageGroupAdmins.ps1 new file mode 100644 index 00000000..954cded8 --- /dev/null +++ b/scripts/Add-EngageGroupAdmins/Add-EngageGroupAdmins.ps1 @@ -0,0 +1,149 @@ +<# +.DESCRIPTION + The sample scripts are not supported under any Microsoft standard support + program or service. The sample scripts are provided AS IS without warranty + of any kind. Microsoft further disclaims all implied warranties including, + without limitation, any implied warranties of merchantability or of fitness for + a particular purpose. The entire risk arising out of the use or performance of + the sample scripts and documentation remains with you. In no event shall + Microsoft, its authors, or anyone else involved in the creation, production, or + delivery of the scripts be liable for any damages whatsoever (including, + without limitation, damages for loss of business profits, business interruption, + loss of business information, or other pecuniary loss) arising out of the use + of or inability to use the sample scripts or documentation, even if Microsoft + has been advised of the possibility of such damages. + +Purpose: + -Bulk-adds admins (owners) to Viva Engage communities using the Microsoft Graph API. + A Viva Engage community admin is simply an owner of the community's underlying + Microsoft 365 group, so this uses the group owners API: + https://learn.microsoft.com/en-us/graph/api/group-post-owners + https://learn.microsoft.com/en-us/graph/api/resources/engagement-api-overview#community-membership-management + +Author: + Dean Cron + +Version: + 1.0 - Initial Release 2023 (legacy Yammer REST API) + 2.0 - Updated to v2 October 2025 (legacy Yammer REST API) + 3.0 - July 2026 - Migrated off the deprecated Yammer REST API to the Microsoft Graph + group-owners API. NOTE: the GroupID column now needs the Microsoft 365 group ID + (a GUID) that backs the community, NOT the legacy numeric Yammer group ID used + by earlier versions of this script. See the README for how to get this ID. + +Requirements: + + 1. An Azure AD (Entra ID) App Registration with the following Microsoft Graph + **application** permission, with admin consent granted: + -Group.ReadWrite.All + + 2. CSV containing group IDs and admins to add to each. See the README for more information + on how to create this and how to find the group ID for a community: + https://github.com/microsoft/FastTrack/tree/master/scripts/Add-EngageGroupAdmins/README.md + + +.EXAMPLE + .\Add-EngageGroupAdmins.ps1 +#> + +<############ STUFF YOU NEED TO MODIFY ############> + +#Point this to the groupadmins.csv you created as per the requirements. +$groupadminsCsvPath = 'C:\temp\groupadmins.csv' + +# Change these to match your environment. Instructions: +# https://learn.microsoft.com/en-us/graph/auth-v2-service?view=graph-rest-1.0 +$ClientId = "clientid" +$TenantId = "tenantid" +$ClientSecret = "clientsecret" + +<############ YOU SHOULD NOT HAVE TO MODIFY ANYTHING BELOW THIS LINE ############> + +#Make sure groupadmins.csv is where it's supposed to be +try { + $groupadminsCsv = Import-Csv $groupadminsCsvPath +} +catch { + Write-Host "Unable to open the input CSV file. Ensure it's located at $groupadminsCsvPath" + Return +} + +function Connect-ToGraph { + param ( + [Parameter(Mandatory = $true)] + [string]$ClientId, + + [Parameter(Mandatory = $true)] + [string]$TenantId, + + [Parameter(Mandatory = $true)] + [string]$ClientSecret + ) + + $authBody = @{ + Grant_Type = "client_credentials" + Scope = "https://graph.microsoft.com/.default" + Client_Id = $ClientId + Client_Secret = $ClientSecret + } + + $Connection = Invoke-RestMethod -Uri https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token -Method POST -Body $authBody + return $Connection.access_token +} + +$accessToken = Connect-ToGraph -ClientId $ClientId -TenantId $TenantId -ClientSecret $ClientSecret +$authHeader = @{ Authorization = "Bearer $accessToken"; 'Content-Type' = 'application/json' } + +Write-Host "Starting to add group admins..." -ForegroundColor Cyan + +$groupadminsCsv | ForEach-Object { + do { + $rateLimitHit = $false + + $gID = $_.GroupID + $mail = $_.Email + + try { + #Add the user as an owner of the group. Group owners are automatically treated as + #admins of the associated Viva Engage community. + $requestBody = @{ '@odata.id' = "https://graph.microsoft.com/v1.0/users/$mail" } | ConvertTo-Json + Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/groups/$gID/owners/`$ref" -Headers $authHeader -Method POST -Body $requestBody | Out-Null + + Write-Host "Successfully added admin $mail to group $gID" -ForegroundColor Green + } + catch { + $statusCode = $_.Exception.Response.StatusCode.Value__ + if ($statusCode -eq "429" -or $statusCode -eq "503") { + #Deal with rate limiting + #https://learn.microsoft.com/en-us/graph/throttling + $rateLimitHit = $true + } + elseif ($statusCode -eq "401" -or $statusCode -eq "403") { + #Thrown when the access token is invalid or lacks Group.ReadWrite.All + Write-Host "Exiting script, Graph API reports ACCESS DENIED. Ensure the app registration has Group.ReadWrite.All (application) permission with admin consent." -ForegroundColor Red + exit + } + elseif ($statusCode -eq "404") { + #Typically thrown when either the group or user isn't found. + Write-Host "Exiting script, Graph API reports 404, typically caused when either the user ($mail) or group ($gID) was not found" -ForegroundColor Red + exit + } + elseif ($statusCode -eq "400") { + #Typically thrown when the user is already an owner of the group + Write-Host "Failed to add" $mail "to group" $gID "- Graph API reports 400, the user may already be an owner" -ForegroundColor Red + } + else { + $l = $_.InvocationInfo.ScriptLineNumber + Write-Host "Failed to add" $mail "to group" $gID -ForegroundColor Red + Write-Host "error $statusCode on line $l" + } + } + if ($rateLimitHit) { + #429 or 503: Sleep for a bit before retrying + Write-Host "Rate limit hit, sleeping for 15 seconds" + Start-Sleep -Seconds 15 + } + } while ($rateLimitHit) +} + +Write-Host "All done!" -ForegroundColor Cyan diff --git a/scripts/Add-YammerGroupAdmins/README.md b/scripts/Add-EngageGroupAdmins/README.md similarity index 55% rename from scripts/Add-YammerGroupAdmins/README.md rename to scripts/Add-EngageGroupAdmins/README.md index 595a180e..27e01d30 100644 --- a/scripts/Add-YammerGroupAdmins/README.md +++ b/scripts/Add-EngageGroupAdmins/README.md @@ -1,50 +1,47 @@ -# Microsoft FastTrack Open Source - Add-YammerGroupAdmins +# Microsoft FastTrack Open Source - Add-EngageGroupAdmins -This sample script will allow a Yammer admin to bulk-add group owners to groups in their network. +This sample script will allow an admin to bulk-add admins (owners) to Viva Engage communities using the Microsoft Graph API. + +**Version 3.0 note:** This script was migrated off the deprecated legacy Yammer REST API to the Microsoft Graph [group owners API](https://learn.microsoft.com/en-us/graph/api/group-post-owners), since the legacy endpoint is undocumented, unsupported, and at risk of being turned off without notice. A Viva Engage community admin is simply an owner of the community's underlying Microsoft 365 group, so this uses `/groups/{groupId}/owners/$ref` rather than a community-specific endpoint. **The ID format has changed as part of this migration** — see below. ## Usage ### Prerequisites -- You must create a new Yammer app registration in Microsoft Entra ID. This app should be configured to grant the following **delegated** permission: +- You must create a new app registration in Microsoft Entra ID. This app should be configured to grant the following **application** permission, with admin consent granted: ``` - Yammer - -access_as_user + Microsoft Graph + -Group.ReadWrite.All ``` - You'll need to create a CSV file containing two columns: - - **GroupID**. This will contain the IDs of the groups you want to add a new admin to. - - **Email**. This will contain the email address of the user you want to assign as admin of the group represented by the GroupID value next to it. - -The CSV should look similar to this: + - **GroupID**. This will contain the Microsoft 365 group ID (a GUID) backing the community you want to add a new admin to. + - **Email**. This will contain the email address (UPN) of the user you want to assign as admin of the community represented by the GroupID value next to it. -![CSV format](groupadminssample.jpg?raw=true "Title") + > ⚠️ **This is not the same ID as the legacy numeric Yammer group ID used by earlier versions of this script.** Graph requires the underlying Microsoft 365 group's GUID. -You can get the group ID of the groups you need to add the admins to in one of two ways: +You can get the Microsoft 365 group ID for a community in one of two ways: -1. Grab the group ID from the group's URL and use a BASE64 decoder on the string at the end as described here: https://support.microsoft.com/en-us/office/how-do-i-find-a-community-s-group-feed-id-in-yammer-9372ab6f-bcc2-4283-bb6a-abf42dec970f -2. Run a network data export going back as far as possible (do not export attachments) and get the group ID from the groups.csv file generated: https://learn.microsoft.com/en-us/rest/api/yammer/network-data-export +1. `GET https://graph.microsoft.com/beta/employeeExperience/communities/{communityId}` — the response includes a `groupId` field with the Microsoft 365 group ID. +2. `GET https://graph.microsoft.com/beta/employeeExperience/communities` and match communities by `displayName` to find the corresponding `groupId`. There are 4 variables you need to change in the script itself. These are located very early in the script just below “<############ STUFF YOU NEED TO MODIFY ############>”: -1. **$ClientId = "ClientIDString"** +1. **$groupadminsCsvPath = 'C:\temp\groupadmins.csv'** + + Point this to the groupadmins.csv file you created as mentioned above. + +2. **$ClientId = "ClientIDString"** >Replace ClientIDString with the Client ID of the app registration you created in the prerequisites. -2. **$TenantId = "TenantIDString"** +3. **$TenantId = "TenantIDString"** - >Replace TenantIDString with the Client ID of the app registration you created in the prerequisites. + >Replace TenantIDString with the Tenant ID of the app registration you created in the prerequisites. -3. **$ClientSecret = "ClientSecretString"** +4. **$ClientSecret = "ClientSecretString"** >Replace ClientSecretString with the client secret value of the app registration you created in the prerequisites. - -4. **$RedirectUri = "https://localhost"** - >Replace this with the redirect Url you set in your app registration (if not set to https://localhost) - -4. **$groupadminsCsvPath = 'C:\temp\groupadmins.csv'** - - Point this to the groupadmins.csv file you created as mentioned above. ### Parameters @@ -54,17 +51,15 @@ None Once you’ve completed the pre-reqs, you’re ready to go. Run the script like so: - .\Add-YammerGroupAdmins.ps1 + .\Add-EngageGroupAdmins.ps1 ### Notes -**If you encounter the following error: 'Get-MsalToken : Error creating window handle.', set your PowerShell window's default terminal application to 'Windows Console Host'. This is due to a [known bug in MSAL](https://github.com/AzureAD/MSAL.PS/issues/58)** - -**This sample calls an undocumented endpoint in the Yammer REST APIs, and as such has no official support provided for it, and may stop working without warning.** +**This sample now uses the Microsoft Graph group-owners API (`/v1.0/groups/{groupId}/owners/$ref`), which is officially documented and supported, unlike the earlier version of this script.** ## Applies To -- Yammer / Viva Engage networks in M365 +- Viva Engage communities in M365 ## Author @@ -72,6 +67,7 @@ Once you’ve completed the pre-reqs, you’re ready to go. Run the script like |----|-------------------------- |Dean Cron, Microsoft|June 23th, 2023| -> Updated to v2|October 2nd, 2025 +-> Updated to v3, migrated to Microsoft Graph|July 22nd, 2026 ## Issues diff --git a/scripts/Add-YammerGroupAdmins/groupadminssample.jpg b/scripts/Add-EngageGroupAdmins/groupadminssample.jpg similarity index 100% rename from scripts/Add-YammerGroupAdmins/groupadminssample.jpg rename to scripts/Add-EngageGroupAdmins/groupadminssample.jpg diff --git a/scripts/Add-YammerGroupAdmins/Add-YammerGroupAdmins.ps1 b/scripts/Add-YammerGroupAdmins/Add-YammerGroupAdmins.ps1 deleted file mode 100644 index 782d3686..00000000 --- a/scripts/Add-YammerGroupAdmins/Add-YammerGroupAdmins.ps1 +++ /dev/null @@ -1,152 +0,0 @@ -<# -.DESCRIPTION - The sample scripts are not supported under any Microsoft standard support - program or service. The sample scripts are provided AS IS without warranty - of any kind. Microsoft further disclaims all implied warranties including, - without limitation, any implied warranties of merchantability or of fitness for - a particular purpose. The entire risk arising out of the use or performance of - the sample scripts and documentation remains with you. In no event shall - Microsoft, its authors, or anyone else involved in the creation, production, or - delivery of the scripts be liable for any damages whatsoever (including, - without limitation, damages for loss of business profits, business interruption, - loss of business information, or other pecuniary loss) arising out of the use - of or inability to use the sample scripts or documentation, even if Microsoft - has been advised of the possibility of such damages. - -Purpose: - -Allows a Yammer admin to bulk-add group owners to groups in their network. - -Author: - Dean Cron - -Version: - 2.0 - -Requirements: - - 1. MSAL.PS PowerShell module. Install it from the PowerShell Gallery with the command: - Install-Module MSAL.PS - - 2. An Azure AD App Registration with the following API permissions: - -Yammer: access_as_user - - 2. CSV containing group IDs and admins to add to each. See the README for more information on how to create this: - https://github.com/microsoft/FastTrack/tree/master/scripts/Add-YammerGroupAdmins/README.md - - -.EXAMPLE - .\Add-YammerGroupAdmins.ps1 -#> - -<############ STUFF YOU NEED TO MODIFY ############> - -#Point this to the groupadmins.csv you created as per the requirements. -$groupadminsCsvPath = 'C:\temp\groupadmins.csv' - -# Change these to match your environment. Instructions: -# https://learn.microsoft.com/en-us/graph/auth-v2-service?view=graph-rest-1.0 -$ClientId = "clientid" -$TenantId = "tenantId" -$RedirectUri = "https://localhost" - -<############ YOU SHOULD NOT HAVE TO MODIFY ANYTHING BELOW THIS LINE ############> - -$Scopes = @("https://api.yammer.com/.default") - -#Check to see if MSAL.PS is installed, if not exit with instructions -if(-not (Get-Module -ListAvailable -Name MSAL.PS)){ - Write-Host "MSAL.PS module not found, please install it from the PowerShell Gallery with the command:" -ForegroundColor Red - Write-Host "Install-Module MSAL.PS" -ForegroundColor Yellow - Return -} - -#Make sure groupadmins.csv is where it's supposed to be -try{ - $groupadminsCsv = Import-Csv $groupadminsCsvPath -} -catch{ - Write-Host "Unable to open the input CSV file. Ensure it's located at $groupadminsCsvPath" - Return -} - -function Get-YammerAuthHeader { - $authToken = Get-MsalToken -ClientId $ClientId -TenantId $TenantId -RedirectUri $RedirectUri -Scopes $Scopes -Interactive - if (-not $authToken) { - Write-Host "Failed to acquire Yammer Auth Token. Please ensure the ClientID, TenantID, and ClientSecret are correct." -ForegroundColor Red - Return - } - else { - return $authToken.AccessToken - } - -} - -$YammerAuthToken = Get-YammerAuthHeader - -Write-Host "Starting to add group admins..." -ForegroundColor Cyan - -$groupadminsCsv | ForEach-Object { - do { - $rateLimitHit = $false - - $gID = $_.GroupID -as [decimal] - $mail = $_.Email - - try - { - $gFullName = $null - - # Add Admin to Group - $requestBody = @{ group_id=$gID; email=$mail } - $addAdmin = Invoke-WebRequest "https://www.yammer.com/api/v1/group_memberships.json" -Headers @{ AUTHORIZATION = "Bearer $YammerAuthToken" } -Method POST -Body $requestBody - $adminID = (convertfrom-json $addAdmin.content).user_id - - #We have the user ID and have added them as a member, now make them an admin of this group - $injectAdmin = Invoke-WebRequest "https://www.yammer.com/api/v1/groups/$gID/make_admin?user_id=$adminID" -Headers @{ AUTHORIZATION = "Bearer $YammerAuthToken" } -UseBasicParsing -Method POST - - #Comment the next line if you'd like to speed this script up slightly. Cosmetic only, used to output group name instead of group ID - $getGroupName = Invoke-WebRequest "https://www.yammer.com/api/v1/groups/$gID.json" -Headers @{ AUTHORIZATION = "Bearer $YammerAuthToken" } -Method GET - - if($getGroupName){ - $gFullName = (convertfrom-json $getGroupName.content).full_name - } - else{ - $gFullName = $gID - } - - Write-Host "Successfully added admin $mail to group $gFullName" -ForegroundColor Green - } - catch { - if( $_.Exception.Response.StatusCode.Value__ -eq "429" -or $_.Exception.Response.StatusCode.Value__ -eq "503" ) - { - #Deal with rate limiting - #https://learn.microsoft.com/en-us/rest/api/yammer/rest-api-rate-limits#yammer-api-rate-limts - $rateLimitHit = $true - } - elseif($_.Exception.Response.StatusCode.Value__ -eq "401"){ - #Thrown when the YammerAuthToken is invalid for the network in question - Write-Host "Exiting script, API reports 401 ACCESS DENIED." -ForegroundColor Red - exit - } - elseif($_.Exception.Response.StatusCode.Value__ -eq "404"){ - #Typically thrown when either the group or user isn't found. - Write-Host "Exiting script, API reports 404, typically caused when either the user ($mail) or group ($gID) was not found" -ForegroundColor Red - exit - } - else{ - $e = $_.Exception.Response.StatusCode.Value__ - $l = $_.InvocationInfo.ScriptLineNumber - Write-Host "Failed to add" $mail "to group" $gID -ForegroundColor Red - Write-Host "error $e on line $l" - } - } - if ($rateLimitHit) { - #429 or 503: Sleep for a bit before retrying - Write-Host "Rate limit hit, sleeping for 15 seconds" - Start-Sleep -Seconds 15 - } - } while ($rateLimitHit) -} - -Write-Host "All done!" -ForegroundColor Cyan - diff --git a/scripts/Delete-AllCommunityPosts/Delete-AllCommunityPosts.ps1 b/scripts/Delete-AllCommunityPosts/Delete-AllCommunityPosts.ps1 index 68827805..87ad864e 100644 --- a/scripts/Delete-AllCommunityPosts/Delete-AllCommunityPosts.ps1 +++ b/scripts/Delete-AllCommunityPosts/Delete-AllCommunityPosts.ps1 @@ -24,6 +24,16 @@ Author: Dean Cron - dean.cron@microsoft.com Version: 1.0 - June 2024 - Initial release 2.0 - November 2025 - Updated auth + 2.1 - July 2026 - Reviewed against current Microsoft Graph documentation. Confirmed there + is no Graph equivalent for this script's functionality: the Microsoft Graph + employeeExperience/communities API only covers community CRUD and membership + management, not reading or deleting conversation/message content. This script must + continue to use the legacy Yammer REST API's messages endpoints + (GET .../messages/in_group, DELETE .../messages/{id}) for the foreseeable future. + No further migration is possible until/unless Microsoft ships a Graph API for + community messages. The existing MSAL.PS + Entra app registration + delegated + access_as_user auth model already satisfies the July 2025 requirement to call the + Yammer API via an Entra app, so no auth changes are needed either. Requirements: 1. MSAL.PS PowerShell module. Install it from the PowerShell Gallery with the command: diff --git a/scripts/Delete-AllCommunityPosts/README.md b/scripts/Delete-AllCommunityPosts/README.md index a81194da..caad1253 100644 --- a/scripts/Delete-AllCommunityPosts/README.md +++ b/scripts/Delete-AllCommunityPosts/README.md @@ -4,6 +4,8 @@ This sample script will allow a Viva Engage admin to delete all messages in an e DELETION CAN'T BE UNDONE. +**July 2026 note - no Graph migration possible:** This script must continue using the legacy Yammer REST API. Microsoft Graph's `employeeExperience/communities` API only supports community CRUD and membership management - it has no endpoints for reading or deleting conversation/message content. Until Microsoft ships a Graph API for community messages (if ever), there's no alternative to the legacy `messages/in_group` and `messages/{id}` endpoints used here. The script's existing auth model (MSAL.PS + Entra app registration + delegated `access_as_user`) already satisfies the July 2025 requirement that Yammer API calls go through an Entra app registration, so no further changes are required on that front. + ## Usage ### Prerequisites diff --git a/scripts/Delete-EngageGroups/Delete-EngageGroups.ps1 b/scripts/Delete-EngageGroups/Delete-EngageGroups.ps1 new file mode 100644 index 00000000..8d6bc2ad --- /dev/null +++ b/scripts/Delete-EngageGroups/Delete-EngageGroups.ps1 @@ -0,0 +1,140 @@ +<# +.DESCRIPTION + The sample scripts are not supported under any Microsoft standard support + program or service. The sample scripts are provided AS IS without warranty + of any kind. Microsoft further disclaims all implied warranties including, + without limitation, any implied warranties of merchantability or of fitness for + a particular purpose. The entire risk arising out of the use or performance of + the sample scripts and documentation remains with you. In no event shall + Microsoft, its authors, or anyone else involved in the creation, production, or + delivery of the scripts be liable for any damages whatsoever (including, + without limitation, damages for loss of business profits, business interruption, + loss of business information, or other pecuniary loss) arising out of the use + of or inability to use the sample scripts or documentation, even if Microsoft + has been advised of the possibility of such damages. + +Purpose: + -Bulk deletes Viva Engage communities using the Microsoft Graph API: + https://learn.microsoft.com/en-us/graph/api/community-delete?view=graph-rest-beta + +Author: + Dean Cron + +Version: + 1.0 - Initial Release 2023 (legacy Yammer REST API) + 2.0 - Updated to use MSAL.PS for authentication Nov 2025 (legacy Yammer REST API) + 3.0 - July 2026 - Migrated off the deprecated Yammer REST API to the Microsoft Graph + community API. NOTE: community IDs are NOT the same as the legacy numeric + Yammer group IDs used by earlier versions of this script. See the README for + how to get the correct IDs for your input CSV. + +Requirements: + + 1. An Azure AD (Entra ID) App Registration with the following Microsoft Graph + **application** permission, with admin consent granted: + -Community.ReadWrite.All + + 2. CSV containing the Viva Engage community IDs of the communities you want deleted. + See the README for how to obtain these IDs. + +.EXAMPLE + .\Delete-EngageGroups.ps1 +#> + +<############ STUFF YOU NEED TO MODIFY ############> + +# Change these to match your environment. Instructions: +# https://learn.microsoft.com/en-us/graph/auth-v2-service?view=graph-rest-1.0 +$ClientId = "clientid" +$TenantId = "tenantid" +$ClientSecret = "clientsecret" + +#Point this to the groupstobedeleted.csv you created as per the requirements. +#The CSV must contain a single column named CommunityId with Viva Engage community IDs (not legacy numeric Yammer group IDs). +$groupsToBeDeletedCSV = 'C:\temp\groupstobedeleted.csv' + +#Change to $false when you're ready to actually delete the communities. DELETION CAN'T BE UNDONE. +$whatIfMode = $true + +<############ YOU SHOULD NOT HAVE TO MODIFY ANYTHING BELOW THIS LINE ############> + +function Connect-ToGraph { + param ( + [Parameter(Mandatory = $true)] + [string]$ClientId, + + [Parameter(Mandatory = $true)] + [string]$TenantId, + + [Parameter(Mandatory = $true)] + [string]$ClientSecret + ) + + $authBody = @{ + Grant_Type = "client_credentials" + Scope = "https://graph.microsoft.com/.default" + Client_Id = $ClientId + Client_Secret = $ClientSecret + } + + $Connection = Invoke-RestMethod -Uri https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token -Method POST -Body $authBody + return $Connection.access_token +} + +#Make sure groupstobedeleted.csv is where it's supposed to be +try { + $groupsCsv = Import-Csv $groupsToBeDeletedCSV -UseCulture +} +catch { + Write-Host "Unable to open the input CSV file. Ensure it's located at $groupsToBeDeletedCSV" + Return +} + +$accessToken = Connect-ToGraph -ClientId $ClientId -TenantId $TenantId -ClientSecret $ClientSecret +$authHeader = @{ Authorization = "Bearer $accessToken" } + +$groupsCsv | ForEach-Object { + do { + $rateLimitHit = $false + $communityId = $_.CommunityId + + try { + #Will it blend? + if ($whatIfMode) { + Write-Host "WhatIf mode enabled, would have successfully deleted community $communityId" -ForegroundColor Green + } + else { + Invoke-RestMethod -Uri "https://graph.microsoft.com/beta/employeeExperience/communities/$communityId" -Headers $authHeader -Method DELETE | Out-Null + Write-Host "Successfully deleted community $communityId" -ForegroundColor Green + } + } + catch { + $statusCode = $_.Exception.Response.StatusCode.Value__ + if ($statusCode -eq "429" -or $statusCode -eq "503") { + #Deal with rate limiting + #https://learn.microsoft.com/en-us/graph/throttling + $rateLimitHit = $true + } + elseif ($statusCode -eq "401" -or $statusCode -eq "403") { + #Thrown when the access token is invalid or lacks Community.ReadWrite.All. Exit here. + Write-Host "Exiting script, Graph API reports ACCESS DENIED. Ensure the app registration has Community.ReadWrite.All (application) permission with admin consent." -ForegroundColor Red + exit + } + elseif ($statusCode -eq "404") { + #Typically thrown when the community isn't found. No exit, try next community. + Write-Host "Graph API reports 404, typically caused if the community ($communityId) was not found" -ForegroundColor Red + } + else { + #Fallback, no idea what happened to get us here. + $l = $_.InvocationInfo.ScriptLineNumber + Write-Host "Failed to delete community" $communityId -ForegroundColor Red + Write-Host "error $statusCode on line $l" + } + } + if ($rateLimitHit) { + #429 or 503: Sleep for a bit before retrying + Write-Host "Rate limit hit, sleeping for 15 seconds" + Start-Sleep -Seconds 15 + } + } while ($rateLimitHit) +} diff --git a/scripts/Delete-YammerGroups/README.md b/scripts/Delete-EngageGroups/README.md similarity index 59% rename from scripts/Delete-YammerGroups/README.md rename to scripts/Delete-EngageGroups/README.md index 8eba9f3c..72689cdb 100644 --- a/scripts/Delete-YammerGroups/README.md +++ b/scripts/Delete-EngageGroups/README.md @@ -1,29 +1,28 @@ -# Microsoft FastTrack Open Source - Delete-YammerGroups +# Microsoft FastTrack Open Source - Delete-EngageGroups -This sample script will allow a Yammer admin to bulk-delete groups/communities +This sample script will allow an admin to bulk-delete Viva Engage communities using the Microsoft Graph API. + +**Version 3.0 note:** This script was migrated off the deprecated legacy Yammer REST API (`www.yammer.com/api/v1/groups`) to the Microsoft Graph [community API](https://learn.microsoft.com/en-us/graph/api/community-delete?view=graph-rest-beta), since the legacy endpoint is undocumented, unsupported, and at risk of being turned off without notice. **The ID format has changed as part of this migration** — see below. ## Usage ### Prerequisites -- You must create a new Yammer app registration in Microsoft Entra ID. This app should be configured to grant the following **delegated** permission: +- You must create a new app registration in Microsoft Entra ID. This app should be configured to grant the following **application** permission, with admin consent granted: ``` - Yammer - -access_as_user + Microsoft Graph + -Community.ReadWrite.All ``` - You'll need to create a CSV file containing one column: - - **GroupID**. This will contain the IDs of the groups you want to delete. - - The CSV should look similar to this: + - **CommunityId**. This will contain the Viva Engage community IDs (Microsoft Graph `community.id` values) of the communities you want to delete. - ![CSV format](bulkdeleteCSV.jpg?raw=true "Title") + > ⚠️ **This is not the same ID as the legacy numeric Yammer group ID used by earlier versions of this script or by the network data export CSVs.** Graph community IDs are opaque base64-style strings, not plain numbers. - You can get the group ID of the groups you need to add the admins to in one of three ways: + You can get the community IDs you need in one of two ways: - 1. Grab the group ID from the group's URL and use a BASE64 decoder on the string at the end as described here: [How do I find a community's group feed ID in Viva Engage?](https://support.microsoft.com/en-us/office/how-do-i-find-a-community-s-group-feed-id-in-yammer-9372ab6f-bcc2-4283-bb6a-abf42dec970f) - 2. Run a network data export going back as far as possible (do not export attachments) and get the group ID from the groups.csv file generated: https://learn.microsoft.com/en-us/rest/api/yammer/network-data-export - 3. If you're doing this pre-native mode migration, you can get the group IDs from the alignment report you're basing your cleanup on. + 1. `GET https://graph.microsoft.com/beta/employeeExperience/communities` and match communities by `displayName`, taking the `id` field from the response. + 2. If you still have the legacy numeric group ID, you can find the Microsoft 365 group behind it and cross-reference it to a community via `GET /employeeExperience/communities` (filtering/matching on the associated `groupId`), since Graph doesn't expose a direct legacy-ID lookup. ### Variables @@ -35,20 +34,17 @@ There are a few variables you need to change in the script itself. These are loc 2. **$TenantId = "TenantIDString"** - >Replace TenantIDString with the Client ID of the app registration you created in the prerequisites. + >Replace TenantIDString with the Tenant ID of the app registration you created in the prerequisites. 3. **$ClientSecret = "ClientSecretString"** >Replace ClientSecretString with the client secret value of the app registration you created in the prerequisites. - -4. **$RedirectUri = "https://localhost"** - >Replace this with the redirect Url you set in your app registration (if not set to https://localhost) -5. **$groupsToBeDeletedCSV = 'C:\temp\groupstobedeleted.csv'** +4. **$groupsToBeDeletedCSV = 'C:\temp\groupstobedeleted.csv'** Point this to the groupstobedeleted.csv file you created as mentioned above. -6. **$whatIfMode = $true** +5. **$whatIfMode = $true** The script runs in a WhatIf mode by default since the group deletion can’t be undone, so it’ll only loop through the CSV and tell you which groups it *would* have deleted, it doesn’t actually take hard action. When you’re ready to have it actually delete groups, change the value to $false @@ -60,15 +56,15 @@ None Once you’ve completed the pre-reqs, you’re ready to go. Run the script like so: - .\Delete-YammerGroups.ps1 + .\Delete-EngageGroups.ps1 ### Notes -**This sample calls an undocumented endpoint in the Yammer REST APIs, and as such has no official support provided for it, and may stop working without warning.** +**This sample now uses the Microsoft Graph community API (`/beta/employeeExperience/communities`), which is officially documented and supported, unlike the earlier version of this script.** The `/beta` Graph endpoint is still subject to change, so monitor the [Graph changelog](https://learn.microsoft.com/en-us/graph/changelog) for updates. ## Applies To -- Yammer / Viva Engage networks in M365 +- Viva Engage communities in M365 ## Author @@ -76,6 +72,7 @@ Once you’ve completed the pre-reqs, you’re ready to go. Run the script like |----|-------------------------- |Dean Cron, Microsoft|July 6th, 2023| |Dean Cron, Microsoft|November 4th, 2025| +|Dean Cron, Microsoft|July 22nd, 2026 - Migrated to Microsoft Graph| ## Issues diff --git a/scripts/Delete-YammerGroups/bulkdeleteCSV.jpg b/scripts/Delete-EngageGroups/bulkdeleteCSV.jpg similarity index 100% rename from scripts/Delete-YammerGroups/bulkdeleteCSV.jpg rename to scripts/Delete-EngageGroups/bulkdeleteCSV.jpg diff --git a/scripts/Delete-EngageUsers/Delete-EngageUsers.ps1 b/scripts/Delete-EngageUsers/Delete-EngageUsers.ps1 new file mode 100644 index 00000000..1119e682 --- /dev/null +++ b/scripts/Delete-EngageUsers/Delete-EngageUsers.ps1 @@ -0,0 +1,169 @@ +<# +.DESCRIPTION + The sample scripts are not supported under any Microsoft standard support + program or service. The sample scripts are provided AS IS without warranty + of any kind. Microsoft further disclaims all implied warranties including, + without limitation, any implied warranties of merchantability or of fitness for + a particular purpose. The entire risk arising out of the use or performance of + the sample scripts and documentation remains with you. In no event shall + Microsoft, its authors, or anyone else involved in the creation, production, or + delivery of the scripts be liable for any damages whatsoever (including, + without limitation, damages for loss of business profits, business interruption, + loss of business information, or other pecuniary loss) arising out of the use + of or inability to use the sample scripts or documentation, even if Microsoft + has been advised of the possibility of such damages. + +Purpose: + -Bulk-removes users from a Viva Engage community using the Microsoft Graph API. + A Viva Engage community's membership is just the membership of its underlying + Microsoft 365 group, so this uses the group members API: + https://learn.microsoft.com/en-us/graph/api/group-delete-member + https://learn.microsoft.com/en-us/graph/api/resources/engagement-api-overview#community-membership-management + + NOTE: This does NOT delete the user's account or identity, and does not remove them + from every community/group they belong to - only from the specific group(s) listed in + the input CSV. There is no Graph (or supported) API to delete a "Yammer user" as a + standalone concept; the legacy Yammer REST API's DELETE /users/{id}.json endpoint + deleted/deactivated the underlying account entirely, which has no direct Graph + equivalent. If you need to fully remove someone's access across every community, either + run this script once per group they belong to, or deprovision/disable their Entra ID + account instead (which is the modern equivalent of deactivating a user tenant-wide). + +Author: + Dean Cron + +Version: + 1.0 - Initial Release 2023 (legacy Yammer REST API - deleted the user's Yammer account) + 2.0 - Updated to v2 November 2025 (legacy Yammer REST API) + 3.0 - July 2026 - Migrated off the deprecated Yammer REST API to the Microsoft Graph + group-members API. This is a change in scope, not just a like-for-like swap: the + old version deleted the user's Yammer account entirely (removing them from every + group/community). This version only removes the user from the specific group listed + for them in the input CSV. See the README for details and options if you need + broader removal. + +Requirements: + + 1. An Azure AD (Entra ID) App Registration with the following Microsoft Graph + **application** permission, with admin consent granted: + -Group.ReadWrite.All + + 2. CSV containing group IDs and users to remove from each. See the README for more + information on how to create this and how to find the group ID for a community: + https://github.com/microsoft/FastTrack/tree/master/scripts/Delete-EngageUsers/README.md + + +.EXAMPLE + .\Delete-EngageUsers.ps1 +#> + +<############ STUFF YOU NEED TO MODIFY ############> + +#Point this to the userstobedeleted.csv you created as per the requirements. +$usersToBeDeletedCSV = 'C:\temp\userstobedeleted.csv' + +# Change these to match your environment. Instructions: +# https://learn.microsoft.com/en-us/graph/auth-v2-service?view=graph-rest-1.0 +$ClientId = "clientid" +$TenantId = "tenantid" +$ClientSecret = "clientsecret" + +#Change to $false when you're ready to actually remove the users. THIS CAN'T BE UNDONE. +$whatIfMode = $true + +<############ YOU SHOULD NOT HAVE TO MODIFY ANYTHING BELOW THIS LINE ############> + +#Make sure userstobedeleted.csv is where it's supposed to be +try { + $usersCsv = Import-Csv $usersToBeDeletedCSV -UseCulture +} +catch { + Write-Host "Unable to open the input CSV file. Ensure it's located at $usersToBeDeletedCSV" + Return +} + +function Connect-ToGraph { + param ( + [Parameter(Mandatory = $true)] + [string]$ClientId, + + [Parameter(Mandatory = $true)] + [string]$TenantId, + + [Parameter(Mandatory = $true)] + [string]$ClientSecret + ) + + $authBody = @{ + Grant_Type = "client_credentials" + Scope = "https://graph.microsoft.com/.default" + Client_Id = $ClientId + Client_Secret = $ClientSecret + } + + $Connection = Invoke-RestMethod -Uri https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token -Method POST -Body $authBody + return $Connection.access_token +} + +$accessToken = Connect-ToGraph -ClientId $ClientId -TenantId $TenantId -ClientSecret $ClientSecret +$authHeader = @{ Authorization = "******" } + +Write-Host "Starting to remove users from groups..." -ForegroundColor Cyan + +$usersCsv | ForEach-Object { + do { + $rateLimitHit = $false + + $gID = $_.GroupID + $mail = $_.Email + + try { + #Resolve the user's directory object ID from their email/UPN. The group-members + #removal endpoint requires the object ID, not the UPN. + #https://learn.microsoft.com/en-us/graph/api/user-get + $user = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$mail`?`$select=id" -Headers $authHeader -Method GET + $userId = $user.id + + if ($whatIfMode) { + Write-Host "WhatIf mode enabled, would have successfully removed $mail (userID $userId) from group $gID" -ForegroundColor Green + } + else { + #Remove the user from the group. This removes their access to the associated + #Viva Engage community, but does NOT delete their account or remove them from + #any other groups/communities. + #https://learn.microsoft.com/en-us/graph/api/group-delete-member + Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/groups/$gID/members/$userId/`$ref" -Headers $authHeader -Method DELETE | Out-Null + Write-Host "Successfully removed $mail from group $gID" -ForegroundColor Green + } + } + catch { + $statusCode = $_.Exception.Response.StatusCode.Value__ + if ($statusCode -eq "429" -or $statusCode -eq "503") { + #Deal with rate limiting + #https://learn.microsoft.com/en-us/graph/throttling + $rateLimitHit = $true + } + elseif ($statusCode -eq "401" -or $statusCode -eq "403") { + #Thrown when the access token is invalid or lacks Group.ReadWrite.All + Write-Host "Exiting script, Graph API reports ACCESS DENIED. Ensure the app registration has Group.ReadWrite.All (application) permission with admin consent." -ForegroundColor Red + exit + } + elseif ($statusCode -eq "404") { + #Typically thrown when either the user, group, or membership isn't found + Write-Host "Graph API reports 404, typically caused when the user ($mail), group ($gID) was not found, or the user isn't a member of that group" -ForegroundColor Red + } + else { + $l = $_.InvocationInfo.ScriptLineNumber + Write-Host "Failed to remove" $mail "from group" $gID -ForegroundColor Red + Write-Host "error $statusCode on line $l" + } + } + if ($rateLimitHit) { + #429 or 503: Sleep for a bit before retrying + Write-Host "Rate limit hit, sleeping for 15 seconds" + Start-Sleep -Seconds 15 + } + } while ($rateLimitHit) +} + +Write-Host "All done!" -ForegroundColor Cyan diff --git a/scripts/Delete-EngageUsers/README.md b/scripts/Delete-EngageUsers/README.md new file mode 100644 index 00000000..c465281f --- /dev/null +++ b/scripts/Delete-EngageUsers/README.md @@ -0,0 +1,101 @@ +# Microsoft FastTrack Open Source - Delete-EngageUsers + +This sample script bulk-removes users from a specific Viva Engage community (Microsoft 365 group) using the Microsoft Graph API. + +**Important - this is a change in scope from earlier versions:** The original version of this script called the legacy Yammer REST API's `DELETE /users/{id}.json` endpoint, which deleted/deactivated the user's Yammer account entirely, removing them from every group/community they belonged to. There is no Graph (or otherwise supported) API to delete a "Yammer user" as a standalone concept. This version instead removes each listed user from **one specific group per CSV row** - it does not delete their account and does not touch any other group/community memberships they may have. + +If you need to remove someone's access across every community they belong to, either: +- Run this script once per group they're a member of (list each group/user pair as its own CSV row), or +- Deprovision/disable their Entra ID account instead, which is the modern equivalent of deactivating a user tenant-wide. + +## Usage + +### Prerequisites + +- You must create a new Azure AD (Entra ID) App Registration with the following Microsoft Graph **application** permission, with admin consent granted: + ``` + Group.ReadWrite.All + ``` + +- You'll need to create a CSV file containing two columns: + - **GroupID**. The Microsoft 365 group ID (a GUID) that backs the community you want to remove the user from. + - **Email**. The UPN/email address of the user to remove. + + The CSV should look similar to this: + + ![CSV format](UserIDSample.png?raw=true "Title") + + You can find the group ID for a community via Microsoft Graph: + `GET https://graph.microsoft.com/beta/employeeExperience/communities?$filter=displayName eq 'Community Display Name'` + The `groupId` property in the response is the value to use. + +### Variables + +There are a few variables you need to change in the script itself. These are located very early in the script just below “<############ STUFF YOU NEED TO MODIFY ############>”: + +1. **$usersToBeDeletedCSV = 'C:\temp\userstobedeleted.csv'** + + Point this to the CSV file you created as mentioned above. + +2. **$ClientId = "clientid"** + + >Replace with the Client ID of the app registration you created in the prerequisites. + +3. **$TenantId = "tenantid"** + + >Replace with your tenant ID. + +4. **$ClientSecret = "clientsecret"** + + >Replace with the client secret value of the app registration you created in the prerequisites. + +5. **$whatIfMode = $true** + + The script runs in a WhatIf mode by default since removal can't be undone, so it'll only loop through the CSV and tell you which users it *would* have removed, it doesn't actually take hard action. When you're ready to have it actually remove users, change the value to $false + +### Parameters + +None + +### Execution + +Once you've completed the pre-reqs, you're ready to go. Run the script like so: + + .\Delete-EngageUsers.ps1 + +## Applies To + +- Viva Engage communities in M365 + +## Author + +|Author|Original Publish Date +|----|-------------------------- +|Dean Cron, Microsoft|November 28th, 2023| +|Dean Cron, Microsoft|November 4th, 2025| +|Dean Cron, Microsoft|July 22nd, 2026| + +## Issues + +Please report any issues you find to the [issues list](../../../../issues). + +## Support Statement + +The scripts, samples, and tools made available through the FastTrack Open Source initiative are provided as-is. These resources are developed in partnership with the community and do not represent official Microsoft software. As such, support is not available through premier or other Microsoft support channels. If you find an issue or have questions please reach out through the issues list and we'll do our best to assist, however there is no associated SLA. + +## Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Legal Notices + +Microsoft and any contributors grant you a license to the Microsoft documentation and other content in this repository under the [MIT License](https://opensource.org/licenses/MIT), see the [LICENSE](LICENSE) file, and grant you a license to any code in the repository under the [MIT License](https://opensource.org/licenses/MIT), see the [LICENSE-CODE](LICENSE-CODE) file. + +Microsoft, Windows, Microsoft Azure and/or other Microsoft products and services referenced in the documentation may be either trademarks or registered trademarks of Microsoft in the United States and/or other countries. The licenses for this project do not grant you rights to use any Microsoft names, logos, or trademarks. Microsoft's general trademark guidelines can be found at http://go.microsoft.com/fwlink/?LinkID=254653. + +Privacy information can be found at https://privacy.microsoft.com/en-us/ + +Microsoft and any contributors reserve all others rights, whether under their respective copyrights, patents,or trademarks, whether by implication, estoppel or otherwise. + diff --git a/scripts/Delete-YammerUsers/UserIDSample.png b/scripts/Delete-EngageUsers/UserIDSample.png similarity index 100% rename from scripts/Delete-YammerUsers/UserIDSample.png rename to scripts/Delete-EngageUsers/UserIDSample.png diff --git a/scripts/Delete-YammerGroups/Delete-YammerGroups.ps1 b/scripts/Delete-YammerGroups/Delete-YammerGroups.ps1 deleted file mode 100644 index a8a02916..00000000 --- a/scripts/Delete-YammerGroups/Delete-YammerGroups.ps1 +++ /dev/null @@ -1,133 +0,0 @@ -<# -.DESCRIPTION - The sample scripts are not supported under any Microsoft standard support - program or service. The sample scripts are provided AS IS without warranty - of any kind. Microsoft further disclaims all implied warranties including, - without limitation, any implied warranties of merchantability or of fitness for - a particular purpose. The entire risk arising out of the use or performance of - the sample scripts and documentation remains with you. In no event shall - Microsoft, its authors, or anyone else involved in the creation, production, or - delivery of the scripts be liable for any damages whatsoever (including, - without limitation, damages for loss of business profits, business interruption, - loss of business information, or other pecuniary loss) arising out of the use - of or inability to use the sample scripts or documentation, even if Microsoft - has been advised of the possibility of such damages. - -Purpose: - -Bulk deletes Yammer groups - -Author: - Dean Cron - -Version: - 1.0 - Initial Release 2023 - 2.0 - Updated to use MSAL.PS for authentication Nov 2025 - -Requirements: - - 1. MSAL.PS PowerShell module. Install it from the PowerShell Gallery with the command: - Install-Module MSAL.PS - - 2. An Azure AD App Registration with the following API permissions: - -Yammer: access_as_user - - 2. CSV containing Yammer group IDs of groups you want deleted. - - -.EXAMPLE - .\Delete-YammerGroups.ps1 -#> - -<############ STUFF YOU NEED TO MODIFY ############> - -# Change these to match your environment. Instructions: -# https://learn.microsoft.com/en-us/graph/auth-v2-service?view=graph-rest-1.0 -$ClientId = "clientid" -$TenantId = "tenantId" -$RedirectUri = "https://localhost" - -#Point this to the groupstobedeleted.csv you created as per the requirements. -$groupsToBeDeletedCSV = 'C:\temp\groupstobedeleted.csv' - -#Change to $false when you're ready to actually delete the groups. DELETION CAN'T BE UNDONE. -$whatIfMode = $true - -<############ YOU SHOULD NOT HAVE TO MODIFY ANYTHING BELOW THIS LINE ############> - -$Scopes = @("https://api.yammer.com/.default") - -#Check to see if MSAL.PS is installed, if not exit with instructions -if(-not (Get-Module -ListAvailable -Name MSAL.PS)){ - Write-Host "MSAL.PS module not found, please install it from the PowerShell Gallery with the command:" -ForegroundColor Red - Write-Host "Install-Module MSAL.PS" -ForegroundColor Yellow - Return -} -function Get-YammerAuthHeader { - $authToken = Get-MsalToken -ClientId $ClientId -TenantId $TenantId -RedirectUri $RedirectUri -Scopes $Scopes -Interactive - if (-not $authToken) { - Write-Host "Failed to acquire Yammer Auth Token. Please ensure the ClientID, TenantID, and ClientSecret are correct." -ForegroundColor Red - Return - } - else { - return @{ AUTHORIZATION = "Bearer $($authToken.AccessToken)" } - } -} - -#Make sure groupstobedeleted.csv is where it's supposed to be -try{ - $groupsCsv = Import-Csv $groupsToBeDeletedCSV -UseCulture -} -catch{ - Write-Host "Unable to open the input CSV file. Ensure it's located at $groupsToBeDeletedCSV" - Return -} - -$authHeader = Get-YammerAuthHeader - -$groupsCsv | ForEach-Object { - do { - $rateLimitHit = $false - $gID = $_.GroupID -as [decimal] - - try - { - #Will it blend? - if($whatIfMode){ - Write-Host "WhatIf mode enabled, would have successfully deleted group $gID" -ForegroundColor Green - } - else{ - $deleteGroup = Invoke-WebRequest "https://www.yammer.com/api/v1/groups/$gID.json" -Headers $authHeader -Method DELETE - Write-Host "Successfully deleted group $gID" -ForegroundColor Green - } - } - catch { - if( $_.Exception.Response.StatusCode.Value__ -eq "429" -or $_.Exception.Response.StatusCode.Value__ -eq "503" ) - { - #Deal with rate limiting - #https://learn.microsoft.com/en-us/rest/api/yammer/rest-api-rate-limits#yammer-api-rate-limts - $rateLimitHit = $true - } - elseif($_.Exception.Response.StatusCode.Value__ -eq "401"){ - #Thrown when the YammerAuthToken is invalid. Exit here. - Write-Host "Exiting script, API reports ACCESS DENIED. Please ensure a valid developer token is set for the YammerAuthToken variable" -ForegroundColor Red - exit - } - elseif($_.Exception.Response.StatusCode.Value__ -eq "404"){ - #Typically thrown when the group isn't found. No exit, try next group. - Write-Host "Exiting script, API reports 404, typically caused if the group ($gID) was not found" -ForegroundColor Red - } - else{ - #Fallback, no idea what happened to get us here. - $e = $_.Exception.Response.StatusCode.Value__ - $l = $_.InvocationInfo.ScriptLineNumber - Write-Host "Failed to delete group" $gID -ForegroundColor Red - Write-Host "error $e on line $l" - } - } - if ($rateLimitHit) { - #429 or 503: Sleep for a bit before retrying - Write-Host "Rate limit hit, sleeping for 15 seconds" - Start-Sleep -Seconds 15 - } - } while ($rateLimitHit) -} diff --git a/scripts/Delete-YammerUsers/Delete-YammerUsers.ps1 b/scripts/Delete-YammerUsers/Delete-YammerUsers.ps1 deleted file mode 100644 index c91f8baa..00000000 --- a/scripts/Delete-YammerUsers/Delete-YammerUsers.ps1 +++ /dev/null @@ -1,133 +0,0 @@ -<# -.DESCRIPTION - The sample scripts are not supported under any Microsoft standard support - program or service. The sample scripts are provided AS IS without warranty - of any kind. Microsoft further disclaims all implied warranties including, - without limitation, any implied warranties of merchantability or of fitness for - a particular purpose. The entire risk arising out of the use or performance of - the sample scripts and documentation remains with you. In no event shall - Microsoft, its authors, or anyone else involved in the creation, production, or - delivery of the scripts be liable for any damages whatsoever (including, - without limitation, damages for loss of business profits, business interruption, - loss of business information, or other pecuniary loss) arising out of the use - of or inability to use the sample scripts or documentation, even if Microsoft - has been advised of the possibility of such damages. - -Purpose: - -Bulk deletes Yammer users - -Author: - Dean Cron - -Version: - 1.0 - -Requirements: - - 1. MSAL.PS PowerShell module. Install it from the PowerShell Gallery with the command: - Install-Module MSAL.PS - - 2. An Azure AD App Registration with the following API permissions: - -Yammer: access_as_user - - 2. CSV containing Yammer user IDs of users you want deleted. - - -.EXAMPLE - .\Delete-YammerUsers.ps1 -#> - -<############ STUFF YOU NEED TO MODIFY ############> - -# Change these to match your environment. Instructions: -# https://learn.microsoft.com/en-us/graph/auth-v2-service?view=graph-rest-1.0 -$ClientId = "clientid" -$TenantId = "tenantId" -$RedirectUri = "https://localhost" - -#Point this to the userstobedeleted.csv you created as per the requirements. -$usersToBeDeletedCSV = 'C:\temp\userstobedeleted.csv' - -#Change to $false when you're ready to actually delete the users. DELETION CAN'T BE UNDONE. -$whatIfMode = $true - -<############ YOU SHOULD NOT HAVE TO MODIFY ANYTHING BELOW THIS LINE ############> - -$Scopes = @("https://api.yammer.com/.default") - -#Check to see if MSAL.PS is installed, if not exit with instructions -if(-not (Get-Module -ListAvailable -Name MSAL.PS)){ - Write-Host "MSAL.PS module not found, please install it from the PowerShell Gallery with the command:" -ForegroundColor Red - Write-Host "Install-Module MSAL.PS" -ForegroundColor Yellow - Return -} - -function Get-YammerAuthHeader { - $authToken = Get-MsalToken -ClientId $ClientId -TenantId $TenantId -RedirectUri $RedirectUri -Scopes $Scopes -Interactive - if (-not $authToken) { - Write-Host "Failed to acquire Yammer Auth Token. Please ensure the ClientID, TenantID, and ClientSecret are correct." -ForegroundColor Red - Return - } - else { - return @{ AUTHORIZATION = "Bearer $($authToken.AccessToken)" } - } -} - -#Make sure userstobedeleted.csv is where it's supposed to be -try{ - $usersCsv = Import-Csv $usersToBeDeletedCSV -UseCulture -} -catch{ - Write-Host "Unable to open the input CSV file. Ensure it's located at $usersToBeDeletedCSV" - Return -} - -$authHeader = Get-YammerAuthHeader - -$usersCsv | ForEach-Object { - do { - $rateLimitHit = $false - $uID = $_.UserID -as [decimal] - - try - { - #Will it blend? - if($whatIfMode){ - Write-Host "WhatIf mode enabled, would have successfully deleted user $uID" -ForegroundColor Green - } - else{ - $deleteUser = Invoke-WebRequest "https://www.yammer.com/api/v1/users/$uID.json" -Headers $authHeader -Method DELETE - Write-Host "Successfully deleted user $uID" -ForegroundColor Green - } - } - catch { - if( $_.Exception.Response.StatusCode.Value__ -eq "429" -or $_.Exception.Response.StatusCode.Value__ -eq "503" ) - { - #Deal with rate limiting - #https://learn.microsoft.com/en-us/rest/api/yammer/rest-api-rate-limits#yammer-api-rate-limts - $rateLimitHit = $true - } - elseif($_.Exception.Response.StatusCode.Value__ -eq "401"){ - #Thrown when the YammerAuthToken is invalid. Exit here. - Write-Host "Exiting script, API reports ACCESS DENIED. Please ensure a valid developer token is set for the YammerAuthToken variable" -ForegroundColor Red - exit - } - elseif($_.Exception.Response.StatusCode.Value__ -eq "404"){ - #Typically thrown when the user isn't found. No exit, try next user. - Write-Host "Exiting script, API reports 404, typically caused if the user ($uID) was not found" -ForegroundColor Red - } - else{ - #Fallback, no idea what happened to get us here. - $e = $_.Exception.Response.StatusCode.Value__ - $l = $_.InvocationInfo.ScriptLineNumber - Write-Host "Failed to delete user" $uID -ForegroundColor Red - Write-Host "error $e on line $l" - } - } - if ($rateLimitHit) { - #429 or 503: Sleep for a bit before retrying - Write-Host "Rate limit hit, sleeping for 15 seconds" - Start-Sleep -Seconds 15 - } - } while ($rateLimitHit) -} diff --git a/scripts/Delete-YammerUsers/README.md b/scripts/Delete-YammerUsers/README.md deleted file mode 100644 index 234bae36..00000000 --- a/scripts/Delete-YammerUsers/README.md +++ /dev/null @@ -1,104 +0,0 @@ -# Microsoft FastTrack Open Source - Delete-YammerUsers - -This sample script will allow a Yammer admin to bulk-delete Yammer users - -## Usage - -### Prerequisites - -- You must create a new Yammer app registration in Microsoft Entra ID. This app should be configured to grant the following **delegated** permission: - ``` - Yammer - -access_as_user - ``` - -- You'll need to create a CSV file containing one column: - - **UserID**. This will contain the IDs of the users you want to delete. - - The CSV should look similar to this: - - ![CSV format](UserIDSample.png?raw=true "Title") - - You can get the user ID of the users you want to delete in one of two ways: - - 1. Run a user export of all users. Settings-> Edit Network Admin Settings -> Export Users -> Export All Users. Grad the IDs of the users you want to delete from here and crearte a new CSV, placing those IDs in a column named UserID. - 2. If you're doing this pre-native mode migration, you can get the user IDs from the alignment report you're basing your cleanup on. - -### Variables - -There are a few variables you need to change in the script itself. These are located very early in the script just below “<############ STUFF YOU NEED TO MODIFY ############>”: - -1. **$ClientId = "ClientIDString"** - - >Replace ClientIDString with the Client ID of the app registration you created in the prerequisites. - -2. **$TenantId = "TenantIDString"** - - >Replace TenantIDString with the Client ID of the app registration you created in the prerequisites. - -3. **$ClientSecret = "ClientSecretString"** - - >Replace ClientSecretString with the client secret value of the app registration you created in the prerequisites. - -4. **$RedirectUri = "https://localhost"** - >Replace this with the redirect Url you set in your app registration (if not set to https://localhost) - -5. **$Global:YammerAuthToken = "BearerTokenString"** - - Replace BearerTokenString with the token you created via the instructions in the prerequisites. The line should look something like this: - - $Global:YammerAuthToken = "21737620380-GFy6awIxfYGULlgZvf43A" - -6. **$usersToBeDeletedCSV = 'C:\temp\userstobedeleted.csv'** - - Point this to the userstobedeleted.csv file you created as mentioned above. - -7. **$whatIfMode = $true** - - The script runs in a WhatIf mode by default since user deletion can’t be undone, so it’ll only loop through the CSV and tell you which users it *would* have deleted, it doesn’t actually take hard action. When you’re ready to have it actually delete users, change the value to $false - -### Parameters - -None - -### Execution - -Once you’ve completed the pre-reqs, you’re ready to go. Run the script like so: - - .\Delete-YammerUsers.ps1 - -## Applies To - -- Yammer / Viva Engage networks in M365 - -## Author - -|Author|Original Publish Date -|----|-------------------------- -|Dean Cron, Microsoft|November 28th, 2023| -|Dean Cron, Microsoft|November 4th, 2025| - -## Issues - -Please report any issues you find to the [issues list](../../../../issues). - -## Support Statement - -The scripts, samples, and tools made available through the FastTrack Open Source initiative are provided as-is. These resources are developed in partnership with the community and do not represent official Microsoft software. As such, support is not available through premier or other Microsoft support channels. If you find an issue or have questions please reach out through the issues list and we'll do our best to assist, however there is no associated SLA. - -## Code of Conduct - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. - -## Legal Notices - -Microsoft and any contributors grant you a license to the Microsoft documentation and other content in this repository under the [MIT License](https://opensource.org/licenses/MIT), see the [LICENSE](LICENSE) file, and grant you a license to any code in the repository under the [MIT License](https://opensource.org/licenses/MIT), see the [LICENSE-CODE](LICENSE-CODE) file. - -Microsoft, Windows, Microsoft Azure and/or other Microsoft products and services referenced in the documentation may be either trademarks or registered trademarks of Microsoft in the United States and/or other countries. The licenses for this project do not grant you rights to use any Microsoft names, logos, or trademarks. Microsoft's general trademark guidelines can be found at http://go.microsoft.com/fwlink/?LinkID=254653. - -Privacy information can be found at https://privacy.microsoft.com/en-us/ - -Microsoft and any contributors reserve all others rights, whether under their respective copyrights, patents,or trademarks, whether by implication, estoppel or otherwise. - diff --git a/scripts/Export-YammerFiles/Export-YammerFiles.ps1 b/scripts/Export-EngageFiles/Export-EngageFiles.ps1 similarity index 91% rename from scripts/Export-YammerFiles/Export-YammerFiles.ps1 rename to scripts/Export-EngageFiles/Export-EngageFiles.ps1 index 8f9407ad..1b3029aa 100644 --- a/scripts/Export-YammerFiles/Export-YammerFiles.ps1 +++ b/scripts/Export-EngageFiles/Export-EngageFiles.ps1 @@ -22,6 +22,10 @@ Author: Version: 1.1 + 1.2 - July 2026 - Reviewed against current Microsoft documentation (still the current, + supported endpoint as of this review). Added a runtime reminder that native-mode + networks won't export actual file attachments via this API. See MC1230453 for the + related March 13, 2026 retirement of the equivalent admin center options. Requirements: 1. MSAL.PS PowerShell module. Install it from the PowerShell Gallery with the command: @@ -36,7 +40,7 @@ Requirements: Required. Sets the end date for the target date range of the export .EXAMPLE - .\Export-YammerFiles.ps1 -startdate 2023-01-14 -enddate 2023-01-31 + .\Export-EngageFiles.ps1 -startdate 2023-01-14 -enddate 2023-01-31 #> Param( @@ -130,6 +134,9 @@ Function Write-Log { Write-Host "Starting export for date range" $StartDate "to" $EndDate -ForegroundColor Green +$nativeModeWarning = "Reminder: for Viva Engage networks in native mode, this API will NOT export actual file attachments (they live in SharePoint, not Yammer's legacy file store). You'll get a files.csv with SharePoint links instead. See MC1230453 and https://learn.microsoft.com/en-us/rest/api/yammer/yammer-files-export-api" +Write-Host $nativeModeWarning -ForegroundColor Yellow + #Populating key vars $activityLogName = "ScriptLog{0}.txt" -f [DateTime]::Now.ToString("yyyy-MM-dd_hh-mm-ss") $activityLog = $rootPath + "\Export" +(Get-Date -Date $StartDate -format "yyyyMMdd") +"to" +(Get-Date -Date $EndDate -format "yyyyMMdd" ) +"\" +$activityLogName diff --git a/scripts/Export-YammerFiles/README.md b/scripts/Export-EngageFiles/README.md similarity index 89% rename from scripts/Export-YammerFiles/README.md rename to scripts/Export-EngageFiles/README.md index 0eb72522..3a9db68d 100644 --- a/scripts/Export-YammerFiles/README.md +++ b/scripts/Export-EngageFiles/README.md @@ -1,4 +1,4 @@ -# Microsoft FastTrack Open Source - Export-YammerFiles +# Microsoft FastTrack Open Source - Export-EngageFiles This sample script calls the Yammer Files Export API to export files from your Yammer network that were uploaded by users during the date range specified in the command. @@ -47,7 +47,7 @@ There are a few variables you need to change in the script itself. These are loc Once you’ve made and saved those changes, you’re ready to go. To run the script, just pass the startdate and enddate for the time period you want exported files from, like so: - .\Export-YammerFiles.ps1 -startdate 2023-01-14 -enddate 2023-01-31 + .\Export-EngageFiles.ps1 -startdate 2023-01-14 -enddate 2023-01-31 Reminder - Those parameters need to be in the YYYY-MM-DD format as shown above. Once complete, the console output will tell you where to find the result, which should be a date-named folder underneath your $rootPath set above. @@ -59,6 +59,8 @@ Each time you run this, it will create a new folder under the $rootPath you spec If your network is in native mode, no files will be downloaded. You'll get a CSV file with information on each file in the network that includes the URL of each. Actual files are only exported from pre-native mode networks where the files are still in Yammer's file store, not SharePoint. +**July 2026 update:** This script still calls the current, Microsoft-documented Files Export API (`www.yammer.com/api/v1/export/requests`) — there is no separate/newer replacement endpoint as of this review. Microsoft has confirmed (Message Center ID **MC1230453**) that the "Include attachments" admin center option is being retired March 13, 2026; for native-mode networks this API already only returns SharePoint links (files.csv), never the actual attachments, so no functional change is expected for native-mode tenants. The script now prints a reminder of this at runtime. + ## Applies To - Yammer / Viva Engage networks in M365 diff --git a/scripts/Export-YammerNetworkData/Export-YammerNetworkData.ps1 b/scripts/Export-EngageNetworkData/Export-EngageNetworkData.ps1 similarity index 82% rename from scripts/Export-YammerNetworkData/Export-YammerNetworkData.ps1 rename to scripts/Export-EngageNetworkData/Export-EngageNetworkData.ps1 index 3bd2a8b3..34a7e8a8 100644 --- a/scripts/Export-YammerNetworkData/Export-YammerNetworkData.ps1 +++ b/scripts/Export-EngageNetworkData/Export-EngageNetworkData.ps1 @@ -23,6 +23,15 @@ Author: Version: 1.0 - June 2024 - Initial release 2.0 - November 2025 - Updated authentication + 2.1 - July 2026 - Reviewed against current Microsoft documentation (still the current, + supported endpoint as of this review). Added a runtime warning for the -IncludeFiles + parameter, which Microsoft's docs confirm only applies to legacy (pre-native mode) + networks. See MC1230453 for the related March 13, 2026 retirement of the equivalent + admin center option. + 2.2 - July 2026 - Removed the -IncludeExternalNetworks parameter/option. External network + export is not supported for native-mode networks (which don't touch external + networks during migration), and Microsoft is retiring the equivalent admin center + option March 13, 2026 (MC1230453). Requirements: 1. MSAL.PS PowerShell module. Install it from the PowerShell Gallery with the command: @@ -37,12 +46,9 @@ Requirements: OPTIONAL. Sets the end date for the target date range of the export. We recommend including this to set a reasonable range to avoid timeouts. .PARAMETER IncludeFiles OPTIONAL. Set this to ‘all’ for CSVs (including messages) and all file attachments, or ‘csv’ for CSVs only (including messages), no file attachments. -.PARAMETER IncludeExternalNetworks - OPTIONAL. Setting this to ‘true’ would result in CSVs and/or file attachments downloaded for the primary network and all associated external networks. - This is unnecessary for native mode migration, as migration doesn’t touch external networks .EXAMPLE - .\Export-YammerNetworkData.ps1 -startdate 2023-01-14 -enddate 2023-01-31 + .\Export-EngageNetworkData.ps1 -startdate 2023-01-14 -enddate 2023-01-31 #> Param( @@ -74,11 +80,7 @@ Param( [Parameter(Mandatory = $false)] [ValidateSet('Csv', 'All')] - [string]$IncludeFiles, - - [Parameter(Mandatory = $false)] - [ValidateSet('true', 'false')] - [string]$IncludeExternalNetworks + [string]$IncludeFiles ) <############ STUFF YOU NEED TO MODIFY ############> @@ -171,10 +173,11 @@ if ($PSBoundParameters.ContainsKey('EndDate')) { if ($PSBoundParameters.ContainsKey('IncludeFiles')) { $Uri += "&include=$IncludeFiles" -} - -if ($PSBoundParameters.ContainsKey('IncludeExternalNetworks')) { - $Uri += "&include_ens=$IncludeExternalNetworks" + if ($IncludeFiles -eq 'All') { + $filesWarning = "-IncludeFiles All was specified, but per Microsoft's documentation this only applies to legacy (pre-native mode) Yammer networks. Native-mode Viva Engage networks will NOT return file attachments regardless of this setting - you'll only get SharePoint links in files.csv. See MC1230453 (retiring the equivalent admin center options March 13, 2026) and https://learn.microsoft.com/en-us/rest/api/yammer/network-data-export" + Write-Host $filesWarning -ForegroundColor Yellow + Write-Log -Level "WARN" -Message $filesWarning -logFile $activityLog + } } #Send the network data export request diff --git a/scripts/Export-YammerNetworkData/README.md b/scripts/Export-EngageNetworkData/README.md similarity index 84% rename from scripts/Export-YammerNetworkData/README.md rename to scripts/Export-EngageNetworkData/README.md index 621ce742..caff0553 100644 --- a/scripts/Export-YammerNetworkData/README.md +++ b/scripts/Export-EngageNetworkData/README.md @@ -1,4 +1,4 @@ -# Microsoft FastTrack Open Source - Export-YammerNetworkData +# Microsoft FastTrack Open Source - Export-EngageNetworkData This sample script calls the Yammer Network Data Export API to export messages and (optionally) files from your Yammer network for the date range specified in the command. @@ -51,22 +51,18 @@ There are a few variables you need to change in the script itself. These are loc **IncludeFiles all/csv** (Default: CSV) OPTIONAL. Set this to ‘all’ to include messages and file attachments, or ‘csv’ for messages only. -**IncludeExternalNetworks true/false** (Default: False) - - OPTIONAL. Set this to ‘true’ to include CSVs and/or file attachments for the primary network and all associated external networks. - NOTE: This is unnecessary for native mode migration, as migration doesn’t touch external networks ### Execution -Once you’ve made and saved those changes, you’re ready to go. To run the script, pass the start date for the export and any of the three optional parameters shown above. For example, if I wanted to export all data from the main network only (not any of the associated external networks), I'd run the script like so: +Once you’ve made and saved those changes, you’re ready to go. To run the script, pass the start date for the export and any of the optional parameters shown above. For example, if I wanted to export all data from the main network, I'd run the script like so: - .\Export-YammerNetworkData.ps1 -startdate 2023-01-14 -enddate 2023-01-31 -IncludeFiles All + .\Export-EngageNetworkData.ps1 -startdate 2023-01-14 -enddate 2023-01-31 -IncludeFiles All Reminder - The StartDate and EndDate parameters need to be in the YYYY-MM-DD format as shown above. Once complete, the console output will tell you where to find the result, which should be a date-named folder underneath your $rootPath set above. ### Notes -Even though you can download files/attachments using this API, we recommend only using this script to download messages if you need a large timeframe and/or have a large number of files in the network. This API starts the download as soon as you execute it, downloading each file one by one, and if there are a lot of files it can result in failure. Generally we recommend using the Files Export API for file downloads, as that packages up all files into one or more zip files on the back-end, and then it downloads the resulting zip(s). This tends to be more reliable. See my Yammer-FilesExport script in this repository to download files from your network. +Even though you can download files/attachments using this API, we recommend only using this script to download messages if you need a large timeframe and/or have a large number of files in the network. This API starts the download as soon as you execute it, downloading each file one by one, and if there are a lot of files it can result in failure. Generally we recommend using the Files Export API for file downloads, as that packages up all files into one or more zip files on the back-end, and then it downloads the resulting zip(s). This tends to be more reliable. See my Export-EngageFiles script in this repository to download files from your network. Console output is minimal. This is more of a ‘fire and go get a cup of coffee’ type thing based on how the API works, so detailed logging is sent to a logfile that will be created in the same folder the export data is saved to. If there are errors during script execution relating to issues making calls to the API, detailed info will be logged to that script log, along with various pieces of key information along the way. The console output will only give basic info on what step it’s on, and in the event of API call failure, just let you know it failed and point you to that log for more information. @@ -74,6 +70,8 @@ Each time you run this, it will create a new folder under the $rootPath you spec If your network is in native mode, no files will be downloaded. Included among the CSV files that do get downloaded, you'll see files.csv, which contains information on each file in the network, including the SPO/OneDrive of each. Actual files are only exported from pre-native mode networks where the files are still in Yammer's file store, not ones that exist in SharePoint. +**July 2026 update:** This script still calls the current, Microsoft-documented Network Data Export API (`www.yammer.com/api/v1/export`) — there is no separate/newer replacement endpoint as of this review. However, Microsoft has confirmed (Message Center ID **MC1230453**) that the "Include attachments" option in the Viva Engage admin center will be retired March 13, 2026, aligning the UI with the API behavior described above: native-mode networks never got attachments through this API to begin with, and that won't change. The script prints a warning at runtime if you pass `-IncludeFiles All` as a reminder that this is a no-op for native-mode networks. The `-IncludeExternalNetworks` parameter/option has been removed entirely — external network export isn't supported for native-mode networks, and Microsoft is retiring the equivalent admin center option on the same date. + ## Applies To - Yammer / Viva Engage networks in M365 diff --git a/scripts/Get-EngageGroupInfo/Get-EngageGroupInfo.ps1 b/scripts/Get-EngageGroupInfo/Get-EngageGroupInfo.ps1 new file mode 100644 index 00000000..1c335d7a --- /dev/null +++ b/scripts/Get-EngageGroupInfo/Get-EngageGroupInfo.ps1 @@ -0,0 +1,177 @@ +<# +.DESCRIPTION + The sample scripts are not supported under any Microsoft standard support + program or service. The sample scripts are provided AS IS without warranty + of any kind. Microsoft further disclaims all implied warranties including, + without limitation, any implied warranties of merchantability or of fitness for + a particular purpose. The entire risk arising out of the use or performance of + the sample scripts and documentation remains with you. In no event shall + Microsoft, its authors, or anyone else involved in the creation, production, or + delivery of the scripts be liable for any damages whatsoever (including, + without limitation, damages for loss of business profits, business interruption, + loss of business information, or other pecuniary loss) arising out of the use + of or inability to use the sample scripts or documentation, even if Microsoft + has been advised of the possibility of such damages. + +Purpose: + -Gets information on each Viva Engage community in your tenant using the Microsoft Graph API: + https://learn.microsoft.com/en-us/graph/api/resources/engagement-api-overview + +Author: + Dean Cron + +Version: + 1.0 - Initial Release 2023 (legacy Yammer REST API) + 2.0 - Updated to use MSAL.PS for authentication Nov 2025 (legacy Yammer REST API) + 3.0 - July 2026 - Migrated off the deprecated Yammer REST API to the Microsoft Graph + communities/groups APIs. The LastMessageAt column has been removed - Viva Engage + conversation/message activity isn't exposed via Graph (the communities API only + covers CRUD and membership, not messaging stats), so there's no Graph equivalent. + NOTE: GroupID in the output is now the Microsoft 365 group ID (a GUID) that backs + the community, NOT the legacy numeric Yammer group ID produced by earlier versions + of this script. + +Requirements: + + 1. An Azure AD (Entra ID) App Registration with the following Microsoft Graph + **application** permissions, with admin consent granted: + -Community.Read.All (or Community.ReadWrite.All) + -GroupMember.Read.All (or Group.Read.All / Group.ReadWrite.All) + +.EXAMPLE + .\Get-EngageGroupInfo.ps1 +#> + +<############ STUFF YOU NEED TO MODIFY ############> + +# Change these to match your environment. Instructions: +# https://learn.microsoft.com/en-us/graph/auth-v2-service?view=graph-rest-1.0 +$ClientId = "clientid" +$TenantId = "tenantid" +$ClientSecret = "clientsecret" + +$ReportOutput = "C:\Temp\YammerGroupInfo{0}.csv" -f [DateTime]::Now.ToString("yyyy-MM-dd_hh-mm-ss") + +<############ YOU SHOULD NOT HAVE TO MODIFY ANYTHING BELOW THIS LINE ############> + +function Connect-ToGraph { + param ( + [Parameter(Mandatory = $true)] + [string]$ClientId, + + [Parameter(Mandatory = $true)] + [string]$TenantId, + + [Parameter(Mandatory = $true)] + [string]$ClientSecret + ) + + $authBody = @{ + Grant_Type = "client_credentials" + Scope = "https://graph.microsoft.com/.default" + Client_Id = $ClientId + Client_Secret = $ClientSecret + } + + $Connection = Invoke-RestMethod -Uri https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token -Method POST -Body $authBody + return $Connection.access_token +} + +$accessToken = Connect-ToGraph -ClientId $ClientId -TenantId $TenantId -ClientSecret $ClientSecret +$authHeader = @{ Authorization = "******" } +$countHeader = @{ Authorization = "******"; ConsistencyLevel = 'eventual' } + +#Function to get all communities, following @odata.nextLink for paging +Function Get-EngageCommunities($uri, $allCommunities) { + if (!$allCommunities) { + $allCommunities = New-Object System.Collections.ArrayList($null) + } + + Write-Host "Retrieving communities page..." -Foreground Yellow + $response = Invoke-RestMethod -Uri $uri -Method GET -Headers $authHeader + $allCommunities.AddRange($response.value) + + if ($response.'@odata.nextLink') { + return Get-EngageCommunities $response.'@odata.nextLink' $allCommunities + } + else { + return $allCommunities + } +} + +$communities = Get-EngageCommunities "https://graph.microsoft.com/beta/employeeExperience/communities" + +#Array to store Result +$ResultSet = @() + +$communities | ForEach-Object { + + $communityId = $_.id + $groupId = $_.groupId + Write-Host "Processing Community:" $_.displayName -f Yellow + + do { + $rateLimitHit = $false + try { + $Result = New-Object PSObject + $Result | Add-Member -MemberType NoteProperty -Name "CommunityId" -Value $communityId + $Result | Add-Member -MemberType NoteProperty -Name "GroupId" -Value $groupId + $Result | Add-Member -MemberType NoteProperty -Name "GroupName" -Value $_.displayName + + #Get the group's member count + #https://learn.microsoft.com/en-us/graph/api/group-list-members + $memberCount = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/groups/$groupId/members/`$count" -Headers $countHeader -Method GET + $Result | Add-Member -MemberType NoteProperty -Name "MemberCount" -Value $memberCount + + #Get the group's owners. Group owners are automatically treated as admins of the + #associated Viva Engage community. + #https://learn.microsoft.com/en-us/graph/api/group-list-owners + $owners = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/groups/$groupId/owners" -Headers $authHeader -Method GET + + $admins = $null + if ($owners.value.Count -gt 0) { + $owners.value | ForEach-Object { + $admins += $_.displayName + " " + $_.userPrincipalName + ";" + } + $Result | Add-Member -MemberType NoteProperty -Name "GroupAdmins" -Value $admins + } + else { + $Result | Add-Member -MemberType NoteProperty -Name "GroupAdmins" -Value "No Admins" + } + + $ResultSet += $Result + } + catch { + $statusCode = $_.Exception.Response.StatusCode.Value__ + if ($statusCode -eq "429" -or $statusCode -eq "503") { + #Deal with rate limiting + #https://learn.microsoft.com/en-us/graph/throttling + $rateLimitHit = $true + } + elseif ($statusCode -eq "401" -or $statusCode -eq "403") { + #Thrown when the access token is invalid or lacks the required permissions + Write-Host "Graph API reports ACCESS DENIED for community" $_.displayName "- ensure the app registration has Community.Read.All and GroupMember.Read.All (application) permissions with admin consent." -ForegroundColor Red + } + elseif ($statusCode -eq "404") { + #Typically thrown when the underlying group isn't found + Write-Host "Graph API reports 404 for community" $_.displayName "- the underlying group may have been deleted." -ForegroundColor Red + } + else { + $l = $_.InvocationInfo.ScriptLineNumber + Write-Host "Failed to get info for community" $_.displayName -ForegroundColor Red + Write-Host "error $statusCode on line $l" + } + + if ($rateLimitHit) { + #429 or 503: Sleep for a bit before retrying + Write-Host "Rate limit hit, sleeping for 10 seconds before retrying community" $_.displayName -ForegroundColor Yellow + Start-Sleep -Seconds 10 + } + } + } while ($rateLimitHit) +} + +#Export Result to csv file +$ResultSet | Export-Csv $ReportOutput -NoTypeInformation + +Write-Host "Report created successfully. See $ReportOutput" -f Green diff --git a/scripts/Get-YammerGroupInfo/README.MD b/scripts/Get-EngageGroupInfo/README.md similarity index 54% rename from scripts/Get-YammerGroupInfo/README.MD rename to scripts/Get-EngageGroupInfo/README.md index 3f68a783..e9600414 100644 --- a/scripts/Get-YammerGroupInfo/README.MD +++ b/scripts/Get-EngageGroupInfo/README.md @@ -1,29 +1,34 @@ -# Microsoft FastTrack Open Source - Get-YammerGroupInfo +# Microsoft FastTrack Open Source - Get-EngageGroupInfo -This sample script gets key information about every group in your Yammer network, including group ID, name, member count, last message date, and group admins. +This sample script gets key information about every Viva Engage community in your tenant using the Microsoft Graph API, including community/group ID, name, member count, and community admins. ## Usage ### Prerequisites -- You must register an app and generate a bearer token (aka Developer Token) in your Yammer network for use with this script, you’ll need it for the next step below. - Detailed instructions on how to generate this can be found in step 2 here: https://techcommunity.microsoft.com/t5/yammer-developer/generating-an-administrator-token/m-p/97058 +- You must create a new Azure AD (Entra ID) App Registration with the following Microsoft Graph **application** permissions, with admin consent granted: + ``` + Community.Read.All (or Community.ReadWrite.All) + GroupMember.Read.All (or Group.Read.All / Group.ReadWrite.All) + ``` -- The account that creates the developer token in the step above MUST have private content mode enabled: - https://learn.microsoft.com/en-us/viva/engage/manage-security-and-compliance/monitor-private-content +There are a few variables you need to change in the script itself. These are located very early in the script just below “<############ STUFF YOU NEED TO MODIFY ############>”: +1. **$ClientId = "clientid"** -There are only 2 variables you may need to change in the script itself. These are located very early in the script just below “<############ STUFF YOU NEED TO MODIFY ############>”: + >Replace with the Client ID of the app registration you created in the prerequisites. -1. **$Global:YammerAuthToken = "BearerTokenString"** +2. **$TenantId = "tenantid"** + + >Replace with your tenant ID. - Replace BearerTokenString with the token you created via the instructions in the prerequisites. The line should look something like this: +3. **$ClientSecret = "clientsecret"** + + >Replace with the client secret value of the app registration you created in the prerequisites. - $Global:YammerAuthToken = "21737620380-GFy6awIxfYGULlgZvf43A" +4. **$ReportOutput = "C:\Temp\YammerGroupInfo{0}.csv" -f [DateTime]::Now.ToString("yyyy-MM-dd_hh-mm-ss")** -2. **$ReportOutput = "C:\Temp\YammerGroupInfo{0}.csv" -f [DateTime]::Now.ToString("yyyy-MM-dd_hh-mm-ss")** - - If you'd like the script to be generated in a specific location, change the path above to reflect your target. + If you'd like the report generated in a specific location, change the path above to reflect your target. ### Parameters @@ -33,17 +38,24 @@ None Once you’ve made and saved those changes, you’re ready to go. Run the script like so: - .\Get-YammerGroupInfo.ps1 + .\Get-EngageGroupInfo.ps1 + +### Notes + +**Breaking change (July 2026):** This script was migrated from the legacy Yammer REST API to Microsoft Graph. The output CSV's `GroupId` column is now the Microsoft 365 group ID (a GUID) that backs the community, not the legacy numeric Yammer group ID produced by earlier versions of this script. There's also a new `CommunityId` column with the Graph community ID. + +The `LastMessageAt` column has been removed. Viva Engage conversation/message activity isn't exposed via Microsoft Graph today - the communities API only covers CRUD and membership, not messaging stats - so there's no Graph equivalent for this field. ## Applies To -- Yammer / Viva Engage networks in M365, including external networks +- Viva Engage communities in M365 ## Author |Author|Original Publish Date |----|-------------------------- |Dean Cron, Microsoft|December 13th, 2023| +|Dean Cron, Microsoft|July 22nd, 2026| ## Issues diff --git a/scripts/Get-YammerGroupInfo/Get-YammerGroupInfo.ps1 b/scripts/Get-YammerGroupInfo/Get-YammerGroupInfo.ps1 deleted file mode 100644 index 08e12edd..00000000 --- a/scripts/Get-YammerGroupInfo/Get-YammerGroupInfo.ps1 +++ /dev/null @@ -1,180 +0,0 @@ -<# -.DESCRIPTION - The sample scripts are not supported under any Microsoft standard support - program or service. The sample scripts are provided AS IS without warranty - of any kind. Microsoft further disclaims all implied warranties including, - without limitation, any implied warranties of merchantability or of fitness for - a particular purpose. The entire risk arising out of the use or performance of - the sample scripts and documentation remains with you. In no event shall - Microsoft, its authors, or anyone else involved in the creation, production, or - delivery of the scripts be liable for any damages whatsoever (including, - without limitation, damages for loss of business profits, business interruption, - loss of business information, or other pecuniary loss) arising out of the use - of or inability to use the sample scripts or documentation, even if Microsoft - has been advised of the possibility of such damages. - -Purpose: - -Gets information on each group in your Yammer network - -Author: - Dean Cron - -Version: - 1.0 - Initial Release 2023 - 2.0 - Updated to use MSAL.PS for authentication Nov 2025 - -Requirements: - - 1. MSAL.PS PowerShell module. Install it from the PowerShell Gallery with the command: - Install-Module MSAL.PS - - 2. An Azure AD App Registration with the following API permissions: - -Yammer: access_as_user - -.EXAMPLE - .\Get-YammerGroupInfo.ps1 -#> - -<############ STUFF YOU NEED TO MODIFY ############> - -# Change these to match your environment. Instructions: -# https://learn.microsoft.com/en-us/graph/auth-v2-service?view=graph-rest-1.0 -$ClientId = "clientid" -$TenantId = "tenantId" -$RedirectUri = "https://localhost" - -$ReportOutput = "C:\Temp\YammerGroupInfo{0}.csv" -f [DateTime]::Now.ToString("yyyy-MM-dd_hh-mm-ss") -<############ YOU SHOULD NOT HAVE TO MODIFY ANYTHING BELOW THIS LINE ############> - -$Scopes = @("https://api.yammer.com/.default") -#Check to see if MSAL.PS is installed, if not exit with instructions -if(-not (Get-Module -ListAvailable -Name MSAL.PS)){ - Write-Host "MSAL.PS module not found, please install it from the PowerShell Gallery with the command:" -ForegroundColor Red - Write-Host "Install-Module MSAL.PS" -ForegroundColor Yellow - Return -} - -function Get-YammerAuthHeader { - $authToken = Get-MsalToken -ClientId $ClientId -TenantId $TenantId -RedirectUri $RedirectUri -Scopes $Scopes -Interactive - if (-not $authToken) { - Write-Host "Failed to acquire Yammer Auth Token. Please ensure the ClientID, TenantID, and ClientSecret are correct." -ForegroundColor Red - Return - } - else { - return $authToken.AccessToken - } - -} - -#Create header with access token -$YammerAuthToken = Get-YammerAuthHeader -$Global:header = @{"Authorization" = "Bearer $YammerAuthToken"} - -#Function to get all groups -Function Get-YammerGroups($page, $allGroups) { - if (!$page) { - $page = 1 - } - - if (!$allGroups) { - $allGroups = New-Object System.Collections.ArrayList($null) - } - - $urlToCall = "https://www.yammer.com/api/v1/groups.json" - $urlToCall += "?page=" + $page; - - #API only returns 50 results per page, so we need to loop through all pages - Write-Host "Retrieving page $page of groups list" -Foreground Yellow - $webRequest = Invoke-WebRequest -Uri $urlToCall -Method Get -Headers $header - - if ($webRequest.StatusCode -eq 200) { - $results = $webRequest.Content | ConvertFrom-Json - - if ($results.Length -eq 0) { - return $allGroups - } - $allGroups.AddRange($results) - } - - if ($allGroups.Count % 50 -eq 0) { - $page = $page + 1 - return Get-YammerGroups $page $allGroups - } - else { - return $allGroups - } -} - -#groups.json will occasionally return duplicates in results. This should remove them. -$results = Get-YammerGroups - -#Array to store Result -$ResultSet = @() - -$results | ForEach-Object { - - $gID = $_.Id -as [decimal] - Write-Host "Processing Group:" $_.Name -f Yellow - do { - $rateLimitHit = $false - try{ - #Get the group properties. - $getGroupInfo = (Invoke-WebRequest "https://www.yammer.com/api/v1/groups/$gID.json" -Headers $header -Method GET).content | ConvertFrom-Json - - $Result = new-object PSObject - $Result | add-member -membertype NoteProperty -name "GroupID" -Value $gID - $Result | add-member -membertype NoteProperty -name "GroupName" -Value $getGroupInfo.name - $Result | add-member -membertype NoteProperty -name "MemberCount" -Value $getGroupInfo.stats.members - $Result | add-member -membertype NoteProperty -name "LastMessageAt" -Value $getGroupInfo.stats.last_message_at - - #If the group has admins, add their info to the results - if($getGroupInfo.has_admin){ - $admins = $null - $getGroupAdmins = (Invoke-WebRequest "https://www.yammer.com/api/v1/groups/$gID/members.json" -Headers $header -Method GET).content | ConvertFrom-Json - - $groupAdmins = $getGroupAdmins.users | Where-Object {$_.is_group_admin -eq "true"} - - $groupAdmins | ForEach-Object { - $admins += $_.full_name + " " + $_.email + ";" - } - - $Result | add-member -membertype NoteProperty -name "GroupAdmins" -Value $admins - $ResultSet += $Result - } - else{ - $Result | add-member -membertype NoteProperty -name "GroupAdmins" -Value "No Admins" - $ResultSet += $Result - } - } - catch { - if($_.Exception.Response.StatusCode.Value__ -eq "404"){ - #Typically thrown when the group isn't found. No exit, try next group. - Write-Host "API reports 404, typically caused if the group "+($_.Name)+" wasn't found or isn't accessible." -ForegroundColor Red - Write-Host "Be sure the account that generated the developer token has Private Content Mode enabled." -ForegroundColor Red - } - elseif($_.Exception.Response.StatusCode.Value__ -eq "429" -or $_.Exception.Response.StatusCode.Value__ -eq "503"){ - #Thrown when rate limiting is hit. No exit, retry. - #https://learn.microsoft.com/en-us/rest/api/yammer/rest-api-rate-limits#yammer-api-rate-limts - $rateLimitHit = $true - } - else{ - #Fallback, no idea what happened to get us here. - $e = $_.Exception.Response.StatusCode.Value__ - $l = $_.InvocationInfo.ScriptLineNumber - Write-Host "Failed to get info for group "$_.Name -ForegroundColor Red - Write-Host "error $e on line $l" -ForegroundColor Red - } - - if ($rateLimitHit) { - #429 or 503: Sleep for a bit before retrying - Write-Host "Rate limit hit, sleeping for 10 seconds before retrying group "$_.Name -ForegroundColor Yellow - Start-Sleep -Seconds 10 - } - } - } while ($rateLimitHit) -} - -#Export Result to csv file -$ResultSet | Export-Csv $ReportOutput -notypeinformation - -Write-Host "Report created successfully. See $ReportOutput" -f Green