REBOL [ Title: "Etsy Client for REBOL" Date: 16-Sep-2012 Author: "Christopher Ross-Gill" Version: 0.1.4 Rights: [ http://creativecommons.org/licenses/by-sa/3.0/ "If you are using this commercially, please consider a donation." ] File: %etsy.r Purpose: "REBOL script to access and utilize the Etsy OAuth API" Usage: http://re-bol.com/etsy_api_tutorial.html Settings: [ ; use if not working with 'do/args Consumer-Key: <consumer-key> Consumer-Secret: <consumer-secret> User-Store: <path-to-saved-users> Scope: [] Sandbox: none ] ] do http://reb4.me/r/etsy-http-hack do http://reb4.me/r/altwebform do http://reb4.me/r/altjson etsy: context bind [ as: func [ [catch] "Set current user" name [string!] "Etsy user name" /local user ][ either user: select users name [ persona: make persona user persona/name ][ either not error? user: try [register][ persona/id: persona/name: name repend users [ name new-line/skip/all third user true 2 ] persona/name ][throw :user] ] ] save-users: func [ "Saves authorized users" /to location [file! url!] "Alternate Storage Location" ][ location: any [location settings/user-store] unless any [file? location url? location][ make error! "No Storage Location Provided" ] save/header location new-line/skip/all users true 2 context [ Title: "Etsy Authorized Users" Date: now/date ] ] authorized-users: func ["Lists authorized users"][extract users 2] myself: echo: func ["Obtain User Profile" [catch]][ unless persona/name error/credentials either attempt [ result: send/with 'get %users/__SELF__ none ] load-result error/connection ] api-call: call: func [ "Open API call" [catch] 'method [word!] action [file!] /with "Includes Parameters with the request" params [none! object! block!] /raw "Returns unloaded response" ][ if block? params [params: context params] either attempt [ result: send/with :method :action :params ] either raw [[result]][load-result] error/connection ] api-call-raw: func [ "Open API call, Not Parsed" [catch] 'method [word!] action [file!] /with "Includes Parameters with the request" params [none! object! block!] ][ if block? params [params: context params] either attempt [ result: send/with :method :action :params ][result] error/connection ] listings: uses [ "Show Recent Listings" limit: 5 offset: 0 min_price: max_price: none ][ either attempt [ result: send/with 'get %listings/active self ] load-result error/connection ] categories: func [[catch]][ either attempt [ result: send/with 'get %taxonomy/categories context [limit: 2] ] load-result error/connection ] create-listing: func [ [catch] quantity [integer!] title [string!] description [string!] price [money!] category [integer!] who [string!] supply [none! logic!] when [string! integer! date!] /local listing result ][ unless persona/name error/credentials listing: context [quantity: title: description: price: category_id: who_made: is_supply: when_made: none] listing/quantity: min 1 quantity listing/title: trim title listing/description: trim description listing/price: find/tail form price "$" listing/category_id: category listing/who_made: switch/default who [ "collective" ["collective"] "someone_else" "someone else" ["someone_else"] ]["i_did"] listing/is_supply: either supply [1][0] listing/when_made: case/all [ string? when ["made_to_order"] date? when [when: when/year] integer? when [ any [ case [ when > 2009 ["2010_2012"] when > 1999 ["2000_2009"] when > 1992 ["1993_1999"] when < 1993 ["before_1993"] ] "made_to_order" ] ] ] either attempt [ result: send/with 'post %/listings listing ] load-result error/connection ] ] context [ ; internals result: none load-result: [load-json result] ; settings settings: make context [ consumer-key: consumer-secret: user-store: sandbox: none scope: [] ] any [ system/script/args system/script/header/settings ] etsy: either settings/sandbox [ http://sandbox.openapi.etsy.com/v2/ ][ http://openapi.etsy.com/v2/ ] users: any [attempt [load settings/user-store] []] persona: context [ id: name: none token: secret: none ] ; helpers uses: func [[catch] proto [block!] spec [block!] /local header][ unless string? header: take proto [make error! "No Header"] proto: context proto func compose [(header) [catch] args [block! object!]] compose/only [ args: make (proto) args do bind (spec) args ] ] ; errors get-http-response: func ["HTTP Hack" port [none! port!]][ port: any [port system/schemes/http] reform next parse do bind [any [response-line "No No Error Found"]] last second get in port/handler 'open none ] raise: use [error][ unless in system/error 'etsy [system/error: make system/error [etsy: none]] error: system/error/etsy: context [ code: 4466874 ; checksum http://www.etsy.com/ type: "Etsy Error" message: none ] func [result [object! string! block!]][ case [ object? result [result: result/message] block? result [result: rejoin result] ] error/message: result throw make error! [etsy message] ] ] error: [ credentials [raise "User must be authorized to use this application"] connection [raise get-http-response none] ] ; HTTP/OAUTH oauth!: context [ oauth_callback: none oauth_consumer_key: settings/consumer-key oauth_token: oauth_nonce: none oauth_signature_method: "HMAC-SHA1" oauth_timestamp: none oauth_version: 1.0 oauth_verifier: oauth_signature: none ] send: use [make-nonce timestamp sign][ make-nonce: does [ enbase/base checksum/secure join now/precise settings/consumer-key 64 ] timestamp: func [/for date [date!]][ date: any [date now] date: form any [ attempt [to integer! difference date 1-Jan-1970/0:0:0] date - 1-Jan-1970/0:0:0 * 86400.0 ] clear find/last date "." date ] sign: func [ method [word!] lookup [url!] oauth [object! none!] params [object! none!] /local out ][ out: copy "" oauth: any [oauth make oauth! []] oauth/oauth_nonce: make-nonce oauth/oauth_timestamp: timestamp oauth/oauth_token: persona/token params: make oauth any [params []] params: sort/skip third params 2 oauth/oauth_signature: enbase/base checksum/secure/key rejoin [ uppercase form method "&" url-encode form lookup "&" url-encode replace/all to-webform params "+" "%20" ] rejoin [ settings/consumer-secret "&" any [persona/secret ""] ] 64 foreach [header value] third oauth [ if value [ repend out [", " form header {="} url-encode form value {"}] ] ] join "OAuth" next out ] send: func [ [catch] method [word!] lookup [file!] /auth oauth [object!] /with params [object! none!] ][ lookup: etsy/:lookup oauth: any [oauth make oauth! []] switch method [ put delete [ params: make context compose [method: (uppercase form method)] any [params []] method: 'post ] ] switch method [ get [ method: compose/deep [ header [Authorization: (sign 'get lookup oauth params)] ] if params [ params: context sort/skip third params 2 append lookup to-webform/prefix params ] ] post put delete [ method: compose/deep [ (method) (either params [to-webform params][""]) [ Authorization: (sign method lookup oauth params) Content-Type: "application/x-www-form-urlencoded" ] ] ] ] lookup: attempt [read/custom lookup method] ] ] register: use [request-broker access-broker][ request-broker: %oauth/request_token access-broker: %oauth/access_token func [ /requester request [function!] /local response verifier verification-page ][ request: any [:request :ask] set persona none response: load-webform send/auth/with 'post request-broker make oauth! [ oauth_callback: "oob" ] context [ scope: form settings/scope ] unless all [ find response 'login_url string? response/login_url parse response/login_url ["http" opt "s" "://" to end] url? load response/login_url ][ make error! "Handshake Response does not contain Login URL" ] persona/token: response/oauth_token persona/secret: response/oauth_token_secret browse response/login_url unless verifier: request "Enter your PIN from Etsy: " [ make error! "Not a valid PIN" ] response: load-webform send/auth 'post access-broker make oauth! [ oauth_verifier: trim/all verifier ] persona/token: response/oauth_token persona/secret: response/oauth_token_secret persona ] ] ]