{-# LANGUAGE DeriveDataTypeable, FlexibleContexts, FlexibleInstances, MultiParamTypeClasses, StandaloneDeriving, ScopedTypeVariables, TemplateHaskell, TypeFamilies, TypeSynonymInstances, UndecidableInstances #-}
{-# OPTIONS_GHC -F -pgmFtrhsx #-}
module Main where
import Control.Arrow (first)
import Control.Applicative(Applicative((<*>), pure), (<$>), (<*),(*>))
import Control.Applicative.Error(Failing(Failure, Success), ErrorMsg, maybeRead')
import Control.Concurrent (MVar, forkIO, killThread)
import Control.Monad(MonadPlus(mplus, mzero), msum)
import Control.Monad.Reader (ask)
import Control.Monad.State (get,put)
import Control.Monad.Trans(MonadIO(..))
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Fixed
import qualified Data.Foldable as F
import Data.Traversable (sequenceA)
import Data.Generics (Data, Typeable, Typeable1)
import Data.Maybe (fromJust, fromMaybe, catMaybes)
import Data.List as List
import Data.Ix
import Data.Set (Set)
import qualified Data.Set
import Data.Time
import Data.Time.Clock.POSIX
import qualified Data.Traversable as T
import Happstack.Facebook.Application as Application (FacebookData, withFacebook, withUserSP, withSessionSP)
import Happstack.Facebook.Common (ApiKey(unApiKey), HasUser(askUser), HasSessionKey, HasFacebookConfig(askFacebookConfig), FbXML(FbXML), FacebookConfig(apiKey, canvasURL), HasFacebookData, User(User, uid), callMethod, FacebookStateS)
import Happstack.Facebook.FacebookT (FacebookT(..), renderAsFBML)
import qualified Happstack.Facebook.Friends as Friends
import qualified Happstack.Facebook.Users as Users
import qualified Happstack.Facebook.Notifications as Notifications
import Happstack.Facebook.Formlets (formletPart)
import Happstack.Facebook.XdReceiver
import Happstack.Server (Conf(port, validator), FilterMonad, ServerMonad, ServerPartT, Response, WebMonad, RqData, ToMessage, dir, look, lookPairs, lookRead, nullConf, nullDir, path, simpleHTTP, toResponse, seeOther, withDataFn, ok)
import Happstack.Data (Default, Version(..), deriveAll, deriveNewData)
import qualified Happstack.Data.IxSet as IxSet
import Happstack.Data.IxSet ((@=), (@+), inferIxSet, getOne, noCalcs)
import Happstack.State
import Happstack.State.ClockTime (ClockTime(TOD))
import Happstack.Util.Cron (cron)
import FeatsOfStrengthConfig (facebookConfig)
import HSP
import HSP.Formlets (fset, input, select, submit, label)
import HSP.Identity(evalIdentity)
import qualified HSX.XMLGenerator as HSX
import Network.URI (parseURI, uriToString)
import System.Console.GetOpt
import System.Environment (getArgs, getProgName)
import System.Exit (exitFailure)
import System.FilePath ((>))
import System.IO (stdout)
import System.Log.Logger (Priority(DEBUG, INFO, ERROR), logM, rootLoggerName, setHandlers, setLevel, updateGlobalLogger)
import System.Log.Handler.Simple (fileHandler, streamHandler)
import System.Locale (TimeLocale(..),defaultTimeLocale)
import System.Time (TimeDiff(..), getClockTime)
import Text.Formlets (Form, check, checkM, generalInput, plug, runFormState, xml)
-- TODO: handle improper redirect when you click 'add it'
-- TODO: handle expired session key for facebook connect
-- TODO: expire contests
-- TODO: move to happstack-extra Happstack.State.ClockTime
deriving instance Typeable TimeDiff
deriving instance Data TimeDiff
instance Version TimeDiff
$(deriveSerialize ''TimeDiff)
-- TODO: move to library
canvasURLS :: FacebookConfig -> ShowS
canvasURLS config = uriToString id $ canvasURL config
-- TODO: move to library
addLink :: FacebookConfig -> String
addLink config =
"http://www.facebook.com/add.php?api_key="++(unApiKey . apiKey $ config) ++ "&v=1.0"
authorize :: (XMLGenerator m, EmbedAsChild m c) => FacebookConfig -> c -> XMLGenT m (HSX.XML m)
authorize config c =
<% c %>
loginSP ::
( XMLGenerator m
, MonadPlus m
, ServerMonad m
, HasFacebookConfig m
)
=> m (HSX.XML m)
loginSP =
unXMLGenT $
dir "login" $
do config <- askFacebookConfig
let key = unApiKey (apiKey config)
next = uriToString id (canvasURL config) ""
uri = "http://www.facebook.com/login.php?v=1.0&api_key=" ++ key ++"&next=" ++ next ++ "&canvas="
Challenge your friends to feats of strength.
| Starts | <% timeStr timeZone (startDate challenge) %> |
| Ends | <% timeStr timeZone (endDate challenge) %> |
| Player | Current Score |
|---|---|
| <% show $ sum (map pushups scores) %> |
Invite additional challengers.
in if not (Set.null (wannabes challenge)) then [ inviteFBML ,<% show $ Set.size (wannabes challenge) %> requests to join.
] else [ inviteFBML] else if Map.member user (participants challenge) then [You are in this challenge.
] else if Set.member user (wannabes challenge) then [You have requested an invitation to this challenge.
] else if Set.member user (invited challenge) then [Accept invitation to join this challenge.
] else [Request invitation to join this challenge.
] %>An error occurred while updating your score: <% show msg %>
(Success cs) -> do let competitors = List.delete user $ nub $ concatMap (Map.keys . participants) (IxSet.toList cs) name = renderAsFBML . evalIdentity $Scored updated successfully.
handleFailure :: [ErrorMsg] -> [XMLGenT m (HSX.XML m)] -> XMLGenT m (HSX.XML m) handleFailure errs formXML =An error occured while attempting to request an invite. <% show msg %>
(Success ()) -> do config <- askFacebookConfig let name = renderAsFBML . evalIdentity $You request to join the challenge has been sent.
(Right [(_,_,_)]) -> appTemplate "Not Friends"You must be friends with
Error calling Friends.AreFriends
, dir "approve" $ challengeSP $ \challenge -> do liftIO $ print ("approve: " ++ show challenge) user <- askUser config <- askFacebookConfig if (user /= owner challenge) then appTemplate "Authorization Error"You are not the owner of this challenge, so you can not approve invitation requests.
else appTemplate "Approve Invitations" (formletPart "approval" ((uriToString id (canvasURL config)) ("approve/" ++ (show . unChallengeId . challengeId $ challenge) ++ "/submit")) (handleSuccess config challenge) handleFailure (approvalForm challenge)) ] where handleSuccess :: FacebookConfig -> Challenge -> ([User], Bool) -> XMLGenT m (HSX.XML m) handleSuccess config challenge (users, True) = do res <- update (AddParticipants (challengeId challenge) users) case res of (Failure msg) -> XMLGenT $ appTemplate "Error"An error occurred while approving invitations: <% show msg %>
(Success ()) -> do let notification = "Congrats, you have been accepted into the push competition." -- TODO: link to the competition page callMethod (Notifications.Send users notification (Just Notifications.UserToUser)) XMLGenT $ appTemplate "Aprroved"Approved invite requests for <% mapM (\(User u) ->
An error occurred while bonging wannabes: <% show msg %>
(Success ()) -> XMLGenT $ appTemplate "Denied"Denied invite requests for <% mapM (\(User u) ->
An error occurred while approving invitations: <% show msg %>
(Success ()) -> XMLGenT $ appTemplate "Aprroved"Approved invite requests for <% mapM (\(User u) ->
Challenges have to been sent the following users.
<% mapM (\u ->An error occurred while accepting the invitation: <% show errs %>
(Success ()) -> appTemplate "Invitation Accepted"Invitation accepted
else do config <- askFacebookConfig appTemplate "Invitation Required"You have not received an invitation to this challenge. Click here to request an invitation.
challengeSP :: ( MonadIO m , MonadPlus m , ServerMonad m ) => (Challenge -> m a) -> m a challengeSP f = path $ \i -> do let cid = ChallengeId i mChallenge <- query (AskChallengeById cid) liftIO $ print (cid, mChallenge) all <- query AllChallenges let mChallenge = getOne (all @= cid) liftIO $ print (cid, all @= cid) case mChallenge of Nothing -> mzero -- FIXME: maybe a real error message? (Just challenge) -> f challenge -- FIXME: this rounds to nearest second even though more precision is available ztToClockTime :: ZonedTime -> ClockTime ztToClockTime zt = TOD (round (utcTimeToPOSIXSeconds (zonedTimeToUTC zt))) 0 -- FIXME: this rounds to nearest second even though more precision is available clockTimeToZonedTime :: TimeZone -> ClockTime -> ZonedTime clockTimeToZonedTime tz (TOD epochSeconds _) = utcToZonedTime tz (posixSecondsToUTCTime (fromIntegral epochSeconds)) impl :: ServerPartT IO Response impl = msum [ {- dir "fbconnect" $ -- FIXME: should come from config msum [ xdReceiverPart , toResponse . FbXML <$> withFacebookConnect facebookConfig (msum [ dir "members" $ withSessionSP members , homepage ]) ] , -} dir "facebook" $ withFacebook facebookConfig $ msum [ withSessionSP (toResponse . FbXML <$> msum [ createChallengeSP , addScoreSP , acceptSP , inviteSP , inviteRequestSP , joinSP , nullDir >> homepage ]) , toResponse <$> loginSP , toResponse . FbXML <$> do config <- askFacebookConfig appTemplate "Hello"This app is about pushups. Click here to add it.
] ] data LogMode = Production | Development deriving (Read, Show, Eq, Ord, Enum, Bounded) setupLogger :: String -> FilePath -> LogMode -> IO () setupLogger progName logDir logMode = do appLog <- fileHandler (logDir > (progName ++ "_root.log")) DEBUG accessLog <- fileHandler (logDir > (progName ++ "_access.log")) INFO stdoutLog <- streamHandler stdout DEBUG case logMode of Development -> do -- Root Log updateGlobalLogger rootLoggerName (setLevel DEBUG . setHandlers [appLog, stdoutLog]) -- Access Log updateGlobalLogger "Happstack.Server.AccessLog.Combined" (setLevel INFO . setHandlers [accessLog]) Production -> do -- Root Log updateGlobalLogger rootLoggerName (setLevel INFO . setHandlers [appLog]) -- Access Log updateGlobalLogger "Happstack.Server.AccessLog.Combined" (setLevel INFO . setHandlers [accessLog]) stateProxy :: Proxy State stateProxy = Proxy main :: IO () main = do -- progname effects where state is stored and what the logfile is named progName <- getProgName args <- getArgs appConf <- case parseConfig args of (Left e) -> do logM progName ERROR (unlines e) exitFailure (Right f) -> return (f $ defaultConf progName) setupLogger progName (logs appConf) (logMode appConf) -- start the state system control <- startSystemState' (store appConf) stateProxy -- start the http server httpTid <- forkIO $ simpleHTTP (httpConf appConf) impl -- checkpoint the state once a day cronTid <- forkIO $ cron (60*60*24) (createCheckpoint control) -- wait for termination signal waitForTermination -- cleanup killThread httpTid killThread cronTid createCheckpoint control shutdownSystem control data AppConf = AppConf { httpConf :: Conf , store :: FilePath , static :: FilePath , imageStore :: FilePath , imageCache :: FilePath , logs :: FilePath , logMode :: LogMode , domain :: String } defaultConf :: String -> AppConf defaultConf progName = AppConf { httpConf = nullConf , store = "_local/" ++ progName ++ "_state" , static = "static" , imageStore = "_local/imageStore" , imageCache = "_local/imageCache" , logs = "" , logMode = Development , domain = "" } opts :: [OptDescr (AppConf -> AppConf)] opts = [ Option [] ["http-port"] (ReqArg (\h c -> c { httpConf = (httpConf c) {port = read h} }) "port") "port to bind http server" , Option [] ["no-validate"] (NoArg (\ c -> c { httpConf = (httpConf c) { validator = Nothing } })) "Turn off HTML validation" , Option [] ["store"] (ReqArg (\h c -> c {store = h}) "PATH") "The directory used for database storage." , Option [] ["static"] (ReqArg (\h c -> c {static = h}) "PATH") "The directory searched for static files" , Option [] ["logs"] (ReqArg (\h c -> c {logs = h}) "PATH") "The directory to store log files in" , Option [] ["log-mode"] (ReqArg (\h c -> c {logMode = read h}) (show ([minBound .. maxBound] :: [LogMode]))) "The logging mode to use" , Option [] ["domain"] (ReqArg (\h c -> c {domain = h}) "URI") ("The domain name associated with this server.") ] parseConfig :: [String] -> Either [String] (AppConf -> AppConf) parseConfig args = case getOpt Permute opts args of (flags,_,[]) -> Right $ \appConf -> foldr ($) appConf flags (_,_,errs) -> Left errs startSystemState' :: (Component st, Methods st) => String -> Proxy st -> IO (MVar TxControl) startSystemState' = runTxSystem . Queue . FileSaver -- TODO: move to library fbStyle :: (XMLGenerator m) => XMLGenT m (HSX.XML m) fbStyle =