Created
March 3, 2010 11:02
-
-
Save liquidz/320527 to your computer and use it in GitHub Desktop.
OAuth Library for Scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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