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
]
]
]