Permalink
Browse files

updated twine as plain app

  • Loading branch information...
0 parents commit 68f8a7cb7664520098315aed54e4f788a40aa0c5 @n8han committed Dec 4, 2010
Showing with 201 additions and 0 deletions.
  1. +3 −0 .gitignore
  2. +34 −0 README
  3. +7 −0 project/build.properties
  4. +7 −0 project/build/TwineProject.scala
  5. +150 −0 src/main/scala/twine/Twine.scala
3 .gitignore
@@ -0,0 +1,3 @@
+target
+lib_managed
+project/boot
34 README
@@ -0,0 +1,34 @@
+Databinder Twine
+================
+
+This is a sample application for Databinder Dispatch[1], built and packaged
+by simple-build-tool[2]. If you've run this from the self extracting installer,
+you should have a twine project directory with a `twine` script inside it.
+
+You can run the `twine` script from anywhere; it might be fun to put it on your
+executable search path. The script refers fully to the project path in its
+classpath, so if you move the project you'll need to regenerate the script by
+running `sbt script` in the project directory. Here's what it looks like to
+run twine from inside twine/
+
+ ./twine
+ # opens browser or prints url to grant access, depending on JVM ability
+ ./twine <pin>
+
+ # pass in the PIN that Twitter assigns, without brackets
+ ./twine I love to tweet from terminal\!
+
+ # print timeline to stdout and stay open, polling for new tweets
+ ./twine
+
+ # remove oauth credentials from ~/.twine
+ ./twine reset
+
+If you'd like to see how Twine ticks, its Scala X-Ray[3] formatted source
+is the best place to start. See: target/classes.sxr/Twine.scala.html
+
+Happy twails.
+
+[1]:http://databinder.net/dispatch/
+[2]:http://code.google.com/p/simple-build-tool/
+[3]:http://github.com/harrah/browse/tree/master
7 project/build.properties
@@ -0,0 +1,7 @@
+#Project Properties
+#Mon Jul 13 23:24:31 EDT 2009
+project.organization=net.databinder
+project.name=twine
+sbt.version=0.7.4
+build.scala.versions=2.8.1
+project.version=1.0-SNAPSHOT
7 project/build/TwineProject.scala
@@ -0,0 +1,7 @@
+import sbt._
+import Process._
+
+class TwineProject(info: ProjectInfo) extends DefaultProject(info) {
+ val configgy = "net.lag" % "configgy" % "2.0.0" intransitive()
+ val dispatch = "net.databinder" %% "dispatch-twitter" % "0.7.8-SNAPSHOT"
+}
150 src/main/scala/twine/Twine.scala
@@ -0,0 +1,150 @@
+/***************~~~~~~~~~~~~~~~~~~~SCALA~~~~~~~~~~~~~~~~~~~***************\
+ *************** ***************
+ *************** Twine, a command line Twitter client! ***************
+ *************** ***************
+\***************~~~~~~~~~~~~~~~~~~~ALACS~~~~~~~~~~~~~~~~~~~************* */
+
+// this is one way to declare your packages in Scala
+package dispatch {
+ // the three imports below use the current `dispatch` scope!
+ import json.JsHttp._
+ import oauth._
+ import twitter._
+
+ package twine {
+ // this singleton object is the application
+ object Twine {
+ // import and nickname Configgy's main access object
+ import _root_.net.lag.configgy.{Configgy => C}
+ // import all the methods, including implicit conversions, defined on dispatch.Http
+ import Http._
+
+ // this will be our datastore
+ val conf = new java.io.File(System.getProperty("user.home"), ".twine.conf")
+ // OAuth application key, top-secret
+ val consumer = Consumer("lrhF8SXnl5q3gFOmzku4Gw", "PbB4Mr8pKAChWmd6AocY6gLmAKzPKaszYnXyIDQhzE")
+ // one single-threaded http access point, please!
+ val http = new Http
+
+ // ---BY YOUR COMMAND---
+ def main(args: Array[String]) {
+ // create config file if it doesn't exist
+ conf.createNewFile()
+ // read config file to C.config
+ C.configure(conf.getPath)
+
+ // This is it, people. All paths return to println with a message for the user,
+ // except `cat` which doesn't. We're going to pattern-match against both the
+ // parameter sequence and a Token that is either there or not there. The
+ // dispatch.oauth.Token(m: Map[...]) method knows about maps with token keys
+ // in them. If these are present under "access", we'll get Some(token)
+
+ println( (args, Token(C.config.configMap("access").asMap)) match {
+ // the only parameter was "reset"; ignore the token and delete the data store
+ case (Array("reset"), _) => conf.delete(); "OAuth credentials deleted."
+ // there are no parameters, but we have a token! Go into `cat`, forever.
+ case (Array(), Some(tok)) => cat(tok, None)
+ // there are some parameters and a token, combine parameters and...
+ case (args, Some(tok)) => (args mkString " ") match {
+ // dang tweet is too long
+ case tweet if tweet.length > 140 =>
+ "%d characters? This is Twitter not NY Times Magazine." format tweet.length
+ // it looks like an okay tweet, let us post it:
+ case tweet => http(Status.update(tweet, consumer, tok) ># { js =>
+ // handling the Status.update response as JSON, we take what we want
+ val Status.user.screen_name(screen_name) = js
+ val Status.id(id) = js
+ // this goes back to our user
+ "Posted: " + (Twitter.host / screen_name / "status" / id.toString to_uri)
+ })
+ }
+ // there was no access token, we must still be in the oauthorization process
+ case _ => get_authorization(args)
+ })
+ }
+ // this `cat` infinitely tail-recurses, MEOW
+ def cat(tok: Token, last_id: Option[BigDecimal]) {
+ // get us some tweets
+ val tweets = http(
+ // so...... FOLD LEFT. `(a /: b)` Since b is an Option, we end up either
+ // with the starting value `a` or a single application of the given function
+ // to it, `a.since_id(b)` which adds that parameter to the request.
+ (Status.friends_timeline(consumer, tok) /: last_id) { _ since_id _ }
+ )
+ // print Twitter in chronological order (HERESY)
+ tweets.reverse foreach { js =>
+ // the tweets were JSON btw
+ val Status.user.screen_name(screen_name) = js
+ val Status.text(text) = js
+ println("%-15s%s" format (screen_name, Status.rebracket(text)) )
+ }
+ // nap time!
+ Thread sleep 60000
+ // use the latest status id if there was one, otherwise the last one
+ cat(tok, tweets.headOption map { Status.id(_) } orElse last_id)
+ }
+ // oauth sesame
+ def get_authorization(args: Array[String]) = {
+ // this time we are matching against a potential request token
+ ((args, Token(C.config.configMap("request").asMap)) match {
+ // one parameter that must be the verifier, and there's a request token
+ case (Array(verifier), Some(tok)) => try {
+ // exchange it for an access token
+ http(Auth.access_token(consumer, tok, verifier)) match {
+ case (access_tok, _, screen_name) =>
+ // nb: we're producing a message, a token type name, and the token itself
+ ("Approved! It's tweetin' time, %s." format screen_name, Some(("access", access_tok)))
+ } } catch {
+ // accidents happen
+ case StatusCode(401, _) =>
+ // no token for you
+ ("Rats! That PIN %s doesn't seem to match." format verifier, None)
+ }
+ // there wasn't a parameter so who cares if we have a request token, just get a new one
+ case _ =>
+ // a request token for the Twine application, kthxbai
+ val tok = http(Auth.request_token(consumer))
+ // generate the url the user needs to go to, to grant us access
+ val auth_uri = Auth.authorize_url(tok).to_uri
+ (( try {
+ // try to open it with the fancy desktop integration stuff,
+ // using reflection so we can still compile on Java 5
+ val dsk = Class.forName("java.awt.Desktop")
+ dsk.getMethod("browse", classOf[java.net.URI]).invoke(
+ dsk.getMethod("getDesktop").invoke(null), auth_uri
+ )
+ "Accept the authorization request in your browser, for the fun to begin."
+ } catch {
+ // THAT went well. We'll just have to pass on that link to be printed.
+ case _ =>
+ "Open the following URL in a browser to permit this application to tweet 4 u:\n%s".
+ format(auth_uri.toString)
+ }) + // so take one of those two messages and a general message
+ "\n\nThen run `twine <pin>` to complete authorization.\n",
+ // and the request token that we got back from Twitter
+ Some(("request", tok)))
+ }) match // against the tuple produced by the last match
+ {
+ // no token, just a message to be printed up in `main`
+ case (message, None) => message
+ // a token of some kind: we should save this in the datastore perhaps
+ case (message, Some((name, tok))) =>
+ val conf_writer = new java.io.FileWriter(conf)
+ // let us also take this opportunity to set the log level
+ conf_writer write (
+ """ |<log>
+ | level = "WARNING"
+ |</log>
+ |<%s>
+ | oauth_token = "%s"
+ | oauth_token_secret = "%s"
+ |</%s>""".stripMargin format (name, tok.value, tok.secret, name)
+ )
+ conf_writer.close
+ message // for you sir!
+ }
+ } // get_authorization
+ } // Twine
+ } // twine
+} // dispatch
+ // earth

0 comments on commit 68f8a7c

Please sign in to comment.