joewiz
7/4/2013 - 7:34 PM

Access OAuth 1.0-based services like the Twitter v1.1 API, with XQuery. (See comments below for explanation.)

Access OAuth 1.0-based services like the Twitter v1.1 API, with XQuery. (See comments below for explanation.)

xquery version "3.0";

(:~ A main module to retrieve the user timeline. :)

import module namespace twitter = "http://history.state.gov/ns/xquery/twitter" at "twitter.xq";
import module namespace twitter-client = "http://history.state.gov/ns/xquery/twitter-client" at "twitter-client.xq";

(: Parameters needed for the user timeline function. :)

let $user-id := ()
let $screen-name := ()
let $since-id := ()
let $count := 10
let $max-id := ()
let $trim-user := true()
let $exclude-replies := false()
let $contributor-details := false()
let $include-rts := false()
let $request-response := 
    twitter:user-timeline(
        $twitter-client:consumer-key, 
        $twitter-client:consumer-secret, 
        $twitter-client:access-token, 
        $twitter-client:access-token-secret, 
        $user-id, 
        $screen-name, 
        $since-id, 
        $count, 
        $max-id, 
        $trim-user, 
        $exclude-replies, 
        $contributor-details, 
        $include-rts
    )
return
    (: Echo the response so we can see the request, the response header, the response body (JSON), and the XML version of the JSON :)
    twitter-client:echo-response($request-response)
xquery version "3.0";

module namespace twitter="http://history.state.gov/ns/xquery/twitter";

(:~ A library module for Twitter API methods. 

    @see https://dev.twitter.com/docs/api/1.1
 :)

import module namespace oauth="http://history.state.gov/ns/xquery/oauth" at "oauth.xq";

declare variable $twitter:api-base-uri := 'https://api.twitter.com/1.1';

(:
    Get the user timeline.
    See https://dev.twitter.com/docs/api/1.1/get/statuses/user_timeline
:)
declare function twitter:user-timeline(
        $consumer-key as xs:string, 
        $consumer-secret as xs:string, 
        $access-token as xs:string, 
        $access-token-secret as xs:string, 
        $user-id as xs:string?,
        $screen-name as xs:string?,
        $since-id as xs:unsignedLong?, (: IDs are too big for xs:integer :)
        $count as xs:integer?,
        $max-id as xs:unsignedLong?,
        $trim-user as xs:boolean?,
        $exclude-replies as xs:boolean?,
        $contributor-details as xs:boolean?,
        $include-rts as xs:boolean? 
        ) {
    let $api-method := '/statuses/user_timeline.json'
    let $http-method := 'GET'
    let $query-string := 
        string-join(
            (
            if ($user-id) then concat('user_id=', $user-id) else (),
            if ($screen-name) then concat('screen_name=', $screen-name) else (),
            if ($since-id) then concat('since_id=', $since-id) else (),
            if ($count) then concat('count=', $count) else (),
            if ($max-id) then concat('max_id=', $max-id) else (),
            if ($trim-user) then concat('trim_user=', $trim-user) else (),
            if ($exclude-replies) then concat('exclude_replies=', $exclude-replies) else (),
            if ($contributor-details) then concat('contributor_details=', $contributor-details) else (),
            if ($include-rts) then concat('include_rts=', $include-rts) else ()
            ),
            '&'
        )
    let $api-url := concat($twitter:api-base-uri, $api-method, '?', $query-string)
    return
        oauth:send-request(
            $consumer-key, 
            $consumer-secret,
            $access-token,
            $access-token-secret,
            $http-method,
            $api-url,
            oauth:nonce(),
            $oauth:signature-method, 
            oauth:timestamp(), 
            $oauth:oauth-version
        )
};
xquery version "3.1";

module namespace twitter-client = "http://history.state.gov/ns/xquery/twitter-client";

(:~ A library module for your application's Twitter credentials and any helper functions for processing 
    the raw results of Twitter requests. You can get your credentials from https://dev.twitter.com/apps/.
    Twitter responds with JSON; despite being text, the HTTP Client returns JSON as binary, so we need
    util:binary-to-text() to get the text.  We use XQuery 3.1 to turn the JSON into XML.

    @see http://exist-db.org/exist/apps/fundocs/view.html?uri=http://exist-db.org/xquery/util
    @see http://github.com/joewiz/xqjson
 :)

import module namespace twitter = "http://history.state.gov/ns/xquery/twitter" at "twitter.xq";
import module namespace util = "http://exist-db.org/xquery/util";
import module namespace ju = "http://joewiz.org/ns/xquery/json-util" at "https://gist.githubusercontent.com/joewiz/d986da715facaad633db/raw/12692fcd025e4a0572d6b7638577fa41f4a8166a/json-util.xqm";

declare variable $twitter-client:consumer-key := ''; (: insert your credentials :)
declare variable $twitter-client:consumer-secret := '';
declare variable $twitter-client:access-token := '';
declare variable $twitter-client:access-token-secret := '';

declare function twitter-client:echo-response($request-response as item()+) {
    let $request := $request-response[1]
    let $response-head := $request-response[2]
    let $response-body := $request-response[3]
    let $json := parse-json(util:binary-to-string($response-body))
    let $xml := ju:json-to-xml($json)
    return 
        (
        $request, 
        $response-head, 
        ju:serialize-json($json),
        $xml
        )
};
xquery version "3.0";

module namespace oauth="http://history.state.gov/ns/xquery/oauth";

(:~ A library module for signing and submitting OAuth requests such as the kind needed for the Twitter v1.1 API.

    The EXPath Crypto library supplies the HMAC-SHA1 algorithm. The EXPath HTTP Client library makes the HTTP requests.
    The OAuth standard requires a "nonce" parameter - a random string.  Since there is no implementation-independent
    nonce function in XQuery, we must rely on implementation-specific functions.  For eXist-db we use util:uuid().

    @see http://oauth.net/core/1.0/
    @see http://tools.ietf.org/html/rfc5849
    @see http://marktrapp.com/blog/2009/09/17/oauth-dummies
    @see http://expath.org/spec/http-client
    @see http://expath.org/spec/crypto 
    @see http://exist-db.org/exist/apps/fundocs/view.html?uri=http://exist-db.org/xquery/util
 :)

import module namespace crypto="http://expath.org/ns/crypto";
import module namespace http="http://expath.org/ns/http-client";
import module namespace util = "http://exist-db.org/xquery/util";

declare function oauth:send-request(
    $consumer-key, 
    $consumer-secret,
    $access-token,
    $access-token-secret,
    $method,
    $url,
    $nonce,
    $signature-method, 
    $timestamp, 
    $version
    ) {
        let $base-url := if (contains($url, '?')) then substring-before($url, '?') else $url
        let $query-string := if (contains($url, '?')) then substring-after($url, '?') else ()
        let $query-string-params :=
            for $param in tokenize($query-string, '&')
            let $name := substring-before($param, '=')
            let $value := substring-after($param, '=')
            return
                <param name="{$name}" value="{$value}"/>
        let $params := 
            (
            $query-string-params,
            <param name="oauth_consumer_key" value="{$consumer-key}"/>,
            <param name="oauth_nonce" value="{$nonce}"/>,
            <param name="oauth_signature_method" value="{$signature-method}"/>,
            <param name="oauth_timestamp" value="{$timestamp}"/>,
            <param name="oauth_token" value="{$access-token}"/>,
            <param name="oauth_version" value="{$version}"/>
            )
        let $parameter-string := oauth:params-to-oauth-string($params, '&amp;')
        let $signature-base-string := 
            string-join(
                (
                upper-case($method),
                encode-for-uri($base-url),
                encode-for-uri($parameter-string)
                )
                ,
                '&amp;'
            )
        let $signing-key := concat(encode-for-uri($consumer-secret), '&amp;', encode-for-uri($access-token-secret))
        let $oauth-signature := crypto:hmac($signature-base-string, $signing-key, 'HmacSha1', 'base64')
        let $final-params := 
            (
            $params, 
            <param name="oauth_signature" value="{$oauth-signature}"/>
            )
        let $final-parameter-string := oauth:params-to-oauth-string($final-params, ', ')
        let $authorization-header-value := concat('OAuth ', $final-parameter-string)
        let $request := 
            <http:request href="{$url}" method="{$method}">
                <http:header name="Authorization" value="{$authorization-header-value}"/>
            </http:request>
        let $response := http:send-request($request)
        return 
            (
            $request
            ,
            $response
            )
};

declare function oauth:nonce() { util:uuid() };

declare variable $oauth:signature-method := 'HMAC-SHA1';

declare variable $oauth:oauth-version := '1.0';

(: Generates an OAuth timestamp, which takes the form of the number of seconds since the Unix Epoch.
   You can test these values against http://www.epochconverter.com/.
   @see http://en.wikipedia.org/wiki/Unix_time
 :)
declare function oauth:timestamp() as xs:unsignedLong {
    let $unix-epoch := xs:dateTime('1970-01-01T00:00:00Z')
    let $now := current-dateTime()
    let $duration-since-epoch := $now - $unix-epoch
    let $seconds-since-epoch :=
        days-from-duration($duration-since-epoch) * 86400 (: 60 * 60 * 24 :)
        +
        hours-from-duration($duration-since-epoch) * 3600 (: 60 * 60 :)
        +
        minutes-from-duration($duration-since-epoch) * 60
        +
        seconds-from-duration($duration-since-epoch)
    return
        xs:unsignedLong($seconds-since-epoch)
};

(: prepares OAuth authentication parameters :)
declare function oauth:params-to-oauth-string($params as element(param)+, $separator as xs:string) {
    string-join(
        for $param in $params
        let $name := encode-for-uri($param/@name)
        let $value := encode-for-uri($param/@value)
        order by $name, $value
        return
            concat($name, '=', $value)
        ,
        $separator
    )
};