azapen6 / okkam-http

Minimal Akka HTTP Client library with implementation of several Authorization Schemes: Basic, OAuth 1.0a and OAuth 2.0 Bearer.

GitHub

Okkam HTTP

Okkam HTTP is a minimal Akka HTTP Client library with implementation of several Authorization Schemes: Basic, OAuth 1.0a and OAuth 2.0 Bearer.

Caution: You should not use Okkam HTTP in server-side application; especially, it is not supposed to be used with Play! framework.

Installation

In your build.sbt, add the following:

scalaVersion := "2.13.0"

libraryDependencies += "com.github.azapen6" %% "okkam-http" % s"0.2.5-a${akkaVersion}-h${akkaHTTPVersion}"

The last two fragments specify the versions of Akka and Akka HTTP which Okkam HTTP depends on. Okkam HTTP v.0.2.5 is available for akkaVersion: 2.5.24 and akkaHTTPVersion: 10.1.9.

Documentations

Scaladoc

For Twitter

Okkam HTTP provides necessary API to make requests to Twitter API both by User Authentication and by Application-only Authentication.

Tweet

The first example is to tweet a text. Before running this example, you have to register your Twitter application in Twitter Application Management and acquire its Consumer Key & Secret and Access Token & Secret for your account.

import okkam.http._
import akka.http.scaladsl.model._
import scala.io.StdIn
import scala.concurrent.Future
import scala.util.{Success, Failure}

object Tweet {

  // Creates an Okkam Http system with default settings.
  val osys = OHttpSystem()

  /**
    * Before creating clients, you must resolve a few implicit values
    * defined by the system. They are necessary to make requests
    * and to handle `Future`.
    *
    * A simple way is to import `osys` itself.
    */
  import osys._

  /**
    * Creates an OAuth 1.0a client instance using your key and access token.
    */
  val twitter = OHttpClient(
    OAuth1.KeyPair(
      "ABC...", // Consumer Key
      "DEF..." // Consumer Secret
    ),
    OAuth1.TokenPair(
      "012...", // Access Token
      "XYZ..." // Access Token Secret
    ))

  def helloWorld = {

    /**
      * Composes a `POST statuses/update` request to tweet "Hello, world!". (see [Type Conversions](#Type-Conversions))
      */
    val request = OHttpRequest.POST(
        https"api.twitter.com/1.1/statuses/update.json?status=Hello, world!"
      )

    /**
      * Makes an authorized request.
      * Here, makes the tweet request with your key and token.
      * The result type is `Future[OHttpRequest]`. (see Note 2)
      */
    val responseFuture = twitter(request)

    /**
      * When the response arrives, prints its status code (e.g. 200 == Ok, 404 == Not Found)
      * and its body as a plain text encoded to UTF-8 data bytes.
      */
    responseFuture onComplete {
      case Success(res) => println(s"${res.status}\n\n${res.utf8String}")
      case Failure(t) => throw t
    }

    /**
      * Note that `Success` in `onComplete` block simply means that
      * no exception is thrown until the response is received.
      * It is not responsible for whether the status is success or not.
      */

  }

  def main(args: Array[String]): Unit = {
    try {
      helloWorld

      /**
        * Pauses the main thread until the Enter key is pressed. (see Note 2)
        */
      StdIn.readLine()
    } finally {
      /**
        * It is mandatory to shutdown the system explicitly.
        * If you miss it, the application can not stop running.
        */
      osys.shutdown
    }
  }

}

Note 1. All of requests are processed in asynchronous manner. You should not block any threads to wait for results, except to wait for user's action. Several patterns to acquire results of Future are described in literature:

Note 2. If you miss the pausing statement at the last of the try block, or press the Enter before the response arrives, the connection will be lost and the response will not arrive. In other words, you can abort waiting for the response abruptly. Make sure the pausing statement waits for user's action, not for the response itself.

Type Conversions

The prefix https is a string interpolator that translates the following string to the corresponding internal expression of the URL starting with the "https" scheme. Characters which should be escaped (not in the unreserved charset defined in [RFC 3896]) are automatically percent encoded. Therefore, the actual request URL is

https://api.twitter.com/1.1/statuses/update.json?status=Hello%2C%20world%21

The following four expressions give the same result as above:

OHttpUrl("https://api.twitter.com/1.1/statuses/update.json?status=Hello, world!")

url"https://api.twitter.com/1.1/statuses/update.json?status=Hello, world!"

"https://api.twitter.com/1.1/statuses/update.json?status=Hello, world!".toUrl

https"api.twitter.com/1.1/statuses/update.json".withQuery(Seq("status" -> "Hello, world!"))

Of course, you can use the http interpolator to declare URLs starting with the "http" scheme.

Implicit conversion from String and akka.http.scaladsl.model.Uri is also available.

val url: OHttpUrl = "https://api.twitter.com/1.1/statuses/update.json?status=Hello, world!"

Internationalized URL

Okkam HTTP fully supports internationalized URL as IRI ([RFC3987]) with IDN.

https"api.twitter.com/1.1/statuses/update.json?status=あざらし"

// Percent encoded hierarchical part

http"www.exapmle.com/あざらし/ペンギン"

/** is translated into
  *
  * http://www.exapmle.com/%E3%81%82%E3%81%96%E3%82%89%E3%81%97/%E3%83%9A%E3%83%B3%E3%82%AE%E3%83%B3
*/

// Punycoded internationalized domain name

http"日本語URL撲滅協会.jp"

/** is translated into
  *
  * http://xn--url-0y9d76qfm4ak2b0xdj60azv9d.jp
  */

Symbolic DSL

You can write an expression to make a request as

val responseFuture = request >> twitter

instead of

val responseFuture = twitter(request)

You can also combine the operator >> and future handling operations such as onComplete, foreach, map and flatMap:

twitter >> request onComplete {
  case Success(res) => println(s"${res.status}\n\n${res.utf8String}")
  case Failure(t) => throw t
}

Tweet with Media

You can upload a picture to Twitter as follows:

import java.nio.Paths

def uploadMedia(fileName: String): Future[String] = {
  // Constructs POST request of `multipart/form-data` that contains data bytes of `picture.jpg`.
  val uploadRequest = OHttpRequest.POST.Multipart.fromFile(
      https"upload.twitter.com/1.1/media/upload.json",
      "media",
      Paths.get(fileName)
    )

  val uploadFuture = twitter(uploadRequest)

  uploadFuture map { res =>
    println(s"${res.status}\n\n${res.utf8String}\n")
    extractMediaId(res)
  }
}

def extractMediaId(response: OHttpResponse): String = {
  val mediaIdOption =
    if (response.status == StatusCodes.OK) {
      /**
        * Extracts the value of 'media_id_string' from the response JSON.
        * This example employs `OAuth2.extractValueFromFlatJson` method,
        * that extracts a string or other value from 'flat' JSON,
        * i.e. it contains neither a nested object nor an array
        */
      OAuth2.extractValueFromFlatJson(response.utf8String, "media_id_string")
    } else
      None

  mediaIdOption match {
    case Some(id) => id
    case None => throw new RuntimeException("Failed to upload media.")
  }
}

You can tweet with the picture as follows:

def awesomePicture(mediaId: String) = {
  val tweetRequest = OHttpRequest.POST(
      https"api.twitter.com/1.1/statuses/update.json".withQuery(
        Seq("status" -> "an awesome picture", "media_ids" -> mediaId)
      )
    )

  val tweetFuture = twitter(tweetRequest)

  tweetFuture onComplete {
    case Success(res) => println(s"${res.status}\n\n${res.utf8String}")
    case Failure(t) => throw t
  }
}

def tweetWithPicture = {
  uploadMedia("picture.jpg") map { awesomePicture _ }
}

User Authentication

The OAuth1Client provides sufficient API for User Authentication, that is the same as OAuth 1.0a Authentication.

OAuth 1.0a Authentication consists of the following three steps.

  1. The Consumer obtains an Request Token.
  2. The User authorizes the Request Token.
  3. The Consumer exchanges the Request Token for an Access Token.

Okkam HTTP processes OAuth 1.0a Authentication as follows:

At first, create an unauthorized client:

val twitter = OHttpClient.createUnauthorizedClient(
  OAuth1.KeyPair(
    "ABC...", // Consumer Key
    "DEF..." // Consumer Secret
  ))
)

Then, make an initializing request for OAuth 1.0a Authentication steps to obtain an unauthorized Request Token that is used in the following steps. When the request results in success, the Request Token is returned by OAuth1.extractToken(response):

val requestTokenFuture: Future[TokenPair] =
  twitter.makeRequestForRequestToken(
    https"api.twitter.com/oauth/request_token"
  ) map { res =>
    extractToken(res) match {
      case Some(token) => token
      case None => throw new RuntimeException("Failed to obtain a request token.")
    }
  }

The succeeding task is to direct the User (maybe you) to open the Service Provider's (e.g. Twitter) authorization page on browser. If the User consents to grant the permission to the Consumer (you), the Service Provider will give a pincode to the User.

requestTokenFuture map { token =>
  println(s"Request Token: ${token.token}\nRequest Token Secret: ${token.secret}\n\n" +
      s"URL: https://api.twitter.com/oauth/authorize?oauth_token=${token.token}\n\n" +
      "Enter the given pincode...")
  token
}

After you successfully receive a pincode, make a final request to exchange the Request Token to an authorized Access Token. When the request results in success, you accomplish to obtain the Access Token by OAuth1.extractToken(response) as well as the Request Token:

val accessTokenFuture: Future[TokenPair] =
  twitter.makeRequestForAccessToken(
    https"api.twitter.com/oauth/access_token",
    requestToken,
    pincode
  ) map { res =>
    extractToken(res) match {
      case Some(token) => token
      case None => throw new RuntimeException("Failed to obtain a request token.")
    }
  }

accessTokenFuture onComplete {
  case Success(token) =>
    println(s"Access Token: ${token.token}\nAccess Token Secret: ${token.secret}")
  case Failure(t) => throw t
}

A complete implementation of OAuth 1.0a Authentication is available: UserAuthentication.scala

Application-only Authentication

The BasicAuthClient and OAuth2BearerClient provide sufficient API for Application-only Authentication, that employs an OAuth 2.0 Bearer Token for authorization.

To obtain a Bearer Token, create a BasicAuthClient instance with setting your Consumer Key to user and your Consumer Secret to password.

val bauth = OHttpClient(
  BasicAuth.User(
    "ABC...", // Consumer Key
    "DEF..." // Consumer Secret
  ))

val requestForToken =
  OHttpRequest.POST.Form(
    https"api.twitter.com/oauth2/token",
    List("grant_type" -> "client_credentials")
  )

val tokenFuture = bauth(requestForToken)

tokenFuture onComplete {
  case Success(res) =>
    val tokenOption =
      if (res.status == StatusCodes.OK)
        OAuth2.extractValueFromFlatJson(res.utf8String, "access_token")
      else
        None

    tokenOption match {
      case Some(token) => println("Bearer Token: " + token)
      case None => throw new RuntimeException("Failed to acquire a token.")
    }
  case Failure(t) => throw t
}

Once a BearerToken is obtained, you can access to Twitter by OAuth2BearerClient with the BearerToken.

val appOnly = OHttpClient(
    OAuth2.BearerToken(
      "AAAAA..." // Bearer Token
    ))

A complete implementation of Application-only Authentication is available: AppOnlyAuthentication.scala