ConvertFrom-OpensearchItemId.ps1

<#PSScriptInfo
 
.VERSION 2023.12.10
 
.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 Adds console export options, CSV and console output. Better calls for running the script after auto-update.
 
 
.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"
        $opensearchItemId = $($opensearchItemId).trim()

        # 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 -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
                    $scriptPath = "$(Join-Path -Path $($installedScript.InstalledLocation) -ChildPath ($scriptName)).ps1"

                    if ($PSVersionTable.PSVersion.Major -ge 6) {
                        Write-Host "Starting the updated script." ; Start-Process pwsh -ArgumentList "-File `"$scriptPath`""; Exit # PowerShell Core and later
                    } else {
                        Write-Host "Starting the updated script." ; Start-Process powershell -ArgumentList "-File `"$scriptPath`""; Exit # Windows PowerShell 5.1
                    }
                    #Write-Host "Starting the updated script." ; & $scriptPath; 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 {
        # Array to store console output results
        $consoleOutputResults = New-Object System.Collections.ArrayList

        do {
            $invalidUrl = $null # Clean up the $invalidUrl variable if detected from previous loop
            $opensearchItemId = Read-Host "Enter itemID(s) from OpenSearch (or type 'q' to quit)"

            if (![string]::IsNullOrWhiteSpace($opensearchItemId) -and $opensearchItemId -ne 'q') {
                $outlookUrl = Get-OutlookUrl -opensearchItemId $opensearchItemId
                $convertedUrl = ([string]$outlookUrl).trim() # Remove any leading/trailing whitespaces from the string
                Write-Host "Output URL: " -ForegroundColor Green -NoNewLine; Write-Host "$convertedUrl" -ForegroundColor Cyan

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

                # 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 results to the ArrayList
                $consoleOutputResults.Add($outputResult) | Out-Null  # Out-Null suppresses the output of Add method
            }

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

        # Check the flag after processing the URLs
        if ($doUrlValidation -and $invalidUrlDetected) {
            Write-Host "One or more invalid URLs were detected. Be sure to validate the output." -ForegroundColor Red -BackgroundColor Yellow
        } elseif ($doUrlValidation -and !$invalidUrlDetected) {
            Write-Host "Validation Passed: URL output appears to be valid" -ForegroundColor Gray -BackgroundColor DarkGreen
        } elseif (!$doUrlValidation) {
            Write-Host "URL Validation Disabled: Skipped Output Check" -ForegroundColor DarkYellow -BackgroundColor DarkGray
        }

        # Prompt the user for CSV export if the console output is greater than 1
        if ($consoleOutputResults.Count -gt 1) {
            Write-Host # Empty line for formatting
            $exportConsoleChoice = Read-Host -Prompt "Would you like to export the output to the Console or a CSV? (Type: 1 for Console, 2 for CSV file)`nEnter anything else to skip"

            switch ($exportConsoleChoice) {
                1 {
                    $($consoleOutputResults.Output_Url) | Format-Table; Write-Host # Empty line for formatting
                    try {
                        Set-Clipboard -Value $($consoleOutputResults.Output_Url)
                        Write-Host "Output successfully copied to clipboard!" -ForegroundColor Green
                    } catch {
                        Write-Host "Error copying to clipboard: " -NoNewLine; Write-Host "$_" -ForegroundColor Yellow
                    }
                    Read-Host -Prompt "Press `"Enter`" to exit" | Out-Null
                    #Write-Host "Press any key to exit." -NoNewLine; [void][System.Console]::ReadKey($false)
                }
                2 {
                    $outputCsvPath = Join-Path -Path $($HOME) -ChildPath "$(Get-Date -UFormat "%Y-%m-%dT%T%Z" | ForEach-Object { $_ -replace ":", "." })-Console_Converted_Urls.csv"

                    # Export CSV properties depending on errors detected
                    if ($doUrlValidation -and $invalidUrlDetected) {
                        $consoleOutputResults | Select-Object -Property Output_Url,Output_Error,Input_ItemID | Export-Csv -Path $outputCsvPath -NoTypeInformation -Delimiter ',' -Encoding UTF8
                    } elseif (!$doUrlValidation -or $doUrlValidation -and !$invalidUrlDetected) {
                        $consoleOutputResults | Select-Object -Property Output_Url| Export-Csv -Path $outputCsvPath -NoTypeInformation -Delimiter ',' -Encoding UTF8
                    }

                    Write-Host "Console results exported to CSV: " -ForegroundColor Green -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
                    }
                }
                Default {
                    break
                }
            }
        }


        Start-Counter -countdownSeconds 3; 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." | Out-Null
            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) | Out-Null


            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
    }
}