PoshBot.GChat.Backend.psm1

using module PoshBot

enum GChatMessageType {
    MESSAGE
    ADDED_TO_SPACE
    REMOVED_FROM_SPACE
    CARD_CLICKED
}

enum GChatMessageSubtype {
    None
    ChannelJoined
    ChannelLeft
    CardClicked
}

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Scope='Class', Target='*')]
class GChatBackend : Backend {

    # The types of message that we care about from GChat
    # All othere will be ignored
    [string[]]$MessageTypes = @(
        'MESSAGE'
        'REMOVED_FROM_SPACE'
        'ADDED_TO_SPACE'
        'CARD_CLICKED'
    ) 

    [int]$MaxMessageLength = 4000

    GChatBackend ([string]$ConfigName,[string]$SheetId,[string]$SheetName,[int]$PollingFrequency) {
        if (!(Get-Module PSGSuite)) {
            Import-Module PSGSuite -MinimumVersion "2.13.0" -Verbose:$false -Force
        }
        if ((Show-PSGSuiteConfig).ConfigName -ne $ConfigName) {
            Switch-PSGSuiteConfig -ConfigName $ConfigName
        }
        $config = [ConnectionConfig]::new()
        $config.Credential = New-Object System.Management.Automation.PSCredential($ConfigName,(ConvertTo-SecureString -String $SheetName -AsPlainText -Force))
        $config.Endpoint = $SheetId
        $conn = [GChatConnection]::New($ConfigName,$SheetId,$SheetName,$PollingFrequency)
        $conn.Config = $config
        $this.Connection = $conn
    }

    # Connect to GChat
    [void]Connect() {
        $this.LogInfo('Connecting to backend')
        $this.LogInfo('Listening for the following message types. All others will be ignored', $this.MessageTypes)
        $this.Connection.Connect()
        $this.BotId = $this.GetBotIdentity()
        $this.LoadRooms()
        $this.LoadUsers()
    }

    # Receive a message from the websocket
    [Message[]]ReceiveMessage() {
        $messages = New-Object -TypeName System.Collections.ArrayList
        try {
            # Read the output stream from the receive job and get any messages since our last read
            $jsonResult = $this.Connection.ReadReceiveJob()

            if ($null -ne $jsonResult -and $jsonResult -ne [string]::Empty) {
                #Write-Debug -Message "[GChatBackend:ReceiveMessage] Received `n$jsonResult"
                $this.LogDebug('Received message', $jsonResult)

                $gChatMessages = @($jsonResult | ConvertFrom-Json)
                foreach ($gChatMessage in $gChatMessages) {
                    $gChatEvent = ConvertFrom-Json $gChatMessage.Event

                    # We only care about certain message types from GChat
                    if ($gChatEvent.type -in $this.MessageTypes) {
                        $msg = [Message]::new()

                        # Set the message type and optionally the subtype
                        #$msg.Type = $gChatEvent.type
                        $this.LogVerbose("New [$($gChatEvent.type)] Chat event received")
                        switch ($gChatEvent.type) {
                            'ADDED_TO_SPACE' {
                                $msg.Type = [MessageType]::Message
                                $msg.SubType = [MessageSubType]::ChannelJoined
                                $msg.To = $gChatEvent.space.name
                                $msg.ToName = $gChatEvent.space.displayName
                                $msg.Text = ($gChatMessage.Event -join '')
                            }
                            'REMOVED_FROM_SPACE' {
                                $msg.Type = [MessageType]::Message
                                $msg.SubType = [MessageSubType]::ChannelLeft
                                $msg.Text = ($gChatMessage.Event -join '')
                            }
                            'MESSAGE' {
                                $msg.Type = [MessageType]::Message
                                $msg.From = $gChatEvent.message.sender.name -replace "users\/",""
                                $msg.FromName = $gChatEvent.message.sender.displayName
                                $msg.To = $gChatEvent.message.thread.name
                                $msg.ToName = $gChatEvent.message.space.displayName
                                $msg.Text = $gChatEvent.message.argumentText.Trim().Replace(' ',' ').Replace(' ',' ')
                                $msg.Id = $gChatEvent.message.name
                                if ($gChatEvent.space.type -eq 'DM') {
                                    $this.LogDebug("MESSAGE is a DM!")
                                    $msg.IsDM = $true
                                    $msg.ToName = "@$($gChatEvent.user.displayName)"
                                }
                            }
                            'CARD_CLICKED' {
                                $msg.Type = [MessageType]::PresenceChange # Hack for now since Google Chat doesn't support presence change and CardClicked is not currently available as a message type in PoshBot. This maps to CARD_CLICKED events sent from Google Chat only!
                                $msg.From = $gChatEvent.user.name
                                $msg.FromName = $gChatEvent.user.displayName
                                $msg.To = $gChatEvent.message.name
                                $msg.ToName = $gChatEvent.message.space.displayName
                                $msg.Text = ($gChatMessage.Event -join '')
                                $msg.Id = $gChatEvent.message.name
                                if ($gChatEvent.space.type -eq 'DM') {
                                    $this.LogDebug("CARD_CLICKED event is a DM!")
                                    $msg.IsDM = $true
                                    $msg.ToName = "@$($gChatEvent.user.displayName)"
                                }
                            }
                        }

                        $this.LogDebug("Message type is [$($msg.Type)`:$($msg.Subtype)] :: From [$($msg.FromName)`:$($msg.From)] :: To [$($msg.ToName)`:$($msg.To)]")

                        $msg.RawMessage = $gChatMessage
                        $this.LogDebug('Raw message', $gChatMessage)
                        # Get time of message
                        $unixEpoch = [datetime]'1970-01-01T00:00:00.0000Z'
                        $msg.Time = if ($gChatEvent.eventTime.seconds) {
                            $unixEpoch.AddSeconds($gChatEvent.eventTime.seconds)
                        }
                        else {
                            (Get-Date).ToUniversalTime()
                        }
                        if ($gChatEvent.type -eq 'REMOVED_FROM_SPACE') {
                            $messages.Add($msg) | Out-Null
                            $this.LoadRooms()
                        }
                        elseif ($gChatEvent.type -eq 'ADDED_TO_SPACE') {
                            $messages.Add($msg) | Out-Null
                            $this.LoadRooms()
                        }
                        else {
                            $messages.Add($msg) | Out-Null
                        }
                    } 
                    else {
                        $this.LogDebug("Message type is [$($gChatEvent.type)]. Ignoring and marking as complete")
                        $fullSheet = Import-GSSheet -SpreadsheetId $this.SheetId -SheetName $this.SheetName -Range "A1:D" -ErrorAction Stop
                        $fullSheetCount = if (!$fullSheet.Count) {
                            1
                        }
                        else {
                            $fullSheet.Count
                        }
                        for ($i = 0; $i -lt $fullSheetCount; $i++) {
                            if ($fullSheet[$i].Id -eq $gChatMessage.Id) {
                                break
                            }
                        }
                        $rowId = $i + 2
                        Export-GSSheet -SpreadsheetId $this.SheetId -Value "Yes" -SheetName $this.SheetName -Range "C$($rowId)" -ErrorAction Stop | Out-Null
                    }
                }
            }
        }
        catch {
            Write-Error $_
        }
        return $messages
    }

    # Send a GChat ping - (not really needed for this implementation)
    [void]Ping() { }

    # Send a message back to GChat
    [void]SendMessage([Response]$Response) {
            if ((Show-PSGSuiteConfig).ConfigName -ne $this.Connection.ConfigName) {
                Switch-PSGSuiteConfig $this.Connection.ConfigName -Verbose
            }
            # Process any custom responses
            $this.LogVerbose("[$($Response.Data.Count)] custom responses and [$($Response.Text.Count)] text responses")
            $this.LogVerbose("Message Details :: [ConfigName:$($this.Connection.ConfigName) | SheetId:$($this.Connection.SheetId) | SheetName:$($this.Connection.SheetName) | PollingFrequency:$($this.Connection.PollingFrequency)]")
            foreach ($customResponse in $Response.Data) {

                [string]$sendTo = $Response.To
                if ($customResponse.DM) {
                    $rawMessageType = (ConvertFrom-Json $Response.OriginalMessage.RawMessage.Event).space.type
                    if ($rawMessageType -ne 'DM') {
                        $this.LogVerbose("Response is [DM] and original message space type is [$($rawMessageType)] - parsing UserID to DM Name")
                        $sendToHash = $this.UserIdToDMName("users/$($Response.MessageFrom)")
                        if ($sendToHash.ContainsKey('name')) {
                            $sendTo = $sendToHash['name']
                            $this.LogVerbose("UserID [$($Response.MessageFrom)] successfully parsed to DM Name [$sendTo]")
                            $respText = "<users/$($Response.MessageFrom)> The information you requested has been sent to you via Direct Message. Thank you!"
                            Send-GSChatMessage -Text $respText -Thread $Response.To -Parent "$($Response.To.Split("/")[0..1] -join "/")"
                        }
                        else {
                            $respText = "<users/$($Response.MessageFrom)> Your request was received, but the information requested is only available to be sent via Direct Message. Please open a Direct Message with me first then submit your command again. Thank you!"
                            Send-GSChatMessage -Text $respText -Thread $Response.To -Parent "$($Response.To.Split("/")[0..1] -join "/")"
                            break
                        }
                    }
                    else {
                        $this.LogVerbose("Response is [DM] and original message space type is [$($rawMessageType)] - no need to parse DM Name")
                    }
                }
                
                switch -Regex ($customResponse.PSObject.TypeNames[0]) {
                    '(.*?)PoshBot\.Card\.Response' {
                        $this.LogVerbose("Custom response is [$($customResponse.PSObject.TypeNames[0])]")
                        $sendParams = @{}
                        $fbText = ''
                        if ($customResponse.CustomData) {
                            $this.LogVerbose("The response includes CustomData! Parsing...")
                            $deserializedItem = try {
                                [System.Management.Automation.PSSerializer]::Deserialize($customResponse.CustomData)
                                $this.LogVerbose("CardResponse::CustomData :: Type [$($customResponse.CustomData.PSObject.TypeNames[0])] :: Succesfully deserialized")
                            }
                            catch {
                                try {
                                    if ($customResponse.CustomData -is [System.Collections.Hashtable] -or $customResponse.CustomData -is [System.Management.Automation.PSCustomObject]) {
                                        $this.LogVerbose("CardResponse::CustomData :: Type [$($customResponse.CustomData.PSObject.TypeNames[0])] :: Item is already correct type")
                                        $customResponse.CustomData
                                    }
                                    elseif ($jsonConvert = ConvertFrom-Json $customResponse.CustomData) {
                                        $this.LogVerbose("CardResponse::CustomData :: Type [$($customResponse.CustomData.PSObject.TypeNames[0])] :: Item is a JSON string, returning converted object")
                                        $jsonConvert
                                    }
                                    else {
                                        $null
                                    }
                                }
                                catch {
                                    $null
                                }
                            }
                            if ($deserializedItem.token -and $deserializedItem.body) {
                                $this.LogVerbose("Deserialized Body", $deserializedItem.body)
                                $this.LogVerbose("Deserialized Token Present", $($null -ne $deserializedItem.token))
                                $deserBody = ConvertTo-Json -InputObject $deserializedItem.body -Depth 20
                                $restParams = @{
                                    ContentType = 'application/json'
                                    Verbose = $false
                                    Headers = @{
                                        Authorization = "Bearer $($deserializedItem.token)"
                                    }
                                    Body = $deserBody
                                }
                                if ($sendTo -like "spaces/*/messages/*") {
                                    $this.LogVerbose("Updating parsed message [$sendTo]")
                                    $updateMask = @()
                                    if ($deserializedItem.body.text) {
                                        $updateMask += 'text'
                                    }
                                    if ($deserializedItem.body.cards) {
                                        $updateMask += 'cards'
                                    }
                                    $restParams['Uri'] = ([Uri]"https://chat.googleapis.com/v1/$($sendTo)?updateMask=$($updateMask -join ',')")
                                    $restParams['Method'] = 'Put'
                                }
                                elseif ($sendTo -like "spaces/*/threads/*") {
                                    $this.LogVerbose("Sending parsed response to thread [$sendTo]")
                                    $deserializedItem.body | Add-Member -MemberType NoteProperty -Name thread -Value $(@{
                                        name = $sendTo
                                    }) -Force
                                    $newDeserBody = ConvertTo-Json -InputObject $deserializedItem.body -Depth 20
                                    $restParams['Body'] = $newDeserBody
                                    $updatedUri = "https://chat.googleapis.com/v1/$($sendTo.Split("/")[0..1] -join "/")/messages"
                                    $restParams['Uri'] = ([Uri]$updatedUri)
                                    $restParams['Method'] = 'Post'
                                }
                                else {
                                    $this.LogVerbose("Sending parsed message to space [$sendTo]")
                                    $restParams['Uri'] = ([Uri]"https://chat.googleapis.com/v1/$($sendTo)/messages")
                                    $restParams['Method'] = 'Post'
                                }
                                Invoke-RestMethod @restParams
                            }
                            else {
                                $this.LogInfo([LogSeverity]::Warning, "Unable to parse Card's CustomData as a GChat response and token! SKIPPING", $customResponse.CustomData)
                            }
                        }
                        else {
                            $this.LogVerbose("The response DOES NOT include CustomData! Parsing PoshBot CardResponse to Google Chat Card object...")
                            $widgets = @()
                            $cardParams = @{}
                            if (-not [string]::IsNullOrEmpty($customResponse.Text)) {
                                $this.LogDebug("Response size [$($customResponse.Text.Length)]")
                                $formattedText = if ($customResponse.LinkUrl) {
                                    "<$($customResponse.LinkUrl)|$($customResponse.Text)>"
                                }
                                else {
                                    $customResponse.Text
                                }
                                $sendParams.Text = $formattedText
                                $fbText = $customResponse.Text
                            }
                            elseif ($customResponse.LinkUrl) {
                                $sendParams.Text = "<$($customResponse.LinkUrl)|View Details>"
                                $fbText = $customResponse.LinkUrl
                            }
                            $sendParams.FallbackText = $fbText
                            if ($customResponse.Title) {
                                $cardParams.HeaderTitle = $customResponse.Title
                            }
                            if ($customResponse.ThumbnailUrl) {
                                $cardParams.HeaderImageUrl = $customResponse.ThumbnailUrl
                                $cardParams.HeaderImageStyle = 'AVATAR'
                            }
                            if ($customResponse.Fields) {
                                $widgets += foreach ($key in $customResponse.Fields.Keys) {
                                    Add-GSChatKeyValue -TopLabel $key -Content $customResponse.Fields[$key]
                                }
                            }
                            if ($customResponse.ImageUrl) {
                                $widgets += Add-GSChatImage -ImageUrl $customResponse.ImageUrl -LinkImage
                            }
                            if ($widgets) {
                                $cardParams.MessageSegment = $widgets
                            }
                            if ($cardParams.Keys.Count) {
                                $card = Add-GSChatCard @cardParams
                                $sendParams.MessageSegment = $card
                            }
                            if ($sendTo -like "spaces/*/messages/*") {
                                $this.LogVerbose("Updating message [$sendTo]", $sendParams)
                                try {
                                    Update-GSChatMessage @sendParams -MessageId $sendTo -Verbose:$false -ErrorAction Stop
                                }
                                catch {
                                    $this.LogInfo([LogSeverity]::Error, $_.Exception.Message, $_)
                                }
                            }
                            elseif ($sendTo -like "spaces/*/threads/*") {
                                $this.LogVerbose("Sending response to thread [$sendTo]", $sendParams)
                                try {
                                    Send-GSChatMessage @sendParams -Thread $sendTo -Parent $($sendTo.Split("/")[0..1] -join "/") -Verbose:$false -ErrorAction Stop
                                }
                                catch {
                                    $this.LogInfo([LogSeverity]::Error, $_.Exception.Message, $_)
                                }
                            }
                            else {
                                $this.LogVerbose("Sending message to space [$sendTo]", $sendParams)
                                try {
                                    Send-GSChatMessage @sendParams -Parent $sendTo -Verbose:$false -ErrorAction Stop
                                }
                                catch {
                                    $this.LogInfo([LogSeverity]::Error, $_.Exception.Message, $_)
                                }
                            }
                        }
                        break
                    }
                    '(.*?)PoshBot\.Text\.Response' {
                        $this.LogVerbose("Custom response is [$($customResponse.PSObject.TypeNames[0])]")
                        $chunks = $this._ChunkString($customResponse.Text)
                        $i = 0
                        foreach ($chunk in $chunks) {
                            $t = if ($customResponse.AsCode) {
                                '```' + $chunk + '```'
                            } else {
                                $chunk
                            }
                            if ($sendTo -like "spaces/*/messages/*") {
                                $this.LogDebug("Updating message [$sendTo]", $t)
                                Update-GSChatMessage -MessageId $sendTo -Text $t -UpdateMask text -Verbose:$false
                            }
                            elseif ($sendTo -like "spaces/*/threads/*") {
                                $this.LogDebug("Sending response to thread [$sendTo]", $t)
                                Send-GSChatMessage -Text $t -Thread $sendTo -Parent $($sendTo.Split("/")[0..1] -join "/") -Verbose:$false
                            }
                            else {
                                $this.LogDebug("Sending message to space [$sendTo]", $t)
                                Send-GSChatMessage -Text $t -Parent $sendTo -Verbose:$false
                            }
                            $i++
                        }
                        break
                    }
                    '(.*?)PoshBot\.File\.Upload' {
                        $this.LogInfo([LogSeverity]::Error, "Custom response is [$($customResponse.PSObject.TypeNames[0])]. Google Chat does not currently support File Upload via API/SDK call.")
                        # TODO: Must build out once Google Chat supports it.
                        break
                    }
                    default {
                        $this.LogVerbose("Custom response is [$($customResponse.PSObject.TypeNames[0])]")
                    }
                }
            }
            if ($Response.Text.Count -gt 0) {
                [string]$sendTo = $Response.To
                if ($customResponse.DM) {
                    $sendToHash = "$($this.UserIdToDMName($Response.MessageFrom))"
                    if ($sendToHash.ContainsKey('name')) {
                        $sendTo = $sendToHash['name']
                    }
                }
                $i = 0
                $total = $Response.Text.Count
                foreach ($item in $Response.Text) {
                    $i++
                    $deserializedItem = try {
                        [System.Management.Automation.PSSerializer]::Deserialize($item)
                        $this.LogVerbose("Text Item [$i/$total] :: Type [$($item.PSObject.TypeNames[0])] :: Succesfully deserialized")
                    }
                    catch {
                        try {
                            if ($item -is [System.Collections.Hashtable] -or $item -is [System.Management.Automation.PSCustomObject]) {
                                $this.LogVerbose("Text Item [$i/$total] :: Type [$($item.PSObject.TypeNames[0])] :: Item is already correct type")
                                $item
                            }
                            elseif ($jsonConvert = ConvertFrom-Json $item) {
                                $this.LogVerbose("Text Item [$i/$total] :: Type [$($item.PSObject.TypeNames[0])] :: Item is a JSON string, returning converted object")
                                $jsonConvert
                            }
                            else {
                                $null
                            }
                        }
                        catch {
                            $null
                        }
                    }
                    if ($deserializedItem.token -and $deserializedItem.body) {
                        $this.LogVerbose("Deserialized Body", $deserializedItem.body)
                        $this.LogVerbose("Deserialized Token Present", $($null -ne $deserializedItem.token))
                        $deserBody = ConvertTo-Json -InputObject $deserializedItem.body -Depth 20
                        $restParams = @{
                            ContentType = 'application/json'
                            Verbose = $false
                            Headers = @{
                                Authorization = "Bearer $($deserializedItem.token)"
                            }
                            Body = $deserBody
                        }
                        if ($sendTo -like "spaces/*/messages/*") {
                            $this.LogVerbose("Updating parsed message [$sendTo]")
                            $updateMask = @()
                            if ($deserializedItem.body.text) {
                                $updateMask += 'text'
                            }
                            if ($deserializedItem.body.cards) {
                                $updateMask += 'cards'
                            }
                            $restParams['Uri'] = ([Uri]"https://chat.googleapis.com/v1/$($sendTo)?updateMask=$($updateMask -join ',')")
                            $restParams['Method'] = 'Put'
                            Invoke-RestMethod @restParams
                        }
                        elseif ($sendTo -like "spaces/*/threads/*") {
                            $this.LogVerbose("Sending parsed response to thread [$sendTo]")
                            $deserializedItem.body | Add-Member -MemberType NoteProperty -Name thread -Value $(@{
                                name = $sendTo
                            }) -Force
                            $newDeserBody = ConvertTo-Json -InputObject $deserializedItem.body -Depth 20
                            $restParams['Body'] = $newDeserBody
                            $updatedUri = "https://chat.googleapis.com/v1/$($sendTo.Split("/")[0..1] -join "/")/messages"
                            $restParams['Uri'] = ([Uri]$updatedUri)
                            $restParams['Method'] = 'Post'
                            Invoke-RestMethod @restParams
                        }
                        else {
                            $this.LogVerbose("Sending parsed message to space [$sendTo]")
                            $restParams['Uri'] = ([Uri]"https://chat.googleapis.com/v1/$($sendTo)/messages")
                            $restParams['Method'] = 'Post'
                            Invoke-RestMethod @restParams
                        }
                    }
                    else {
                        $chunks = $this._ChunkString($item)
                        foreach ($t in $chunks) {
                            $this.LogDebug("Sending response back to GChat channel [$($Response.To)]", $t)
                            if ($Response.To -like "spaces/*/messages/*") {
                                $this.LogDebug("Updating message [$($Response.To)]", $t)
                                Update-GSChatMessage -MessageId $Response.To -Text $t -UpdateMask text -Verbose:$false
                            }
                            elseif ($Response.To -like "spaces/*/threads/*") {
                                $this.LogDebug("Sending response to thread [$($Response.To)]", $t)
                                Send-GSChatMessage -Text $t -Thread $Response.To -Parent $($Response.To.Split("/")[0..1] -join "/") -Verbose:$false
                            }
                            else {
                                $this.LogDebug("Sending message to space [$($Response.To)]", $t)
                                Send-GSChatMessage -Text $t -Parent $Response.To -Verbose:$false
                            }
                        }
                    }
                }
            }
    }

    # Add a reaction to an existing chat message
    [void]AddReaction([Message]$Message, [ReactionType]$Type, [string]$Reaction) {
        $this.LogDebug("Reactions are not yet supported in Google Chat - Ignoring")
        # TODO: Must build out once Google Chat supports it.
    }

    # Remove a reaction from an existing chat message
    [void]RemoveReaction([Message]$Message, [ReactionType]$Type, [string]$Reaction) {
        $this.LogDebug("Reactions are not yet supported in Google Chat - Ignoring")
        # TODO: Must build out once Google Chat supports it.
    }

    # Resolve a channel name to an Id
    [string]ResolveChannelId([string]$ChannelName) {
        if ($ChannelName -match '^#') {
            $ChannelName = $ChannelName.TrimStart('#')
        }
        $channelId = ($this.Connection.LoginData.channels | Where-Object name -eq $ChannelName).id
        if (-not $ChannelId) {
            $channelId = ($this.Connection.LoginData.channels | Where-Object id -eq $ChannelName).id
        }
        $this.LogDebug("Resolved channel [$ChannelName] to [$channelId]")
        return $channelId
    }

    # Populate the list of users the GChat team
    [void]LoadUsers() {
        $this.LogVerbose('Getting Google Chat users')
        $allUsers = Get-GSUser -Filter "isSuspended -eq '$false' changePasswordAtNextLogin -eq '$false'" -Verbose:$false
        $this.LogVerbose("[$($allUsers.Count)] users returned")
        $allUsers | ForEach-Object {
            $user = [GChatPerson]::new()
            $user.Id = "users/$($_.Id)"
            $user.NickName = $_.Name.FullName
            $user.FullName = $_.Name.FullName
            $user.FirstName = $_.Name.GivenName
            $user.LastName = $_.Name.FamilyName
            $user.Email = $_.PrimaryEmail
            $user.Phones = $_.Phones
            $user.IsAdmin = $_.IsAdmin
            $user.IsDelegatedAdmin = $_.IsDelegatedAdmin
            $user.IsEnforcedIn2Sv = $_.IsEnforcedIn2Sv
            $user.IsEnrolledIn2Sv = $_.IsEnrolledIn2Sv
            $user.OrgUnitPath = $_.OrgUnitPath
            $user.CreationTimeRaw = $_.CreationTimeRaw
            $user.CreationTime = $_.CreationTime
            $user.LastLoginTimeRaw = $_.LastLoginTimeRaw
            $user.LastLoginTime = $_.LastLoginTime
            $user.ThumbnailPhotoUrl = $_.ThumbnailPhotoUrl
            if (-not $this.Users.ContainsKey("users/$($_.Id)")) {
                $this.LogDebug("Adding user [users/$($_.Id):$($_.Name.FullName)]")
                $this.Users["users/$($_.Id)"] =  $user
            }
        }

        foreach ($key in $this.Users.Keys | Where-Object {($_ -replace 'users\/','') -notin $allUsers.Id}) {
            $this.LogDebug("Removing outdated user [$key]")
            $this.Users.Remove($key)
        }
    }

    # Populate the list of channels in the GChat team
    [void]LoadRooms() {
        $this.LogVerbose('Getting Google Chat spaces')
        $allChannels = Get-GSChatSpace -Verbose:$false
        $this.LogVerbose("[$($allChannels.Count)] spaces returned")

        $allChannels | ForEach-Object {
            $channel = [GChatChannel]::new()
            $channel.Id = $_.Name
            if ($_.DisplayName) {
                $channel.Name = $_.DisplayName
            }
            else {
                $channel.Name = "DM"
            }
            $channel.Type = $_.Type
            $channelMembers = Get-GSChatMember -Space $_.Name -Verbose:$false
            $channel.MemberCount = $channelMembers.Count
            foreach ($member in $channelMembers) {
                $channel.Members.Add($member, $null)
            }
            $this.LogDebug("Adding space: $($_.DisplayName):$($_.Name)")
            $this.Rooms[$_.Name] = $channel
        }

        foreach ($key in $this.Rooms.Keys | Where-Object {$_ -notin $allChannels.Name}) {
            $this.LogDebug("Removing outdated channel [$key]")
            $this.Rooms.Remove($key)
        }
    }

    # Get the bot identity Id
    [string]GetBotIdentity() {
        $id = $this.Connection.LoginData.self.id
        $this.LogVerbose("Bot identity is [$id]")
        return $id
    }

    # Determine if incoming message was from the bot
    [bool]MsgFromBot([string]$From) {
        $frombot = ($this.BotId -eq $From)
        if ($fromBot) {
            $this.LogDebug("Message is from bot [From: $From == Bot: $($this.BotId)]. Ignoring")
        } else {
            $this.LogDebug("Message is not from bot [From: $From <> Bot: $($this.BotId)]")
        }
        return $fromBot
    }

    # Get a user by their Id
    [GChatPerson]GetUser([string]$UserId) {
        $user = $this.Users[$UserId]
        if (-not $user) {
            $this.LogDebug([LogSeverity]::Warning, "User [$UserId] not found. Refreshing users")
            $this.LoadUsers()
            $user = $this.Users[$UserId]
        }

        if ($user) {
            $this.LogDebug("Resolved user [$UserId]", $user)
        } else {
            $this.LogDebug([LogSeverity]::Warning, "Could not resolve user [$UserId]")
        }
        return $user
    }

    [hashtable]GetUserInfo([string]$UserId) {
        if ($UserId -notlike "users/*") {
            $UserId = "users/$UserId"
        }
        $user = $this.Users[$UserId]
        if (-not $user) {
            $this.LogDebug([LogSeverity]::Warning, "User [$UserId] not found. Refreshing users")
            $this.LoadUsers()
            $user = $this.Users[$UserId]
        }

        if ($user) {
            $this.LogDebug("Resolved user [$UserId]", $user)
            return $user.ToHash()
        } else {
            $this.LogDebug([LogSeverity]::Warning, "Could not resolve user [$UserId]")
            return $null
        }
    }

    # Get a user Id by their name
    [string]UsernameToUserId([string]$Username) {
        $Username = $Username.TrimStart('@')
        $user = $this.Users.Values | Where-Object {$_.Nickname -eq $Username -or $_.Email -eq $Username -or $_.FullName -eq $Username}
        $id = $null
        if ($user) {
            $id = $user.Id
        } else {
            # User each doesn't exist or is not in the local cache
            # Refresh it and try again
            $this.LogDebug([LogSeverity]::Warning, "User [$Username] not found. Refreshing users")
            $this.LoadUsers()
            $user = $this.Users.Values | Where-Object {$_.Nickname -eq $Username -or $_.Email -eq $Username -or $_.FullName -eq $Username}
            if (-not $user) {
                $id = $null
            } else {
                $id = $user.Id
            }
        }
        if ($id) {
            $this.LogDebug("Resolved [$Username] to [$id]")
        } else {
            $this.LogDebug([LogSeverity]::Warning, "Could not resolve user [$Username]")
        }
        return $id
    }

    # Get a user name by their Id
    [string]UserIdToUsername([string]$UserId) {
        $name = $null
        if ((Get-GSChatConfig).Spaces.ContainsKey("$UserId")) {
            $name = $this.Users[$UserId].Nickname
        } else {
            $this.LogDebug([LogSeverity]::Warning, "User [$UserId] not found. Refreshing users")
            $this.LoadUsers()
            $name = $this.Users[$UserId].Nickname
        }
        if ($name) {
            $this.LogDebug("Resolved [$UserId] to [$name]")
        } else {
            $this.LogDebug([LogSeverity]::Warning, "Could not resolve user [$UserId]")
        }
        return $name
    }

    # Get a user name by their Id
    [hashtable]UserIdToDMName([string]$UserId) {
        $hash = @{}
        if ((Get-GSChatConfig).Spaces.ContainsKey($UserId)) {
            $hash['name'] = (Get-GSChatConfig).Spaces[$UserId]
        }
        if ($hash.ContainsKey('name')) {
            $this.LogDebug("Resolved [$UserId] to DM name [$($hash['name'])]")
        } 
        else {
            $this.LogDebug([LogSeverity]::Warning, "Could not resolve user [$UserId] to a DM. Advising user to DM the bot to initialize the space first.")
        }
        return $hash
    }

    # Get the channel name by Id
    [string]ChannelIdToName([string]$ChannelId) {
        $name = $null
        if ($this.Rooms.ContainsKey($ChannelId)) {
            $name = $this.Rooms[$ChannelId].Name
        } else {
            $this.LogDebug([LogSeverity]::Warning, "Channel [$ChannelId] not found. Refreshing channels")
            $this.LoadRooms()
            $name = $this.Rooms[$ChannelId].Name
        }
        if ($name) {
            $this.LogDebug("Resolved [$ChannelId] to [$name]")
        } else {
            $this.LogDebug([LogSeverity]::Warning, "Could not resolve channel [$ChannelId]")
        }
        return $name
    }

    # Break apart a string by number of characters
    hidden [System.Collections.ArrayList] _ChunkString([string]$Text) {
        $chunks = [regex]::Split($Text, "(?<=\G.{$($this.MaxMessageLength)})", [System.Text.RegularExpressions.RegexOptions]::Singleline)
        $this.LogDebug("Split response into [$($chunks.Count)] chunks")
        return $chunks
    }

    # Resolve a reaction type to an emoji
    hidden [string]_ResolveEmoji([ReactionType]$Type) {
        $emoji = [string]::Empty
        Switch ($Type) {
            'Success'        { return 'white_check_mark' }
            'Failure'        { return 'exclamation' }
            'Processing'     { return 'gear' }
            'Warning'        { return 'warning' }
            'ApprovalNeeded' { return 'closed_lock_with_key'}
            'Cancelled'      { return 'no_entry_sign'}
            'Denied'         { return 'x'}
        }
        return $emoji
    }

    # Translate formatted @mentions like @bod@domain.com into @devblackops
    hidden [string]_ProcessMentions([string]$Text) {
        $processed = $Text

        $mentions = $processed | Select-String -Pattern '(@\S*|@\S*)' -AllMatches | ForEach-Object {
            $_.Matches | ForEach-Object {
                [pscustomobject]@{
                    FormattedId = $_.Value
                    UnformattedId = $_.Value.TrimStart('<@').TrimEnd('>')
                }
            }
        }
        $mentions | ForEach-Object {
            if ($name = $this.UsernameToUserId($_.UnformattedId)) {
                $processed = $processed -replace $_.FormattedId, "<users/$($name)>"
                $this.LogDebug($processed)
            } else {
                $this.LogDebug([LogSeverity]::Warning, "Unable to translate @mention [$($_.FormattedId)] into a username")
            }
        }

        return $processed
    }
}

class GChatChannel : Room {
    [string]$Id
    [string]$Name
    [string]$Type
    [int]$MemberCount
}

class GChatConnection : Connection {

    [string]$ConfigName
    [String]$SheetId
    [string]$SheetName
    [int]$PollingFrequency
    [bool]$Connected
    [object]$ReceiveJob = $null

    GChatConnection([string]$ConfigName,[string]$SheetId,[string]$SheetName,[int]$PollingFrequency) {
        if ((Show-PSGSuiteConfig).ConfigName -ne $ConfigName) {
            Switch-PSGSuiteConfig -ConfigName $ConfigName
        }
        $this.ConfigName = $ConfigName
        $this.SheetId = $SheetId
        $this.SheetName = $SheetName
        $this.PollingFrequency = $PollingFrequency
    }

    # Connect to GChat and start receiving messages
    [void]Connect() {
        if ($null -eq $this.ReceiveJob -or $this.ReceiveJob.State -ne 'Running') {
            $this.LogDebug('Connecting to Google Sheet MQ')
            #$this.TestConnect()
            $this.StartReceiveJob()
        } else {
            $this.LogDebug([LogSeverity]::Warning, 'Receive job is already running')
        }
    }

    # Log in to GChat with the bot token and get a URL to connect to via websockets
    [void]TestConnect() { 
        try {
            if (Import-GSSheet -SpreadsheetId $this.SheetId -SheetName $this.SheetName -Range "A1" -ErrorAction Stop) {
                $this.LogVerbose("Connection to Sheet validated!")
            }
            else {
                $this.LogInfo([LogSeverity]::Error, 'Failed to connect to Sheet!')
            }
        } 
        catch {
            $this.LogInfo([LogSeverity]::Error, 'Failed to connect to Sheet!')
            throw $_
        }
    }

    # Setup the websocket receive job
    [void]StartReceiveJob() {
        $recv = {
            [CmdLetBinding()]
            Param
            (
                [parameter(Mandatory = $true,Position = 0)]
                $ConfigName,
                [parameter(Mandatory = $true,Position = 1)]
                $SheetId,
                [parameter(Mandatory = $true,Position = 2)]
                $SheetName,
                [parameter(Mandatory = $true,Position = 3)]
                $PollingFrequency
            )
            if (!(Get-Module PSGSuite)) {
                Import-Module PSGSuite -MinimumVersion "2.13.0" -Verbose:$false -Force
            }
            if ((Show-PSGSuiteConfig).ConfigName -ne $ConfigName) {
                Switch-PSGSuiteConfig -ConfigName $ConfigName
            }
            # Connect to Google Sheet MQ
            Write-Warning "[GChatBackend:ReceiveJob] Connecting to Google Sheet MQ at [$($SheetId)::$($SheetName)]"

            # Receive messages and put on output stream so the backend can read them
            while ($true) {
                $completeCount = 0
                Write-Verbose "[GChatBackend:ReceiveJob] Polling Sheet MQ for new messages"
                try {
                    if ($fullSheet = Import-GSSheet -SpreadsheetId $SheetId -SheetName $SheetName -ErrorAction Stop) {
                        $fullSheetCount = if ($fullSheet.Id[0] -eq 'Event') {
                            0
                        }
                        elseif (!$fullSheet.Count) {
                            1
                        }
                        else {
                            $fullSheet.Count
                        }
                        Write-Verbose "[GChatBackend:ReceiveJob] Received [$fullSheetCount] new messages"
                        for ($i = 0; $i -lt $fullSheetCount; $i++) {
                            if ($fullSheet[$i].Acked -eq "No") {
                                $message = $fullSheet[$i]
                                    $msg = ConvertFrom-Json -InputObject $message.Event
                                    Write-Verbose "Message ID [$($message.Id)] received from [$($msg.user.email)] with text [$($msg.message.argumentText)]. Sending serialized event JSON to Output stream"
                                    ConvertTo-Json -InputObject $message -Depth 20
                                    $rowId = $i + 2 - $completeCount
                                    Export-GSSheet -SpreadsheetId $SheetId -Value "Yes" -SheetName $SheetName -Range "C$($rowId)" -ErrorAction Stop | Out-Null
                                    $completeCount++
                            }
                        }
                    }
                }
                catch {
                    Write-Warning $_
                }
                finally {
                    $sleepParams = @{}
                    if ($PollingFrequency -ge 1000) {
                        $sleepParams['Milliseconds'] = $PollingFrequency
                    }
                    else {
                        $sleepParams['Seconds'] = $PollingFrequency
                    }
                    Start-Sleep @sleepParams
                }
            }
        }
        try {
            $this.LogVerbose("Starting Google Sheet MQ receive job [ConfigName:$($this.ConfigName) | SheetId:$($this.SheetId) | SheetName:$($this.SheetName) | PollingFrequency:$($this.PollingFrequency)]")
            $this.ReceiveJob = Start-Job -Name ReceiveSheetMessages -ScriptBlock $recv -ArgumentList $this.ConfigName,$this.SheetId,$this.SheetName,$this.PollingFrequency -ErrorAction Stop -Verbose
            $this.Connected = $true
            $this.Status = [ConnectionStatus]::Connected
            $this.LogInfo("Started Google Sheet MQ receive job [$($this.ReceiveJob.Id)]")
        } catch {
            $this.LogInfo([LogSeverity]::Error, "$($_.Exception.Message)", [ExceptionFormatter]::Summarize($_))
        }
    }

    # Read all available data from the job
    [string]ReadReceiveJob() {
        # Read stream info from the job so we can log them
        $infoStream = $this.ReceiveJob.ChildJobs[0].Information.ReadAll()
        $warningStream = $this.ReceiveJob.ChildJobs[0].Warning.ReadAll()
        $errStream = $this.ReceiveJob.ChildJobs[0].Error.ReadAll()
        $verboseStream = $this.ReceiveJob.ChildJobs[0].Verbose.ReadAll()
        $debugStream = $this.ReceiveJob.ChildJobs[0].Debug.ReadAll()
        foreach ($item in $infoStream) {
            $this.LogInfo($item.ToString())
        }
        foreach ($item in $warningStream) {
            $this.LogInfo([LogSeverity]::Warning, $item.ToString())
        }
        foreach ($item in $errStream) {
            $this.LogInfo([LogSeverity]::Error, $item.ToString())
        }
        foreach ($item in $verboseStream) {
            $this.LogVerbose($item.ToString())
        }
        foreach ($item in $debugStream) {
            $this.LogVerbose($item.ToString())
        }

        # The receive job stopped for some reason. Reestablish the connection if the job isn't running
        if ($this.ReceiveJob.State -ne 'Running') {
            $this.LogInfo([LogSeverity]::Warning, "Receive job state is [$($this.ReceiveJob.State)]. Attempting to reconnect...")
            Start-Sleep -Seconds 5
            $this.Connect()
        }

        if ($this.ReceiveJob.HasMoreData) {
            return $this.ReceiveJob.ChildJobs[0].Output.ReadAll()
        } else {
            return $null
        }
    }

    # Stop the receive job
    [void]Disconnect() {
        $this.LogInfo('Closing connection')
        if ($this.ReceiveJob) {
            $this.LogInfo("Stopping receive job [$($this.ReceiveJob.Id)]")
            $this.ReceiveJob | Stop-Job -Confirm:$false -PassThru | Remove-Job -Force -ErrorAction SilentlyContinue
        }
        $this.Connected = $false
        $this.Status = [ConnectionStatus]::Disconnected
    }
}

class GChatMessage : Message {

    [GChatMessageType]$MessageType = [GChatMessageType]::MESSAGE

    GChatMessage(
        [string]$To,
        [string]$From,
        [string]$Body = [string]::Empty
    ) {
        $this.To = $To
        $this.From = $From
        $this.Body = $Body
    }
}

class GChatPerson : Person {
    [string]$Id
    [string]$FullName
    [string]$FirstName
    [string]$LastName
    [string]$Email
    [string]$Phones
    [bool]$IsAdmin
    [bool]$IsDelegatedAdmin
    [bool]$IsEnforcedIn2Sv
    [bool]$IsEnrolledIn2Sv
    [string]$OrgUnitPath
    [string]$CreationTimeRaw
    [datetime]$CreationTime
    [string]$LastLoginTimeRaw
    [datetime]$LastLoginTime
    [string]$ThumbnailPhotoUrl

    [hashtable]ToHash() {
        $hash = @{}
        $this | Get-Member -MemberType Property | Foreach-Object {
            $hash.Add($_.Name, $this.($_.name))
        }
        return $hash
    }
}
function New-PoshBotGChatBackend {
    <#
    .SYNOPSIS
    Create a new instance of a Google Chat backend

    .DESCRIPTION
    Create a new instance of a Google Chat backend

    .PARAMETER Configuration
    The hashtable containing backend-specific properties on how to create the Google Chat backend instance.

    .EXAMPLE
    PS C:\> $backendConfig = @{Name = 'PSGSuiteBot'; ConfigName = 'domain1'; SheetId = '1H7mJoKfE8BGRnOSEF893JK032olstpyjOjNcO5sK3mjg'; SheetName = 'Queue'; PollingFrequency = 5}
    PS C:\> $backend = New-PoshBotGChatBackend -Configuration $backendConfig

    Create a Google Chat backend using the specified values
    
    .INPUTS
    Hashtable
    
    .OUTPUTS
    GChatBackend
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Scope='Function', Target='*')]
    [cmdletbinding()]
    param(
        [parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('BackendConfiguration')]
        [hashtable[]]$Configuration
    )
    Process {
        foreach ($item in $Configuration) {
            if (-not $item.SheetId) {
                throw 'Configuration is missing [SheetId] parameter'
            } else {
                if (-not $item.ConfigName) {
                    $item['ConfigName'] = (Show-PSGSuiteConfig).ConfigName
                }
                if (-not $item.SheetName) {
                    $item['SheetName'] = 'Queue'
                }
                if (-not $item.PollingFrequency) {
                    $item['PollingFrequency'] = 1500
                }
                Write-Verbose "Creating new GChat backend instance:`n$(([PSCustomObject]$item | Format-List * | Out-String).Trim())"
                $backend = [GChatBackend]::new($item.ConfigName,$item.SheetId,$item.SheetName,$item.PollingFrequency)
                if ($item.Name) {
                    $backend.Name = $item.Name
                }
                $backend
            }
        }
    }
}

Export-ModuleMember -Function 'New-PoshBotGChatBackend'

function New-PoshBotGChatCardResponse {
    <#
    .SYNOPSIS
        Tells PoshBot to send a specially formatted response. Also includes pipeline support for Google Chat message segment and can be used as a direct swap-in for Send-GSChatMessage
    .DESCRIPTION
        Responses from PoshBot commands can either be plain text or formatted. Returning a response with New-PoshBotRepsonse will tell PoshBot
        to craft a specially formatted message when sending back to the chat network.
    .PARAMETER Type
        Specifies a preset color for the card response. If the [Color] parameter is specified as well, it will override this parameter.
        | Type | Color | Hex code |
        |---------|--------|----------|
        | Normal | Greed | #008000 |
        | Warning | Yellow | #FFA500 |
        | Error | Red | #FF0000 |
    .PARAMETER Text
        The text response from the command.
    .PARAMETER DM
        Tell PoshBot to redirect the response to a DM channel.
    .PARAMETER Title
        The title of the response. This will be the card title in chat networks like Slack.
    .PARAMETER ThumbnailUrl
        A URL to a thumbnail image to display in the card response.
    .PARAMETER ImageUrl
        A URL to an image to display in the card response.
    .PARAMETER LinkUrl
        Will turn the title into a hyperlink
    .PARAMETER Fields
        A hashtable to display as a table in the card response.
    .PARAMETER COLOR
        The hex color code to use for the card response. In Slack, this will be the color of the left border in the message attachment.
    .PARAMETER CustomData
        Any additional custom data you'd like to pass on. Useful for custom backends, in case you want to pass a specifically formatted response
        in the Data stream of the responses received by the backend. Any data sent here will be skipped by the built-in backends provided with PoshBot itself.
    .PARAMETER MessageSegment
        Google Chat message segments sent through the pipeline.
    .EXAMPLE
        Add-GSChatTextParagraph -Text "Guys...","We <b>NEED</b> to <i>stop</i> spending money on <b>chocolate</b>!" |
        Add-GSChatKeyValue -TopLabel "Chocolate Budget" -Content '$5.00' -Icon DOLLAR |
        Add-GSChatKeyValue -TopLabel "Actual Spending" -Content '$5,000,000!' -BottomLabel "WTF" -Icon AIRPLANE |
        Add-GSChatImage -ImageUrl "https://media.tenor.com/images/f78545a9b520ecf953578b4be220f26d/tenor.gif" -LinkImage |
        Add-GSChatCardSection -SectionHeader "Dollar bills, y'all" |
        Add-GSChatButton -Text "Launch nuke" -OnClick (Add-GSChatOnClick -ActionMethodName launchNuke -ActionParameters @{decryptCodes = $true;callUN = $true}) |
        Add-GSChatButton -Text "Unleash hounds" -OnClick (Add-GSChatOnClick -ActionMethodName unleashHounds) |
        Add-GSChatCardSection -SectionHeader "What should we do?" |
        Add-GSChatCardAction -ActionLabel "CardAction" -OnClick (Add-GSChatOnClick -Url "https://vaporshell.io/") |
        Add-GSChatCard -HeaderTitle "Makin' moves with" -HeaderSubtitle "DEM GOODIES" -OutVariable card |
        Add-GSChatTextParagraph -Text "This message sent by <b>PSGSuite</b>!" |
        Add-GSChatCardSection -SectionHeader "Additional Info" |
        New-PoshBotGChatCardResponse -Text "Budget Report" -DM

        Create a Google Chat card response
    .OUTPUTS
        PoshBotCardResponse
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Scope='Function', Target='*')]
    [cmdletbinding()]
    param(
        [ValidateSet('Normal', 'Warning', 'Error')]
        [string]
        $Type = 'Normal',
        [switch]
        $DM,
        [string]
        $Text = [string]::empty,
        [string]
        $Title,
        [ValidateScript({
            $uri = $null
            if ([system.uri]::TryCreate($_, [System.UriKind]::Absolute, [ref]$uri)) {
                return $true
            } else {
                $msg = 'ThumbnailUrl must be a valid URL'
                throw [System.Management.Automation.ValidationMetadataException]$msg
            }
        })]
        [string]
        $ThumbnailUrl,
        [ValidateScript({
            $uri = $null
            if ([system.uri]::TryCreate($_, [System.UriKind]::Absolute, [ref]$uri)) {
                return $true
            } else {
                $msg = 'ImageUrl must be a valid URL'
                throw [System.Management.Automation.ValidationMetadataException]$msg
            }
        })]
        [string]
        $ImageUrl,
        [ValidateScript({
            $uri = $null
            if ([system.uri]::TryCreate($_, [System.UriKind]::Absolute, [ref]$uri)) {
                return $true
            } else {
                $msg = 'LinkUrl must be a valid URL'
                throw [System.Management.Automation.ValidationMetadataException]$msg
            }
        })]
        [string]
        $LinkUrl,
        [hashtable]
        $Fields,
        [ValidateScript({
            if ($_ -match '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$') {
                return $true
            } else {
                $msg = 'Color but be a valid hexidecimal color code e.g. #008000'
                throw [System.Management.Automation.ValidationMetadataException]$msg
            }
        })]
        [string]
        $Color = '#D3D3D3',
        [object]
        $CustomData,
        [parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [Object[]]
        $MessageSegment
    )
    Begin {
        $sendParams = @{}
        $finalSegment = @()
        $response = [ordered]@{
            PSTypeName = 'PoshBot.Card.Response'
            Type = $Type
            Text = $Text.Trim()
            DM = $PSBoundParameters['DM']
        }
        foreach ($key in $PSBoundParameters.Keys) {
            switch -Regex ($key) {
                'Text' {
                    $sendParams.Text = $Text.Trim()
                }
                '(Title|ThumbnailUrl|ImageUrl|LinkUrl|Fields|CustomData)' {
                    $response.$key = $PSBoundParameters[$key]
                }
            }
        }
        if (!$PSBoundParameters['Color']) {
            $response.Color = $Color
        }
        else {
            switch ($Type) {
                'Normal' {
                    $response.Color = '#008000'
                }
                'Warning' {
                    $response.Color = '#FFA500'
                }
                'Error' {
                    $response.Color = '#FF0000'
                }
            }
        }
    }
    Process {
        foreach ($segment in $MessageSegment) {
            $finalSegment += $segment
        }
    }
    End {
        if ($finalSegment) {
            $json = $finalSegment | Send-GSChatMessage -BodyPassThru @sendParams
            $response.CustomData = $json
        }
        [pscustomobject]$response
    }
}

Export-ModuleMember -Function 'New-PoshBotGChatCardResponse'