16
- July
2019
Posted By : Zeng Yinghua (Sandy)
PowerShell Module for Microsoft Planner

Recently I fall in love with Microsoft Graph. 🙂 I was honored invited to MMSMOA talk about Intune Graph with David Falkus and Timmy Andersson. We talked about what is Microsoft Graph, how to start use it and how to use Intune Graph PowerShell SDK. Last week,  Tom Degreef asked if there is PowerShell Module for Microsoft Planner. So I did some research, and got an idea that how about make my own PowerShell module for Microsoft Planner using Microsoft Graph. I have never upload anything to PowerShell Gallery, this will full fill my bucket list as well. 🙂

You can find the module directly from PowerShell Gallery and my Github

Let’s break down some details of this module.

Authentication

The module is using Native Application that hosted in my own tenant by default, it uses permission Group.Read.All, Group.ReadWrite.All, User.Read and User.ReadBasic.All, these are the minimum permission requirement to create Planner plans, buckets and tasks. But, I would really hope you use own application for this module, because it will give you more control of those permissions, or if you wants to add more actions in your scripts. Here are the steps how to create this native app.

  1. Go to your Azure Portal, Click on Azure Active Directory, click on App registrations, then New registration

  2. Input a name example Planner PowerShell. Supported account types choose organizational directory only. You can also use any organizational directory,  if you manage multiple tenants and wants use this app to all your tenants.
    Redirect URI, choose Public client (mobile & desktop), and value as urn:ietf:wg:oauth:2.0:oob

  3. After registered this app, go to Authentication, change Default client type from to Yes, so that this will be  a public native client

  4. Click on API permissions, choose Microsoft Graph, then add Delegated permissions: Group.Read.All, Group.ReadWrite.All, User.Read and User.ReadBasic.All

  5. (Optional) Grant admin consent. This is very much depend on your own environment and usage. Without admin consent, normal users aren’t able to run this application. If you are the only admin who use this app, then you don’t need grant consent to others. But if you want another non-admin person use this module, you should grant admin consent. Because this is using delegated permission, the required permissions will be a combination of 1) what the user has permissions to do and 2) what the application has permissions to do. (Read the details from this blog https://developer.microsoft.com/en-us/graph/blogs/30daysmsgraph-day-11-azure-ad-application-permissions/ )

 

Update Planner Module environment

After register your own app, copy it’s application client ID to your note.

Then install the PlannerModule, and update the module to use your own application instead of the default one.

 

How to use this module

At first, this module is not 100% ready, there is no delete function yet, it can create plans, buckets, tasks, assign tasks to users, add checklist, add labels, assign labels, add descriptions, create Office 365 groups, add user to Office 365 groups.  It doesn’t handle “for each” objects, only the Invoke-AssignPlannerTask can add multiple array.

Here are some examples. Note: I will update those example in my Github.

# PlannerModule
PowerShell module for Microsoft Planner

#Examples:

#Check Planner PowerShell module

$PlannerModule = Get-Module -Name "PlannerModule" -ListAvailable

if ($PlannerModule -eq $null)
{
  Write-host "Planner PowerShell module not found, Start install the module"
  Install-Module "PlannerModule" -AllowClobber -Force
}


#Connect to Microsoft Planner
#Connect-Planner
Connect-Planner -ForceNonInteractive True

#Definde variables
$GroupName = "A NewPlan 01"
$PlanName = "PowerShell Test Plan 03"
$BucketName = "PowerShell bucket"
$TaskName = "Test Task"

#Create new plan with Private O365 Group (this will also create new O365 Group), can also create public group
#$result01 = New-PlannerPlan -PlanName $PlanName -visibility Private
#$PlannerPlanID = $result01.id


#Create New Office 365 Group
$responde = New-AADUnifiedGroup -GroupName $GroupName -visibility Private
$GroupID = $($responde.id)

#sometimes there is delay creating the group.
Start-Sleep 10

#create new plan using exsiting O365 Group
$responde1 = New-PlannerPlanToGroup -PlanName $PlanName -GroupID $GroupID
$PlannerPlanID = $($responde1.id)

#Create new Bucket
$responde2 = New-PlannerBucket -PlanID $PlannerPlanID -BucketName $BucketName
$PlannerPlanBucketID = $responde2.id

#Create task
$responde3 = New-PlannerTask -PlanID $PlannerPlanID -TaskName $TaskName -BucketID $PlannerPlanBucketID -startDate "2019.6.3" -dueDate "2019.6.30"
$PlannerPlanTaskID = $responde3.id

#Assign task to users
Invoke-AssignPlannerTask -TaskID $PlannerPlanTaskID -UserPrincipalNames "user01@yourdomain.com", "user02@yourdomain.com"

#Add task check list
Add-PlannerTaskChecklist -TaskID $PlannerPlanTaskID -Title "Check1" -IsChecked $false
Add-PlannerTaskChecklist -TaskID $PlannerPlanTaskID -Title "Check2" -IsChecked $true
Add-PlannerTaskChecklist -TaskID $PlannerPlanTaskID -Title "Check3" -IsChecked $false

#add task description
Add-PlannerTaskDescription -TaskID $PlannerPlanTaskID -Description "This is test task created by powershell planner module"

#Add or update labels
Update-PlannerPlanCategories -PlanID $PlannerPlanID -category1 "Kieken" -category2 "smart" -category3 "very smart" -category4 "wise" -category5 "something"

#Assign Planner Task lables
Invoke-AssignPlannerTaskCategories -TaskID $PlannerPlanTaskID -category1 $false -category2 $true -category3 $true -category4 $false -category5 $false -category6 $false

 

Hope you like this module. 🙂

 

 

 

Comments

  • Hi Sandy,
    I’ve been needing to import existing data into planner – so thank you very much for this. I have been filling out the module slightly – to make a few calls more consistent and idempotent.

    I noticed a typo in a cmdlet name: Update-PlannerModuleEnvironment – not: Update-PlannerModuelEnvironment
    I also noticed you are using Invoke in some places where Get might be more appropriate?

    currently – I am trying to get the comments/attachments going – to complete the import functionality.

    ———————————— please see below for code ———————————

    $BaseURI = “https://graph.microsoft.com/beta”

    function Invoke-PlannerRestMethod {
    Param (
    [String] $uri,
    [ValidateSet(“GET”, “POST”)][String] $Method=”GET”,
    [String] $Body=””
    )

    try {
    if (!($uri.StartsWith(“https://”))) {
    $uri = $BaseURI + $uri
    }
    if ($Body) {
    Invoke-RestMethod -Uri $uri -Headers $authToken -Method $Method -Body $Body
    }
    else {
    Invoke-RestMethod -Uri $uri -Headers $authToken -Method $Method
    }

    }
    Catch {
    $ex = $_.Exception
    if ($($ex.Response.StatusDescription) -match ‘Unauthorized’)
    {
    Write-Error “Unauthorized, Please check your permissions and use the ‘Connect-Planner’ command to authenticate”
    }
    Write-Error “Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)”
    break
    }
    }

    function Get-PlannerPlanList {
    param ([String] $GroupId)
    return (Invoke-PlannerRestMethod -uri “/Groups/$GroupID/planner/plans”).value
    }
    function Get-PlannerPlanDetails {
    param ([String] $PlanId)
    return (Invoke-PlannerRestMethod -uri “/planner/plans/$planid/details”)
    }
    function Get-PlannerBucketList {
    param ([String] $PlanId)
    return (Invoke-PlannerRestMethod -uri “/planner/plans/$PlanId/buckets”).value
    }
    function Get-PlannerBucketDetails {
    param ([String] $BucketId)
    return (Invoke-PlannerRestMethod -uri “/planner/buckets/$BucketId”)
    }
    function Get-PlannerTaskList {
    param ([String] $BucketId)
    return (Invoke-PlannerRestMethod -uri “/planner/buckets/$BucketId/tasks”).value
    }
    function Set-BucketTask {
    param (
    [String] $PlanId, # Need to supply either planId or BucketId
    [String] $BucketId,
    [String] $TaskName,
    [String] $TaskDescription,
    [Array] $UserPrincipalNames,
    [DateTime] $StartDate = (Get-Date),
    [DateTime] $dueDate
    )
    if ($BucketId) {
    $PlanId = (Get-PlannerBucketDetails -BucketId $BucketId).PlanID
    }
    else {
    $BucketId = (Get-PlannerBucketList -PlanId $PlanId)[0].BucketId # Get the default bucket – ie TODO…
    }
    $TasksList = Get-PlannerTaskList -BucketId $BucketId
    $Taskid = ($TasksList | ? {$_.title -like $TaskName}).id
    if (!$Taskid) {
    if ($dueDate) {
    $response = New-PlannerTask -BucketID $BucketId -PlanID $PlanId -TaskName $TaskName -startDate $StartDate -dueDate $dueDate
    }
    else {
    $response = New-PlannerTask -BucketID $BucketId -PlanID $PlanId -TaskName $TaskName -startDate $StartDate
    }
    $TaskId = $response.id
    }
    if ($TaskDescription) {
    Add-PlannerTaskDescription -TaskID $TaskId -Description $TaskDescription
    }
    if ($UserPrincipalNames) {
    $ValidatesUserNames=@()
    # Validate users – so we don’t get an unexplained fail
    foreach ($item in $UserPrincipalNames) {
    if (!(Get-AADUserDetails -UserPrincipalName $item)) {
    Write-Warning (“Could not validate user: “+ $item)
    }
    else { $ValidatesUserNames+=$item }
    }
    Invoke-AssignPlannerTask -TaskID $TaskId -UserPrincipalNames $ValidatesUserNames # note: bug in implementation – reports all users added multiple times
    }
    return $TaskId
    }
    function Set-PlannerBucket {
    param (
    [String] $PlanId,
    [String] $BucketName
    )
    $BucketsList = Invoke-ListPlannerPlanBuckets -PlanID $PlanId
    # $buckets = Get-PlannerBucketList -PlanId $PlanId
    $bucketid = ($BucketsList | Where-Object {$_.name -like $BucketName}).id
    if (!$bucketid) {
    $response = New-PlannerBucket -PlanId $PlanId -BucketName $BucketName
    $bucketid = $response.id
    }
    return $bucketid
    }
    function Set-PlannerPlan {
    param (
    [String] $GroupId,
    [String] $PlanName
    )
    $plansList = Get-PlannerPlanList -GroupId $GroupID
    $planid = ($plansList | ? {$_.title -like $planname}).id
    if (!($planid)) {
    $response = New-PlannerPlanToGroup -PlanName $PlanName -GroupID $GroupID
    $planid = $response.id
    }
    return $planid
    }

    • Zeng Yinghua (Sandy)

      August 3, 2019 at 03:15

      Hi Mark, you are right, now I also see it that few place could have use GET, not Inovke. Thanks for finding the typo. Since the module is out already for sometime (those older versions), I really don’t want to change the naming now, it might break other peoples scripts (if anyone is using it), thinking maybe can add the new naming and write help info in those old functions. I really like your Invoke-PlannerRestMethod function, that is very handy.

  • I will send through the latest code soon – I have added some type-safe stuff – and am using this quite a lot atm.
    Is this the best place to post it?

    General notes:
    fields need to be json safe – and some fields have size and character restrictions.
    1. max 10 attachments per task.
    2. max 20 checklist items with 100 character limit.
    3. Item title – max 200 characters.

    Kind regards

    Mark

  • here is the modified code: note – you might want to pull out the logging to file I put in.

    Import-Module “MicrosoftTeams”
    Import-Module “PlannerModule”

    $BaseURI = “https://graph.microsoft.com/beta”
    function Invoke-PlannerRestMethod {
    Param (
    [String] $uri,
    [ValidateSet(“Get”, “Post”, “Patch”,”Put”)][String] $Method=”Get”,
    [String] $Body=””,
    [String] $Infile,
    [HashTable] $AdditionalHeaders,
    [Int] $Retries = 3,
    [Int] $PauseBetweenRetries = 4
    )

    # We need the AuthToken at a minimum
    $Headers = $authToken.Clone()
    if ($AdditionalHeaders) {
    foreach ($item in $AdditionalHeaders.keys) {
    $Headers.Add($item,$AdditionalHeaders[$item])
    }
    }

    for ($i = 0 ; $i -le $Retries; $i++) {
    try {
    if (!($uri.StartsWith(“https://”))) {
    $uri = $BaseURI + $uri
    }
    if ($Infile) {
    Invoke-RestMethod -Uri $uri -Headers $Headers -Method $Method -InFile $Infile
    }
    elseif ($Body) {
    Invoke-RestMethod -Uri $uri -Headers $Headers -Method $Method -Body $Body
    }
    else {
    Invoke-RestMethod -Uri $uri -Headers $Headers -Method $Method
    }
    $i=$Retries
    break;
    }
    Catch {
    $ex = $_.Exception
    if ($($ex.Response.StatusDescription) -match ‘Unauthorized’)
    {
    Write-Error “Unauthorized, Please check your permissions and use the ‘Connect-Planner’ command to authenticate”
    }
    elseif((($($ex.Response.StatusDescription) -match ‘Not Found’) -or ($($ex.Response.statuscode) -match ‘GatewayTimeout’) -or ($($ex.Response.statuscode) -match ‘429’) -or ($($ex.Response.StatusDescription) -match ‘Bad Request’ ))-and $i -lt $Retries) {
    Write-Warning ” >Trying call again – uri: $uri” | Out-Null
    Sleep $PauseBetweenRetries # The initial comment may need time before the thread is exposed
    continue;
    }

    Write-Error “Request to $Uri failed with HTTP Status $($ex.Response.statuscode)”
    if ($body) {
    Write-Error “Body details: $body”
    }
    $errDetails=@”
    ——————————
    Date: $(Get-Date)
    Error: $($ex.Response.statuscode)
    ListName: $currentListName
    ListId: $currentIdList
    TaskName: $taskName
    Call: $Method – $uri
    Body: $Body

    “@
    $errDetails | Out-File “C:\logs\plannerConversion_Errors.log” -Append

    break;
    }
    }
    }

    function Get-PlannerTokenExpired {
    Param ([Int]$WarningSeconds=0)
    if (!$authToken) {
    return $true
    }
    if ($authToken[“ExpiresOn”].UtcDateTime -gt (get-date).ToUniversalTime().AddSeconds(-$Seconds)) {
    return $False
    }
    return $true
    }

    function Get-PlannerSafeTaskField ([String] $TaskName, [int] $Length=255) {
    $TaskName = [System.Web.HttpUtility]::JavaScriptStringEncode($TaskName.Replace(“’”,”‘”).Replace(“‘”,”‘”).Replace(‘“’,'”‘).Replace(‘”’,'”‘).Replace(“?”,”\u003fs”).Trim())
    if ($Length -gt 0) {
    if ($TaskName.Length -gt $Length) {$TaskName=$TaskName.Substring(0,$Length)}
    }
    return $TaskName
    }

    function Get-PlannerPlanList {
    param ([String] $GroupId)
    return (Invoke-PlannerRestMethod -uri “/Groups/$GroupID/planner/plans”).value
    }
    function Get-PlannerPlanDetails {
    param ([String] $PlanId)
    return (Invoke-PlannerRestMethod -uri “/planner/plans/$planid/details”)
    }
    function Get-PlannerBucketList {
    param ([String] $PlanId)
    return (Invoke-PlannerRestMethod -uri “/planner/plans/$PlanId/buckets”).value
    }
    function Get-PlannerBucketDetails {
    param ([String] $BucketId)
    return (Invoke-PlannerRestMethod -uri “/planner/buckets/$BucketId”)
    }
    function Get-PlannerTaskList {
    param ([String] $BucketId)
    return (Invoke-PlannerRestMethod -uri “/planner/buckets/$BucketId/tasks”).value
    }
    function Get-PlannerTaskIdFromName {
    param (
    [String] $BucketId,
    [String] $TaskName
    )
    $TaskSafeName = Get-PlannerSafeTaskField $TaskName
    $TasksList = Get-PlannerTaskList -BucketId $BucketId
    return ($TasksList | ? {$_.title -eq $TaskName -or $_.title -eq $TaskSafeName}).id
    }
    function Get-PlannerTaskComments {
    param (
    [String] $TaskId,
    [String] $GroupID
    )
    $TaskDetails = Get-PlannerTask -TaskID $TaskId
    if ($TaskDetails.conversationThreadId) {
    if (!$GroupID) {
    $PlannerDetails = Get-PlannerPlan -PlanID $TaskDetails.planId
    $GroupID = $PlannerDetails.Owner
    }
    $results = (Invoke-PlannerRestMethod -uri “/groups/$GroupID/threads/$($TaskDetails.conversationThreadId)/posts”).value

    return (Invoke-PlannerRestMethod -uri “/groups/$GroupID/threads/$($TaskDetails.conversationThreadId)/posts”).value
    }
    }

    function Get-PlannerTaskAttachments([String]$TaskId) {
    $TaskExternalDetails = Get-PlannerTaskDetails -TaskID $TaskId
    $attachmentsList=@()
    foreach ($item in $TaskExternalDetails.references.psobject.Properties) {
    $name = [System.Web.HttpUtility]::UrlDecode($item.name).replace(“%20″,” “)
    $aliasType = if ($name -Like “*sharepoint.com*”){“sharepoint”}else{“weblink”}
    $attachmentsList+=[PSCustomObject]@{Path=$name;Name=$item.value.alias;Type=$aliasType }
    }
    return $attachmentsList
    }

    function Get-PlannerTaskCheckList([String]$TaskId) {
    $TaskExternalDetails = Get-PlannerTaskDetails -TaskID $TaskId
    $checkItemsList=@()
    foreach ($item in $TaskExternalDetails.checklist.psobject.Properties) {
    $Title = [System.Web.HttpUtility]::UrlDecode($item.Title).replace(“%20″,” “)
    $Checked = $item.isChecked
    $checkItemsList+=[PSCustomObject]@{Title=$Title;Checked=$Checked }
    }
    return $checkItemsList
    }

    function Set-BucketTask {
    param (
    [String] $PlanId, # Need to supply either planId or BucketId
    [String] $BucketId,
    [String] $TaskName,
    [String] $TaskDescription = $null,
    [Array] $UserPrincipalNames,
    [DateTime] $StartDate = (Get-Date),
    [Nullable[DateTime]] $dueDate
    )
    $TaskName = Get-PlannerSafeTaskField $TaskName
    if ($BucketId) {
    $PlanId = (Get-PlannerBucketDetails -BucketId $BucketId).PlanID
    }
    else {
    $BucketId = (Get-PlannerBucketList -PlanId $PlanId)[0].BucketId # Get the default bucket – ie TODO…
    }
    $Taskid = Get-PlannerTaskIdFromName -BucketId $BucketId -TaskName $TaskName
    if (!$Taskid) {
    try {
    Write-Host “New-PlannerTask -BucketID ‘$BucketId’ -PlanID ‘$PlanId’ -TaskName ‘$TaskName’ -startDate ‘$StartDate’ -dueDate ‘$dueDate'” | out-null
    if ($dueDate) {
    $response = New-PlannerTask -BucketID $BucketId -PlanID $PlanId -TaskName $TaskName -startDate $StartDate -dueDate $dueDate
    }
    else {
    $response = New-PlannerTask -BucketID $BucketId -PlanID $PlanId -TaskName $TaskName -startDate $StartDate
    }
    }
    Catch {
    $a = $a
    }
    $TaskId = $response.id
    while (!(Get-PlannerTask -TaskID $Taskid )) {
    Write-Warning “Waiting for task creation to complete” | out-null
    sleep 5
    }
    }
    if ($TaskDescription) {
    $TaskDescription = Get-PlannerSafeTaskField -TaskName $TaskDescription -Length -1
    $Response = Add-PlannerTaskDescription -TaskID $TaskId -Description $TaskDescription
    }
    if ($UserPrincipalNames) {
    $ValidatesUserNames=@()
    # Validate users – so we don’t get an unexplained fail
    foreach ($item in $UserPrincipalNames) {
    if (!(Get-AADUserDetails -UserPrincipalName $item)) {
    Write-Warning (“Could not validate user: “+ $item) | Out-Null
    }
    else { $ValidatesUserNames+=$item }
    }
    $Response = Invoke-AssignPlannerTask -TaskID $TaskId -UserPrincipalNames $ValidatesUserNames # note: bug in implementation – reports all users added multiple times
    }
    return $TaskId
    }
    function Set-PlannerBucket {
    param (
    [String] $PlanId,
    [String] $BucketName
    )
    $BucketsList = Invoke-ListPlannerPlanBuckets -PlanID $PlanId
    $bucketid = ($BucketsList | Where-Object {$_.name -like $BucketName}).id
    if (!$bucketid) {
    $response = New-PlannerBucket -PlanId $PlanId -BucketName $BucketName
    $bucketid = $response.id
    }
    return $bucketid
    }
    function Set-PlannerPlan {
    param (
    [String] $GroupId,
    [String] $PlanName
    )
    $plansList = Get-PlannerPlanList -GroupId $GroupID
    $planid = ($plansList | ? {$_.title -like $planname}).id
    if (!($planid)) {
    $response = New-PlannerPlanToGroup -PlanName $PlanName -GroupID $GroupID
    $planid = $response.id
    }
    return $planid
    }
    function Add-PlannerTaskAttachment {
    Param (
    [String] $TaskId, # required
    [String] $GroupId,
    [String] $Attachment
    )

    $AttachmentBody= @”
    {
    “@odata.type”: “#microsoft.graph.fileAttachment”,
    “name”: “name-value”,
    “contentBytes”: “base64-contentBytes-value”
    }
    “@
    }
    function Add-PlannerTaskComment {
    Param (
    [String] $TaskId, # required
    [String] $GroupId,
    [String] $Comment
    )
    $Comment = Get-PlannerSafeTaskField -TaskName $Comment -Length -1
    $TaskDetails = Get-PlannerTask -TaskID $TaskId
    $title = Get-PlannerSafeTaskField -TaskName $TaskDetails.Title
    $ConversationThreadId = $TaskDetails.conversationThreadId
    # we need the owner GroupId – as conversation is only linked to the task
    if (!$GroupId) {
    $PlannerDetails = Get-PlannerPlan -PlanID $TaskDetails.planId
    $groupId = $PlannerDetails.Owner
    }
    if ($ConversationThreadId) {
    $ConversationReplyBody=@”
    {
    “post”: {
    “body”: {
    “contentType”: “html”,
    “content”: “$comment”
    }
    }
    }
    “@
    Try {
    $threadDetails = Invoke-PlannerRestMethod -uri “/Groups/$GroupID/threads/$ConversationThreadId/reply” -Method POST -Body $ConversationReplyBody -PauseBetweenRetries 10 -Retries 5
    }
    Catch {
    Write-Warning “Could not write this reply comment: $comment” | out-null
    }
    Write-Host “Added comment to Task: $TaskId – comment: $comment” -ForegroundColor Cyan | Out-Null
    } else {
    $ConversationBody = @”
    {
    ‘topic’: ‘Comments on task “$title”‘,
    ‘threads’:[
    {
    ‘posts’:[
    {
    ‘body’:{
    ‘contentType’:’html’,
    ‘content’:’$comment’
    },
    ‘newParticipants’: [{
    ’emailAddress’: {
    ‘name’: ‘Mark Nelson’,
    ‘address’: ‘mark.nelson@rfg.com.au’
    }
    }]
    }
    ]
    }
    ]
    }
    “@
    Try {
    $threadDetails = Invoke-PlannerRestMethod -uri “/Groups/$GroupID/conversations” -Method POST -Body $ConversationBody
    }
    Catch {
    Write-Warning “Could not write this comment: $comment” | out-null
    }
    if (!$threadDetails.threads.count) {
    Write-Host “Could not add comment Thread to Task: $title – initial comment: $comment” | Out-Null
    break;
    }
    $AttachThreadToTaskbody=”{‘conversationThreadId’: ‘$($threadDetails.threads[0].id)’}”
    $hdr = @{“If-Match”=$TaskDetails.’@odata.etag’}
    $threadDetails = Invoke-PlannerRestMethod -uri “/planner/tasks/$TaskId” -Method PATCH -Body $AttachThreadToTaskbody -AdditionalHeaders $hdr
    Write-Host “Added initial comment to Task: $TaskId – comment: $comment” -ForegroundColor Cyan | Out-Null
    }
    }

    function Get-PlannerSharepointDriveRoot {
    param ([String] $GroupId)
    return (((Invoke-PlannerRestMethod -uri “/groups/$GroupID/drives”).value | ? {$_.name -eq “Documents”}).id)
    }
    function Get-PlannerSharepointFolderContents {
    # note: @microsoft.graph.downloadUrl – this is only valid for 1 hour
    param (
    [String] $PlannerSharepointDriveRootId,
    [String] $SharepointFolderId = “root”
    )
    #POST /drives/{drive-id}/items/{parent-item-id}/children
    return ((Invoke-PlannerRestMethod -uri “/drives/$PlannerSharepointDriveRootId/items/$SharepointFolderId/children”).value)
    }

    function Set-PlannerSharepointFolder {
    Param (
    [String] $GroupId,
    [String] $FolderPath
    )
    # need to remove/replace some characters: / \ : * ? ” | # %
    $FolderList = $FolderPath.replace(“:”,”;”).replace(“/”,”\”).Replace(“?”,””).Replace(“*”,””).Replace(“#”,””).Replace(“%”,””).Replace(“|”,”!”).Replace(‘”‘,”‘”).Replace(“’”,”‘”).Replace(“”,”}”).TrimEnd(“.”).Trim().Split(“\”)

    # Start at the root folder
    $SharepointDocumentsRootId = Get-PlannerSharepointDriveRoot -GroupId $GroupId
    $DriveRoot = Get-PlannerSharepointFolderContents -PlannerSharepointDriveRootId $SharepointDocumentsRootId
    $ParentFolderId = $DriveRoot[0].id
    $directoryList = Get-PlannerSharepointFolderContents -PlannerSharepointDriveRootId $SharepointDocumentsRootId -SharepointFolderId $ParentFolderId

    # Now determine folders
    $SharepointPath=””
    foreach($item in $FolderList) {
    if ($item) {
    $SharepointPath += “$item\”
    $FilteredDirectoryList = $directoryList | ? {$_.folder -and $_.name -eq $item}
    if ($FilteredDirectoryList) {
    $ParentFolderId = $FilteredDirectoryList.Id
    $directoryList = Get-PlannerSharepointFolderContents -PlannerSharepointDriveRootId $SharepointDocumentsRootId -SharepointFolderId $ParentFolderId
    }
    else {
    $item = [System.Web.HttpUtility]::JavaScriptStringEncode($item)

    Write-Host ” Creating SharepointFolder: $SharepointPath” -ForegroundColor Green | Out-Null
    $directoryList=$Null
    $body=@”
    {
    “name”: “$item”,
    “folder”: { },
    “@microsoft.graph.conflictBehavior”: “rename”
    }
    “@
    $result = (Invoke-PlannerRestMethod -uri “/drives/$SharepointDocumentsRootId/items/$ParentFolderId/children” -Body $body -Method Post)
    $ParentFolderId = $result.Id
    }
    }
    }
    return $ParentFolderId
    }

    function Set-PlannerSharePointFolderItem {
    param (
    [String] $FilePath,
    [String] $GroupId,
    [String] $SharepointFolderId,
    [Switch] $Overwrite
    )
    # do our initial validation
    $FileName = Split-Path -path $FilePath -Leaf
    if (!(Test-Path $FilePath)) {
    Write-Warning “Could not find source file: $FilePath” | Out-Null
    return
    }

    # Get the planner “Root”
    $PlannerSharepointDriveRootId = Get-PlannerSharepointDriveRoot -GroupId $GroupId
    if (!$PlannerSharepointDriveRootId) {
    Write-Warning “Could not determine planner drive root for groupId: $GroupId” | Out-Null
    return
    }

    if (!$SharepointFolderId) {
    # get the 1st “folder” (level to be able to write files to)
    $SharePointTopLevelFolders = Get-PlannerSharepointFolderContents -PlannerSharepointDriveRootId $PlannerSharepointDriveRootId
    if (!$SharePointTopLevelFolders) {
    Write-Warning “Could not determine planner drive root for PlannerSharepointDriveRootId: $PlannerSharepointDriveRootId” | Out-Null
    return
    }
    $SharepointFolderId = $SharePointTopLevelFolders[0].id # do we need to do an idiot check for “General” ?
    }

    # Get the Files in this site
    $SharePointFileList = Get-PlannerSharepointFolderContents -PlannerSharepointDriveRootId $PlannerSharepointDriveRootId -SharepointFolderId $SharepointFolderId
    $UsedFileName = [System.Web.HttpUtility]::JavaScriptStringEncode($FileName.Replace(‘#’,’_’).Replace(‘”‘,”‘”))
    if (!$Overwrite.IsPresent) {
    # Get a unique filename
    $counter = 0
    $fnExt = [IO.Path]::GetExtension($FileName)
    While ($($SharePointFileList.name) -contains $UsedFileName) {
    $counter+=1
    $UsedFileName = $filename.Replace($fnExt, (“{0}{1}” -f $counter,$fnExt))
    }
    }
    $uri = “/drives/{0}/items/{1}:/{2}:/content” -f $PlannerSharepointDriveRootId, $SharepointFolderId, $UsedFileName
    $Response = Invoke-PlannerRestMethod -uri $uri -Method Put -Infile $FilePath
    return $Response
    }

    function Add-PlannerTaskAttachmentItem {
    param (
    [String] $TaskId,
    [String] $AttachmentHtmlLink,
    [String] $AttachmentFriendlyName
    )
    $PreviewPriority = ” !” # seems to be an issue if this is anything else.
    # Add plannerAttachment
    $TaskExternalDetails = Get-PlannerTaskDetails -TaskID $TaskId
    $Alias = [System.Web.HttpUtility]::HtmlEncode($AttachmentFriendlyName).replace(“:”,”%3A”).replace(“.”,”%2E”)
    $PlannerFilename = [System.Web.HttpUtility]::HtmlEncode(($AttachmentHtmlLink.ToString())).replace(“:”,”%3A”).replace(“.”,”%2E”)
    Write-Host “Adding attachment to taskId: $TaskId – Attachment: $Alias” -ForegroundColor Cyan | Out-Null
    $BodyAttachment = @”
    {
    “references” : {
    “$PlannerFilename”: {
    “@odata.type”: “#microsoft.graph.plannerExternalReference”,
    “previewPriority”: “$PreviewPriority”,
    “alias”: “$Alias”
    }
    }
    }
    “@
    $hdr = @{“If-Match”=$TaskExternalDetails.’@odata.etag’}
    Invoke-PlannerRestMethod -uri “/planner/tasks/$TaskId/details” -Method PATCH -Body $BodyAttachment -AdditionalHeaders $hdr
    }

    function Add-PlannerTaskAttachment {
    Param (
    [String] $WebOrFilePath,
    [String] $GroupId,
    [String] $TaskId,
    [String] $SharepointFolderId
    )
    if (Test-Path $WebOrFilePath) {

    $PlannerFileHandle = Set-PlannerSharePointFolderItem -FilePath $WebOrFilePath -GroupId $GroupId -SharepointFolderId $SharepointFolderId
    $AttachmentHtmlLink = $PlannerFileHandle.webUrl
    $AttachmentFriendlyName = $PlannerFileHandle.name
    }
    else {
    Try {
    $webRequest = Invoke-WebRequest $WebOrFilePath -UseBasicParsing -DisableKeepAlive -Method Head -TimeoutSec 5
    $AttachmentHtmlLink = $webRequest.BaseResponse.ResponseUri.ToString()
    $AttachmentFriendlyName = $webRequest.BaseResponse.ResponseUri.Host
    }
    Catch {
    # we can just ignore this atm.. will not add if website does not resolve
    }
    }
    if ($AttachmentHtmlLink -and $AttachmentFriendlyName) {
    Add-PlannerTaskAttachmentItem -TaskId $TaskId -AttachmentHtmlLink $AttachmentHtmlLink -AttachmentFriendlyName $AttachmentFriendlyName
    }
    else {
    Write-Warning “Could not attach file or website to the planner task – as could not validate the attachment exists” | Out-Null
    }
    }

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.