slide

Using azure functions and buffer API for automation

Ned Bellavance
7 min read

Cover

In a previous post I mentioned how I am using Buffer, Feedly, and Zapier to automate parts of my online persona. In this post I will talk about how I found a workflow that wasn’t support by Zapier, and how I used the API from Buffer and Azure Functions to automate post generation.

I run two podcasts for work, Buffer Overflow and AnexiPod. Both of them have been running for over a year, and I thought it would be cool to repost the previous year’s episode on Thursday for Throwback Thursdays (TBT). I figured that I should be able to set up a workflow in Zapier that would check daily to see if there was a post from a year ago, and then repost that for the upcoming Thursday slot. Sadly, that action did not exist in Zapier. How could I fix this? Well I know how to interact with websites and APIs via PowerShell due to my work in Azure and Azure Stack. So I figured I could write a script that runs as a scheduled task. The script would have to do the following:

  • Poll the source feed to see if there was a post a year ago that day
    • If no, do nothing
    • If yes, proceed
  • Create a new Buffer item for the upcoming Thursday including the previous post and the #TBT hashtag

The next decision I had to make was where to run the script. I could have it running on my desktop at home, but then I would need to make sure that my desktop was always on and transfer the task when I replace or rebuild it. Instead I thought of Azure Functions, which is something I’ve been meaning to dig into anyway. I know that you can write functions in PowerShell, so that meant I could develop the script locally and then adapt it for Azure Functions.

Step one was to poll the source feed. Since this is a podcast, there is already an XML feed available. So I chose to write a function that takes a URL and a date. The function pulls the feed in as an XML object, and then uses the date to filter the posts for a specific publish date. These fields are pretty standardized for podcasts, so this should be pretty portable. The function returns one or more post objects, or null if no posts for that date are found.

###Function Get blog post from a year ago for AnexiPod
###Takes returns blog post URLs if any found.  Returns null otherwise
 function Get-BlogPosts {
     Param(
         $date = (Get-Date).AddYears(-1),
         $url = "https://www.anexinet.com/blog/category/anexipod/feed/"
     )

    $response = [xml](Invoke-WebRequest -Uri $url -UseBasicParsing)
    $items = $response.rss.channel.item
    $returnItems = $items | Where-Object{([DateTime]$_.pubDate).Date -eq $date.Date}

    if($returnItems){
        return $returnItems
    }else{
        return $null
    }

 }

The next step was to add interaction with the Buffer API. In order to do that, I had to create an application on the Buffer side. The application information is not particularly important for my purposes. Here is the application information I used. The key is that this is a native application, and not a web application, meaning that there is no sign-in page that Buffer has to redirect the authentication request back to. Buffer App

Once I created the application, I had to figure out how to interact with the API. The authentication piece uses an Access Token that is generated when the application is created. All requests made to the API should include the Access Token in the header or in the URI string. That access token is linked to your account on Buffer, so if you are only using this application with your personal Buffer account then you are good to go. If you want to access other accounts in Buffer, then you will need to generate an access token for each through an OAuth process. I worked all that out after I figured out how to generate the access token for other accounts, so maybe that little tidbit will save you some trouble.

Armed with the access token, I first had to get the profiles associated with my Buffer account. Each profile corresponds to a social media account of some kind, for instance I have three profiles for Twitter, LinkedIn, and Facebook respectively. Here is the bit of script that retrieves the profile information.

    #Build request header
    $requestheader = @{Authorization="Bearer $access_token"}
    #Get profiles
    $profiles = Invoke-RestMethod -Method Get -Uri "$url/profiles.json" -Headers $requestheader -UseBasicParsing

Then I had to create the buffer item for a post that was found. When I tried to do that the first time I got an error, [Disallowed Key Characters]. Unfortunately, I had no idea what that meant and the Google machine wasn’t very helpful. I turned to Twitter and hit up Buffer. They got back to me, and after a little back and forth I discovered that I was sending the POST data in JSON form, which they don’t support. They just want an ‘&’ separated string of key value pairs. Once we got all that worked out, I was able to get the POST portion of my script working. Big props to the Buffer team for being helpful and responsive.

#Create post
    $body = ""
    $text = "#TBT Check out last year's #AnexiPod episode $($postInfo.title) $($postInfo.link)"
    foreach($profile in $profiles){
        $body += "profile_ids[]=$($profile._id)&"
    }
    $body += "text=$text"
    $body += "&scheduled_at=$($thursday.GetDateTimeFormats("s"))"
    Write-Output "Body is $body";
    Write-output "URL is $url";
    Invoke-RestMethod -Method Post -Uri "$url/updates/create.json?access_token=$access_token" -Body $body -ContentType $content_type -UseBasicParsing -Verbose

Finally I adapted the script for Azure Functions. The main thing I had to figure out there was how to store my access token in Azure Key Vault, so it wasn’t sitting in plain text in my Function code. Thank goodness someone else has already figured that out, and I just followed the steps here. At the end of the day I now have an Azure Function that runs on a daily trigger and does exactly what I want. This was a great exercise because now I know more about interacting with APIs, creating Azure Functions, and I have a script I can adapt for other automation tasks. Hooray!

Here’s the full Azure Function for those that are interested:

Write-Output "PowerShell Timer trigger function executed at:$(get-date)";

###Function Get blog post from a year ago for AnexiPod
###Takes returns blog post URLs if any found.  Returns null otherwise
 function Get-BlogPosts {
     Param(
         $date = (Get-Date).AddYears(-1),
         $url = "https://www.anexinet.com/blog/category/anexipod/feed/"
     )

    $response = [xml](Invoke-WebRequest -Uri $url -UseBasicParsing)
    $items = $response.rss.channel.item
    $returnItems = $items | Where-Object{([DateTime]$_.pubDate).Date -eq $date.Date}

    if($returnItems){
        return $returnItems
    }else{
        return $null
    }

 }

  function Add-BufferPost {
    Param(
        $url = "https://api.bufferapp.com/1",
        $postInfo,
        $access_token,
        $content_type = "application/x-www-form-urlencoded"

    )

    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

    #Build request header
    $requestheader = @{Authorization="Bearer $access_token"}
    #Get profiles
    $profiles = Invoke-RestMethod -Method Get -Uri "$url/profiles.json" -Headers $requestheader -UseBasicParsing

    #Find the next Thursday to post
    $thursday = get-date
    while($thursday.DayOfWeek -ne "Thursday"){
        $thursday = $thursday.AddDays(1)
    }
    $thursday = $thursday.AddHours(4)
    #Create post
    $body = ""
    $text = "#TBT Check out last year's #AnexiPod episode $($postInfo.title) $($postInfo.link)"
    foreach($profile in $profiles){
        $body += "profile_ids[]=$($profile._id)&"
    }
    $body += "text=$text"
    $body += "&scheduled_at=$($thursday.GetDateTimeFormats("s"))"
    Write-Output "Body is $body";
    Write-output "URL is $url";
    Invoke-RestMethod -Method Post -Uri "$url/updates/create.json?access_token=$access_token" -Body $body -ContentType $content_type -UseBasicParsing -Verbose

 }

#Get endpoint and password for MSI
$endpoint = $env:MSI_ENDPOINT;

$secret = $env:MSI_SECRET;

#Vault URI to get AuthN token
$vaultTokenURI = 'https://vault.azure.net&api-version=2017-09-01';

#Key vault secret to retrieve
$vaultSecretURI = 'https://functions.vault.azure.net/secrets/buffer-access-key/9aa4e760ae4240f1a3976b2ee379cff6/?api-version=2015-06-01';

$header = @{'Secret' = $secret};

# Get Key Vault AuthN Token
$authenticationResult = Invoke-RestMethod -Method Get -Headers $header -Uri ($endpoint +'?resource=' +$vaultTokenURI);

# Use Key Vault AuthN Token to create Request Header
$requestHeader = @{ Authorization = "Bearer $($authenticationResult.access_token)" }

# Call the Vault and Retrieve Creds
$buffer_secret = Invoke-RestMethod -Method GET -Uri $vaultSecretURI -ContentType 'application/json' -Headers $requestHeader

$access_token = $buffer_secret.value;

 Write-Output "Retrieved access token $access_token";

$date = (Get-Date).AddYears(-1);

 $posts = Get-BlogPosts -date $date;

 Write-Output "Retrieved blog posts";

 Write-Output $posts;

 if($posts){
     Write-Output "Found $($posts.count) for $date";
     foreach($post in $posts){
         Write-Output "Writing post";
         Write-Output $post.title;
        Add-BufferPost -postInfo $post -access_token $access_token;
     }
 }
 else{
     Write-Output "No posts found for $date";
 }