#lang racket
(require xml
(planet gh/http/request)
(planet gh/http/head)
"util.rkt"
"keys.rkt"
"exn.rkt"
"post.rkt"
)
(define sns-endpoint (make-parameter
(endpoint "sns.us-east-1.amazonaws.com" #f)))
(provide sns-endpoint)
(define/contract/provide (sns params [result-proc values])
(((listof (list/c symbol? string?)))
((xexpr? . -> . list?)) . ->* .
list?)
(ensure-have-keys)
(define common-params
`((AWSAccessKeyId ,(public-key))
(SignatureMethod "HmacSHA1")
(SignatureVersion "2")
(Timestamp ,(timestamp))
(Version "2010-03-31")))
(define all-params (sort (append params common-params)
(lambda (a b)
(string<? (symbol->string (car a))
(symbol->string (car b))))))
(define str-to-sign
(string-append "GET" "\n"
(endpoint->host:port (sns-endpoint)) "\n"
"/" "\n"
(dict->form-urlencoded all-params)))
(define signature (sha1-encode str-to-sign))
(define signed-params (append all-params `((Signature ,signature))))
(define qp (dict->form-urlencoded signed-params))
(define uri (endpoint->uri (sns-endpoint) (string-append "/?" qp)))
(define x
(call/input-request
"1.1" "GET" uri '()
(lambda (in h)
(define e (read-entity/xexpr in h))
(match (extract-http-code h)
[200 e]
[else (raise (header&response->exn:fail:aws
h e (current-continuation-marks)))]))))
(append (result-proc x)
(match (tags x 'NextToken)
[(list `(NextToken () ,token))
(sns (set-next-token params token)
result-proc)]
[else '()])))
(define/contract/provide (create-topic name)
(string? . -> . string?)
(first (sns `((Action "CreateTopic")
(Name ,name))
(lambda (x) (map third (tags x 'TopicArn))))))
(define/contract/provide (delete-topic arn)
(string? . -> . any)
(void (sns `((Action "DeleteTopic")
(TopicArn ,arn)))))
(define/contract/provide (get-topic-attributes arn)
(string? . -> . (listof (cons/c symbol? string?)))
(sns `((Action "GetTopicAttributes")
(TopicArn ,arn))
(lambda (x)
(for/list ([x (in-list (tags x 'entry))])
(match x
[(list 'entry '() junk ...
(list 'key '() key)
junk ...
(list 'value '() val ...)
junk ...)
(cons (string->symbol key) (string-join val ""))]
[else (cons 'bad "val")])))))
(define/contract/provide (list-topics)
(-> (listof string?))
(sns `((Action "ListTopics"))
(lambda (x) (map third (tags x 'TopicArn)))))
(struct subscription (owner topic-arn subscription-arn protocol endpoint)
#:transparent)
(provide (struct-out subscription))
(define (xexpr->subscriptions x)
(for/list ([x (in-list (tags x 'member))])
(subscription (first-tag-value x 'Owner)
(first-tag-value x 'TopicArn)
(first-tag-value x 'SubscriptionArn)
(first-tag-value x 'Protocol)
(first-tag-value x 'Endpoint))))
(define/contract/provide (list-subscriptions)
(-> (listof subscription?))
(sns `((Action "ListSubscriptions"))
xexpr->subscriptions))
(define/contract/provide (list-subscriptions-by-topic arn)
(string? . -> . (listof subscription?))
(sns `((Action "ListSubscriptionsByTopic")
(TopicArn ,arn))
xexpr->subscriptions))
(define/provide (sns-protocol? s)
(match s
[(or "http" "https" "email" "email-json" "sms" "sqs" )
#t]
[else #f]))
(define/contract/provide (subscribe endpoint protocol topic-arn)
(string? sns-protocol? string? . -> . string?)
(first (sns `((Action "Subscribe")
(Endpoint ,endpoint)
(Protocol ,protocol)
(TopicArn ,topic-arn))
(lambda (x)
(list (first-tag-value x 'SubscriptionArn))))))
(define/contract/provide (unsubscribe subscription-arn)
(string? . -> . any)
(void (sns `((Action "Unsubscribe")
(SubscriptionArn ,subscription-arn)))))
(define/contract/provide (publish arn msg
#:subject [subject "No subject"]
#:json? [json? #f])
((string? string?)
(#:subject (or/c #f string?) #:json? boolean?)
. ->* . any)
(sns (append `((Action "Publish")
(Message ,msg)
(TopicArn ,arn)
(Subject ,subject))
(if json?
`((MessageStructure "json"))
`()))))
(module+ test
(require rackunit
"tests/data.rkt")
(define (member? x xs)
(not (not (member x xs))))
(test-case
"sns"
(read-keys)
(define arn (create-topic (test/topic)))
(check-true (member? arn (list-topics)))
(check-equal? (assoc 'TopicArn (get-topic-attributes arn))
(cons 'TopicArn arn))
(check-equal? (subscribe (test/recipient) "email" arn)
"pending confirmation")
(publish arn "Test" #:subject "Test")
(publish arn "{\"default\": \"Test\"}" #:subject "Test" #:json? #t)
(delete-topic arn)
(check-false (member? arn (list-topics)))
(void))
)