Skip to content

Instantly share code, notes, and snippets.

@liquidz
Created March 3, 2010 11:02
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save liquidz/320527 to your computer and use it in GitHub Desktop.
Save liquidz/320527 to your computer and use it in GitHub Desktop.
OAuth Library for Scala
package com.uo.liquidz.http
import java.net.{URL, HttpURLConnection}
import java.io.{BufferedReader, InputStreamReader, BufferedWriter, OutputStreamWriter, IOException}
// ssl
import java.security.{KeyManagementException, NoSuchAlgorithmException, SecureRandom}
import java.security.cert.{CertificateException, X509Certificate}
import javax.net.ssl.{HttpsURLConnection, KeyManager, SSLContext, TrustManager, X509TrustManager}
import Simply._
import com.uo.liquidz.io._
trait Retryer {
val DEFAULT_RETRY_COUNT = 5
val DEFAULT_RETRY_SLEEP = 1000
def retry[A](f: => A):Option[A] = this.retry(DEFAULT_RETRY_COUNT, DEFAULT_RETRY_SLEEP)(f)
def retry[A](limit:Int)(f: => A):Option[A] = this.retry(limit, DEFAULT_RETRY_SLEEP)(f)
def retry[A](limit:Int, sleepTime:Int)(f: => A):Option[A] = {
def loop(count:Int):Option[A] = {
try{
if(count <= limit){ Some(f) } else { None }
} catch {
case _ => {
Thread.sleep(sleepTime)
loop(count + 1)
}
}
}
loop(0)
}
}
trait SimpleHttpRequest extends Retryer {
type Param = Map[String, String]
var retryCount = DEFAULT_RETRY_COUNT
def get(url:String):String
def get(url:String, headers:Param):String
def post(url:String, data:Param):String
def post(url:String, data:Param, headers:Param):String
def setRetry(n:Int) = if(n >= 0) this.retryCount = n
def setRetry(f:Boolean) = if(f == false) this.retryCount = 0
}
class DefaultSimpleHttpRequest extends SimpleHttpRequest {
def get(url:String):String = this.get(url, null)
def get(url:String, headers:Param):String = retry(this.retryCount){
this.getContentsBody(this.getHttpURLConnection(url, "GET", headers))
} match {
case Some(x) => x
case None => throw(new IOException("method: GET, url: " + url))
}
def post(url:String, data:Param):String = this.post(url, data, null)
def post(url:String, data:Param, headers:Param):String = retry(this.retryCount){
val http = this.getHttpURLConnection(url, "POST", headers)(h => h.setDoOutput(true))
SIO.writer(http.getOutputStream)(bw => {
bw.write(data.toUrlParameterString)
bw.flush
})
this.getContentsBody(http)
} match {
case Some(x) => x
case None => throw(new IOException("method: POST, url: " + url + ", data: " + data.toUrlParameterString))
}
protected def getHttpURLConnection(url:String, method:String, headers:Param)
(implicit interrupt:HttpURLConnection => Unit):HttpURLConnection = {
val u = new URL(url)
var urlConn:HttpURLConnection = null
u.getProtocol match {
case "http" => {
urlConn = u.openConnection.asInstanceOf[HttpURLConnection]
}
case "https" => {
urlConn = u.openConnection.asInstanceOf[HttpsURLConnection]
ignoreValidateCertification(urlConn.asInstanceOf[HttpsURLConnection])
}
}
urlConn.setRequestMethod(method)
urlConn.setInstanceFollowRedirects(false)
if(headers != null){
headers.foreach(h => urlConn.setRequestProperty(h._1, h._2))
}
interrupt(urlConn)
urlConn.connect
urlConn
}
protected def getContentsBody(http:HttpURLConnection):String = {
val sb = new StringBuilder(1024)
SIO.reader(http.getInputStream).foreach(line => sb.append(line + "\n"))
sb.toString
}
private def ignoreValidateCertification(httpsConn:HttpsURLConnection):Unit = {
val km:Array[KeyManager] = null
val tm:Array[TrustManager] = Array(
new X509TrustManager(){
def checkClientTrusted(arg0:Array[X509Certificate], arg1:String){}
def checkServerTrusted(arg0:Array[X509Certificate], arg1:String){}
def getAcceptedIssuers():Array[X509Certificate] = null
}
)
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(km, tm, new SecureRandom())
httpsConn.setSSLSocketFactory(sslContext.getSocketFactory)
}
}
package com.uo.liquidz.io
import java.io._
trait IOCommon {
def using[A, B<:{def close():Unit}](closable:B)(f:B => A):A =
try { f(closable) } finally { closable.close }
}
class SimpleReader(in:InputStreamReader) extends IOCommon {
def apply(index:Int):String = withReaderStream(_(index))
def apply[A](f:Stream[String] => A):A = withReaderStream(f)
def foreach(f:String => Unit):Unit = withReaderStream(_.foreach(f))
def toList():List[String] = withReaderStream(_.toList)
def withReaderStream[B](f:Stream[String] => B):B =
withReaderStream[String, B](x => x.readLine)(f)
def withReaderStream[A, B](getter:BufferedReader => A)(f:Stream[A] => B):B = {
using(new BufferedReader(in)){ br =>
f(Stream.const(() => getter(br)).map(_()).takeWhile(_ != null))
}
}
}
class SimpleWriter(out:OutputStreamWriter) extends IOCommon {
def apply[A](f:BufferedWriter => A):A = {
using(new BufferedWriter(out))(f)
}
}
object SIO {
def reader(in:InputStreamReader) = new SimpleReader(in)
def reader(in:InputStream) = new SimpleReader(new InputStreamReader(in))
def reader(path:String) = new SimpleReader(new FileReader(path))
def writer(out:OutputStreamWriter) = new SimpleWriter(out)
def writer(out:OutputStream) = new SimpleWriter(new OutputStreamWriter(out))
def writer(path:String) = new SimpleWriter(new FileWriter(path))
}
package com.uo.liquidz.oauth
import java.net.URLEncoder
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import org.apache.commons.codec.binary.Base64.encodeBase64
import Simply._
import com.uo.liquidz.http.{Retryer, SimpleHttpRequest, DefaultSimpleHttpRequest}
case class Token(token:String, tokenSecret:String)
object OAuthBase {
type Param = Map[String, String]
}
import OAuthBase._
object key { // {{{
val consumer = "oauth_consumer_key"
val secret = "oauth_secret_key"
val sigMethod = "oauth_signature_method"
val token = "oauth_token"
val tokenSecret = "oauth_token_secret"
val callback = "oauth_callback"
val nonce = "oauth_nonce"
val timestamp = "oauth_timestamp"
val ver = "oauth_version"
val sign = "oauth_signature"
val verifier = "oauth_verifier"
val method = "method"
val charset = "charset"
} // }}}
class OAuth(oauthHttpRequest:SimpleHttpRequest, params:Param) extends Retryer {
val version = "1.0"
val consumer = params("consumer")
val consumerSecret = params("consumerSecret")
var signatureMethod = params("signatureMethod", "HMAC-SHA1")
var token = params("token", "")
var tokenSecret = params("tokenSecret", "")
var method = params(key.method, "GET")
var charset = params(key.charset, "UTF-8")
// SimpleHttpRequest側のリトライはオフ
oauthHttpRequest.setRetry(false)
// =this
def this(params:Param){
this(new DefaultSimpleHttpRequest, params)
}
def this(consumer:String, consumerSecret:String){
this(Map("consumer" -> consumer, "consumerSecret" -> consumerSecret))
}
def this(oauthHttpRequest:SimpleHttpRequest, consumer:String, consumerSecret:String){
this(oauthHttpRequest, Map("consumer" -> consumer, "consumerSecret" -> consumerSecret))
}
// =getRequestToken
def getRequestToken(requestTokenUrl:String):Option[Token] =
this.getRequestToken(requestTokenUrl, null)
def getRequestToken(requestTokenUrl:String, callback:String):Option[Token] = try {
retry(DEFAULT_RETRY_COUNT){
val url = this.getSignedUrl(
requestTokenUrl,
false,
this.oauthBasicMap ++ (if(callback != null) Map(key.callback -> callback) else Map())
)
this.oauthHttpRequest.get(url)
} match {
case Some(cont) => {
val res = this.str2param(cont)
this.token = res(key.token)
this.tokenSecret = res(key.tokenSecret)
Some(Token(this.token, this.tokenSecret))
}
case None => None
}
} catch {
case _ => None
}
// =getAuthorizeUrl
def getAuthorizeUrl(authorizeUrl:String):String = if(this.token != "")
authorizeUrl + "?" + key.token + "=" + this.token else null
// =getAccessToken
def getAccessToken(accessTokenUrl:String, verifier:String):Option[Param] = try {
retry(DEFAULT_RETRY_COUNT){
val url = this.getSignedUrl(
accessTokenUrl,
true,
this.oauthBasicMap ++ Map(
key.token -> this.token,
key.verifier -> verifier
)
)
this.oauthHttpRequest.get(url)
} match {
case Some(cont) => {
val res = this.str2param(cont)
this.token = res(key.token)
this.tokenSecret = res(key.tokenSecret)
Some(res)
}
case None => None
}
} catch {
case _ => None
}
// =apiAccess
def apiAccess(url:String, params:Param):String = {
this.method match {
case "GET" => {
retry(DEFAULT_RETRY_COUNT){
this.oauthHttpRequest.get(
this.getSignedUrl(url, true,
this.oauthBasicMap ++ Map(key.token -> this.token) ++ params
),
Map("Authorization" -> "OAuth")
)
} match {
case Some(x) => x
case None => null
}
}
case "POST" => {
retry(DEFAULT_RETRY_COUNT){
this.oauthHttpRequest.post(url, params,
this.makeSignedHeader(url, params)
)
} match {
case Some(x) => x
case None => null
}
}
case _ => null
}
}
// =makeSignedHeader
def makeSignedHeader(url:String, params:Param):Param = {
val baseParam = this.oauthBasicMap + (key.token -> this.token)
val paramString = (baseParam ++ params).toUrlParameterList(this.encode).sort(_ < _).join("&").trim
val toSign = List(this.method, url, paramString).map(this.encode).join("&").trim
val header = baseParam.foldLeft(List[String]())((res, item) => {
val key = this.encode(item._1.asInstanceOf[String])
val value = this.encode(item._2.asInstanceOf[String])
(key + "=\"" + value + "\"") :: res
}).sort(_ < _).join(",").trim
Map("Authorization" -> ("OAuth " + header + "," + key.sign + "=\"" + this.getSignature(toSign, true) + "\""))
}
// =getSignedUrl
protected def getSignedUrl(url:String, useTokenSecret:Boolean, params:Param):String = {
val paramString = params.toUrlParameterList(this.encode).sort(_ < _).join("&").trim
val toSign = List(this.method, url, paramString).map(this.encode).join("&").trim
url + "?" + paramString + "&" + key.sign + "=" + this.getSignature(toSign, useTokenSecret)
}
// =getSignature
protected def getSignature(base:String, useTokenSecret:Boolean):String = {
val key = List(this.consumerSecret, if(useTokenSecret) this.tokenSecret else "").join("&")
val keySpec = new SecretKeySpec(key.getBytes(this.charset), this.getHmacAlgorithm)
val mac = Mac.getInstance(this.getHmacAlgorithm)
mac.init(keySpec)
this.encode(new String(encodeBase64(mac.doFinal(base.getBytes(this.charset)))))
}
// =getHmacAlgorithm
protected def getHmacAlgorithm = this.signatureMethod match {
case "HMAC-SHA1" => "HmacSHA1"
case "HMAC-SHA256" => "HmacSHA256"
}
// =oauthBasicMap
protected def oauthBasicMap = Map(
key.consumer -> this.consumer,
key.nonce -> this.nonce,
key.sigMethod -> this.signatureMethod,
key.timestamp -> this.timestamp,
key.ver -> this.version
)
protected def encode(str:String):String =
URLEncoder.encode(str).replace("+", "%20").replace("*", "%2A").replace("%7E", "~").replace("%2B", "%20")
protected def str2param(str:String):Param = {
str.split("&").foldLeft(Map[String, String]())((res, item) => {
val kv = item.split("=")
if(kv.length == 2) res + (kv(0) -> kv(1)) else res
})
}
private def timestamp = (System.currentTimeMillis / 1000).toString
private def nonce = System.nanoTime.toString
}
package com.uo.liquidz
import scala.runtime.RichString
import scala.xml.{XML, NodeSeq, Node}
import java.net.URLEncoder
class ExtendedList[A](ls:List[A]){ // {{{
def join(deli:String):String = {
if(ls.isEmpty) "" else ls.foldLeft("")(_ + deli + _.toString).substring(deli.length)
}
} // }}}
class ExtendedString(str:String){ // {{{
val len = str.length
def println:String = {Predef.println(str); str}
def takeRight(n:Int):String = str.substring(len - n, len)
def dropRight(n:Int):String = str.substring(0, len - n)
} // }}}
class ExtendedMap[A, B](m:Map[A, B]){ // {{{
def apply(key:A, default:B):B = get(key, default).get
def get(key:A, default:B):Option[B] = if(m.contains(key)) m.get(key) else Some(default)
def toUrlParameterList(encoder:String => String)(implicit interrupt:String => String):List[String] = {
m.foldLeft(List[String]())((res, x) => {
interrupt(encoder(x._1.toString) + "=" + encoder(x._2.toString)) :: res
}).reverse
}
def toUrlParameterList()(implicit interrupt:String => String):List[String] =
this.toUrlParameterList(URLEncoder.encode)(interrupt)
def toUrlParameterString(encoder:String => String):String = {
new ExtendedList(this.toUrlParameterList(encoder).reverse).join("&")
}
def toUrlParameterString:String = this.toUrlParameterString(URLEncoder.encode)
} // }}}
class ExtendedNodeSeq(ns:NodeSeq){ // {{{
def apply(path:String):NodeSeq = this.q(path)
def q(path:String*):NodeSeq = path.foldLeft(ns)((res, x) => res \ x)
def qq(path:String*):NodeSeq = path.foldLeft(ns)((res, x) => res \\ x)
} // }}}
object Simply {
implicit def list2extendedList[A](ls:List[A]) = new ExtendedList(ls)
implicit def string2extendedString(str:String) = new ExtendedString(str)
implicit def richString2extendedString(str:RichString) = new ExtendedString(str.toString)
implicit def map2extendedMap[A, B](m:Map[A, B]) = new ExtendedMap(m)
implicit def nodeSeq2extendedNodeSeq(ns:NodeSeq) = new ExtendedNodeSeq(ns)
implicit def nodeSeq2string(ns:NodeSeq):String = ns.text
}
package com.uo.liquidz.oauth
import java.net.URLEncoder
import scala.xml.{XML, Elem, NodeSeq, Node}
import com.uo.liquidz.http.{SimpleHttpRequest, DefaultSimpleHttpRequest}
import OAuthBase._
import Simply._
case class TwitterAuthorizeResult(token:String, tokenSecret:String, url:String)
case class TwitterAccessResult(token:String, tokenSecret:String, userId:String, screenName:String)
case class TwitterUser(
id:String, name:String, screenName:String,
location:String, description:String, profileImage:String
)
case class Tweet(
created_at:String, id:String, text:String, replyId:String,
replyUser:String, favorited:Boolean, user:TwitterUser
)
class Twitter(oauthHttpRequest:SimpleHttpRequest, params:Param) extends OAuth(oauthHttpRequest, params){
private val requestTokenUrl = "http://twitter.com/oauth/request_token"
private val authorizeUrl = "http://twitter.com/oauth/authorize"
private val accessTokenUrl = "http://twitter.com/oauth/access_token"
def this() = this(new DefaultSimpleHttpRequest, Map("consumer" -> "", "consumerSecret" -> ""))
def this(params:Param) = this(new DefaultSimpleHttpRequest, params)
def this(consumer:String, consumerSecret:String) =
this(new DefaultSimpleHttpRequest, Map("consumer" -> consumer, "consumerSecret" -> consumerSecret))
def this(oauthHttpRequest:SimpleHttpRequest, consumer:String, consumerSecret:String) =
this(oauthHttpRequest, Map("consumer" -> consumer, "consumerSecret" -> consumerSecret))
// =authorize
def authorize:Option[TwitterAuthorizeResult] = this.authorize(null)
def authorize(callback:String):Option[TwitterAuthorizeResult] = {
super.getRequestToken(this.requestTokenUrl, callback) match {
case Some(Token(t, s)) => Some(TwitterAuthorizeResult(t, s, this.getAuthorizeUrl))
case None => None
}
}
def getRequestToken:Option[Token] = this.getRequestToken(null)
override def getRequestToken(callback:String):Option[Token] = {
super.getRequestToken(this.requestTokenUrl, callback)
}
def getAuthorizeUrl = super.getAuthorizeUrl(this.authorizeUrl)
// =access
def access(verifier:String):Option[TwitterAccessResult] = {
super.getAccessToken(this.accessTokenUrl, verifier) match {
case Some(m) => {
if(m.contains("user_id") && m.contains("screen_name")){
Some(TwitterAccessResult(
m(key.token),
m(key.tokenSecret),
m("user_id"),
m("screen_name")
))
} else {
None
}
}
case None => None
}
}
def update(status:String):String = {
this.withMethod[String]("POST"){
this.apiAccess("http://api.twitter.com/1/statuses/update.xml", Map(
"status" -> status
))
}
}
def home:List[Tweet] = this.home(Map("page" -> "1"))
def home(params:Param):List[Tweet] = {
this.withMethod[List[Tweet]]("GET"){
var result = List[Tweet]()
val res = this.apiAccess("http://api.twitter.com/1/statuses/home_timeline.xml", params)
XML.loadString(res).qq("status").map(this.getStatus).toList
}
}
def search(query:String):List[Tweet] = this.search(Map("q" -> query))
def search(params:Param):List[Tweet] = {
val xml = XML.loadString(
oauthHttpRequest.get(
"http://search.twitter.com/search.atom?" + params.toUrlParameterString(x =>
URLEncoder.encode(x).replace("%2B", "+").replace("%3D", "%3A")
)
)
)
xml.qq("entry").map(entry => {
val name = entry.q("author", "name").text
Tweet(
entry.q("published").text,
entry.q("id").split(":")(2),
entry.q("title").text,
"", "", false,
TwitterUser("",
name.drop(name.indexOf("(") + 1).dropRight(1),
name.take(name.indexOf("(") - 1),
entry.q("twitter:geo").text,
"",
entry.q("link").filter(_("@type") == "image/png")(0)("@href").text
)
)
}).toList
}
protected def getUserInfo(xml:NodeSeq):TwitterUser = {
TwitterUser(
xml("id").text,
xml("name").text,
xml("screen_name").text,
xml("location").text,
xml("description").text,
xml("profile_image_url").text
)
}
protected def getStatus(xml:NodeSeq):Tweet = {
Tweet(
xml("created_at").text,
xml("id").text,
xml("text").text,
xml("in_reply_to_status_id").text,
xml("in_reply_to_user_id").text,
if(xml("favorited") == "true") true else false,
this.getUserInfo(xml("user")(0))
)
}
private def withMethod[A](method:String)(f: => A):A = {
val _method = this.method
this.method = method
val res = f
this.method = _method
res
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment