IchicraftWidgets.psm1

function Add-IchicraftWidgetsApp {
    [CmdletBinding()]
    param(
        [parameter(
            Mandatory = $false, 
            HelpMessage = "Connection with permissions to add the app to the tenant Application Catalog")]
        [SharePointPnP.PowerShell.Commands.Base.SPOnlineConnection]
        $connection
    )
    
    process {
        [SharePointPnP.PowerShell.Commands.Base.SPOnlineConnection]$connection = Get-PnPConnection;

        if ($null -eq $connection) {
            throw "No SPOnlineConnection is available, connect to the target site with the Connect-PnPOnline Cmdlet"
        }

        if ($connection.ConnectionType -ne [SharePointPnP.PowerShell.Commands.Enums.ConnectionType]::TenantAdmin) {
            throw "Current SPOnlineConnection is not a TenantAdmin connection $($connection.ConnectionType), connect to the tenant admin site with the Connect-PnPOnline Cmdlet"
        }

        # Download the Widgets SPPKG package
        Write-Host "Start package download"
        Start-BitsTransfer -Source https://ichicraft.blob.core.windows.net/widgetboard/public/0.2.3/ichicraft-widgetboard.sppkg -Destination "$($env:userprofile)\downloads"

        # Add the package to the sites app catalog
        Write-Host "Start adding the Widgets app"
        Retry-Command -ScriptBlock {
            Add-PnPApp -Path "$($env:userprofile)\downloads\ichicraft-widgetboard.sppkg" -Scope Tenant -Publish -Overwrite -ErrorAction Stop
        } -Verbose

        # remove the download
        Remove-Item -LiteralPath "$($env:userprofile)\downloads\ichicraft-widgetboard.sppkg"
    }
}

function Approve-IchicraftWidgetsTenantServicePrincipalPermissionRequests {
    [CmdletBinding()]
    param(
        [parameter(
            Mandatory = $false, 
            HelpMessage = "Connection with permissions to approve the TenantServicePrincipalPermissionRequest (Approve-PnPTenantServicePrincipalPermissionRequest)")]
        [SharePointPnP.PowerShell.Commands.Base.SPOnlineConnection]
        $connection
    )
    
    process {
        if (-not $connection) {
            [SharePointPnP.PowerShell.Commands.Base.SPOnlineConnection]$connection = Get-PnPConnection;
        }

        if ($null -eq $connection) {
            throw "No SPOnlineConnection is available, connect to the target site with the Connect-PnPOnline Cmdlet"
        }

        if ($connection.ConnectionType -ne [SharePointPnP.PowerShell.Commands.Enums.ConnectionType]::TenantAdmin) {
            throw "Current SPOnlineConnection is not a TenantAdmin connection $($connection.ConnectionType), connect to the tenant admin site with the Connect-PnPOnline Cmdlet"
        }

        # Approve the API permission requests
        Write-Host "Start approving the API permission requests"
        Retry-Command -ScriptBlock {
            Get-PnPTenantServicePrincipalPermissionRequests | Where-Object { $_.PackageName -eq "Ichicraft-WidgetBoardWebpart" } | ForEach-Object { Approve-PnPTenantServicePrincipalPermissionRequest -RequestId $_.Id -Force -ErrorAction SilentlyContinue }
        }  -Verbose
    }
}

function Add-IchicraftWidgetsClientSideWebPart {
    [CmdletBinding()]
    param(
        [parameter(
            Mandatory = $false, 
            HelpMessage = "Connection to the site where the webpart should be added")]
        [SharePointPnP.PowerShell.Commands.Base.SPOnlineConnection]
        $connection
    )
    
    process {
        if (-not $connection) {
            [SharePointPnP.PowerShell.Commands.Base.SPOnlineConnection]$connection = Get-PnPConnection;
        }

        if ($null -eq $connection) {
            throw "No SPOnlineConnection is available, connect to the target site with the Connect-PnPOnline Cmdlet"
        }

        if ($connection.ConnectionType -ne [SharePointPnP.PowerShell.Commands.Enums.ConnectionType]::O365) {
            throw "Current SPOnlineConnection is not a O365 connection $($connection.ConnectionType), connect to the target site with the Connect-PnPOnline Cmdlet"
        }

        $siteUrl = $connection.Url

        $widgetsApp = Get-PnPApp -Identity "Ichicraft-WidgetBoardWebpart" -Scope Tenant -ErrorAction SilentlyContinue

        if ($null = $widgetsApp) {
            throw "The Ichicraft-WidgetBoardWebpart app is not found in the tenant Application Catalog. Please run Add-IchicraftWidgetsApp first."
        }
        
        # Install the package to the site
        Write-Host "Start installing the Widgets app to $siteUrl"
        Retry-Command -ScriptBlock {
            Install-PnPApp -Identity "Ichicraft-WidgetBoardWebpart" -Scope Tenant -ErrorAction Stop
        } -Verbose

        # Add a full-with section to the page
        Write-Host "Start adding a full-width section to the homepage of $siteUrl"
        Retry-Command -ScriptBlock {
            Add-PnPClientSidePageSection -Page "home" -SectionTemplate OneColumnFullWidth -Order 0
        } -Verbose

        # Add the Widgets webpart to the page
        Write-Host "Start adding the Widgets webpart"
        Retry-Command -ScriptBlock {
            Add-PnPClientSideWebPart -Page "home" -Component "Widget Board" -Section 1 -Column 1 -ErrorAction Stop
        } -Verbose

        write-host "Finished, now try and open $siteUrl to configure your widgetboard"
    }
}

function Export-IchicraftWidgetsConfiguration {
    [CmdletBinding()]
    # This script ensures the lists needed by the Ichicraft Widgets webpart
    param(
        [parameter(
            Mandatory = $false, 
            HelpMessage = "Connection to the site from which to export the configuration")]
        [SharePointPnP.PowerShell.Commands.Base.SPOnlineConnection]
        $connection,
        $outputFilePath = "./adminConfig.json"
    )

    process {
        if (-not $connection) {
            [SharePointPnP.PowerShell.Commands.Base.SPOnlineConnection]$connection = Get-PnPConnection;
        }

        if ($null -eq $connection) {
            throw "No SPOnlineConnection is available, connect to the target site with the Connect-PnPOnline Cmdlet"
        }

        if ($connection.ConnectionType -ne [SharePointPnP.PowerShell.Commands.Enums.ConnectionType]::O365) {
            throw "Current SPOnlineConnection is not a O365 connection $($connection.ConnectionType), connect to the target site with the Connect-PnPOnline Cmdlet"
        }

        $siteUrl = $connection.Url

        $adminConfigItem = Get-PnPListItem -List "WidgetBoard_AdminConfig" -ErrorAction SilentlyContinue | Select-Object -First 1

        if (-not $adminConfigItem) {
            throw "No configuration found in site $siteUrl"
        }

        $config = $adminConfigItem["Config"];

        $config | Out-File -Encoding utf8 -Force -FilePath $outputFilePath -Append:$false
    }
}

function CreateConfigList($name, $connection) {
    $list = Get-PnPList -Identity $name -ErrorAction:SilentlyContinue -Connection $connection

    if (-not $list) {
        New-PnPList -Title $name  -Template GenericList -Hidden -EnableContentTypes:$false -EnableVersioning:$true -OnQuickLaunch:$false  -Connection $connection
    }

    Set-PnPList -Identity $name -Description 'Used by ichicraft widgets' -EnableAttachments:$false -Connection $connection

    # Set NoCrawl
    $list = Get-PnPList -Identity $name -Connection $connection
    $list.NoCrawl = $true
    $list.Update()
    $list.Context.ExecuteQuery()

    if (-not (Get-PnPField -List "WidgetBoard_UserConfig" -Identity "Config" -ErrorAction SilentlyContinue -Connection $connection)) {
        # Add the config field
        Add-PnPField -List $name -DisplayName "Config" -InternalName "Config" -Type Note -AddToDefaultView -Connection $connection
    }    
}

function Initialize-IchicraftWidgetsWebpart {
    [CmdletBinding()]
    # This script ensures the lists needed by the Ichicraft Widgets webpart
    param(
        [parameter(
            Mandatory = $false, 
            HelpMessage = "Connection to the site with the webpart")]
        [SharePointPnP.PowerShell.Commands.Base.SPOnlineConnection]
        $connection,
        $configFilePath = "./adminConfig.json"
    )

    process {
        if (-not $connection) {
            [SharePointPnP.PowerShell.Commands.Base.SPOnlineConnection]$connection = Get-PnPConnection;
        }

        if ($null -eq $connection) {
            throw "No SPOnlineConnection is available, connect to the target site with the Connect-PnPOnline Cmdlet"
        }

        if ($connection.ConnectionType -ne [SharePointPnP.PowerShell.Commands.Enums.ConnectionType]::O365) {
            throw "Current SPOnlineConnection is not a O365 connection $($connection.ConnectionType), connect to the target site with the Connect-PnPOnline Cmdlet"
        }

        $siteUrl = $connection.Url

        # create adminconfig list
        CreateConfigList -name "WidgetBoard_AdminConfig"

        $adminConfig = Get-Content -Path $configFilePath -Raw -Encoding UTF8
        $item = Get-PnPListItem -List "WidgetBoard_AdminConfig" | Select-Object -First 1

        if ($item) {
            write-host "Updating admin config list item"
            $item = Set-PnPListItem -Identity $item.Id -List "WidgetBoard_AdminConfig" -Values @{"Title" = "Config"; "Config" = $adminConfig } 
        }
        else {
            write-host "Adding admin config list item"
            $item = Add-PnPListItem  -List "WidgetBoard_AdminConfig" -Values @{"Title" = "Config"; "Config" = $adminConfig } 
        }

        # create userconfig list
        CreateConfigList -name "WidgetBoard_UserConfig" 

        $list = Get-PnPList -Identity "WidgetBoard_UserConfig"
        $list.ReadSecurity = 2
        $list.WriteSecurity = 2
        $list.Context.ExecuteQuery()

        # create assets list
        $list = Get-PnPList -Identity "WidgetBoard_Assets" -ErrorAction:SilentlyContinue

        if (-not $list) {
            New-PnPList -Title "WidgetBoard_Assets" -Template DocumentLibrary -Hidden -EnableContentTypes:$false -EnableVersioning:$false -OnQuickLaunch:$false -ErrorAction SilentlyContinue
        }

        # Tell the service the board has been installed
        [int]$rand = Get-Random -Maximum 10000 -Minimum 1000
        Invoke-RestMethod -Method Post -Uri "https://ichicraft-widgetboard.azurewebsites.net/config/update/etag/$($rand)?referrer=$siteUrl"
    }
}

function Retry-Command {
    [CmdletBinding()]
    param (
        [parameter(Mandatory, ValueFromPipeline)] 
        [ValidateNotNullOrEmpty()]
        [scriptblock] $ScriptBlock,
        [int] $RetryCount = 100,
        [int] $TimeoutInSecs = 5,
        [string] $description = ""
    )
        
    process {
        $Attempt = 1
        $Flag = $true

        Write-Host "[START] $description"
        
        do {
            try {
                $PreviousPreference = $ErrorActionPreference
                $ErrorActionPreference = 'Stop'
                Invoke-Command -ScriptBlock $ScriptBlock -OutVariable Result              
                $ErrorActionPreference = $PreviousPreference

                # flow control will execute the next line only if the command in the scriptblock executed without any errors
                # if an error is thrown, flow control will go to the 'catch' block
                Write-Host "[FINISHED] $description `n"
                $Flag = $false
            }
            catch {
                if ($Attempt -gt $RetryCount) {
                    Write-Host "[FAILED] $description! Total retry attempts: $RetryCount"
                    $Flag = $false
                }
                else {
                    Write-Verbose "[Error Message] $($_.exception.message) `n"
                    Write-Verbose "[$Attempt/$RetryCount] $description. Retrying in $TimeoutInSecs seconds..."
                    Start-Sleep -Seconds $TimeoutInSecs
                    $Attempt = $Attempt + 1
                }
            }
        }
        While ($Flag)
        
    }
}

Export-ModuleMember -Function Add-IchicraftWidgetsApp, Add-IchicraftWidgetsClientSideWebPart, Approve-IchicraftWidgetsTenantServicePrincipalPermissionRequests, Initialize-IchicraftWidgetsWebpart, Export-IchicraftWidgetsConfiguration;