Posted in:

I’ve been having lots of fun recently kicking the tyres of Azure Functions, and one of the ideas I wanted to try out was to see if I could schedule tweets to be sent. That way I could periodically tweet links to the best of nearly 10 years of content here on my blog, or link to my Pluralsight courses.

Now of course there are already services out there like Buffer and Edgar that do this for you, but for a price. But like all good developers I relish the challenge of re-inventing the wheel, and thanks to the generous free grant with Azure Functions, I’ll be able to get my own poor man’s tweet scheduler up and running without paying a penny!

So how does it work?

Well, Azure functions supports scheduled tasks, so I could pick a certain time every day or few days and randomly select a link to share. But I wanted my tweets to go out at random times, and preferably go live during waking hours in the US which is where the bulk of my Twitter followers are from. That appears not to be possible with a simple cron expression, so I decided to use two functions.

Scheduling the Tweets

The first function runs daily at midnight (using a Timer trigger with a cron expression of 0 0 0 * * *) and it’s job is to randomly pick a tweet to send, and a time to send it.

How does it get the tweet? Well, I use a SaaS file binding for that. I can connect my Azure function to OneDrive, and set it up to read my list of tweets from a text file with a specified path.

And how does it send a scheduled tweet? Well, for that I decided to send a message to a queue, but delayed by a certain amount of time. I decided that I’d take 15:00 UTC which I think is roughly when the USA starts work and then pick a random number of minutes up to about 23:00 UTC to give my European readers a fighting chance of seeing it before going to bed. Unfortunately, the built-in Azure Functions Storage Queue’s output binding only gives us access to the CloudQueueMessage which doesn’t let us schedule a time. So I opted to simply write the code myself to connect to the queue and send it with a delay.

Let’s look at the code for this first function. First, of all, here’s the bindings section of my functions.json file:

{
  "bindings": [
    {
      "name": "myTimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 0 0 * * *"
    },
    {
      "type": "apiHubFile",
      "name": "tweetFile",
      "path": "AzureFunctions/tweets.txt",
      "connection": "onedrive_ONEDRIVE",
      "direction": "in"
    }
  ],
  "disabled": false
}

As you can see, the timer contains the cron expression, and the OneDrive connection, which is of type “apiHubFile”, uses the “onedrive_ONEDRIVE” connection that you can set up to connect to your OneDrive (or DropBox / GoogeDrive if you prefer) in the portal by clicking “new” and authorizing your Azure Functions app to connect:

image

Also there’s the path, which is hardcoded to a simple text file in my OneDrive containing a list of tweets.

And what about the code? Well, here’s the timer function:

#r "Microsoft.WindowsAzure.Storage" 
using System;
using System.Configuration;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Queue;

public static async Task Run(TimerInfo myTimer, string tweetFile, TraceWriter log)
{    
    log.Info($"Tweet Scheduler Fired {DateTime.Now}, {myTimer.Schedule}, {myTimer.ScheduleStatus}, {myTimer.IsPastDue}");
    var tweets = tweetFile.Split("\n\r".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
    var random = new Random();
    var tweet = tweets[random.Next(tweets.Length + 1)];

    var now = DateTime.UtcNow;
    var scheduled = new DateTime(now.Year, now.Month, now.Day, 15, 0, 0);
    scheduled = scheduled.AddMinutes(random.Next(60 * 8));
    if (scheduled < now) scheduled = scheduled.AddDays(1);

    await SendScheduled("tweets", tweet, scheduled, log);
    log.Info($"Scheduled {tweet} for: {scheduled}");
}

It’s not too complicated, except that to use the Azure storage classes you need a special reference as described here. The contents of my tweet file just come straight in as a string which I need to split into lines. And I created a custom SendScheduled function to actually perform the delayed sending of the message:

private static async Task SendScheduled(string queueName, string messageContent, DateTime scheduledTimeUtc, TraceWriter log)
{
    var connectionString = ConfigurationManager.AppSettings["AzureWebJobsStorage"]; 
    var storageAccount = CloudStorageAccount.Parse(connectionString);
    var queueClient = storageAccount.CreateCloudQueueClient();
    var queue = queueClient.GetQueueReference(queueName);
    queue.CreateIfNotExists();
    var message = new CloudQueueMessage(messageContent);
    var delay = (scheduledTimeUtc - DateTime.UtcNow);
    if (delay < TimeSpan.Zero) delay = TimeSpan.Zero;
    log.Info($"Delay is {delay}");
    await queue.AddMessageAsync(message, null, delay, null, null);
}

Sending the Tweets

The next piece of the puzzle is listening to that queue and actually sending the tweets. The easy part is setting up a new function to listen on the queue.

The slightly harder part was sending the tweet. I decided to use the TweetInvi NuGet package to send the tweets. This is pretty easy to use once you’ve set up the necessary access keys. There are full instructions at the TweetInvi site, but the basic gist of it is that you need to go to apps.twitter.com and set up a new app. I called mine “Serverless Twit”:

image

Inside that app there are two sets of tokens and secrets you need to set up, but once you’ve done that, we can put them in our Function App settings, and use them with the TweetInvi library like this:

using System;
using System.Configuration;
using Tweetinvi;
using Tweetinvi.Core.Extensions;
using Tweetinvi.Core.Parameters;

public static void Run(string myTweet, TraceWriter log)
{
    log.Info($"Need to tweet: {myTweet}");

    var consumerKey = ConfigurationManager.AppSettings["TwitterConsumerKey"];
    var consumerSecret = ConfigurationManager.AppSettings["TwitterConsumerSecret"];
    var accessToken = ConfigurationManager.AppSettings["TwitterAccessToken"];
    var accessTokenSecret = ConfigurationManager.AppSettings["TwitterAccessTokenSecret"];
    Auth.SetUserCredentials(consumerKey, consumerSecret, accessToken, accessTokenSecret);
    
    var twitterLength = myTweet.TweetLength();
    if (twitterLength > 140)
    {
        log.Warning($"Tweet too long {twitterLength}");
    }

    var publishedTweet = Tweet.PublishTweet(myTweet);
    // by default TweetInvi doesn't throw exceptions: https://github.com/linvi/tweetinvi/wiki/Exception-Handling
    if (publishedTweet == null)
    {
        log.Error($"Failed to publish");
    }
    else
    {
        log.Info($"Published tweet {publishedTweet.Id}");
    }
}

There’s one more thing we need to do, and that’s tell Azure Functions where to find the TweetInvi assemblies. This is done by creating a project.json file in our function folder (same place as the function.json) and adding a dependency on the version of the NuGet package we want:

{
  "frameworks": {
    "net46":{
      "dependencies": {
        "TweetinviAPI": "1.1.1"
      }
    }
   }
}

And that’s all there is to it! Now all my tens of twitter followers who are actually real people will have the delight of seeing a daily link to some random thing I’ve written or built in the past. Hopefully I won’t annoy them all into unfollowing me!

Want to learn more about how easy it is to get up and running with Azure Functions? Be sure to check out my Pluralsight courses Azure Functions Fundamentals and Microsoft Azure Developer: Create Serverless Functions

Comments

Comment by David Ferreira

Loved the Pluralsight course (ran through the whole thing this morning). Great idea, this. Tweet witty (or witless) remarks at random times. Love it.

David Ferreira
Comment by Mark Heath

thanks David. Have turned off the random tweeting for now, as it kept picking the same ones and was annoying me (and probably some followers). Need to introduce some more intelligent randomness to it next.

Mark Heath
Comment by David Ferreira

I have a database of about 217 (so, not "about") silly quotes I've collected over the misspent years, so if I did something like this, it would be pretty easy to stick those in some storage and then just march through the list. I figure by the time I wrap around either my follower count will be 0 or will have turned over so the replays will be new for someone. :-)
Still, a great idea. Hope you do more Pluralsight courses.

David Ferreira