vocabulinkApp = serve (Proxy :: Proxy VocabulinkAPI) app
 where app = liftIO frontPage
        :<|> redirect "https://github.com/jekor/vocabulink/tarball/master"

-- Some permanent URIs are essentially static files. To display them, we make
-- use of the article system (formatting, metadata, etc). You could call these
-- elevated articles. We use articles because the system for managing them
-- exists already (revision control, etc)

-- Each @.html@ file is actually an HTML fragment. These happen to be generated
-- from Muse Mode files by Emacs, but we don't really care where they come
-- from.

-- Other articles are dynamic and can be created without recompilation. We just
-- have to rescan the filesystem for them. They also live in the @/article@
-- namespace (specifically at @/article/title@).

        :<|> ((maybeNotFound . liftIO . articlePage)

-- We have 1 page for getting a listing of all published articles.

         :<|> liftIO articlesPage
         :<|> maybeNotFound (liftIO (articlePage "help"))
         :<|> maybeNotFound (liftIO (articlePage "privacy"))
         :<|> maybeNotFound (liftIO (articlePage "terms-of-use"))
         :<|> maybeNotFound (liftIO (articlePage "source"))
         :<|> maybeNotFound (liftIO (articlePage "api")))

-- Link Pages

-- Vocabulink revolves around links---the associations between words or ideas. As
-- with articles, we have different functions for retrieving a single link or a
-- listing of links. However, the dispatching is complicated by the fact that
-- members can operate upon links (we need to handle the @POST@ method).

-- For clarity, this dispatches:

-- GET    /link/10               → link page
-- POST   /link/10/stories       → add a linkword story

        :<|> ((\ n -> maybeNotFound (liftIO (getStory n))
                 :<|> (\ story -> liftIO (editStory n story) >> return NoContent)
                 :<|> (\ data' -> do
                           let story = bodyVarRequired "story" data'
                           liftIO $ editStory n story
                           -- TODO: This redirect masks the result of editStory
                           redirect (toS referrerOrVocabulink)))

         -- TODO: Find a way to negotiate content types without always rendering the link HTML
         --       (which is not needed for the JSON case).
         :<|> (\ n -> maybeNotFound (liftIO ((\ l -> case l of Nothing -> return Nothing; Just l' -> (Just . LinkOutput l') `fmap` linkPage l') =<< linkDetails n))
                 :<|> maybeNotFound (liftIO ((\ l -> case l of Nothing -> return Nothing; Just l' -> Just `fmap` compactLinkPage l') =<< linkDetails n))
                -- TODO: 404 when link number doesn't exist
                 :<|> liftIO (linkStories n)
                 :<|> (\ data' -> do
                           let story = bodyVarRequired "story" data'
                           liftIO $ addStory n story
                           -- TODO: This redirect masks the result of editStory
                           redirect (toS ("/link/" ++ show n)))))

-- Retrieving a listing of links is easier.

        :<|> (\ (Language ol) (Language dl) -> do
                  links <- liftIO (languagePairLinks ol dl)
                  liftIO (linksPage ("Links from " ++ ol ++ " to " ++ dl) links))

-- Readers

        :<|> (\ lang name' page -> maybeNotFound (liftIO (readerPage lang name' page)))

-- Searching

        :<|> (\ q -> do
                links <- if q == ""
                           then return []
                           else liftIO (linksContaining q)
                liftIO (linksPage ("Search Results for \"" <> q <> "\"") links))

-- Review

-- Members review their links by interacting with the site in a vaguely
-- REST-ish way. The intent behind this is that in the future they will be able
-- to review their links through different means such as a desktop program or a
-- phone application.

-- GET  /review       → review page (app)
-- PUT  /review/n     → add a link for review
-- GET  /review/next  → retrieve the next links for review
-- POST /review/n     → mark link as reviewed

-- (where n is the link number)

-- Reviewing links is one of the only things that logged-in-but-unverified
-- members are allowed to do.

        :<|> ((\ (Language learn) (Language known) -> liftIO (reviewPage learn known))
         :<|> (\ n -> withLoggedInMember (\ m -> liftIO (newReview m n) >> return NoContent))
         :<|> withLoggedInMember (liftIO . reviewStats)
         :<|> (\ statType start end tzOffset ->
                 withLoggedInMember (\ m -> liftIO ((case statType of StatTypeDaily -> dailyReviewStats; StatTypeDetailed -> detailedReviewStats) m start end tzOffset)))
         :<|> (\ n data' ->
                 do let grade = read (bodyVarRequired "grade" data')
                        recallTime = read (bodyVarRequired "time" data')
                        reviewedAt' = maybe Nothing readMaybe (bodyVar "when" data')
                    reviewedAt <- case reviewedAt' of
                                    Nothing -> liftIO epochTime
                                    Just ra -> return ra
                    -- TODO: Sanity-check this time. It should at least not be in the future.
                    withLoggedInMember (\ m -> liftIO $ scheduleNextReview m n grade recallTime reviewedAt >> return NoContent)))

-- Dashboard

        :<|> withLoggedInMember (const (liftIO dashboardPage))

-- Membership

-- Becoming a member is simply a matter of filling out a form.

-- Note that in some places where I would use a PUT I've had to append a verb
-- to the URL and use a POST instead because these requests are often made to
-- HTTPS pages from HTTP pages and can't be done in JavaScript without a lot of
-- not-well-supported cross-domain policy hacking.

        :<|> ((\ data' ->
                 let username = bodyVarRequired "username" data'
                     email = bodyVarRequired "email" data'
                     password = bodyVarRequired "password" data'
                     learned = bodyVar "learned" data'
                 in liftIO (signup username email password learned) >>= \ c -> cookieBounce [c] MsgSuccess "Welcome! Please check your email to confirm your account.")

-- But to use most of the site, we require email confirmation.

         :<|> (\ hash -> liftIO (confirmEmail hash) >>= \case
                           Left pg -> return pg
                           Right c -> cookieBounce [c] MsgSuccess "Congratulations! You've confirmed your account.")
         :<|> (case ?member of
                 Nothing -> do
                   liftIO (simplePage "Please Login to Resend Your Confirmation Email" [ReadyJS "V.loginPopup();"] mempty)
                 Just m  -> do
                   liftIO $ resendConfirmEmail m
                   bounce MsgSuccess "Your confirmation email has been sent.")

-- Logging in is a similar process.

         :<|> (\ data' -> do
                 let userid = bodyVarRequired "userid" data'
                     password = bodyVarRequired "password" data'
                 liftIO (login userid password) >>= \ c -> cookieBounce [c] MsgSuccess "Login successful.")

-- Logging out can be done without a form.

         :<|> cookieRedirect [emptyAuthCookie {setCookieMaxAge = Just (secondsToDiffTime 0)}] "https://www.vocabulink.com/"
         :<|> (\ data' -> do
                 let password = bodyVarRequired "password" data'
                 liftIO (deleteAccount password)
                 cookieBounce [emptyAuthCookie {setCookieMaxAge = Just $ secondsToDiffTime 0}] MsgSuccess "Your account was successfully deleted.")
         :<|> ((\ data' -> do
                  let email = bodyVarRequired "email" data'
                  liftIO (sendPasswordReset email)
                  bounce MsgSuccess "Password reset instructions have been sent to your email.")
          :<|> (liftIO . passwordResetPage)
          :<|> (\ hash data' -> do
                  let password = bodyVarRequired "password" data'
                  liftIO (passwordReset hash password) >>= \ c -> cookieBounce [c] MsgSuccess "Password reset successfully.")
          :<|> (\ data' -> do
                  let oldPassword = bodyVarRequired "old-password" data'
                      newPassword = bodyVarRequired "new-password" data'
                  liftIO (changePassword oldPassword newPassword)
                  bounce MsgSuccess "Password changed successfully."))

         :<|> (\ data' -> do
                let email = bodyVarRequired "email" data'
                    password = bodyVarRequired "password" data'
                liftIO (changeEmail email password)
                bounce MsgSuccess "Email address changed successfully. Please check your email to confirm the change."))

-- Member Pages

         :<|> (\ username ->
                 maybeNotFound (liftIO (maybe (return Nothing) (\ m -> memberPage m >>= \ m' -> return (Just m')) =<< memberByName username))
            :<|> liftIO (usernameAvailable username))

         :<|> (liftIO . emailAvailable)

-- ``reply'' is used here as a noun.

         :<|> (\ n data' -> do
                 let body = bodyVarRequired "body" data'
                 liftIO $ withVerifiedMember (\m -> storeComment (memberNumber m) body (Just n))
                 redirect (toS referrerOrVocabulink))

-- Everything Else

-- For Google Webmaster Tools, we need to respond to a certain URI that acts as
-- a kind of ``yes, we really do run this site''.

         :<|> return (unlines [ "User-agent: *"
                              , "Disallow:" ])

         :<|> return "google-site-verification: google1e7c25c4bdfc5be7.html"

-- Finally, we get to an actual page of the site: the front page.

frontPage 
vocabulinkApp = serve (Proxy :: Proxy VocabulinkAPI) app
 where app = liftIO frontPage
        :<|> redirect "https://github.com/jekor/vocabulink/tarball/master"

-- Some permanent URIs are essentially static files. To display them, we make
-- use of the article system (formatting, metadata, etc). You could call these
-- elevated articles. We use articles because the system for managing them
-- exists already (revision control, etc)

-- Each @.html@ file is actually an HTML fragment. These happen to be generated
-- from Muse Mode files by Emacs, but we don't really care where they come
-- from.

-- Other articles are dynamic and can be created without recompilation. We just
-- have to rescan the filesystem for them. They also live in the @/article@
-- namespace (specifically at @/article/title@).

        :<|> ((maybeNotFound . liftIO . articlePage)

-- We have 1 page for getting a listing of all published articles.

         :<|> liftIO articlesPage
         :<|> maybeNotFound (liftIO (articlePage "help"))
         :<|> maybeNotFound (liftIO (articlePage "privacy"))
         :<|> maybeNotFound (liftIO (articlePage "terms-of-use"))
         :<|> maybeNotFound (liftIO (articlePage "source"))
         :<|> maybeNotFound (liftIO (articlePage "api")))

-- Link Pages

-- Vocabulink revolves around links---the associations between words or ideas. As
-- with articles, we have different functions for retrieving a single link or a
-- listing of links. However, the dispatching is complicated by the fact that
-- members can operate upon links (we need to handle the @POST@ method).

-- For clarity, this dispatches:

-- GET    /link/10               → link page
-- POST   /link/10/stories       → add a linkword story

        :<|> ((\ n -> maybeNotFound (liftIO (getStory n))
                 :<|> (\ story -> liftIO (editStory n story) >> return NoContent)
                 :<|> (\ data' -> do
                           let story = bodyVarRequired "story" data'
                           liftIO $ editStory n story
                           -- TODO: This redirect masks the result of editStory
                           redirect (toS referrerOrVocabulink)))

         -- TODO: Find a way to negotiate content types without always rendering the link HTML
         --       (which is not needed for the JSON case).
         :<|> (\ n -> maybeNotFound (liftIO ((\ l -> case l of Nothing -> return Nothing; Just l' -> (Just . LinkOutput l') `fmap` linkPage l') =<< linkDetails n))
                 :<|> maybeNotFound (liftIO ((\ l -> case l of Nothing -> return Nothing; Just l' -> Just `fmap` compactLinkPage l') =<< linkDetails n))
                -- TODO: 404 when link number doesn't exist
                 :<|> liftIO (linkStories n)
                 :<|> (\ data' -> do
                           let story = bodyVarRequired "story" data'
                           liftIO $ addStory n story
                           -- TODO: This redirect masks the result of editStory
                           redirect (toS ("/link/" ++ show n)))))

-- Retrieving a listing of links is easier.

        :<|> (\ (Language ol) (Language dl) -> do
                  links <- liftIO (languagePairLinks ol dl)
                  liftIO (linksPage ("Links from " ++ ol ++ " to " ++ dl) links))

-- Readers

        :<|> (\ lang name' page -> maybeNotFound (liftIO (readerPage lang name' page)))

-- Searching

        :<|> (\ q -> do
                links <- if q == ""
                           then return []
                           else liftIO (linksContaining q)
                liftIO (linksPage ("Search Results for \"" <> q <> "\"") links))

-- Review

-- Members review their links by interacting with the site in a vaguely
-- REST-ish way. The intent behind this is that in the future they will be able
-- to review their links through different means such as a desktop program or a
-- phone application.

-- GET  /review       → review page (app)
-- PUT  /review/n     → add a link for review
-- GET  /review/next  → retrieve the next links for review
-- POST /review/n     → mark link as reviewed

-- (where n is the link number)

-- Reviewing links is one of the only things that logged-in-but-unverified
-- members are allowed to do.

        :<|> ((\ (Language learn) (Language known) -> liftIO (reviewPage learn known))
         :<|> (\ n -> withLoggedInMember (\ m -> liftIO (newReview m n) >> return NoContent))
         :<|> withLoggedInMember (liftIO . reviewStats)
         :<|> (\ statType start end tzOffset ->
                 withLoggedInMember (\ m -> liftIO ((case statType of StatTypeDaily -> dailyReviewStats; StatTypeDetailed -> detailedReviewStats) m start end tzOffset)))
         :<|> (\ n data' ->
                 do let grade = read (bodyVarRequired "grade" data')
                        recallTime = read (bodyVarRequired "time" data')
                        reviewedAt' = maybe Nothing readMaybe (bodyVar "when" data')
                    reviewedAt <- case reviewedAt' of
                                    Nothing -> liftIO epochTime
                                    Just ra -> return ra
                    -- TODO: Sanity-check this time. It should at least not be in the future.
                    withLoggedInMember (\ m -> liftIO $ scheduleNextReview m n grade recallTime reviewedAt >> return NoContent)))

-- Dashboard

        :<|> withLoggedInMember (const (liftIO dashboardPage))

-- Membership

-- Becoming a member is simply a matter of filling out a form.

-- Note that in some places where I would use a PUT I've had to append a verb
-- to the URL and use a POST instead because these requests are often made to
-- HTTPS pages from HTTP pages and can't be done in JavaScript without a lot of
-- not-well-supported cross-domain policy hacking.

        :<|> ((\ data' ->
                 let username = bodyVarRequired "username" data'
                     email = bodyVarRequired "email" data'
                     password = bodyVarRequired "password" data'
                     learned = bodyVar "learned" data'
                 in liftIO (signup username email password learned) >>= \ c -> cookieBounce [c] MsgSuccess "Welcome! Please check your email to confirm your account.")

-- But to use most of the site, we require email confirmation.

         :<|> (\ hash -> liftIO (confirmEmail hash) >>= \case
                           Left pg -> return pg
                           Right c -> cookieBounce [c] MsgSuccess "Congratulations! You've confirmed your account.")
         :<|> (case ?member of
                 Nothing -> do
                   liftIO (simplePage "Please Login to Resend Your Confirmation Email" [ReadyJS "V.loginPopup();"] mempty)
                 Just m  -> do
                   liftIO $ resendConfirmEmail m
                   bounce MsgSuccess "Your confirmation email has been sent.")

-- Logging in is a similar process.

         :<|> (\ data' -> do
                 let userid = bodyVarRequired "userid" data'
                     password = bodyVarRequired "password" data'
                 liftIO (login userid password) >>= \ c -> cookieBounce [c] MsgSuccess "Login successful.")

-- Logging out can be done without a form.

         :<|> cookieRedirect [emptyAuthCookie {setCookieMaxAge = Just (secondsToDiffTime 0)}] "https://www.vocabulink.com/"
         :<|> (\ data' -> do
                 let password = bodyVarRequired "password" data'
                 liftIO (deleteAccount password)
                 cookieBounce [emptyAuthCookie {setCookieMaxAge = Just $ secondsToDiffTime 0}] MsgSuccess "Your account was successfully deleted.")
         :<|> ((\ data' -> do
                  let email = bodyVarRequired "email" data'
                  liftIO (sendPasswordReset email)
                  bounce MsgSuccess "Password reset instructions have been sent to your email.")
          :<|> (liftIO . passwordResetPage)
          :<|> (\ hash data' -> do
                  let password = bodyVarRequired "password" data'
                  liftIO (passwordReset hash password) >>= \ c -> cookieBounce [c] MsgSuccess "Password reset successfully.")
          :<|> (\ data' -> do
                  let oldPassword = bodyVarRequired "old-password" data'
                      newPassword = bodyVarRequired "new-password" data'
                  liftIO (changePassword oldPassword newPassword)
                  bounce MsgSuccess "Password changed successfully."))

         :<|> (\ data' -> do
                let email = bodyVarRequired "email" data'
                    password = bodyVarRequired "password" data'
                liftIO (changeEmail email password)
                bounce MsgSuccess "Email address changed successfully. Please check your email to confirm the change."))

-- Member Pages

         :<|> (\ username ->
                 maybeNotFound (liftIO (maybe (return Nothing) (\ m -> memberPage m >>= \ m' -> return (Just m')) =<< memberByName username))
            :<|> liftIO (usernameAvailable username))

         :<|> (liftIO . emailAvailable)

-- ``reply'' is used here as a noun.

         :<|> (\ n data' -> do
                 let body = bodyVarRequired "body" data'
                 liftIO $ withVerifiedMember (\m -> storeComment (memberNumber m) body (Just n))
                 redirect (toS referrerOrVocabulink))

-- Everything Else

-- For Google Webmaster Tools, we need to respond to a certain URI that acts as
-- a kind of ``yes, we really do run this site''.

         :<|> return (unlines [ "User-agent: *"
                              , "Disallow:" ])

         :<|> return "google-site-verification: google1e7c25c4bdfc5be7.html"

-- Finally, we get to an actual page of the site: the front page.
vocabulinkApp = serve (Proxy :: Proxy VocabulinkAPI) app
 where app = liftIO frontPage
        :<|> redirect "https://github.com/jekor/vocabulink/tarball/master"

-- Some permanent URIs are essentially static files. To display them, we make
-- use of the article system (formatting, metadata, etc). You could call these
-- elevated articles. We use articles because the system for managing them
-- exists already (revision control, etc)

-- Each @.html@ file is actually an HTML fragment. These happen to be generated
-- from Muse Mode files by Emacs, but we don't really care where they come
-- from.

-- Other articles are dynamic and can be created without recompilation. We just
-- have to rescan the filesystem for them. They also live in the @/article@
-- namespace (specifically at @/article/title@).

        :<|> ((maybeNotFound . liftIO . articlePage)

-- We have 1 page for getting a listing of all published articles.

         :<|> liftIO articlesPage
         :<|> maybeNotFound (liftIO (articlePage "help"))
         :<|> maybeNotFound (liftIO (articlePage "privacy"))
         :<|> maybeNotFound (liftIO (articlePage "terms-of-use"))
         :<|> maybeNotFound (liftIO (articlePage "source"))
         :<|> maybeNotFound (liftIO (articlePage "api")))

-- Link Pages

-- Vocabulink revolves around links---the associations between words or ideas. As
-- with articles, we have different functions for retrieving a single link or a
-- listing of links. However, the dispatching is complicated by the fact that
-- members can operate upon links (we need to handle the @POST@ method).

-- For clarity, this dispatches:

-- GET    /link/10               → link page
-- POST   /link/10/stories       → add a linkword story

        :<|> ((\ n -> maybeNotFound (liftIO (getStory n))
                 :<|> (\ story -> liftIO (editStory n story) >> return NoContent)
                 :<|> (\ data' -> do
                           let story = bodyVarRequired "story" data'
                           liftIO $ editStory n story
                           -- TODO: This redirect masks the result of editStory
                           redirect (toS referrerOrVocabulink)))

         -- TODO: Find a way to negotiate content types without always rendering the link HTML
         --       (which is not needed for the JSON case).
         :<|> (\ n -> maybeNotFound (liftIO ((\ l -> case l of Nothing -> return Nothing; Just l' -> (Just . LinkOutput l') `fmap` linkPage l') =<< linkDetails n))
                 :<|> maybeNotFound (liftIO ((\ l -> case l of Nothing -> return Nothing; Just l' -> Just `fmap` compactLinkPage l') =<< linkDetails n))
                -- TODO: 404 when link number doesn't exist
                 :<|> liftIO (linkStories n)
                 :<|> (\ data' -> do
                           let story = bodyVarRequired "story" data'
                           liftIO $ addStory n story
                           -- TODO: This redirect masks the result of editStory
                           redirect (toS ("/link/" ++ show n)))))

-- Retrieving a listing of links is easier.

        :<|> (\ (Language ol) (Language dl) -> do
                  links <- liftIO (languagePairLinks ol dl)
                  liftIO (linksPage ("Links from " ++ ol ++ " to " ++ dl) links))

-- Readers

        :<|> (\ lang name' page -> maybeNotFound (liftIO (readerPage lang name' page)))

-- Searching

        :<|> (\ q -> do
                links <- if q == ""
                           then return []
                           else liftIO (linksContaining q)
                liftIO (linksPage ("Search Results for \"" <> q <> "\"") links))

-- Review

-- Members review their links by interacting with the site in a vaguely
-- REST-ish way. The intent behind this is that in the future they will be able
-- to review their links through different means such as a desktop program or a
-- phone application.

-- GET  /review       → review page (app)
-- PUT  /review/n     → add a link for review
-- GET  /review/next  → retrieve the next links for review
-- POST /review/n     → mark link as reviewed

-- (where n is the link number)

-- Reviewing links is one of the only things that logged-in-but-unverified
-- members are allowed to do.

        :<|> ((\ (Language learn) (Language known) -> liftIO (reviewPage learn known))
         :<|> (\ n -> withLoggedInMember (\ m -> liftIO (newReview m n) >> return NoContent))
         :<|> withLoggedInMember (liftIO . reviewStats)
         :<|> (\ statType start end tzOffset ->
                 withLoggedInMember (\ m -> liftIO ((case statType of StatTypeDaily -> dailyReviewStats; StatTypeDetailed -> detailedReviewStats) m start end tzOffset)))
         :<|> (\ n data' ->
                 do let grade = read (bodyVarRequired "grade" data')
                        recallTime = read (bodyVarRequired "time" data')
                        reviewedAt' = maybe Nothing readMaybe (bodyVar "when" data')
                    reviewedAt <- case reviewedAt' of
                                    Nothing -> liftIO epochTime
                                    Just ra -> return ra
                    -- TODO: Sanity-check this time. It should at least not be in the future.
                    withLoggedInMember (\ m -> liftIO $ scheduleNextReview m n grade recallTime reviewedAt >> return NoContent)))

-- Dashboard

        :<|> withLoggedInMember (const (liftIO dashboardPage))

-- Membership

-- Becoming a member is simply a matter of filling out a form.

-- Note that in some places where I would use a PUT I've had to append a verb
-- to the URL and use a POST instead because these requests are often made to
-- HTTPS pages from HTTP pages and can't be done in JavaScript without a lot of
-- not-well-supported cross-domain policy hacking.

        :<|> ((\ data' ->
                 let username = bodyVarRequired "username" data'
                     email = bodyVarRequired "email" data'
                     password = bodyVarRequired "password" data'
                     learned = bodyVar "learned" data'
                 in liftIO (signup username email password learned) >>= \ c -> cookieBounce [c] MsgSuccess "Welcome! Please check your email to confirm your account.")

-- But to use most of the site, we require email confirmation.

         :<|> (\ hash -> liftIO (confirmEmail hash) >>= \case
                           Left pg -> return pg
                           Right c -> cookieBounce [c] MsgSuccess "Congratulations! You've confirmed your account.")
         :<|> (case ?member of
                 Nothing -> do
                   liftIO (simplePage "Please Login to Resend Your Confirmation Email" [ReadyJS "V.loginPopup();"] mempty)
                 Just m  -> do
                   liftIO $ resendConfirmEmail m
                   bounce MsgSuccess "Your confirmation email has been sent.")

-- Logging in is a similar process.

         :<|> (\ data' -> do
                 let userid = bodyVarRequired "userid" data'
                     password = bodyVarRequired "password" data'
                 liftIO (login userid password) >>= \ c -> cookieBounce [c] MsgSuccess "Login successful.")

-- Logging out can be done without a form.

         :<|> cookieRedirect [emptyAuthCookie {setCookieMaxAge = Just (secondsToDiffTime 0)}] "https://www.vocabulink.com/"
         :<|> (\ data' -> do
                 let password = bodyVarRequired "password" data'
                 liftIO (deleteAccount password)
                 cookieBounce [emptyAuthCookie {setCookieMaxAge = Just $ secondsToDiffTime 0}] MsgSuccess "Your account was successfully deleted.")
         :<|> ((\ data' -> do
                  let email = bodyVarRequired "email" data'
                  liftIO (sendPasswordReset email)
                  bounce MsgSuccess "Password reset instructions have been sent to your email.")
          :<|> (liftIO . passwordResetPage)
          :<|> (\ hash data' -> do
                  let password = bodyVarRequired "password" data'
                  liftIO (passwordReset hash password) >>= \ c -> cookieBounce [c] MsgSuccess "Password reset successfully.")
          :<|> (\ data' -> do
                  let oldPassword = bodyVarRequired "old-password" data'
                      newPassword = bodyVarRequired "new-password" data'
                  liftIO (changePassword oldPassword newPassword)
                  bounce MsgSuccess "Password changed successfully."))

         :<|> (\ data' -> do
                let email = bodyVarRequired "email" data'
                    password = bodyVarRequired "password" data'
                liftIO (changeEmail email password)
                bounce MsgSuccess "Email address changed successfully. Please check your email to confirm the change."))

-- Member Pages

         :<|> (\ username ->
                 maybeNotFound (liftIO (maybe (return Nothing) (\ m -> memberPage m >>= \ m' -> return (Just m')) =<< memberByName username))
            :<|> liftIO (usernameAvailable username))

         :<|> (liftIO . emailAvailable)

-- ``reply'' is used here as a noun.

         :<|> (\ n data' -> do
                 let body = bodyVarRequired "body" data'
                 liftIO $ withVerifiedMember (\m -> storeComment (memberNumber m) body (Just n))
                 redirect (toS referrerOrVocabulink))

-- Everything Else

-- For Google Webmaster Tools, we need to respond to a certain URI that acts as
-- a kind of ``yes, we really do run this site''.

         :<|> return (unlines [ "User-agent: *"
                              , "Disallow:" ])

         :<|> return "google-site-verification: google1e7c25c4bdfc5be7.html"

-- Finally, we get to an actual page of the site: the front page.