In Part 5 of this series, I covered Google OAuth with PSAuthClient and decrypting AWS Lambda environment variables. In this post, I’d like to cover some code for the WordPress REST API and RSS XML updates.
In the previous iteration of the Meatgrinder process, new posts were added by email. However, I didn’t like the idea of using an email in the process because I wanted the new Meatgrinder to be as self-contained as possible, and using PowerShell’s Invoke-WebRequest
with the REST API meets that requirement.
Some notes on the process: A WordPress application password is recommended as it simplifies authentication. Author, tags, and categories are their respective unique IDs in each WordPress database. Featured images can’t be uploaded along with posts; they have to be uploaded and then their unique image ID associated with the post’s unique ID. The thumbnail is read from S3 into the /tmp
directory of the Lambda function at run time using Read-S3Object
. The decrypt
cmdlet in this sample is the helper function I wrote to decrypt AWS Lambda environment variables.
# Creating the credential object
$app_pwd = decrypt -ciphertext $env:app_pwd
$securepass = ConvertTo-SecureString $app_pwd -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($env:app_user, $securepass)
# Begin building the post
$title = "New post title!"
$content = "Here's a brand new vBrownBag.com post!"
# Fun one-liner that creates an excerpt if the content is longer than 124 chars
$excerpt = $content.Length -gt 124 ? $content.Substring(0, 124) + " ..." : $content
# Retrieving the featured image from S3 as $thumbnail, and reading in the bytes
$thumbnail = Read-S3Object -BucketName $env:bucket -Key "$thumbnail.jpg" -File (Join-Path $env:TEMP "$thumbnail.jpg")
$thumbnailBytes = [System.IO.File]::ReadAllBytes($thumbnail)
# Uploading the featured image and retrieving the $imageId
$response = Invoke-WebRequest -Uri "$env:url/wp-json/wp/v2/media" -Method Post -Authentication Basic -Credential $credential -Headers @{
"Content-Type" = "image/jpeg"
"Content-Disposition" = "attachment; filename=`"$thumbnail.jpg`""
} -Body $thumbnailBytes
$imageId = ($response.Content | ConvertFrom-Json).id
# Updating the featured image title & alt text for $imageId
$imageJson = @{
"date" = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
"title" = $title
"alt_text" = "Featured image for " + $title
} | ConvertTo-Json -Depth 10
Invoke-WebRequest -Uri "$env:url/wp-json/wp/v2/media/$imageId" -Method Post -Authentication Basic -Credential $credential -Headers @{"Content-Type" = "application/json" } -Body $imageJson
$postJson = @{
"date" = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
"author" = $authorId
"title" = $title
"excerpt" = $excerpt
"content" = $content
"comment_status" = "closed"
"ping_status" = "closed"
"featured_media" = $imageId
"status" = $status # "publish" or "pending"
"categories" = @("1", "2")
} | ConvertTo-Json -Depth 10
$response = Invoke-WebRequest -Uri "$env:url/wp-json/wp/v2/posts" -Method Post -Authentication Basic -Credential $credential -Headers @{"Content-Type" = "application/json" } -Body $postJson
Next, let’s cover some XML code that I’m using to update our RSS file. The process is rather simple: download the latest version of the XML file to /tmp
using Invoke-WebRequest
, save a time-stamped backup copy to S3, read in the contents of the XML file, create a new entry, remove any entries that are than 2 years old, make sure there’s no more than 300 entries in the file (just in case!), write the updated XML to disk, then upload it to the website. The createRssElement
helper function was from the original Meatgrinder script, and its job is to simplify XML element creation. Lastly, the Send-SFTPFile
cmdlet is from the Transferetto module.
function createRssElement {
param([string]$elementName, [string]$value, $parent, [string]$namespaceURI = $null)
if ($namespaceURI) {
$thisNode = $xml.CreateElement("itunes", $elementName, "http://www.itunes.com/dtds/podcast-1.0.dtd")
}
else {
$thisNode = $xml.CreateElement($elementName)
}
$thisNode.InnerText = $value
$null = $parent.AppendChild($thisNode)
return $thisNode
}
$xmlUrl = $env:rssxml_url + $env:rssxml_filename
$xmlbackupFileName = (Get-Date -Format "yyyyMMddHHmmss") + " $($env:rssxml_filename)"
$xmlbackupPath = Join-Path $env:TEMP $env:rssxml_filename
# Download the file to /tmp
Invoke-WebRequest -Uri $xmlUrl -OutFile $xmlbackupPath | Out-Null
# Backup to S3
Write-S3Object -BucketName $bucket -File $xmlbackupPath -Key $xmlbackupFileName
# Read in the contents of the file
$xml = [xml] (Get-Content $xmlbackupPath)
# Remove anything with a bad date or more than 2 years old
$xml.rss.channel.Item | Where-Object {
$pubDate = [DateTime]::MinValue
$parseSuccessful = [DateTime]::TryParse($_.pubDate, [ref]$pubDate)
if (-not $parseSuccessful -or $pubDate -lt (Get-Date).AddYears(-2)) {
$xml.rss.channel.RemoveChild($_) | Out-Null
}
}
# Create the new entry
$rssChannel = $xml.rss.channel
$thisItem = createRssElement -elementName 'item' -value '' -parent $rssChannel
$null = createRssElement -elementName 'title' -value $title -parent $thisItem
$null = createRssElement -elementName 'link' -value 'http://vbrownbag.com' -parent $thisItem
$null = createRssElement -elementName 'description' -value $description -parent $thisItem
$null = createRssElement -elementName 'guid' -value $url -parent $thisItem
$enclosure = createRssElement -elementName 'enclosure' -parent $thisItem
$null = $enclosure.SetAttribute('url', $url)
$null = $enclosure.SetAttribute('length', $mp4file.Size)
$null = $enclosure.SetAttribute('type', 'video/mp4')
$null = createRssElement -elementName 'category' -value "Podcasts" -parent $thisItem
$pubDate = Get-Date -Format 'r'
$null = createRssElement -elementName 'pubDate' $pubDate -parent $thisItem
# Sort by date and remove any duplicates
$orderedItems = $rssChannel.Item | Group-Object -Property pubdate | ForEach-Object {
if ($_.Group.Count -gt 1) {
for ($i = 1; $i -lt $_.Group.Count; $i++) {
Write-Output "Duplicate item: $($_.Group[$i].title)"
}
}
$duplicatesCount += ($_.Group.Count - 1)
$_.Group[0]
} | Sort-Object { [DateTime]::Parse($_.pubDate) } -Descending
# Only use the first 300 because Apple doesn't like more than that
if ($orderedItems.Count -gt 300) {
$orderedItems = $orderedItems | Select-Object -First 300
}
# Remove all items from the channel
$itemsToRemove = $xml.rss.channel.item.Clone()
foreach ($item in $itemsToRemove) {
$xml.rss.channel.RemoveChild($item) | Out-Null
}
# Add the ordered items back to the channel
foreach ($item in $orderedItems) {
$xml.rss.channel.AppendChild($item) | Out-Null
}
# Timestamp this newest version of the file and write to disk
$xml.rss.channel.lastBuildDate = (Get-Date).ToUniversalTime().ToString("r")
$xmlWriterSettings = new-object System.Xml.XmlWriterSettings
$xmlWriterSettings.CloseOutput = $true
$xmlWriterSettings.IndentChars = " "
$xmlWriterSettings.Indent = $true
$xmlWriterSettings.NewLineOnAttributes = $true
$xmlWriterSettings.NewLineHandling = [System.Xml.NewLineHandling]::Replace
$xmlWriter = [System.Xml.XmlWriter]::Create($xmlbackupPath, $xmlWriterSettings)
$null = $xml.WriteTo($xmlWriter)
$xmlWriter.Close()
# SFTP the file to where it lives
$sftp_password = decrypt -ciphertext $env:sftp_password
$sftp_credential = New-Object System.Management.Automation.PSCredential($env:sftp_username, (ConvertTo-SecureString $sftp_password -AsPlainText -Force))
$sftpClient = Connect-SFTP -Server $env:sftp_host -Credential $sftp_credential -Port $env:sftp_port
Send-SFTPFile -SftpClient $SftpClient -LocalPath $filepath -RemotePath (Join-Path $env:sftp_destination $env:rssxml_filename) -AllowOverride
Well, I think I’ve reached the logical end of this series. We’ve covered a lot of ground, and I’ve learned a whole lot. Thanks for reading along with me! 🙂