Building a Twitter Bot with Vapor

Learn how to build a Twitter bot and create your own tweet automation tools with Vapor and Server Side Swift. By Beau Nouvelle.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

The Redis Queue Driver

There are two drivers available for working with queues in Vapor: Redis and Fluent. The Fluent driver lets you store your jobs within a database. Because this project doesn’t have a database and all the quotes are only strings in a Swift file, you’ll use the Redis driver.

With the starter project still open in Xcode, navigate to Package.swift. In the first dependencies array, add:

.package(url: "https://github.com/vapor/queues-redis-driver.git", from: "1.0.0")

Next, add this code to the dependency array in your App target.

.product(name: "QueuesRedisDriver", package: "queues-redis-driver")

This code adds the QueuesRedisDriver package to the project and sets up a reference to it within the App target.

Save this file and Swift Package Manager will download and build the Redis driver. If you set everything up correctly, you’ll now see four new packages added to the Package Dependencies list.

List of package dependencies

Note: If the new dependencies are not fetched automatically select FileSwift PackagesReset Package Caches.

Tweet Scheduling

In Vapor, scheduled jobs are objects that conform to ScheduledJob and configured using run(context: QueueContext).

To set up a scheduled job, create a new folder named Jobs inside SourcesApp. Then add a new Swift file inside Jobs. Name it SendTweetJob.swift and populate it with:

import Foundation
import Vapor
import Queues

struct SendTweetJob: ScheduledJob {
  // 1
  func run(context: QueueContext) -> EventLoopFuture<Void> {
    // 2
    context.logger.info("Posting Scheduled Tweet")
    // 3
    return context.eventLoop.makeSucceededFuture(())
  }
}

There’s not a whole lot going on in this code at the moment, but here’s how it works:

  1. This method executes when ScheduledJob triggers.
  2. Here, you send a log to the console. You’re not sending any tweets yet, but you’ll need this feedback for testing.
  3. This forces SendTweetJob to always succeed, even if a tweet fails to send. Handling errors is beyond the scope of this tutorial. For more details, check out Chapter 4 of Server-Side Swift with Vapor.

Next, open configure.swift and the following import:

import QueuesRedisDriver

To set up job scheduling, add this code inside configure(_ app: Application) before try routes(app):

// 1
try app.queues.use(.redis(url: "redis://127.0.0.1:6379"))

// 2
app.queues.schedule(SendTweetJob())
  .everySecond()

// 3
try app.queues.startScheduledJobs()

Here’s a code breakdown:

  1. You tell Vapor to use Redis to manage its queues.
  2. You set up a schedule for the SendTweetJob created in the previous step. In this case, the job runs every second. When it’s time to start sending tweets, you’ll change this to a daily schedule.
  3. This final step tells Vapor that setup for all queues is complete, and they can start running!

Click the Xcode play button to build and run!

After a short wait, you’ll see the job logging to the console once every second:

[ NOTICE ] Server starting on http://127.0.0.1:8080
[ INFO ] Posting Scheduled Tweet
[ INFO ] Posting Scheduled Tweet

With that in place, it’s time to start working with the Twitter API to send some tweets!

Interacting With the Twitter API

It may surprise you to know the official Twitter documentation uses status updates when referring to what is more commonly known as tweeting. Therefore the statuses/update endpoint is the one you’ll use to tweet!

API Keys and Secrets

Open OAuth.swift and at the top you’ll find four properties relating to Authorization Keys:

let apiKey = "replace with API Key"
let apiSecret = "replace with API Key Secret"
let accessToken = "replace with Access Token"
let accessSecret = "replace with Access Token Secret"

Remember the keys you collected earlier? You’ll need to use them now. If you lose these keys, you can regenerate them in the Twitter Developer Console.

Replace the placeholder strings with your keys and tokens.

Managing Tweets

Now it’s time to build a mechanism to select quotes to tweet.
Inside SourcesAppQuotes, create a new file named QuoteManager.swift and add:

import Foundation
import Vapor

class QuoteManager {
  // 1
  static let shared = QuoteManager()

  // 2
  private var quoteBucket = Quotes.steveJobs

  // 3
  private func nextQuote() -> String {
    if quoteBucket.isEmpty {
      quoteBucket = Quotes.steveJobs
    }
    return quoteBucket.removeFirst()
  }

  // TODO: Add tweetQuote method
}

Here’s a code breakdown:

  1. QuoteManager is a singleton to ensure quoteBucket isn’t de-initialized between jobs.
  2. It has a list of quotes the job will pull from when it’s time to send a tweet.
  3. This method removes and returns the first quote stored in quoteBucket. When the bucket is empty, it will refill and start the cycle again.

Now, replace // TODO: Add tweetQuote method with the following:

@discardableResult
// 1
func tweetQuote(with client: Client) -> EventLoopFuture<ClientResponse> {
  // 2
  let nonce = UUID().uuidString
  // 3
  let timestamp = String(Int64(Date().timeIntervalSince1970))
  // 4
  let quote = nextQuote()

  // TODO: Add OAuth
}

Here’s a code breakdown:

  1. run(context: QueueContext) from a previous step gives you access to a QueueContext, which has an application.client that you can pass to this method when running a ScheduledJob.
  2. The nonce is a one-time use string. In this case, you use Swift’s UUID to create it. In practice, you can use anything here, providing it won’t ever clash with any other nonce submitted to Twitter. The nonce protects against people sending duplicate requests.
  3. This tells Twitter when the request was created. Twitter rejects any requests with a timestamp too far in the past.
  4. This removes and returns the first quote in the bucket, and refills it if it becomes empty.

You’re almost there. Now you need some authentication and then you’ll be ready to tweet :]

Twitter OAuth

Locate // TODO: Add OAuth and replace it with:

// 1
let signature = OAuth.tweetSignature(nonce: nonce, timestamp: timestamp, quote: quote)
// 2
let headers = OAuth.headers(nonce: nonce, timestamp: timestamp, signature: signature)
// 3
let tweetURI = URI(string: tweetURI.string.appending("?status=\(quote.urlEncodedString())"))

// TODO: post tweet

The first two lines call helper methods on OAuth. While the code backing these methods isn’t too complex, it’s beyond the scope of this tutorial. If you’d like to know more about how Twitter authenticates, check out the documentation.

Take a look at this code, and you’ll notice:

  1. It generates the signature using the nonce, timestamp and quote. This method is specific to sending POST requests to the statuses/update endpoint and is responsible for combining and converting all parameters into a single string.
  2. It applies the signature from the previous step to the headers. Request headers have a variety of uses including telling the remote server what type of device is making the request, as well as what data types may be present. In this case, all this code does is create an authorization header.
  3. The tweetURI has the quote appended as a URL query. You may also be familiar with adding the content you’re sending to the body of a POST request, but this is how Twitter does things.

You may have noticed that some parameters appear multiple times. Look at the nonce and you’ll see that it’s used in the creation of the signature and the oAuthHeaders. But if the signature already contains the nonce why does it also need to go in the header?

It’s in the header so Twitter can be certain that no request tampering occurred between the time it left your server and arrived at theirs. This also explains why the quote is also part of the signature.

It’s time to tweet, finally!