PSFront.psm1

#region Private functions
function InvokeFrontRestMethod {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestMethod]$Method,

        [Parameter()]
        [String]$URL = "https://api2.frontapp.com",

        [Parameter(Mandatory)]
        [String]$Endpoint,

        [Parameter()]
        [String]$Path,

        [Parameter()]
        [hashtable]$Body,

        [Parameter()]
        [System.Collections.Specialized.NameValueCollection]$Query,

        [Parameter(Mandatory)]
        [SecureString]$ApiKey
    )

    if (-not ("System.Web.HttpUtility" -as [Type])) {
        Add-Type -AssemblyName "System.Web" -ErrorAction "Stop"
    }

    $Params = @{
        Method                  = $Method
        URI                     = "{0}/{1}" -f $URL, $Endpoint
        Headers                 = @{
            "Authorization" = "Bearer {0}" -f [PSCredential]::new("none", $ApiKey).GetNetworkCredential().Password
        }
        ContentType             = "application/json"
        ErrorAction             = "Stop"
        ErrorVariable           = "InvokeRestMethodError"
    }

    if ($PSBoundParameters.ContainsKey("Path")) {
        $Params["URI"] = "{0}/{1}" -f $Params["URI"], $Path
    }

    if ($PSBoundParameters.ContainsKey("Query")) {
        $Params["URI"] = "{0}?{1}" -f $Params["URI"], $Query.ToString()
    }

    if ($PSBoundParameters.ContainsKey("Body")) {
        $Params["Body"] = $Body | ConvertTo-Json
    }

    $Params["URI"] = [System.Web.HttpUtility]::UrlDecode($Params["URI"])

    do {
        Write-Verbose "Calling Front API"
        Write-Verbose ("URI: '{0}'" -f $Params["URI"])
        Write-Verbose ("Body: '{0}'" -f $Params["Body"])

        try {
            $Data = Invoke-RestMethod @Params
        }
        catch {
            # The web exception class is different for Core vs Windows
            if ($InvokeRestMethodError.ErrorRecord.Exception.GetType().FullName -match "HttpResponseException|WebException") {
                $ExceptionMessage = $InvokeRestMethodError.Message | 
                    ConvertFrom-Json | 
                    Select-Object -ExpandProperty "_error" |
                    Select-Object -ExpandProperty "message"
                    
                $ErrorId = "{0}{1}" -f 
                    [Int][System.Net.HttpStatusCode]$InvokeRestMethodError.ErrorRecord.Exception.Response.StatusCode, 
                    [String][System.Net.HttpStatusCode]$InvokeRestMethodError.ErrorRecord.Exception.Response.StatusCode

                switch -Regex ($InvokeRestMethodError.ErrorRecord.Exception.Response.StatusCode) {
                    "Unauthorized" {
                        $Exception = [System.UnauthorizedAccessException]::new($ExceptionMessage)
                        $ErrorRecord = [System.Management.Automation.ErrorRecord]::new(
                            $Exception,
                            $ErrorId,
                            [System.Management.Automation.ErrorCategory]::AuthenticationError,
                            $Params['Uri']
                        )
                        $PSCmdlet.ThrowTerminatingError($ErrorRecord)
                    }
                    "BadRequest|Conflict" {
                        $Exception = [System.ArgumentException]::new($ExceptionMessage)
                        $ErrorRecord = [System.Management.Automation.ErrorRecord]::new(
                            $Exception,
                            $ErrorId,
                            [System.Management.Automation.ErrorCategory]::InvalidArgument,
                            $Params['Uri']
                        )
                        $PSCmdlet.ThrowTerminatingError($ErrorRecord)
                    }
                    "NotFound" {
                        $Exception = [System.Management.Automation.ItemNotFoundException]::new($ExceptionMessage)
                        $ErrorRecord = [System.Management.Automation.ErrorRecord]::new(
                            $Exception,
                            $ErrorId,
                            [System.Management.Automation.ErrorCategory]::ObjectNotFound,
                            $Params['Uri']
                        )
                        $PSCmdlet.ThrowTerminatingError($ErrorRecord)
                    }
                    "ServiceUnavailable" {
                        $Exception = [System.InvalidOperationException]::new($ExceptionMessage)
                        $ErrorRecord = [System.Management.Automation.ErrorRecord]::new(
                            $Exception,
                            $ErrorId,
                            [System.Management.Automation.ErrorCategory]::ResourceUnavailable,
                            $Params['Uri']
                        )
                        $PSCmdlet.ThrowTerminatingError($ErrorRecord)
                    }
                    "TooManyRequests" {
                        [int]$Seconds = [int]$InvokeRestMethodError.InnerException.Response.Headers.GetValues("Retry-After")[0] + 1
                        Write-Verbose ("Exceeded number of requests allowed, will wait {0} second(s) until retrying" -f $Seconds)
                        Start-Sleep -Seconds $Seconds

                        $Params = @{
                            Method   = $Method
                            Endpoint = $Endpoint
                            ApiKey   = $ApiKey
                        }

                        if ($PSBoundParameters.ContainsKey("Path")) {
                            $Params["Path"] = $Path
                        }
                    
                        if ($PSBoundParameters.ContainsKey("Query")) {
                            $Params["Query"] = $Query
                        }
                    
                        if ($PSBoundParameters.ContainsKey("Body")) {
                            $Params["Body"] = $Body
                        }

                        return InvokeFrontRestMethod @Params
                    }
                    default {
                        $Exception = [System.InvalidOperationException]::new($ExceptionMessage)
                        $ErrorRecord = [System.Management.Automation.ErrorRecord]::new(
                            $Exception,
                            $ErrorId,
                            [System.Management.Automation.ErrorCategory]::InvalidOperation,
                            $Params['Uri']
                        )
                        $PSCmdlet.ThrowTerminatingError($ErrorRecord)
                    }
                }
            }
            else {
                $PSCmdlet.ThrowTerminatingError($_)
            }
        }

        if ($Params["URI"] -ne $Data._pagination.next) {
            # This could be null, depending if pagination is needed or not
            # Update the URI just in case the loop isn't finished yet
            $Params["URI"] = $Data._pagination.next
        }
        else {
            $Exception = [System.InvalidOperationException]::new("Pagination failure with Front API (the same URL was given for next page)")
            $ErrorRecord = [System.Management.Automation.ErrorRecord]::new(
                $Exception,
                $ErrorId,
                [System.Management.Automation.ErrorCategory]::OperationStopped,
                $Params['Uri']
            )
            $PSCmdlet.ThrowTerminatingError($ErrorRecord)
        }

        if ($Data._results) {
            Write-Output $Data._results
        }
        else {
            Write-Output $Data
        }
        
    } until ([String]::IsNullOrWhiteSpace($Data._pagination.next))

}
#endregion

#region Public functions
function Invoke-FrontRestMethod {
    <#
    .SYNOPSIS
        Arbitrarily interact with Front's Core API.
    .DESCRIPTION
        Arbitrarily interact with Front's Core API. Since this function is for no particular endpoint in Front's API, this can be useful for occasions when Front have added a new endpoint, query or body parameter to their API specification and one of the other dedicated functions of PSFront have not yet been updated.
    .EXAMPLE
        PS C:\> Invoke-FrontRestMethod -Method GET -Endpoint tags -ApiKey $secret
        
        Lists all tags in Front: https://dev.frontapp.com/reference/tags-1#get_tags
    .EXAMPLE
        PS C:\> Invoke-FrontRestMethod -Method GET -Endpoint teams -Path "tim_2daq7/tags" -ApiKey $secret
        
        Lists all tags for the team with id tim_2daq7 in Front: https://dev.frontapp.com/reference/tags-1#get_teams-team-id-tags-1
    .INPUTS
        This function does not accept pipeline input.
    .OUTPUTS
        System.Management.Automation.PSObject
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [Microsoft.PowerShell.Commands.WebRequestMethod]$Method,

        [Parameter(Mandatory)]
        [String]$Endpoint,

        [Parameter()]
        [String]$Path,

        [Parameter()]
        [hashtable]$Body,

        [Parameter()]
        [hashtable]$Query,

        [Parameter(Mandatory)]
        [SecureString]$ApiKey
    )

    $Params = @{
        Method      = $Method
        Endpoint    = $Endpoint
        ApiKey      = $ApiKey
        ErrorAction = "Stop"
    }

    switch ($PSBoundParameters.Keys) {
        "Path" {
            $Params["Path"] = $Path
        }
        "Body" {
            $Params["Body"] = $Body
        }
        "Query" { 
            $QueryString = [System.Web.HttpUtility]::ParseQueryString('')

            foreach ($item in $Query.GetEnumerator()) {
                $QueryString.Add($item.Key, $item.Value)
            }

            $Params["Query"] = $QueryString
        }
    }

    try {
        InvokeFrontRestMethod @Params
    }
    catch {
        Write-Error -ErrorRecord $_
    }
}

function New-FrontComment {
    <#
    .SYNOPSIS
        Short description
    .DESCRIPTION
        Long description
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    .NOTES
        General notes
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Low")]
    param (
        [Parameter(Mandatory)]
        [String]$ConversationId,

        [Parameter(Mandatory)]
        [String]$AuthorId,

        [Parameter(Mandatory)]
        [String]$Comment,

        [Parameter(Mandatory)]
        [SecureString]$ApiKey
    )

    $Params = @{
        Method = "POST"
        Endpoint = "conversations"
        Path = "{0}/comments" -f $ConversationId
        Body = @{
            author_id = $AuthorId
            body = $Comment
        }
        ApiKey = $ApiKey
    }

    try {
        if ($PSCmdlet.ShouldProcess(
            ('Would add commment "{0}" as author "{1}" to conversation "{2}"' -f $Comment, $AuthorId, $ConversationId),
            "Are you sure you want to continue?",
            ('Adding commment "{0}" as author "{1}" to conversation "{2}"' -f $Comment, $AuthorId, $ConversationId))) {
                InvokeFrontRestMethod @Params
            }
    }
    catch {
        Write-Error -ErrorRecord $_
    }
}

function Get-FrontMessageTemplate {
    <#
    .SYNOPSIS
        Short description
    .DESCRIPTION
        Long description
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    #>

    [CmdletBinding(DefaultParameterSetName="ById")]
    param (
        [Parameter(Mandatory, ParameterSetName="ById")]
        [String]$Id,

        [Parameter(Mandatory, ParameterSetName="ByFolderId")]
        [String]$FolderId,

        [Parameter(ParameterSetName="All")]
        [Switch]$All,

        [Parameter(Mandatory)]
        [SecureString]$ApiKey
    )

    $Params = @{
        Method   = "GET"
        ApiKey   = $ApiKey
    }

    switch ($PSCmdlet.ParameterSetName) {
        "ById" {
            $Params["Endpoint"] = "message_templates"
            $Params["Path"]     = $id
        }
        "ByFolderId" {
            $Params["Endpoint"] = "message_template_folders"
            $Params["Path"]     = "{0}/message_templates" -f $FolderId
        }
        "All" {
            $Params["Endpoint"] = "message_templates"
        }
    }

    InvokeFrontRestMethod @Params
}

function Update-FrontMessageTemplate {
    <#
    .SYNOPSIS
        Short description
    .DESCRIPTION
        Long description
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [String]$Id,

        [Parameter()]
        [String]$Name,

        [Parameter()]
        [String]$Subject,

        [Parameter()]
        [String]$Body,

        [Parameter()]
        [String]$FolderId,

        [Parameter()]
        [String[]]$InboxId,

        [Parameter(Mandatory)]
        [SecureString]$ApiKey
    )

    $Params = @{
        Method   = "PATCH"
        ApiKey   = $ApiKey
        Endpoint = "message_templates"
        Path     = $id
    }

    $Body = @{}

    switch ($PSBoundParameters.Keys) {
        "Name" {
            $Params["Body"] = $Body["name"] = $Name
        }
        "Subject" {
            $Params["Body"] = $Body["subject"] = $Subject
        }
        "Body" {
            $Params["Body"] = $Body["body"] = $Body
        }
        "FolderId" {
            $Params["FolderId"] = $Body["folder_id"] = $FolderId
        }
        "InboxId" {
            $Params["InboxId"] = $Body["inbox_ids"] = @($InboxId)
        }
    }

    InvokeFrontRestMethod @Params
}

function Add-FrontConversationTag {
    <#
    .SYNOPSIS
        Short description
    .DESCRIPTION
        Long description
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Low")]
    param (
        [Parameter(Mandatory)]
        [String]$ConversationId,

        [Parameter(Mandatory)]
        [String[]]$TagId,

        [Parameter(Mandatory)]
        [SecureString]$ApiKey
    )

    $Params = @{
        Method = "POST"
        Endpoint = "conversations"
        Path = "{0}/tags" -f $ConversationId
        Body = @{
            "tag_ids" = @($TagId)
        }
        ApiKey = $ApiKey
    }

    try {
        if ($PSCmdlet.ShouldProcess(
            ('Would add tag ID(s) "{0}" to conversation "{1}"' -f [String]::Join('", "', $TagId), $ConversationId),
            "Are you sure you want to continue?",
            ('Adding tag ID(s) "{0}" to conversation "{1}"' -f [String]::Join('", "', $TagId), $ConversationId))) {
                InvokeFrontRestMethod @Params
            }
    }
    catch {
        Write-Error -ErrorRecord $_
    }
}

#TODO Finish implementing the other search criteria in the query parameter

function Find-FrontConversation {
    <#
    .SYNOPSIS
        Short description
    .DESCRIPTION
        Long description
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    .NOTES
        General notes
    #>

    param (
        [Parameter()]
        [String[]]$InboxId,

        [String[]]$Keyword,

        [Parameter()]
        [String[]]$Recipient,

        [Parameter()]
        [String[]]$From,

        [Parameter()]
        [String[]]$To,

        [Parameter()]
        [String[]]$CC,

        [Parameter()]
        [String[]]$BCC,

        [Parameter()]
        [String[]]$TagId,

        [Parameter()]
        [String[]]$TopicId,

        [Parameter()]
        [String[]]$Contact,

        [Parameter()]
        [String[]]$Participant,

        [Parameter()]
        [String[]]$Assignee,

        [Parameter()]
        [String[]]$Author,

        [Parameter()]
        [String[]]$Mention,

        [Parameter()]
        [String[]]$Commenter,

        [Parameter()]
        [String[]]$Status,

        [Parameter()]
        [datetime]$Before,

        [Parameter()]
        [datetime]$After,

        [Parameter(Mandatory)]
        [SecureString]$ApiKey
    )

    [System.Collections.Generic.List[String]]$Query = @()

    switch ($PSBoundParameters.Keys) {
        'Keyword' {
            foreach ($item in $Keyword) {
                $Query.add('"{0}"' -f $item)
            }
        }
        'InboxId' {
            foreach ($item in $InboxId) {
                $Query.add('inbox:{0}' -f $item)
            }
        }
        'TagId' {
            foreach ($item in $TagId) {
                $Query.add('tag:{0}' -f $item)
            }
        }
        'TopicId' {
            foreach ($item in $TopicId) {
                $Query.add('topic:{0}' -f $item)
            }
        }
        'Contact' {
            foreach ($item in $Contact) {
                $Query.add('contact:{0}' -f $item)
            }
        }
        'Status' {
            foreach ($item in $Status) {
                $Query.add('is:{0}' -f $item)
            }
        }
        'Recipient' {
            foreach ($item in $Recipient) {
                $Query.add('recipient:{0}' -f $item)
            }
        }
        'From' {
            foreach ($item in $From) {
                $Query.add('from:{0}' -f $item)
            }
        }
        'To' {
            foreach ($item in $To) {
                $Query.add('to:{0}' -f $item)
            }
        }
        'CC' {
            foreach ($item in $CC) {
                $Query.add('cc:{0}' -f $item)
            }
        }
        'BCC' {
            foreach ($item in $BCC) {
                $Query.add('bcc:{0}' -f $item)
            }
        }
        'Before' {
            $Query.add('before:{0}' -f (Get-Date $Before -UFormat '%s'))
        }
        'After' {
            $Query.add('after:{0}' -f (Get-Date $After -UFormat '%s'))
        }
    }

    $Params = @{
        Method = 'GET'
        Endpoint = 'conversations'
        Path = 'search/{0}' -f [String]::Join(' ', $Query)
        ApiKey = $ApiKey
    }

    try {
        InvokeFrontRestMethod @Params
    }
    catch {
        Write-Error -ErrorRecord $_
    }
}

function Get-FrontConversation {
    <#
    .SYNOPSIS
        Short description
    .DESCRIPTION
        Long description
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    #>

    param (
        [Parameter(Mandatory)]
        [String]$ConversationId,
        
        [Parameter(Mandatory)]
        [SecureString]$ApiKey
    )

    $Params = @{
        Method   = "GET"
        Endpoint = "conversations"
        Path     = $ConversationId
        ApiKey   = $ApiKey
    }
    
    try {
        InvokeFrontRestMethod @Params
    }
    catch {
        Write-Error -ErrorRecord $_
    }
}

function Get-FrontConversationMessages {
    <#
    .SYNOPSIS
        Short description
    .DESCRIPTION
        Long description
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    #>

    param (
        [Parameter(Mandatory)]
        [String]$ConversationId,

        [Parameter(Mandatory)]
        [SecureString]$ApiKey
    )

    $Params = @{
        Method   = "GET"
        Endpoint = "conversations"
        Path     = "{0}/messages" -f $ConversationId
        ApiKey   = $ApiKey
    }
    
    try {
        InvokeFrontRestMethod @Params
    }
    catch {
        Write-Error -ErrorRecord $_
    }
}

function Get-FrontInbox {
    <#
    .SYNOPSIS
        Short description
    .DESCRIPTION
        Long description
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    #>

    param (
        [Parameter()]
        [String]$Id,

        [Parameter(Mandatory)]
        [SecureString]$ApiKey
    )

    $Params = @{
        Method   = "GET"
        Endpoint = "inboxes"
        ApiKey   = $ApiKey
    }

    if ($PSBoundParameters.ContainsKey("Id")) {
        $Params["Path"] = $Id
    }

    try {
        InvokeFrontRestMethod @Params
    }
    catch {
        Write-Error -ErrorRecord $_
    }
}

function Remove-FrontConversationTag {
    <#
    .SYNOPSIS
        Short description
    .DESCRIPTION
        Long description
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = "High")]
    param (
        [Parameter(Mandatory)]
        [String]$ConversationId,

        [Parameter(Mandatory)]
        [String[]]$TagId,

        [Parameter(Mandatory)]
        [SecureString]$ApiKey
    )

    $Params = @{
        Method = "DELETE"
        Endpoint = "conversations"
        Path = "{0}/tags" -f $ConversationId
        Body = @{
            "tag_ids" = @($TagId)
        }
        ApiKey = $ApiKey
    }

    try {
        if ($PSCmdlet.ShouldProcess(
            ('Would remove tag ID(s) "{0}" to conversation "{1}"' -f [String]::Join('", "', $TagId), $ConversationId),
            "Are you sure you want to continue?",
            ('Removing tag ID(s) "{0}" to conversation "{1}"' -f [String]::Join('", "', $TagId), $ConversationId))) {
                InvokeFrontRestMethod @Params
            }
    }
    catch {
        Write-Error -ErrorRecord $_
    }
}

#TODO Need to do more here than just tags

function Update-FrontConversation {
    <#
    .SYNOPSIS
        Short description
    .DESCRIPTION
        Long description
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    #>

    param (
        [Parameter(Mandatory)]
        [String]$ConversationId,

        [Parameter(Mandatory)]
        [String[]]$TagId,

        [Parameter(Mandatory)]
        [SecureString]$ApiKey
    )

    $Params = @{
        Method = "PATCH"
        Endpoint = "conversations"
        Path = $ConversationId
        Body = @{
            "tag_ids" = @($TagId)
        }
        ApiKey = $ApiKey
    }

    try {
        InvokeFrontRestMethod @Params
    }
    catch {
        Write-Error -ErrorRecord $_
    }
}

function Get-FrontMessageTemplateFolder {
    <#
    .SYNOPSIS
        Short description
    .DESCRIPTION
        Long description
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    #>

    param (
        [Parameter()]
        [String]$Id,

        [Parameter(Mandatory)]
        [SecureString]$ApiKey
    )

    $Params = @{
        Method   = "GET"
        Endpoint = "message_template_folders"
        ApiKey   = $ApiKey
    }

    if ($PSBoundParameters.ContainsKey("Id")) {
        $Params["Path"] = $Id
    }

    InvokeFrontRestMethod @Params
}

function Get-FrontTag {
    <#
    .SYNOPSIS
        Short description
    .DESCRIPTION
        Long description
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    #>

    param (
        [Parameter()]
        [String]$Id,

        [Parameter(Mandatory)]
        [SecureString]$ApiKey
    )

    $Params = @{
        Method   = "GET"
        Endpoint = "tags"
        ApiKey   = $ApiKey
    }

    if ($PSBoundParameters.ContainsKey("Id")) {
        $Params["Path"] = $Id
    }
    
    try {
        InvokeFrontRestMethod @Params
    }
    catch {
        Write-Error -ErrorRecord $_
    }
}
#endregion

#region Types functions
#endregion