ConvertFrom-OpensearchItemId.ps1

<#PSScriptInfo
 
.VERSION 2023.12.8
 
.GUID dd297485-342b-4999-8c56-e9e969edb01a
 
.AUTHOR Kent Sapp (@cksapp)
 
.COMPANYNAME
 
.COPYRIGHT © 2023 | Kent Sapp
 
.TAGS Datto-Internal, OpenSearch
 
.LICENSEURI https://gist.github.com/cksapp/2a46f9b4342877b62586ff2f99931fc1#file-license
 
.PROJECTURI https://gist.github.com/cksapp/2a46f9b4342877b62586ff2f99931fc1
 
.ICONURI https://gist.github.com/assets/66083310/3e673212-80b1-4fbd-a0c7-2d489a378dbc
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES Lots of changes here, better handling of errors and URL validation, user input will now work for both yes and no options and default to checking validation if null input.
Also added script update prompt function. Looking good for first Release Candidate!
 
 
.PRIVATEDATA
 
 
#>


<#
.SYNOPSIS
    1. Modifies item IDs by replacing '-' with '+' and '_' with '/'
    2. Converts the modified item ID to URL encoded format
    3. Generates URL links for the modified item IDs
 
.DESCRIPTION
    This is an interactive script that modifies ItemId(s) to a URL encoded format and generates a Microsoft link to the affected item.
 
.NOTES
    This function is designed to modify item IDs from Datto OpenSearch and provide a link to the Outlook Web App calendar URL.
    This is a PowerShell adaptation for the original `EWSreplacer.bat` from @Liam Cowan for the PT [#4315344](https://kaseya.zendesk.com/agent/tickets/4315344)
#>



function Get-OutlookUrl {
    [CmdletBinding()]
    param (
        [Parameter(
            Position = 0, 
            Mandatory = $true, 
            ValueFromPipeline = $true, 
            ValueFromPipelineByPropertyName = $true
        )]
        [string]
        $opensearchItemId
    )

    process {
        if ($opensearchItemId -eq 'q') {
            return 'q'
        }
        Write-Verbose -Message "Processing OpensearchItemId: $opensearchItemId"

        # Step 1: Replaces '-' with '+' and '_' with '/'
        $originalMsItemId = $opensearchItemId -replace '-', '+' -replace '_', '/'
        Write-Verbose -Message "Step 1 - Microsoft ItemID: $originalMsItemId"

        # Add Type System.Web if not already added
        Try {
            [System.Web.HttpUtility]$HttpUtilityTest
        } Catch {
            Add-Type -AssemblyName System.Web
        }
        # Step 2: Convert to URL encoded format
        $urlEncodedItemId = [System.Web.HttpUtility]::UrlEncode($originalMsItemId)
        Write-Verbose -Message "Step 2 - URL Encoded: $urlEncodedItemId"

        # Step 3: Generate OWA calendar URL link
        [System.Uri]$owaUrl = "https://outlook.office.com/calendar/item/$urlEncodedItemId"
        Write-Verbose -Message "Step 3 - Converted URL: $owaUrl"


        ## Back to the validation step here not working, was not testing against powershell 5.
        ## Some logic added to handle the exceptions seen.

        # Validate URL link created
        if ([string]::IsNullOrWhiteSpace($doUrlValidation) -or $doUrlValidation -eq $true) {
            try {
                $response = Invoke-WebRequest -Uri $owaUrl -UseBasicParsing -ErrorAction SilentlyContinue

                if ($response.StatusCode -eq 200) {
                    Write-Verbose -Message "Validation for $($owaUrl.AbsoluteUri) successful."
                    #Write-Host "Validation for $($owaUrl.AbsoluteUri) successful."
                }
            } catch {
                # Handling for error (417) when validation run on PowerShell 5.1
                if ($_.Exception.Message -notlike '*The remote server returned an error: (417).*') {
                    $script:invalidUrl = $true  # Set the flag to true for invalid URLs
                    [string]$script:urlValidationError = "$($_.Exception.Message)"

                    Write-Host "$($_.Exception.Message)" -ForegroundColor Red
                    Write-Host "Output URL validation appears to have encountered an error. Check the original input `"" -ForegroundColor Yellow -NoNewLine
                    Write-Host "$opensearchItemId" -ForegroundColor Gray -NoNewLine
                    Write-Host "`" and try again." -ForegroundColor Yellow
                }
            }
        } elseif ($doUrlValidation -eq $false) {
            Write-Verbose "Output Validation Disabled: Skipping URL Check"
        }


        # Cast the URI back to a string type and return the variable
        [String]$owaLink = $owaUrl.AbsoluteUri
        $owaLink
    }
}

function Update-PSScriptInteractive {
    [CmdletBinding()]
    param (
        [Parameter(
            Position = 0,
            Mandatory = $True,
            ValueFromPipeline = $True,
            ValueFromPipelineByPropertyName = $True
        )]
        [String]
        $scriptName
        
    )

    process {
        Write-Host "Checking for any updates to the script..." -ForegroundColor Black -BackgroundColor White

        # Check if the Script is installed
        $installedScript = Get-InstalledScript $scriptName -ErrorAction SilentlyContinue
        $onlineScript = Find-Script -Name $scriptName -ErrorAction SilentlyContinue

        if (!$installedScript) {
            try {
                Write-Host "Script `"$scriptName`" does not appear to be installed, attempting to install." -ForegroundColor Yellow
                Install-Script $scriptName -Scope "CurrentUser" -ErrorAction Stop -Verbose #-Force
                Write-Host "Script `"$scriptName`" successfully installed." -ForegroundColor Green
            } catch {
                Write-Error "Error installing Script $scriptName`: $_"
                throw
            }
        } else {
            Write-Verbose "Script $scriptName is already installed on the local system."

            # Prompt to update the Script if the online version seen is higher
            if ($installedScript.version -eq $onlineScript.version) {
                Write-Host "$scriptName version installed is $($installedScript.version) which matches the online version $($onlineScript.version)`n" -ForegroundColor Green
            } elseif ($installedScript.version -gt $onlineScript.version) {
                Write-Host "$scriptName version installed is $($installedScript.version) which is greater than the online version $($onlineScript.version)`nStrange, but okay I guess?`n" -ForegroundColor Gray
            } elseif ($installedScript.version -lt $onlineScript.version) {
                Write-Host "$scriptName version installed is $($installedScript.version) which is less than the online version $($onlineScript.version)" -ForegroundColor DarkYellow -BackgroundColor DarkGray
                $upgradeScriptChoice = Read-Host -Prompt "Would you like to update now? (Select, Y/yes or N/no)"

                if ([string]::IsNullOrWhiteSpace($upgradeScriptChoice) -or $upgradeScriptChoice.ToLower() -eq 'y' -or $upgradeScriptChoice -eq 'yes') {
                    Write-Host "Updating $scriptName from version $($installedScript.version) to $($onlineScript.version)."
                    Update-Script -Name $scriptName -Force
                    
                    Start-Process -FilePath "$(Join-Path -Path $($installedScript.InstalledLocation) -ChildPath ($scriptName)).ps1" <#-WindowStyle Normal#>; Exit
                }
                Write-Host # Empty line for formatting
            }
        }
    }
}

function Start-Counter {
    param (
        $countdownSeconds = 5
    )

    # Countdown
    for ($i = $countdownSeconds; $i -ge 1; $i--) {
        Write-Host "Exit in $i seconds..."
        Start-Sleep -Seconds 1
    }
}

# Checks for update to script and prompt user if available
Update-PSScriptInteractive -scriptName "ConvertFrom-OpensearchItemId"

# URL Validation choice user input
$urlValidationChoice = $(Write-Host "Warning: This can add additional processing and time on significantly larger datasets`n" -ForegroundColor yellow -NoNewLine) + $(Write-Host "Attempt URL validation? (Select, Y/yes or N/no): " -NoNewLine; Read-Host)

# Enable URL validation check, takes in user input and converts to boolean value
if ([string]::IsNullOrWhiteSpace($urlValidationChoice) -or $urlValidationChoice.ToLower() -eq 'y' -or $urlValidationChoice.ToLower() -eq 'yes') {
    $doUrlValidation = $true
} elseif ($urlValidationChoice.ToLower() -eq 'n' -or $urlValidationChoice.ToLower() -eq 'no')  {
    $doUrlValidation = $false
}
Write-Host # Empty line for formatting

# Script portion to allow for user selection
$sourceMethod = Read-Host "Select your input method (Type: 1 for Console, 2 for CSV file) then `"Enter`""

switch ($sourceMethod) {
    1 {
        do {
            $opensearchItemId = Read-Host "Enter itemID(s) from OpenSearch (or type 'q' to quit)"
            $outlookUrl = Get-OutlookUrl -opensearchItemId $opensearchItemId
            $convertedUrl = ([string]$outlookUrl).trim() # Remove any leading/trailing whitespaces from the string

            if ($opensearchItemId -ne 'q') {
                Write-Host "Output URL: " -ForegroundColor Green -NoNewLine
                Write-Host "$convertedUrl" -ForegroundColor Cyan
            }

        } while ($opensearchItemId -ne 'q')
        #} until ($opensearchItemId -eq 'q')

        Start-Counter; break
    }

    2 {
        # Get user input for CSV file path
        $csvFilePath = Read-Host "Enter the path to the CSV file"
        $csvFilePath = $csvFilePath -replace '"', '' # Remove any quotes from the file path if found


        # Import CSV file and filter the data for just ItemIds
        try {
            $sourceCsvData = Import-Csv -Path $csvFilePath -Delimiter ','
            Write-Verbose -Message "File successfully imported."

            # Check if the file extension is .csv
            $fileType = (Get-Item $csvFilePath).Extension
            if ($fileType -ne '.csv') {
                throw "Error: The file `"$((Get-Item $csvFilePath).Name)`" does not appear to be a CSV. Check the input and try again."
            }

            # Filter CSV data to include only columns with labels containing "ItemId"
            $csvItemIds = $sourceCsvData | ForEach-Object {
                $_.PSObject.Properties | Where-Object { $_.Name -like 'ItemId*' } | ForEach-Object {
                    $_.Value
                }
            }

            # Check if any columns with labels containing "ItemId" were found
            if (-not $csvItemIds) {
                throw "Error: No header found containing 'ItemId' in the CSV file. Check the formatting and try again."
            }

        } catch {
            $csvImportError = $_.Exception.Message
            Write-Host "$csvImportError" -ForegroundColor Red

            Read-Host -Prompt "Press any key to exit the script."
            Start-Counter; break
        }



        # Array to store results
        $convertedCsvArray = [System.Collections.ArrayList]::new()

        # Convert each $csvItemIds and put them back into the array
        foreach ($opensearchItemId in $csvItemIds) {
            $invalidUrl = $null # Clean up the $invalidUrl variable if detected from previous loop

            # Convert the Item Ids and get the results
            Write-Host "Processing CSV ItemId: $opensearchItemId"
            $outlookUrl = Get-OutlookUrl -opensearchItemId $opensearchItemId
            $convertedUrl = ([string]$outlookUrl).trim() # Remove any leading/trailing whitespaces from the string


            # Create a custom object with the "Output_Url" property
            $outputResult = [PSCustomObject]@{
                "Output_Url" = "$convertedUrl"
            }

            # Check for valid ItemId input, outputs error if 'q' quit detected
            if ($opensearchItemId -eq 'q') {
                $outputErrorMessage = "Invalid Input ItemId: `"$opensearchItemId`" found. Check the input and try again."
                Write-Host "$outputErrorMessage" -ForegroundColor Yellow

                Add-Member -InputObject $outputResult -MemberType NoteProperty -Name "Output_Error" -Value "$outputErrorMessage"
                Add-Member -InputObject $outputResult -MemberType NoteProperty -Name "Input_ItemID" -Value "$opensearchItemId"
            }

            # Check for valid URL, outputs error if invalid URL seen
            # If $invalidUrl was detected then set flag for invalid URL detected, and add custom error message to $outputResult
            if ($invalidUrl) {
                $invalidUrlDetected = $true

                $outputErrorMessage = "Check the input ItemID or the URL output, and try again. Failed URL Validation with: `"$urlValidationError`""
                Add-Member -InputObject $outputResult -MemberType NoteProperty -Name "Output_Error" -Value "$outputErrorMessage"
                Add-Member -InputObject $outputResult -MemberType NoteProperty -Name "Input_ItemID" -Value "$opensearchItemId"
            }

            # Add the output result to the ArrayList
            $convertedCsvArray.Add($outputResult)


            Write-Host "Output URL: " -ForegroundColor Green -NoNewLine
            Write-Host "$convertedUrl" -ForegroundColor Cyan
        }



        # Get parent directory, and filename from the input CSV file path
        $csvDirectory = (Get-Item $csvFilePath).Directory.FullName
        $csvFileName = (Get-Item $csvFilePath).BaseName
        $outputCsvPath = Join-Path -Path $csvDirectory -ChildPath "$csvFileName-Converted_Urls.csv"

        # Check the flag after processing the URLs. Exports results to a new CSV file in the source directory
        if ($doUrlValidation -and $invalidUrlDetected) {
            Write-Host "One or more invalid URLs were detected. Be sure to validate the output." -ForegroundColor Red -BackgroundColor Yellow
            $convertedCsvArray | Select-Object -Property Output_Url,Output_Error,Input_ItemID | Export-Csv -Path $outputCsvPath -NoTypeInformation -Delimiter ',' -Encoding UTF8
        } elseif ($doUrlValidation -and !$invalidUrlDetected) {
            Write-Host "Validation Passed: URL output appears to be valid" -ForegroundColor Gray -BackgroundColor DarkGreen
            $convertedCsvArray | Select-Object -Property Output_Url| Export-Csv -Path $outputCsvPath -NoTypeInformation -Delimiter ',' -Encoding UTF8
        } elseif (!$doUrlValidation) {
            Write-Host "URL Validation Disabled: Skipped Output Check" -ForegroundColor DarkYellow -BackgroundColor DarkGray
            $convertedCsvArray | Select-Object -Property Output_Url| Export-Csv -Path $outputCsvPath -NoTypeInformation -Delimiter ',' -Encoding UTF8
        }

        Write-Host "Results exported to: " -NoNewLine
        Write-Host "$outputCsvPath" -ForegroundColor DarkGray

        $openFileExport = Read-Host -Prompt "Do you want to open the file? (Select, Y/yes or N/no)"
        if ($openFileExport.ToLower() -eq 'y' -or $openFileExport.ToLower() -eq 'yes') {
            Invoke-Item $outputCsvPath
        }
        Exit
        #Start-Counter; break
    }
    
    default {
        # Default switch, catch any invalid user input and exit
        Write-Host "Invalid choice. Exiting the script." -ForegroundColor Yellow -BackgroundColor Red

        Start-Counter; break
    }
}