plugins/CleverReach/Public/peoplestage/invoke-upload.ps1
function Invoke-Upload{ [CmdletBinding()] param ( [Parameter(Mandatory=$false)][Hashtable] $InputHashtable ) begin { #----------------------------------------------- # START TIMER #----------------------------------------------- $processStart = [datetime]::now #$inserts = 0 #----------------------------------------------- # LOG #----------------------------------------------- $moduleName = "UPLOAD" # Start the log Write-Log -message $Script:logDivider Write-Log -message $moduleName -Severity INFO # Log the params, if existing Write-Log -message "INPUT:" if ( $InputHashtable ) { $InputHashtable.Keys | ForEach-Object { $param = $_ Write-Log -message " $( $param ) = '$( $InputHashtable[$param] )'" -writeToHostToo $false } } #----------------------------------------------- # DEBUG MODE #----------------------------------------------- Write-Log "Debug Mode: $( $Script:debugMode )" #----------------------------------------------- # PARSE MESSAGE #----------------------------------------------- #$script:debug = $InputHashtable $uploadOnly = $false # TODO add an option to turn off tagging for upload only If ( "" -eq $InputHashtable.MessageName ) { #Write-Log "A" $uploadOnly = $true $mailing = [Mailing]::new(999, "UploadOnly") } else { #Write-Log "B" Write-Log "Parsing message: '$( $InputHashtable.MessageName )' with '$( $Script:settings.nameConcatChar )'" $mailing = [Mailing]::new($InputHashtable.MessageName) Write-Log "Got chosen message entry with id '$( $mailing.mailingId )' and name '$( $mailing.mailingName )'" #$mailing = [Mailing]::new($InputHashtable.MessageName) #Write-Log "Got chosen message entry with id '$( $mailing.mailingId )' and name '$( $mailing.mailingName )'" } #----------------------------------------------- # DEFAULT VALUES #----------------------------------------------- $uploadSize = $Script:settings.upload.uploadSize Write-Log "Got UploadSize of $( $uploadSize ) rows/objects" #-Severity WARNING If ($uploadSize -gt 1000 ) { Write-Log "UploadSize has been set to more than 1000 rows. Using max of 1000 now!" -Severity WARNING $uploadSize = 1000 } # Currently CleverReach support 40 attributes $maxAttributesCount = 40 #----------------------------------------------- # CHECK INPUT FILE #----------------------------------------------- # Checks input file automatically $file = Get-Item -Path $InputHashtable.Path Write-Log -Message "Got a file at $( $file.FullName )" # Add note in log file, that the file is a converted file if ( $file.FullName -match "\.converted$") { Write-Log -message "Be aware, that the exports are generated in Codepage 1252 and not UTF8. Please change this in the Channel Editor." -severity ( [LogSeverity]::WARNING ) } # Count the rows # [ ] if this needs to much performance, this is not needed If ( $Script:settings.upload.countRowsInputFile -eq $true ) { $rowsCount = Measure-Rows -Path $file.FullName -SkipFirstRow Write-Log -Message "Got a file with $( $rowsCount ) rows" } else { Write-Log -Message "RowCount of input file not activated" } #throw [System.IO.InvalidDataException] $msg #Write-Log -Message "Debug Mode: $( $Script:debugMode )" #----------------------------------------------- # CHECK CLEVERREACH CONNECTION #----------------------------------------------- try { Test-CleverReachConnection } catch { #$msg = "Failed to connect to CleverReach, unauthorized or token is expired" #Write-Log -Message $msg -Severity ERROR Write-Log -Message $_.Exception -Severity ERROR throw [System.IO.InvalidDataException] $msg exit 0 } #Write-Log -Message "Debug Mode: $( $Script:debugMode )" } process { try { #----------------------------------------------- # CREATE GROUP IF NEEDED #----------------------------------------------- # If lists contains a concat character (id+name), use the list id # if no concat character is present, take the whole string as name for a new list and search for it... if not present -> new list! # if no list is present, just take the current date and time # If listname is valid -> contains an id, concatenation character and and a name -> use the id try { $createNewGroup = $false # No need for the group creation now $list = [MailingList]::new($InputHashtable.ListName) $listName = $list.mailingListName $groupId = $list.mailingListId Write-Log "Got chosen list/group entry with id '$( $list.mailingListId )' and name '$( $list.mailingListName )'" # Asking for details and possibly throw an exception $g = Invoke-CR -Object "groups" -Path "/$( $groupId )" -Method GET -Verbose } catch { # Listname is the same as the message means nothing was entered -> check the name if ( $InputHashtable.ListName -ne $InputHashtable.MessageName ) { # Try to search for that group and select the first matching entry or throw exception $groups = Invoke-CR -Object "groups" -Method "GET" -Verbose # Check how many matches are available $matchingGroups = @( $groups | where-object { $_.name -eq $InputHashtable.ListName } ) # put an array around because when the return is one object, it will become a pscustomobject switch ( $matchingGroups.Count ) { # No match -> new group 0 { $createNewGroup = $true $listName = $InputHashtable.ListName Write-Log -message "No matched group -> create a new one" -severity INFO } # One match -> use that one! 1 { $createNewGroup = $false # No need for the group creation now $listName = $matchingGroups.name $groupId = $matchingGroups.id Write-Log -message "Matched one group -> use that one" -severity INFO } # More than one match -> throw exception Default { $createNewGroup = $false # No need for the group creation now Write-Log -message "More than one match -> throw exception" -severity ERROR throw [System.IO.InvalidDataException] "More than two groups with that name. Please choose a unique list." } } # String is empty, create a generic group name } else { $createNewGroup = $true $listName = [datetime]::Now.ToString("yyyyMMdd_HHmmss") Write-Log -message "Create a new group with a timestamp" -severity INFO } } # Create a new group (if needed) if ( $createNewGroup -eq $true ) { $body = [PSCustomObject]@{ "name" = "$( $listName )" } $newGroup = Invoke-CR -Object "groups" -Body $body -Method "POST" -Verbose $groupId = $newGroup.id Write-Log -message "Created a new group with id $( $groupId )" -severity INFO } #----------------------------------------------- # GET GENERAL STATISTICS FOR LIST #----------------------------------------------- Write-Log "Getting stats for group $( $groupId )" $groupStats = Invoke-CR -Object "groups" -Path "/$( $groupId )/stats" -Method GET -Verbose <# { "total_count": 4, "inactive_count": 0, "active_count": 4, "bounce_count": 0, "avg_points": 69.5, "quality": 3, "time": 1685545449, "order_count": 0 } #> $groupStats.psobject.properties | ForEach-Object { Write-Log " $( $_.Name ): $( $_.Value )" } #----------------------------------------------- # LOAD HEADER AND FIRST ROWS #----------------------------------------------- # Read first 100 rows $deliveryFileHead = Get-Content -Path $file.FullName -ReadCount 100 -TotalCount 201 -Encoding utf8 $deliveryFileCsv = ConvertFrom-Csv $deliveryFileHead -Delimiter "`t" $headers = [Array]@( $deliveryFileCsv[0].psobject.properties.name ) <# $headers | ForEach { $header = $_ $sqliteParameterObject = $sqliteDeliveryInsertCommand.CreateParameter() $sqliteParameterObject.ParameterName = ":$( $header -replace '[^a-z0-9]', '' )" [void]$sqliteDeliveryInsertCommand.Parameters.Add($sqliteParameterObject) [void]$sqliteDeliveryCreateFields.Add( """$( $header )"" TEXT" ) }#> $reservedFieldsCheck = Compare-Object -ReferenceObject $headers -DifferenceObject $Script:settings.upload.reservedFields -IncludeEqual If ( ( $reservedFieldsCheck | Where-Object { $_.SideIndicator -eq "==" } ).count -gt 0 ) { $msg = "You have used reserved fields:" Write-Log -Message $msg -Severity ERROR $reservedFieldsCheck | Where-Object { $_.SideIndicator -eq "==" } | ForEach-Object { Write-Log -Message " $( $_.InputObject )" } throw [System.IO.InvalidDataException] $msg exit 0 } #----------------------------------------------- # CHECK ATTRIBUTES #----------------------------------------------- $requiredFields = @( $InputHashtable.EmailFieldName, $InputHashtable.UrnFieldName ) $reservedFields = @( $Script:settings.upload.reservedFields ) #@("tags") Write-Log -message "Required fields: $( $requiredFields -join ", " )" Write-Log -message "Reserved fields: $( $reservedFields -join ", " )" $csvAttributesNames = $headers | Where-Object { $_ -notin $reservedFields } #$csvAttributesNames = Get-Member -InputObject $dataCsv[0] -MemberType NoteProperty | where { $_.Name -notin $reservedFields } Write-Log -message "Loaded csv attributes $( $csvAttributesNames -join ", " )" $attributeParam = [Hashtable]@{ "reservedFields" = $reservedFields "requiredFields" = $requiredFields "csvAttributesNames" = $csvAttributesNames "csvUrnFieldname" = $InputHashtable.UrnFieldName "responseUrnFieldname" = $Script:settings.responses.urnFieldName "groupId" = $groupId } $attributes = Sync-Attributes @attributeParam #----------------------------------------------- # BEGIN AN EXCLUSION LIST #----------------------------------------------- $exclusionList = [System.Collections.ArrayList]@() #----------------------------------------------- # LOAD DEACTIVATED/UNSUBSCRIBES #----------------------------------------------- # Prepare inactives query as security net $deactivatedGlobalFilterBody = [PSCustomObject]@{ "groups" = [Array]@() "operator" = "AND" "rules" = [Array]@(, [PSCustomObject]@{ "field" = "deactivated" "logic" = "bg" "condition" = "1" } ) "orderby" = "activated desc" "detail" = 0 } # Load global inactive receivers (unsubscribed) If ( $Script:settings.upload.excludeGlobalDeactivated -eq $true ) { $globalDeactivated = @( (Invoke-CR -Object "receivers" -Path "filter.json" -Method POST -Verbose -Paging -Body $deactivatedGlobalFilterBody.PsObject.Copy()) ) # use a copy so the reference is not changed because it will used a second time $script:debug = $globalDeactivated Write-Log -Message "Adding $( $globalDeactivated.count ) global inactive receivers to exclusion list" If ( $globalDeactivated.Count -gt 0 ) { $exclusionList.AddRange( @( $globalDeactivated.email ) ) } # TODO use this list for exclusions } # Prepare local inactives query as security net $deactivatedLocalFilterBody = $deactivatedGlobalFilterBody.PsObject.Copy() $deactivatedGlobalFilterBody.groups = [Array]@(,$groupId) # Runtime filter with paging If ( $Script:settings.upload.excludeLocalDeactivated -eq $true ) { $localDeactivated = @( (Invoke-CR -Object "receivers" -Path "filter.json" -Method POST -Verbose -Paging -Body $deactivatedLocalFilterBody) ) Write-Log -Message "Adding $( $localDeactivated.count ) local inactive receivers to exclusion list" If ( $localDeactivated.count ) { $exclusionList.AddRange( @( $localDeactivated.email ) ) } } #----------------------------------------------- # LOAD BOUNCES #----------------------------------------------- # Load global bounces as a list $bounced = @( Invoke-CR -Object "bounces" -Method GET -Verbose -Paging ) # Log Write-Log -Message "There are currently $( $bounced.count ) bounces in your account" $c | Group-Object category | ForEach-Object { Write-Log -Message " $( $_.Name ): $( $_.Count )" } # Add to list If ( $Script:settings.upload.excludeBounces -eq $true ) { Write-Log -Message "Adding $( $bounced.count ) bounced receivers to exclusion list" If ( $bounced.count -gt 0 ) { [void]$exclusionList.AddRange( $bounced.email ) } } #----------------------------------------------- # BUILDING THE TAG TO USE #----------------------------------------------- Switch ( $InputHashtable.mode ) { "taggingOnly" { #$tags = ,$params.MessageName -split "," $tags = [Array]@(,$mailing.mailingName) # TODO only allow one tag for the moment, but can easily be extended to multiple ones } Default { # Combination of a source, a random letter, 7 more random characters and a timestamp $tag = [System.Text.StringBuilder]::new() [void]$tag.Append( $Script:settings.upload.tagSource ) [void]$tag.Append( "." ) [void]$tag.Append(( Get-RandomString -length 1 -ExcludeSpecialChars -ExcludeUpperCase -ExcludeNumbers )) [void]$tag.Append(( Get-RandomString -length 7 -ExcludeSpecialChars -ExcludeUpperCase )) [void]$tag.Append( "_" ) [void]$tag.Append( $processStart.toString("yyyyMMddHHmmss") ) If ( ($uploadOnly -eq $true -and $Script:settings.upload.useTagForUploadOnly -eq $true) -or $uploadOnly -eq $false ) { $tags = [Array]@(, $tag.ToString() ) } else { $tags = [Array]@() } } } Write-Log -Message "Using the tag: $( $tags -join ", " )" #----------------------------------------------- # GO THROUGH FILE IN PARTS #----------------------------------------------- # Start stream reader $reader = [System.IO.StreamReader]::new($file.FullName, [System.Text.Encoding]::UTF8) [void]$reader.ReadLine() # Skip first line. #$Script:debug = $reader $globalAtts = $attributes.global | Where-Object { $_.name -in $headers } #$localAtts = $localAttributes | where { $_.name -in $headers } $i = 0 # row counter $v = 0 # valid counter $j = 0 # uploaded entries counter $k = 0 # upload batches counter $checkObject = [System.Collections.ArrayList]@() $uploadObject = [System.Collections.ArrayList]@() while ($reader.Peek() -ge 0) { # raw empty receivers template: https://rest.cleverreach.com/explorer/v3/#!/groups-v3/upsertplus_post $uploadEntry = [PSCustomObject]@{ #"registered" #"activated" #"deactivated" "email" = "" #$dataCsv[$i].email "global_attributes" = [PSCustomObject]@{} "attributes" = [PSCustomObject]@{} "tags" = [Array]@() } # values of current row $values = $reader.ReadLine().split("`t") # put in email address $emailIndex = $headers.IndexOf($InputHashtable.EmailFieldName) $uploadEntry.email = $values[$emailIndex] # go through every header and fill it into the object <# For ( $x = 0; $x -lt $values.Count; $x++ ) { Switch ( $header[$x] ) { # Email address, normally email $InputHashtable.EmailFieldName { $uploadEntry."email" = $values[$x] break } # Global attribute ({ $globalAtts -contains $PSItem }) { $uploadEntry."global_attributes" | Add-Member -MemberType NoteProperty -Name $header[$x] -Value $values[$x] break } # Local attribute ({ $localAtts -contains $PSItem }) { $uploadEntry."attributes" | Add-Member -MemberType NoteProperty -Name $header[$x] -Value $values[$x] break } } } $uploadEntry.tags = $tags #> # Global attributes $globalAtts | ForEach-Object { $attrName = $_.name # using description now rather than name, because the comparison is made on descriptions $attrDescription = $_.description $value = $null $nameIndex = $headers.IndexOf($attrName) $descriptionIndex = $headers.IndexOf($attrDescription) # If nothing found, the index is -1 If ( $nameIndex -ge 0) { $value = $values[$nameIndex] } elseif ( $descriptionIndex -ge 0 ) { $value = $values[$descriptionIndex] } If( $null -ne $value ) { $uploadEntry.global_attributes | Add-Member -MemberType NoteProperty -Name $attrName -Value $value } } # New local attributes $attributes.new | ForEach-Object { $attrName = $_.name # using description now rather than name, because the comparison is made on descriptions $attrDescription = $_.description $value = $null $nameIndex = $headers.IndexOf($attrName) $descriptionIndex = $headers.IndexOf($attrDescription) # If nothing found, the index is -1 If ( $nameIndex -ge 0) { $value = $values[$nameIndex] } elseif ( $descriptionIndex -ge 0 ) { $value = $values[$descriptionIndex] } If( $null -ne $value ) { $uploadEntry.attributes | Add-Member -MemberType NoteProperty -Name $attrName -Value $value } } # Existing local attributes $attributes.local | ForEach-Object { $attrName = $_.name # using description now rather than name, because the comparison is made on descriptions $attrDescription = $_.description $value = $null $nameIndex = $headers.IndexOf($attrName) $descriptionIndex = $headers.IndexOf($attrDescription) # If nothing found, the index is -1 If ( $nameIndex -ge 0) { $value = $values[$nameIndex] } elseif ( $descriptionIndex -ge 0 ) { $value = $values[$descriptionIndex] } If( $null -ne $value ) { $uploadEntry.attributes | Add-Member -MemberType NoteProperty -Name $attrName -Value $value } } # Tags <# In the array of tags, prepend a "-" to the tag you want to be removed. To remove all tags with a specific origin, simply specify "*" instead of any tag name. #> $uploadEntry.tags = $tags # Add entry to the check object [void]$checkObject.Add( $uploadEntry ) # Do an validation every n records when threshold is reached or if it is the last row $i += 1 if ( ( $i % $uploadSize ) -eq 0 -or $reader.EndOfStream -eq $true) { # Commit every 50k records Write-Log "CHECK at row $( $i )" # Validate receivers If ( $Script:settings.upload.validateReceivers -eq $true -and $createNewGroup -eq $false ) { # TODO validate receivers through cleverreach, check abount bounces Write-Log "Validate email addresses" Write-Log " $( $checkObject.count ) rows" $validateObj = [PSCustomObject]@{ "emails" = [Array]@( $checkObject.email ) "group_id" = $groupId "invert" = $false } $validatedAddresses = @(, (Invoke-CR -Object "receivers" -Path "/isvalid" -Method POST -Verbose -Body $validateObj )) #$Script:debug = $validatedAddresses $v += $validatedAddresses.count Write-Log " $( $validatedAddresses.count ) returned valid addresses" If ( $Script:settings.upload.excludeNotValidReceivers -eq $true) { # TODO remove invalid receivers Write-Log " Removing invalid addresses" $checkObject = [System.Collections.ArrayList]@( $checkObject | Where-Object { $_.email -in $validatedAddresses } ) } else { Write-Log " Not removing invalid addresses" } } Write-Log " $( $checkObject.count ) left rows" $checkObject = [System.Collections.ArrayList]@( $checkObject | Where-Object { $_.email -notin $exclusionList } ) Write-Log " $( $checkObject.count ) left rows after using exclusion list" # Add checked objects to uploadobject [void]$uploadObject.AddRange( $checkObject ) # Clear the current object completely $checkObject.Clear() } # Do an upload when threshold is reached if ( $uploadObject.Count -ge $uploadSize -or $reader.EndOfStream -eq $true ) { # Commit, when size is reached $uploadFinished = $false Write-Log "UPLOAD at row $( $i )" Write-Log " $( $uploadObject.count ) objects ready for upload" Do { Write-Log " $( ( $uploadObject[0..$uploadSize] ).count ) objects/rows will be uploaded" $uploadBody = $uploadObject[0..( $uploadSize - 1 )] # Output the request body for debug purposes Write-Log -Message "Debug Mode: $( $Script:debugMode )" If ( $Script:debugMode -eq $true ) { $tempFile = ".\$( $i )_$( [guid]::NewGuid().tostring() )_request.txt" Set-Content -Value ( ConvertTo-Json $uploadBody -Depth 99 ) -Encoding UTF8 -Path $tempFile } # As a response we get the full profiles of the receivers back $upload = @( Invoke-CR -Object "groups" -Path "/$( $groupId )/receivers/upsertplus" -Method POST -Verbose -Body $uploadBody ) # Count the successful upserted profiles $j += $upload.count $k += 1 # Output the response body for debug purposes #If ( $Script:debugMode -eq $true ) { $script:debug += $upload $tempFile = ".\$( $i )_$( [guid]::NewGuid().tostring() )_response.txt" Set-Content -Value ( ConvertTo-Json $upload -Depth 99 ) -Encoding UTF8 -Path $tempFile #} # TODO check how to log the returned values - please be aware, that the second number is not the final index, it is the amount that should be removed $uploadObject.RemoveRange(0,$uploadBody.Count) # Do an extra round for remaining records AND if it is the last row If ( $uploadObject.count -gt 0 -and $reader.EndOfStream -eq $true) { $uploadFinished = $true } else { $uploadFinished = $true # Otherwise always do only one upload } } Until ( $uploadFinished -eq $true ) } } Write-Log "Stats for upload" Write-Log " checked $( $i ) rows" Write-Log " $( $v ) valid rows" Write-Log " $( $j ) uploaded records" Write-Log " $( $k ) uploaded batches" #----------------------------------------------- # GET GENERAL STATISTICS FOR LIST #----------------------------------------------- Write-Log "Getting stats for group $( $groupId )" $groupStats = Invoke-CR -Object "groups" -Path "/$( $groupId )/stats" -Method GET -Verbose <# { "total_count": 4, "inactive_count": 0, "active_count": 4, "bounce_count": 0, "avg_points": 69.5, "quality": 3, "time": 1685545449, "order_count": 0 } #> $groupStats.psobject.properties | ForEach-Object { Write-Log " $( $_.Name ): $( $_.Value )" } #----------------------------------------------- # GET STATISTICS FOR TAG #----------------------------------------------- Write-Log "Getting tag stats for tag $( $tags ) for group $( $groupId )" $tagQuery = [PSCustomObject]@{ "tag" = $tags "group_id" = $groupId "active" = $true } $tagCount = 0 $tagCount += Invoke-CR -Object "tags" -Path "/count" -Method GET -Verbose -Query $tagQuery Write-Log "Got confirmed $( $tagCount ) receivers for tag $( $tags ) in group $( $groupId )" } catch { $msg = "Error during uploading data. Abort!" Write-Log -Message $msg -Severity ERROR -WriteToHostToo $false Write-Log -Message $_.Exception -Severity ERROR throw $_.Exception } finally { # Close the file reader, if open # If the variable is not already declared, that shouldn't be a problem try { $reader.Close() } catch { } #----------------------------------------------- # STOP TIMER #----------------------------------------------- $processEnd = [datetime]::now $processDuration = New-TimeSpan -Start $processStart -End $processEnd Write-Log -Message "Needed $( [int]$processDuration.TotalSeconds ) seconds in total" Write-Host "Uploaded $( $j ) record. Confirmed $( $tagcount ) receivers with tag '$( $tags )'" } #----------------------------------------------- # RETURN VALUES TO PEOPLESTAGE #----------------------------------------------- # count the number of successful upload rows $recipients = $j #$dataCsv.Count # TODO work out what to be saved # put in the source id as the listname $transactionId = $groupId #$Script:processId #$targetGroup.targetGroupId # TODO or try to log the used tag? # return object $return = [Hashtable]@{ # Mandatory return values "Recipients"=$recipients "TransactionId"=$transactionId # General return value to identify this custom channel in the broadcasts detail tables "CustomProvider"= $Script:settings.providername "ProcessId" = $Script:processId # More values for broadcast "Tag" = ( $tags -join ", " ) "GroupId" = $groupId # Some more information for the broadcasts script #"EmailFieldName"= $params.EmailFieldName #"Path"= $params.Path #"UrnFieldName"= $params.UrnFieldName #"TargetGroupId" = $targetGroup.targetGroupId # More information about the different status of the import #"RecipientsIgnored" = $status.report.total_ignored #"RecipientsQueued" = $recipients #"RecipientsSent" = $status.report.total_added + $status.report.total_updated } # log the return object Write-Log -message "RETURN:" $return.Keys | ForEach-Object { $param = $_ Write-Log -message " $( $param ) = '$( $return[$param] )'" -writeToHostToo $false } # return the results $return } end { } } |