#lang racket/base
(require racket/match
racket/class
racket/file
racket/dict
racket/pretty
"private/util.rkt"
mzlib/etc
web-server/dispatch
web-server/servlet-env
web-server/servlet/web
web-server/servlet/web-cells
web-server/http
web-server/http/bindings
web-server/http/xexpr
web-server/formlets
(only-in web-server/formlets/lib pure)
(planet ryanc/webapi:1))
(provide main)
(define auth-wc (make-web-cell #f))
(define-values (setup-dispatch _setup-url)
(dispatch-rules
[("setup" "start") start-setup]
[("setup" "oauth2") continue-oauth2]))
(define (start-setup req)
(match (get-pref 'auth)
[(list 'oauth2-refresh-token refresh-token)
(let ([oauth2 (oauth2/refresh-token google-auth-server the-client refresh-token)])
(send oauth2 validate!) (web-cell-shadow auth-wc oauth2))]
[_ (void)])
(welcome-page))
(define (continue-oauth2 req)
(let ([bindings (request-bindings/raw req)])
(cond [(bindings-assq #"code" bindings)
=> (lambda (code-b)
(let* ([auth-code (bytes->string/utf-8 (binding:form-value code-b))]
[oauth2 (oauth2/auth-code google-auth-server the-client auth-code
#:redirect-uri "http://localhost:8000/setup/oauth2")])
(send oauth2 validate!) (web-cell-shadow auth-wc oauth2)
(redirect/get)
(manage-page)))]
[(bindings-assq #"error" bindings)
=> (lambda (err-b)
(let ([err (bytes->string/utf-8 (binding:form-value err-b))])
(error-page
`(h2 "Authorization failed")
`(p ,err))))]
[else (error-page
`(h2 "Internal error")
`(p "Bad response from authorization server."))])))
(define (welcome-page)
(define oauth2 (web-cell-ref auth-wc))
(when oauth2 (send oauth2 validate!))
(send/suspend/dispatch
(lambda (make-url)
(wrap-page
`(div
(h2 "Setup Overview")
(p "Scriblogify is a tool for creating blog posts from Scribble documents.")
(p "You can use Scriblogify purely as a local document processor, but you can also "
"use it to automatically post blog entries and upload embedded images "
"to image hosting services. (Currently only Blogger and Picasa Web Albums "
"are supported.)")
(p "A " (em "profile") " consists of a blog" mdash "where to upload posts" mdash
"together with, optionally, a web album" mdash
"where to store images that are part of a post.")
(p (em "Warning: Use Scriblogify at your own risk!"))
,@(cond [oauth2
`((h3 "Use existing authorization")
(p "You have already authorized Scriblogify to access your Blogger "
,(cond [(member picasa-scope (send oauth2 get-scopes))
"and Picasa Web Albums accounts."]
[else
"account only."]))
(p (a ([href ,(make-url (lambda _ (manage-page)))])
"Continue to profile management page.")))]
[else '()])
,@(cond [oauth2
`((h3 "Change authorization"))]
[else
`((h3 "Grant authorization")
(p "To create profiles, you must first authorize Scriblogify to access and "
"modify your Blogger and (optionally) Picasa Web Albums accounts."))])
,@(let ([auth-url
(lambda (include-picasa?)
(send google-auth-server get-auth-request-url
#:client the-client
#:scopes (if include-picasa?
(list blogger-scope picasa-scope)
(list blogger-scope))
#:redirect-uri "http://localhost:8000/setup/oauth2"))])
`((p (a ([href ,(auth-url #t)])
"Authorize access to your Blogger and Picasa Web Albums accounts."))
(p (a ([href ,(auth-url #f)])
"Authorize access to your Blogger account only."))))
,@(cond [oauth2
`((p (a ([href ,(make-url
(lambda _
(send oauth2 revoke!)
(web-cell-shadow auth-wc #f)
(set-pref 'auth #f)
(redirect/get)
(welcome-page)))])
"Revoke access to your accounts.")))]
[else '()])
(h3 "Done")
(p (a ([href "/quit"]) "Quit the setup servlet."))
)))))
(define (manage-page)
(define oauth2 (web-cell-ref auth-wc))
(define profiles (or (get-pref 'profiles) null))
(send/suspend/dispatch
(lambda (make-url)
(wrap-page
`(div
(h2 "Manage Profiles")
(p "Your setup information is stored at:")
(pre ,(path->string profile-pref-file))
(div (h3 "Authorization")
(p "You may save an authorization token in your setup information file. "
"The token will be used automatically when you run Scriblogify. "
"If you do not save your authorization token, you will be prompted again for "
"authorization each time you run Scriblogify.")
,@(let ([saved-auth (get-pref 'auth)])
(match saved-auth
[(list 'oauth2-refresh-token _)
`((p "Your authorization token is currently saved.")
(a ([href ,(make-url (lambda _ (set-pref 'auth #f) (redirect/get) (manage-page)))])
"Remove the authorization token."))]
[#f
`((p "No authorization token is currently saved.")
(a ([href ,(make-url
(lambda _
(set-pref 'auth `(oauth2-refresh-token ,(send oauth2 get-refresh-token)))
(redirect/get)
(manage-page)))])
"Save an authorization token."))]
[_
`((p "Your setup information file contains an invalid authorization record.")
(a ([href ,(make-url (lambda _ (set-pref 'auth #f) (redirect/get) (manage-page)))])
"Remove the invalid authorization record."))])))
(div (h3 "New Profile")
(p (a ([href ,(make-url (lambda (req) (new-profile-page)))])
"Create a new profile.")))
(div (h3 "Existing Profiles")
(ul
,@(for/list ([(name-sym info) (in-dict profiles)])
(let ([name (symbol->string name-sym)])
(match info
[`((blogger ,blog-id ,blog-title) ,album-info)
`(li (em ,name)
" (blog: " ,blog-title
,@(match album-info
[`(picasa ,album-id ,album-title)
`(", album: " ,album-title)]
[#f
`(", no album")])
") "
(a ([href ,(make-url
(lambda _
(set-pref 'profiles
(dict-remove (or (get-pref 'profiles) null)
name-sym))
(redirect/get)
(manage-page)))])
"[delete]"))])))))
(div (h3 "Done")
(p (a ([href "/quit"]) "Quit the setup servlet."))))))))
(define (new-profile-page)
(define oauth2 (web-cell-ref auth-wc))
(define bu (blogger #:oauth2 oauth2))
(define pu (and (member picasa-scope (send oauth2 get-scopes)) (picasa #:oauth2 oauth2)))
(define profile-formlet
(formlet
(div ([class "profile_form"])
(div (div "Choose a name for the new profile:")
,(=> input-symbol
name))
(div (div "Choose a blog:")
,(=> (select-input (send bu list-blogs)
#:display (lambda (b) (send b get-title)))
blog))
(div ,(if pu
`(div "Choose an album:")
`(div "Access to Picasa Web Albums not authorized."))
,(=> (if pu
(select-input (cons #f (send pu list-albums))
#:display (lambda (a)
(if a
(send a get-title)
"(None)")))
(pure #f))
album))
(div ([class "profile_form_submit"])
(input ([type "submit"] [value "Create profile"] [name "submit"]))))
(list name blog album)))
(let ([req
(send/suspend
(lambda (k-url)
(wrap-page
`(div
(h2 "New Profile")
(form ([action ,k-url]) ,@(formlet-display profile-formlet))
))))])
(let ([result (formlet-process profile-formlet req)])
(match result
[(list name blog album)
(cond [(equal? (symbol->string name) "")
(error-page `(p "Invalid profile name: \"" ,(symbol->string name) "\"."))]
[else
(set-pref 'profiles
(dict-set (or (get-pref 'profiles) null)
name
`((blogger ,(send blog get-id) ,(send blog get-title))
,(and album `(picasa ,(send album get-id) ,(send album get-title))))))
(begin (redirect/get) (manage-page))])]))))
(define (error-page . contents)
(wrap-page
`(div ,@contents)))
(define (done-page . _)
(redirect-to "/quit"))
(define (wrap-page main)
(response/xexpr
`(html (head (title "Scriblogify Setup")
(link ([rel "stylesheet"] [type "text/css"] [href "/style.css"])))
(body
(div ([class "main_body"])
(h1 "Scriblogify")
,main)))))
(define here (this-expression-source-directory))
(define (main)
(serve/servlet setup-dispatch
#:launch-browser? #t
#:quit? #t
#:banner? #f
#:port 8000
#:servlet-path "/setup/start"
#:servlet-regexp #rx"^/setup/"
#:extra-files-paths (list (build-path here "private" "setup"))))