EvotecIT / PSTeams

PSTeams is a PowerShell Module working on Windows / Linux and Mac. It allows sending notifications to Microsoft Teams via WebHook Notifications. It's pretty flexible and provides a bunch of options. Initially, it only supported one sort of Team Cards but since version 2.X.X it supports Adaptive Cards, Hero Cards, List Cards, and Thumbnail Cards. All those new cards have their own cmdlets and the old version of creating Teams Cards stays as-is for compatibility reasons.
MIT License
409 stars 41 forks source link

Add a function to convert a PSObject to Teams fact #15

Closed theramiyer closed 4 years ago

theramiyer commented 4 years ago

Thought it would help those who would like to convert a PSObject to a Teams fact. We had such a requirement at work, and I wrote this function for it.

PrzemyslawKlys commented 4 years ago

Thank you. I think that we should add support for Hashtable/OrderedDictionary to it as well.

Also, please fix $Array+= by changing it to GenericList -> https://evotec.xyz/powershell-few-tricks-about-hashtable-and-array-i-wish-i-knew-when-i-started/ - it's not best practice to use += anymore.

theramiyer commented 4 years ago

Ah, wish I'd known that before! Please see if this is OK. I tested it to be working as expected.

theramiyer commented 4 years ago

We'll work on adding hashtable/OD support as well, in the future?

PrzemyslawKlys commented 4 years ago

The way you fixed it doesn't make sense. If you use Begin/Process/End you need to use GenericList. If you don't use Begin/Process/End you can use it that way. It's mostly related whether you planned on using pipeline, as with current approach if you pass $ArrayOfObjects | YourFunction it will overwrite content as far as I read the code.

PrzemyslawKlys commented 4 years ago

Ah I see you fixed the code. It's only in process now. So that should work.

PrzemyslawKlys commented 4 years ago

As for OD/Hashtable I would say do it now? The difference is very minimal - you would need to check if (Object -is [Type]) or similar. But you also need to support non-pipeline. RIght now only pipeline is supported as far as I can see? Are you good with that change or do you need me to help you with that?

theramiyer commented 4 years ago

So, I wanted it to work with the pipeline. Otherwise the change is quite trivial like you mentioned. Do you think the pipeline method would not be used by many?

theramiyer commented 4 years ago

Right now both work; the pipeline as well as parameter input. But only PSObject is accepted. Probably I could add another parameter in a separate parameter set called InputHash or something, and handle it that way? Hold on. Lemme update the PR. See if that’s acceptable.

theramiyer commented 4 years ago

@PrzemyslawKlys I've made it this way now:

PSObject has been kept the default.

PrzemyslawKlys commented 4 years ago

Hi, sorry for the late response. I've been busy with other modules. coming but to this one...

Do you think it would be possible to cover both cases with one variable and work in the pipeline? Because I guess this is an issue that you're talking about? Or is there something preventing that? I guess it would work, but I haven't played a lot with pipeline support.

PrzemyslawKlys commented 4 years ago

If not, I'll be ok, with accepting this PR as is. Seems good to me :-)

theramiyer commented 4 years ago

Hi! Actually, I don't know why I didn't think of using the same variable and detecting the type. Let me look into it; I'll send an update to the PR. :)

theramiyer commented 4 years ago

Right, I remember now. I felt I would not be able to define its data type in the parameter section. Defining it as PSObject and accepting Hashtable input messed up the function. Not defining the datatype caused other issues. But I've tried a hack. Please let me know if this works. :smiley:

theramiyer commented 4 years ago

Oh, there seems to be an issue with the MacOS build. Something about the image not being available, I suppose.

PrzemyslawKlys commented 4 years ago

What if we do this?

function ConvertTo-TeamsFact {
    <#
    .SYNOPSIS
    Convert a PSCustomObject or a Hashtable to Teams facts.

    .DESCRIPTION
    Teams facts are name-value pairs. This module helps convert a PSObject or a Hashtable to Teams facts (only one level deep).

    .PARAMETER InputObject
    The Hashtable or PSObject that is output by another cmdlet.

    .EXAMPLE
    Get-ChildItem | Select-Object -First 1 | ConvertTo-TeamsFact
    .EXAMPLE
    @{ Product = 'Microsoft Teams'; Developer = 'Microsoft Corporation'; ReleaseYear = '2018' } | ConvertTo-TeamsFact

    .NOTES
    Ram Iyer (https://ramiyer.me)
    #>

    [CmdletBinding()]
    param (
        # The input object
        [Array][Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        $InputObject
    )

    begin { }
    process {
        foreach ($Object in $InputObject) {
            if ($Object -is [System.Collections.IDictionary]) {
                $Facts = foreach ($Key in $Object.Keys) {
                    New-TeamsFact -Name $Key -Value $Object.$Key
                }
            } elseif ($Object -is [psobject]) {
                $Facts = foreach ($Property in $Object.PsObject.Properties) {
                    New-TeamsFact -Name $Property.Name -Value $Property.Value
                }
            } else {
                # elseif (($Object -is [int]) -or ($Object -is [long]) -or ($Object -is [string]) -or ($Object -is [char]) -or ($Object -is [bool]) -or ($Object -is [byte]) -or ($Object -is [double]) -or ($Object -is [decimal]) -or ($Object -is [single]) -or ($Object -is [array]) -or ($Object -is [xml])) {
                Write-Error -Message 'The input is neither a PSObject nor a Hashtable. Operation aborted.' -Category InvalidData
                break
            }
            $Facts
        }
    }
}

@{ Product = 'Microsoft Teams'; Developer = 'Microsoft Corporation'; ReleaseYear = '2018' } | ConvertTo-TeamsFact
[PSCustomObject] @{ Product = 'Microsoft Teams'; Developer = 'Microsoft Corporation'; ReleaseYear = '2018' }, @{ Product = 'Microsoft Teams'; Developer = 'Microsoft Corporation'; ReleaseYear = '2018' } | ConvertTo-TeamsFact

$Tests = @(
    @{ Product = 'Microsoft Teams'; Developer = 'Microsoft Corporation'; ReleaseYear = '2018' }
    [PSCustomObject] @{ Product = 'Microsoft Teams'; Developer = 'Microsoft Corporation'; ReleaseYear = '2018' }
)
ConvertTo-TeamsFact -InputObject $Tests
PrzemyslawKlys commented 4 years ago

Actually instead of using hashtable you can use IDictionary which covers both $HashTable and OrderedDictionary.

PrzemyslawKlys commented 4 years ago

And can you please add those tests as part of testing in pester? As for the Mac failure it's because the image was depreciated. I'll fix this.

theramiyer commented 4 years ago

The problem with adding a catch-all else block is that PowerShell implicitly converts the string into PSObject. That's why I had to place the [PSObject] part in the end. I'll change hashtable to System.Collection.IDictionary, though.

Unfortunately I've never worked with Pester, but let me give it a shot. :)

theramiyer commented 4 years ago

Also, I feel it would be good if each record is presented as a section in the output. This way, the reader would know that each of these is a separate set of facts. Otherwise, something like this could happen:

Product: Microsoft Teams
Developer: Microsoft Corporation
Year: 2018
Product: Microsoft SQL Server
Developer: Microsoft Corporation
Year: 1992
etc.

Instead, if each of these comes as a separate section, it would be more distinguishable.

Product: Microsoft Teams
Developer: Microsoft Corporation
Year: 2018


Product: Microsoft SQL Server
Developer: Microsoft Corporation
Year: 1992


What do you think?

theramiyer commented 4 years ago

I'm not sure the Pester test I've written is good enough, but do let me know. This is my first ever. LOL.

theramiyer commented 4 years ago

Reverted the test. I'll try again.

theramiyer commented 4 years ago

Okay @PrzemyslawKlys, Pester tests are done as well. :)

PrzemyslawKlys commented 4 years ago

Looks great! Have you noticed what I did in my example in regards to type [Array] and how I process InputObject with foreach? This should allow for non-pipeline use of array of hashtables?

PrzemyslawKlys commented 4 years ago

Also for an additional module for testing if it's hashtable you can probably do $Variable.GetType() -Should ... don't think it's worth additional download.

PrzemyslawKlys commented 4 years ago

Also, I feel it would be good if each record is presented as a section in the output. This way, the reader would know that each of these is a separate set of facts. Otherwise, something like this could happen:

Product: Microsoft Teams Developer: Microsoft Corporation Year: 2018 Product: Microsoft SQL Server Developer: Microsoft Corporation Year: 1992 etc.

Instead, if each of these comes as a separate section, it would be more distinguishable.

Product: Microsoft Teams Developer: Microsoft Corporation Year: 2018

Product: Microsoft SQL Server Developer: Microsoft Corporation Year: 1992

What do you think?

Not sure what you mean? Like giving people a choice between output facts only or facts divided by sections? or what is your question?

theramiyer commented 4 years ago

Also for an additional module for testing if it's hashtable you can probably do $Variable.GetType() -Should ... don't think it's worth additional download.

Sure, I thought we could compare the entire hash table to ensure that only those values are being generated. Otherwise sometimes when you pass a string, you get multiple other elements in the output hashtable. Anyway, I'd implemented the type check using the following line, now I've changed it to not use an additional module:

$Output | Should -BeOfType 'System.Collections.Specialized.OrderedDictionary'
theramiyer commented 4 years ago

Also, I feel it would be good if each record is presented as a section in the output. This way, the reader would know that each of these is a separate set of facts. Otherwise, something like this could happen: Product: Microsoft Teams Developer: Microsoft Corporation Year: 2018 Product: Microsoft SQL Server Developer: Microsoft Corporation Year: 1992 etc. Instead, if each of these comes as a separate section, it would be more distinguishable. Product: Microsoft Teams Developer: Microsoft Corporation Year: 2018 Product: Microsoft SQL Server Developer: Microsoft Corporation Year: 1992 What do you think?

Not sure what you mean? Like giving people a choice between output facts only or facts divided by sections? or what is your question?

I see. Hold on, I'll send an update.

Actually, there is a new issue. I was trying to say that the input should not be an array. If the input is an array, it could cause confusion to the person reading the message---there would be clutter. For instance, this kind of output could confuse:

name                           Name
value                          LICENSE
type                           fact
name                           LastWriteTime
value                          03/05/2020 19:43:54
type                           fact
name                           Length
value                          1068
type                           fact
name                           Name
value                          PSTeams.AzurePipelines.yml
type                           fact
name                           LastWriteTime
value                          04/12/2020 10:38:30
type                           fact
name                           Length
value                          1188
type                           fact

I feel each set should be in a separate section, like this (just as a matter of better UX):

name                           Name
value                          LICENSE
type                           fact
name                           LastWriteTime
value                          03/05/2020 19:43:54
type                           fact
name                           Length
value                          1068
type                           fact
<section break>
name                           Name
value                          PSTeams.AzurePipelines.yml
type                           fact
name                           LastWriteTime
value                          04/12/2020 10:38:30
type                           fact
name                           Length
value                          1188
type                           fact

But perhaps we could handle it later. For now, I've implemented your suggestion so that the behaviour is the same regardless of pipeline or inline input. Thank you!

PrzemyslawKlys commented 4 years ago

But the output is not really going to be visible for the user? It's only visible if you output it to screen which is not supposed to happen.

If you mean what is displayed in Teams to separate one Hashtable from another hashtable separated with a section you probably need to create a separate cmdlet (or at least change the name) to something like ConvertTo-TeamsSectionFact or something with a switch "SeparateObjectsWithSections". I'm just guessing here - not sure if it makes sense ;-)

theramiyer commented 4 years ago

But the output is not really going to be visible for the user? It's only visible if you output it to screen which is not supposed to happen.

If you mean what is displayed in Teams to separate one Hashtable from another hashtable separated with a section you probably need to create a separate cmdlet (or at least change the name) to something like ConvertTo-TeamsSectionFact or something with a switch "SeparateObjectsWithSections". I'm just guessing here - not sure if it makes sense ;-)

Right, working on that Teams Section cmdlet. LOL.

theramiyer commented 4 years ago

If you could wait a bit before merging, I'll change this function into one that creates sections. :)

PrzemyslawKlys commented 4 years ago

Sure, no hurry. I'm glad you are enjoying this and learning along the way.

PrzemyslawKlys commented 4 years ago

So do you want me to merge this or you have an update coming?

theramiyer commented 4 years ago

Hi! Please give me a few hours; I'll send an update. Of course, that will be a separate cmdlet. Thought I'll discuss this; do we want to restrict the input for ConvertTo-TeamsFacts to a single object instead of an array of objects? Here's what I was thinking:

We have ConvertTo-TeamsFact as an internal function, that's not exposed. This function takes in only one object and converts it to Teams-like JSON.

We have a ConvertTo-TeamsSection as the exposed function, that accepts an array of objects, converts each to a set of facts, and creates a section for each object. Here's a before and after:

Before


Name: John Doe
City: New York
Name: Jane Doe
City: Amsterdam
Name: Tom Roberts
City: London


After


Name: John Doe
City: New York


Name: Jane Doe
City: Amsterdam


Name: Tom Roberts
City: London


theramiyer commented 4 years ago

There, I've updated the PR with the new function. Added a slight enhancement as well, for better UX.

The PR Quality Review isn't passing for some reason, though; says, 'Error getting analysis'.

Didn't write a test because this function is just a wrapper.

PrzemyslawKlys commented 4 years ago

I don't even check the PR Quality Review ;-) DOn't worry about that.

theramiyer commented 4 years ago

Weird, there seems to be an issue with the Pester module. The only difference between the first and the second parameter sets in Invoke-Pester is the parameter called OutputFile; the parameter does not exist in the first set, while it is mandatory in the second. PowerShell is getting confused which one to pick and asks for the value for OutputFile.

I ran the test script with an older version of Pester on my Linux and Windows machines, and it worked as expected. The version of Pester on my machines is 4.8.1. Pester 5.0.1 has this issue.

PrzemyslawKlys commented 4 years ago

There is: https://evotec.xyz/converting-pester-v4-to-pester-v5-basics/

I need to fix this for all my projects :-)

PrzemyslawKlys commented 4 years ago

5.0.1 was supposed to make it easier by adding compatibility, but i guess it failed a bit. I would still want it to use new syntax ;-)

theramiyer commented 4 years ago

Right. I think they need to change the DefaultParameterSet.

For now do we set the OutputFile to some TEMP directory? Anyway the containers will be purged, right?

PrzemyslawKlys commented 4 years ago

https://twitter.com/nohwnd/status/1265921488022052870 - according to @nohwnd it's already deprecated while being added.

The changes are simple:

PrzemyslawKlys commented 4 years ago

And if we use code outside of IT block we need to define it like:

    foreach ($Key in $TestimoConfiguration['Forest'].Keys) {
        $PSDefaultParameterValues = @{
            "It:TestCases" = @{ Key = $Key; TestimoConfiguration = $TestimoConfiguration }
        }
        It -Name "Test Source $Key should not be NULL" {
            $TestimoConfiguration['Forest'].$Key | Should -Not -Be $Null
        }
        It -Name "Test Source $Key should contain Enable" {
            $TestimoConfiguration['Forest'].$Key.Keys | Should -Contain 'Enable'
        }
        It -Name "Test Source $Key should contain Source" {
            $TestimoConfiguration['Forest'].$Key.Keys | Should -Contain 'Source'
        }
    }
}

Basically the variables defined outside of It block need to be passed via hashtable to It block.

theramiyer commented 4 years ago

I'm going through your blog post. :)
I also think I should start following the changes in Pester. Even though I don't use it at the moment, I do plan to. Should learn and keep updated.

PrzemyslawKlys commented 4 years ago

I think Invoke-Pester is missing some parameter because it fails (as below) yet it shows success....

image

nohwnd commented 4 years ago

$result = Invoke-Pester -Path $PSScriptRoot\Tests #-Output Detailed Yeah that does not enable to Exit on error, so you won't terminate. You should be able to just use the previous way of calling pester, if you are no 5.0.1. The parameter set is deprecated, but there is no alternative now that would be great. So just keep using it :)

PrzemyslawKlys commented 4 years ago

@nohwnd but as you can see above @theramiyer tried and it was stuck with

Weird, there seems to be an issue with the Pester module. The only difference between the first and the second parameter sets in Invoke-Pester is the parameter called OutputFile; the parameter does not exist in the first set, while it is mandatory in the second. PowerShell is getting confused which one to pick and asks for the value for OutputFile.

So it would seem this needs fixing?

nohwnd commented 4 years ago

Gotcha, yeah that would be a bug, because the parameter used to be in a parameter set of it's own. It should not be mandatory. Please PR, or file a bug on Pester.

PrzemyslawKlys commented 4 years ago

@nohwnd

image

This wasn't mandatory ;-) The question is if simple fix by removing mandatory parameter will be sufficient or all things go sideways.

nohwnd commented 4 years ago

PR it and we shall see. Can't really fix it right now, but I can release later today if someone fixes it.

PrzemyslawKlys commented 4 years ago

Merged and fixed some code. Wanted to support both options.

Get-ChildItem | Select-Object -First 2 | ConvertTo-TeamsFact

ConvertTo-TeamsFact -InputObject (Get-ChildItem | Select-Object -First 2)
PrzemyslawKlys commented 4 years ago

Sorry, it took so long to merge. Definitely my fault. Great job on the PR. Thank you again.