From f7a038e0083fc1379350cf56843a31e014ea20d8 Mon Sep 17 00:00:00 2001 From: Luciano Perezzini Date: Fri, 5 Jan 2018 19:46:52 -0300 Subject: [PATCH 01/12] kelvinToCelsius function --- src/Weather.hs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Weather.hs b/src/Weather.hs index 64392d5..9a9cd99 100644 --- a/src/Weather.hs +++ b/src/Weather.hs @@ -7,6 +7,7 @@ module Weather , getPressure , getHumidity , getCurrentWeatherFromGeoLoc + , kelvinToCelsius ) where import Config @@ -82,12 +83,17 @@ getCurrentWeatherFromGeoLoc geoLoc = do key <- key let lat = Text.pack $ show $ Location.getLat geoLoc let long = Text.pack $ show $ Location.getLong geoLoc + putStrLn "Inside getCurrentWeatherFromGeoLoc 1" let opts = defaults & param "APPID" .~ [key] & param "lat" .~ [lat] & param "lon" .~ [long] req <- getWith opts (Text.unpack endpoint) + putStrLn "Inside getCurrentWeatherFromGeoLoc 2" let headerStatusCode = req ^. responseStatus . statusCode let apiStatus = req ^? responseBody . Lens.key "cod" . Lens._Integer if Json.httpRequestOk headerStatusCode && apiRequestOk apiStatus then return (decode $ req ^. responseBody) else return $ Nothing + +kelvinToCelsius :: Double -> Double +kelvinToCelsius k = k - 273.5 From dc32d27f833156791cf6355567cf24f9ab4cb24f Mon Sep 17 00:00:00 2001 From: Luciano Perezzini Date: Fri, 5 Jan 2018 19:47:05 -0300 Subject: [PATCH 02/12] Export ID --- src/User.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/User.hs b/src/User.hs index ba229a7..7df3766 100644 --- a/src/User.hs +++ b/src/User.hs @@ -4,6 +4,7 @@ module User ( User(..) , Name(..) + , ID , newName , getID , getName From be855cf7536a2ef090bb183c20f780a852b09357 Mon Sep 17 00:00:00 2001 From: Luciano Perezzini Date: Fri, 5 Jan 2018 19:47:28 -0300 Subject: [PATCH 03/12] Render current weather --- src/Html.hs | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/Html.hs b/src/Html.hs index ece6a34..3d9413f 100644 --- a/src/Html.hs +++ b/src/Html.hs @@ -20,7 +20,7 @@ import User import Location import Interest import News --- import Weather +import Weather import Utils userInfoToHtml :: User -> Html @@ -62,17 +62,17 @@ articleToHtml article = let h3 $ toHtml $ description p $ toHtml $ url --- currentWeatherToHtml :: Weather -> Html --- currentWeatherToHtml weather = let --- temp = show $ Weather.getTemp weather --- pressure = show $ Weather.getPressure weather --- humidity = show $ Weather.getHumidity weather --- in H.div $ do --- h4 "The current weather: " --- ul $ do --- li $ toHtml $ "Temperature: " ++ temp --- li $ toHtml $ "Pressure: " ++ pressure --- li $ toHtml $ "Humidity: " ++ humidity +currentWeatherToHtml :: Weather -> Html +currentWeatherToHtml weather = let + (temp, pressure, humidity) = (show $ Weather.kelvinToCelsius $ Weather.getTemp weather + , show $ Weather.getPressure weather + , show $ Weather.getHumidity weather) + in H.div $ do + h4 "The current weather: " + ul $ do + li $ toHtml $ "Temperature: " ++ temp + li $ toHtml $ "Pressure: " ++ pressure + li $ toHtml $ "Humidity: " ++ humidity welcomeMailTemplate :: User -> Html welcomeMailTemplate user = docTypeHtml $ do @@ -84,12 +84,14 @@ welcomeMailTemplate user = docTypeHtml $ do renderWelcomeMailTemplate :: User -> String renderWelcomeMailTemplate user = renderHtml $ welcomeMailTemplate user -dailyMailTemplate :: News -> Html -dailyMailTemplate news = let +dailyMailTemplate :: User -> News -> Weather -> Html +dailyMailTemplate user news weather = let total = News.getNewsTotal news totalStr = show total :: String - totalHeader = "Total articles: " ++ totalStr + totalHeader = "total articles: " ++ totalStr articles = News.getNewsArticles news + + userFirstName = Text.unpack $ User.getFirstName $ User.getName user in if total == 0 then H.div $ do h3 "Application couldn't retrieve news articles matching your interests today." @@ -97,9 +99,10 @@ dailyMailTemplate news = let H.head $ do H.title "dailyHASK" body $ do - h2 "The following news articles match your interests and were published today" - h3 $ toHtml $ totalHeader + h2 $ toHtml $ userFirstName ++ ", the following news articles match your interests and were published today (" ++ totalHeader ++ ")" + -- h3 $ toHtml $ totalHeader + currentWeatherToHtml weather ul $ forM_ articles (li . articleToHtml) -renderDailyMailTemplate :: News -> String -renderDailyMailTemplate news = renderHtml $ dailyMailTemplate news +renderDailyMailTemplate :: User -> News -> Weather -> String +renderDailyMailTemplate user news weather = renderHtml $ dailyMailTemplate user news weather From 96f7a8bcf0da0dfcd8e2e93faa3d83d0a170d86c Mon Sep 17 00:00:00 2001 From: Luciano Perezzini Date: Fri, 5 Jan 2018 19:48:08 -0300 Subject: [PATCH 04/12] Parse BSON to user record --- app/Main.hs | 45 ++++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/app/Main.hs b/app/Main.hs index eb8e0b4..47b0b9d 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -20,7 +20,7 @@ import Interest import Mail import News import Location --- import Weather +import Weather import Html import Database as DB import Error as E @@ -76,20 +76,43 @@ doWork :: IO () doWork = let workActions :: SMTPConnection -> Bson.Document -> IO () workActions conn user = do - let email = Bson.typed $ Bson.valueAt "email" user :: Text + let _id = M.fromJust $ Bson.lookup "_id" user :: User.ID + + let name = M.fromJust $ Bson.lookup "name" user :: Bson.Document + let firstName = M.fromJust $ Bson.lookup "first" name :: Text + let lastName = M.fromJust $ Bson.lookup "last" name :: Text + let name' = Name firstName lastName :: User.Name + + let email = M.fromJust $ Bson.lookup "email" user :: Text + + let location = M.fromJust $ Bson.lookup "location" user :: Bson.Document + let address = M.fromJust $ Bson.lookup "address" location :: Text + let lat = M.fromJust $ Bson.lookup "lat" location :: Double + let long = M.fromJust $ Bson.lookup "long" location :: Double + let location' = GeoLoc address lat long :: GeoLoc + let interests = Bson.typed $ Bson.valueAt "interests" user :: [Interest] + + let userRecord = User _id name' email location' interests :: User + news <- News.getNews interests - if M.isNothing news + putStrLn "News articles retrieved from API..." + currentWeather <- Weather.getCurrentWeatherFromGeoLoc $ User.getLocation userRecord + putStrLn "Weather information retrieved from API..." + if M.isNothing news || M.isNothing currentWeather then E.callError "Error. Main: couldn't retrive news articles. Aborting..." else let news' = M.fromJust news - in Mail.send conn email "Your dailyHASK" "plain text body" (Html.renderDailyMailTemplate news') + currentWeather' = M.fromJust currentWeather + in do + Mail.send conn email "Your dailyHASK" "plain text body" (Html.renderDailyMailTemplate userRecord news' currentWeather') + putStrLn "Daily mail sent to user/s..." work :: [Bson.Document] -> SMTPConnection -> IO () work users conn = do mapM_ (workActions conn) users in do - putStrLn "doWork started..." + putStrLn "Processing database..." collection <- collection pipe <- DB.open users <- DB.findAll pipe [] collection @@ -98,7 +121,7 @@ doWork = let Mail.auth conn work users conn Mail.closeConnection conn - putStrLn "doWork finished." + putStrLn "Process finished." main :: IO () main = do @@ -117,14 +140,10 @@ main' h m = do if line == "Y" || line == "y" then main' h m else forever $ do - now <- Date.getCurrentTimeFromServer - when (scheduleMatches schedule now) doWork + -- now <- Date.getCurrentTimeFromServer + -- when (scheduleMatches schedule now) doWork + doWork threadDelay 60000000 -- delay 1 minute to skip schedule - -- doWork where cronSpec = Text.pack (m ++ " " ++ h ++ " * * *") schedule = either (E.callError "Error at configuring cron schedule (it should not happen). Aborting...") id (parseCronSchedule cronSpec) - -test = do - now <- Date.getCurrentTimeFromServer - return $ show $ now From 86faf6a06b94dc02b8ea154cde4b1485b4d9d3c8 Mon Sep 17 00:00:00 2001 From: Luciano Perezzini Date: Fri, 5 Jan 2018 19:58:37 -0300 Subject: [PATCH 05/12] Error fix --- app/Main.hs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/Main.hs b/app/Main.hs index 47b0b9d..7c07d7d 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -140,9 +140,8 @@ main' h m = do if line == "Y" || line == "y" then main' h m else forever $ do - -- now <- Date.getCurrentTimeFromServer - -- when (scheduleMatches schedule now) doWork - doWork + now <- Date.getCurrentTimeFromServer + when (scheduleMatches schedule now) doWork threadDelay 60000000 -- delay 1 minute to skip schedule where cronSpec = Text.pack (m ++ " " ++ h ++ " * * *") From ecbe3ccd5747147398c9d3a27dc6e7aeccba4c8e Mon Sep 17 00:00:00 2001 From: Luciano Perezzini Date: Mon, 8 Jan 2018 16:14:56 -0300 Subject: [PATCH 06/12] Haddock Document code --- src/Config.hs | 7 +++++++ src/Database.hs | 17 +++++++++++++++++ src/Date.hs | 8 ++++++++ src/Error.hs | 6 ++++++ src/Interest.hs | 8 ++++++++ src/Location.hs | 21 +++++++++++++++++++-- src/Mail.hs | 17 +++++++++++++++++ src/News.hs | 35 ++++++++++++++++++++++++++++++++--- src/Url.hs | 11 +++++++---- src/User.hs | 21 +++++++++++++++++++++ src/Utils.hs | 17 +++++++++++++++++ src/Weather.hs | 31 +++++++++++++++++-------------- 12 files changed, 176 insertions(+), 23 deletions(-) diff --git a/src/Config.hs b/src/Config.hs index 2fa71ce..936a9b5 100644 --- a/src/Config.hs +++ b/src/Config.hs @@ -1,3 +1,9 @@ +{-| +Module : Config + +Definition of functions to get values of certain keys form a configuration file named app.cfg placed in root folder +-} + module Config ( getValue @@ -12,6 +18,7 @@ import qualified Error as E appConfigFile :: String appConfigFile = "/app.cfg" +-- |The 'getValue' function gets a value from a given key stored in /app.cfg getValue :: String -> IO (Maybe String) getValue key = do currDir <- Dir.getCurrentDirectory diff --git a/src/Database.hs b/src/Database.hs index 1d76d16..3ca25d1 100644 --- a/src/Database.hs +++ b/src/Database.hs @@ -1,3 +1,9 @@ +{-| +Module : Database + +Asbtraction of the Haskell MongoDB driver +-} + {-# LANGUAGE OverloadedStrings #-} module Database @@ -42,12 +48,16 @@ db = do then E.callError "Error: database.db config file value not found" else return $ M.fromJust value +-- |The 'open' function returns a TCP connection to a database defined in the +-- /app.cfg file, a 'MongoDB.Pipe' open :: IO MongoDB.Pipe open = do server <- server pipe <- MongoDB.connect $ MongoDB.host server return $ pipe +-- |The 'close' function closes the TCP connection the database setted in the +-- config file /app.cfg close :: MongoDB.Pipe -> IO () close pipe = MongoDB.close pipe @@ -58,36 +68,43 @@ run pipe act = do exec <- MongoDB.access pipe MongoDB.master db' act return $ exec +-- |The 'insert' function inserts a document into a given collection insert :: MongoDB.Pipe -> MongoDB.Collection -> Bson.Document -> IO Bson.Value insert pipe collection document = do exec <- run pipe $ MongoDB.insert collection document return $ exec +-- |The 'insertMany' function inserts a set of documents into a given collection insertMany :: MongoDB.Pipe -> MongoDB.Collection -> [Bson.Document] -> IO [Bson.Value] insertMany pipe collection documents = do exec <- run pipe $ MongoDB.insertMany collection documents return $ exec +-- |The 'count' function counts documents, from a collection, that preserve some property count :: MongoDB.Pipe -> MongoDB.Selector -> MongoDB.Collection -> IO Int count pipe fields collection = do exec <- run pipe $ MongoDB.count $ MongoDB.select fields collection return $ exec +-- |The 'save' function updates documents, from a collection, that preserve some property save :: MongoDB.Pipe -> MongoDB.Collection -> MongoDB.Selector -> IO () save pipe collection fields = do exec <- run pipe $ MongoDB.save collection fields return $ exec +-- |The 'delete' function drops documents, from a collection, that preserve some property delete :: MongoDB.Pipe -> MongoDB.Selector -> MongoDB.Collection -> IO () delete pipe fields collection = do exec <- run pipe $ MongoDB.delete $ MongoDB.select fields collection return $ exec +-- |The 'findOne' function fetches just one document, the first one from a collection, that preserve some property findOne :: MongoDB.Pipe -> MongoDB.Selector -> MongoDB.Collection -> IO (Maybe Bson.Document) findOne pipe fields collection = do exec <- run pipe $ MongoDB.findOne $ MongoDB.select fields collection return $ exec +-- |The 'findAll' function fetches all documents from a given collection. findAll :: MongoDB.Pipe -> MongoDB.Selector -> MongoDB.Collection -> IO [Bson.Document] findAll pipe fields collection = do exec <- run pipe $ MongoDB.find (MongoDB.select fields collection) >>= MongoDB.rest diff --git a/src/Date.hs b/src/Date.hs index 98bb80c..0e12910 100644 --- a/src/Date.hs +++ b/src/Date.hs @@ -1,3 +1,9 @@ +{-| +Module : Date + +Abstraction of 'Date.Time' library: definition of certain functions concerning actual project +-} + module Date ( today @@ -8,6 +14,7 @@ import Data.Time.Clock import Data.Time.Calendar import Data.Time.LocalTime +-- |The 'getCurrentTimeFromServer' returns current time of server using actual time zone, and convert it to 'UTCTime' format getCurrentTimeFromServer :: IO UTCTime getCurrentTimeFromServer = do tz <- getCurrentTimeZone @@ -15,6 +22,7 @@ getCurrentTimeFromServer = do let localDiffTime = timeOfDayToTime $ localTimeOfDay $ utcToLocalTime tz now :: DiffTime return $ UTCTime day localDiffTime +-- |The 'today' function returns current y-m-d format today :: IO String today = do UTCTime day _ <- getCurrentTime diff --git a/src/Error.hs b/src/Error.hs index 1018e9c..9c88d42 100644 --- a/src/Error.hs +++ b/src/Error.hs @@ -1,6 +1,12 @@ +{-| +Module : Error +Description : Definition of error functions +-} + module Error ( callError ) where +-- |The 'callError' function just calls 'Prelude' 'error' function callError = error diff --git a/src/Interest.hs b/src/Interest.hs index f63a754..0e0b74d 100644 --- a/src/Interest.hs +++ b/src/Interest.hs @@ -1,3 +1,9 @@ +{-| +Module : Interest + +Definition of 'Interest' type along multiple functions concerning 'Interest' values +-} + module Interest ( Interest @@ -9,8 +15,10 @@ import Data.Text as Text hiding (map) type Interest = Text +-- |The 'toDataType' maps a list of strings to a list of 'Interest' values toDataType :: [String] -> [Interest] toDataType list = map pack list +-- |The 'fromDataType' maps a list of 'Interest' values to a list of strings fromDataType :: [Interest] -> [String] fromDataType list = map unpack list diff --git a/src/Location.hs b/src/Location.hs index 2e3cbce..cd5e6fb 100644 --- a/src/Location.hs +++ b/src/Location.hs @@ -1,3 +1,10 @@ +{-| +Module : Location + +Definition of 'GeoLoc' data type along multiple functions concerning 'GeoLoc' values. Defines +-- a GET request to retrieve geographic location from Google Maps API services +-} + {-# LANGUAGE OverloadedStrings #-} module Location @@ -17,7 +24,7 @@ module Location import Config import Url import Utils -import Json +import Http import Error as E import Data.Aeson @@ -50,18 +57,23 @@ instance FromJSON GeoLoc where long <- location .: "lng" return (GeoLoc address lat long) +-- |The 'getAddress' function takes a 'GeoLoc' value and extracts the address value from it getAddress :: GeoLoc -> Address getAddress = address +-- |The 'getLat' function extracts 'lat' value from a 'GeoLoc' value getLat :: GeoLoc -> Latitude getLat = lat +-- |The 'getLong' function extracst 'long' value from a 'GeoLoc' value getLong :: GeoLoc -> Longitude getLong = long +-- |The 'stringToAddress' function maps a string to an 'Address' value stringToAddress :: String -> Address stringToAddress = Text.pack +-- |The 'addressToString' function maps an 'Address' value to a string addressToString :: Address -> String addressToString = Text.unpack @@ -84,8 +96,12 @@ apiRequestOk t = if t == "OK" || t == "ok" then True else False +-- |The 'getGeoLocFromString' takes a string geographic location address and makes a +-- GET request to Google Maps API services to retrieve 'lat' and 'long'. +-- It returns 'Nothing' in case the request fails getGeoLocFromString :: String -> IO (Maybe GeoLoc) getGeoLocFromString address = do + putStrLn "Start of GET request from locations API..." endpoint <- endpoint key <- key let address' = Text.pack $ Utils.replaceCharByCharInString ' ' '+' address @@ -93,6 +109,7 @@ getGeoLocFromString address = do req <- getWith opts (Text.unpack endpoint) let headerStatusCode = req ^. responseStatus . statusCode let apiStatus = req ^. responseBody . Lens.key "status" . Lens._String - if Json.httpRequestOk headerStatusCode && apiRequestOk apiStatus + putStrLn "End of GET request from locations API" + if Http.isGETRequestOk headerStatusCode && apiRequestOk apiStatus then return (decode $ req ^. responseBody) else return $ Nothing diff --git a/src/Mail.hs b/src/Mail.hs index 1a09296..26bdd6e 100644 --- a/src/Mail.hs +++ b/src/Mail.hs @@ -1,3 +1,10 @@ +{-| +Module : Mail + +HaskellNet-SSL package abstraction, and definition of types along functions to handle +-- them +-} + {-# LANGUAGE OverloadedStrings #-} module Mail @@ -26,12 +33,16 @@ import Network.HaskellNet.SMTP.SSL as SMTP type Address = Text type Subject = Text +-- |The 'stringToAddress' function maps a string to an 'Address' value stringToAddress :: String -> Address stringToAddress = Text.pack +-- |The 'addressToString' function maps an 'Address' value to a string addressToString :: Address -> String addressToString = Text.unpack +-- |The 'isAddressValid' function takes an 'Address' and validates it according +-- to the HTML standard isAddressValid :: Address -> Bool isAddressValid address = EValidate.isValidEmail address @@ -63,15 +74,19 @@ smtpMailAddressAlias = do then E.callError "Error: smtp.mail.address.alias config value not found" else return $ M.fromJust value +-- |'connect' connects to an SMTP server using a SMTP hostname from a config file connect :: IO SMTPConnection connect = do hostname <- smtpHostname conn <- SMTP.connectSMTPSSL hostname return $ conn +-- |The 'closeConnection' function takes a SMTP connection and closes it closeConnection :: SMTPConnection -> IO () closeConnection conn = SMTP.closeSMTP conn +-- |The 'auth' function takes a SMTP connection and logs in using predefined SMTP user name, user password, and an +-- established SMTP connection auth :: SMTPConnection -> IO () auth conn = do userName <- smtpUserName @@ -81,6 +96,8 @@ auth conn = do then E.callError "Mail. Auth denied. Aborting..." else return $ () +-- |The 'send' function takes a SMTP connetion, a receiver, a subject, a plain text body, a HTML body, and +-- sends an e-mail send :: SMTPConnection -> Text -> Text -> Text -> String -> IO () send conn receiver subject plainTextBody htmlBody = do let receiver' = Text.unpack receiver :: String diff --git a/src/News.hs b/src/News.hs index 0b62a34..927a9b6 100644 --- a/src/News.hs +++ b/src/News.hs @@ -1,3 +1,12 @@ +{-| +Module : News + +Definition of 'News' and 'Articles' data types along multiple functions to handle them. +Defines a GET request to retrieve news from NewsApi.org services that match multiple interests +and were published today at some time +-} + + {-# LANGUAGE OverloadedStrings #-} module News @@ -18,7 +27,7 @@ module News import Config import Interest -import Json +import Http import Url import Date import Utils @@ -65,30 +74,46 @@ instance FromJSON News where articles <- v .: "articles" return (News total articles) +-- |The 'getNewsTotal' function takes a 'News' value and returns the 'total' value from it getNewsTotal :: News -> Int getNewsTotal = total +-- |The 'getNewsArticles' function takes a 'News' value and returns the 'articles' value from it getNewsArticles :: News -> [Article] getNewsArticles = articles +-- |The 'getArticleSourceName' function takes an 'Article' value and maybe returns it's source name. It +-- returns 'Nothing' in case of the value is JSON null. getArticleSourceName :: Article -> Maybe Text getArticleSourceName = sourceName +-- |The 'getArticleAuthor' function takes an 'Article' value and maybe returns it's author. It +-- returns 'Nothing' in case of the value is JSON null. getArticleAuthor :: Article -> Maybe Text getArticleAuthor = author +-- |The 'getArticleTitle' function takes an 'Article' value and maybe returns it's title. It +-- returns 'Nothing' in case of the value is JSON null. getArticleTitle :: Article -> Maybe Text getArticleTitle = title +-- |The 'getArticleDescripton' function takes an 'Article' value and maybe returns it's description. It +-- returns 'Nothing' in case of the value is JSON null. getArticleDescripton :: Article -> Maybe Text getArticleDescripton = description +-- |The 'getArticleUrl' function takes an 'Article' value and returns the URL pointing to the corresponding article. It +-- returns 'Nothing' in case of the value is JSON null. getArticleUrl :: Article -> Url getArticleUrl = url +-- |The 'getArticleUrlToImage' function takes an 'Article' value and maybe returns it's image URL. It +-- returns 'Nothing' in case of the value is JSON null. getArticleUrlToImage :: Article -> Maybe Url getArticleUrlToImage = urlToImage +-- |The 'getArticlePublishedAt' function takes an 'Article' value and maybe returns the date-time it was published. It +-- returns 'Nothing' in case of the value is JSON null. getArticlePublishedAt :: Article -> Maybe Text getArticlePublishedAt = publishedAt @@ -113,8 +138,11 @@ apiRequestOk t = if t == "OK" || t == "ok" then True else False +-- |The 'getNews' function takes a list of interests and returns: a value of type 'News' mathing these interests, +-- or 'Nothing' in case the GET request to NewsApi.org fails. The news are from today and sorted by popularity getNews :: [Interest] -> IO (Maybe News) getNews interests = do + putStrLn "Start of GET request from news API..." endpoint <- endpoint "api.news.endpoint.everything" key <- key today <- Date.today @@ -122,12 +150,13 @@ getNews interests = do let interests' = Text.pack $ Utils.connectListOfStrings (Interest.fromDataType interests) " OR " let opts = defaults & param "apiKey" .~ [key] & param "q" .~ [interests'] - & param "sortBy" .~ ["publishedAt"] + & param "sortBy" .~ ["popularity"] & param "language" .~ ["en"] & param "from" .~ [today'] req <- getWith opts (Text.unpack endpoint) let headerStatusCode = req ^. responseStatus . statusCode let apiStatus = req ^. responseBody . Lens.key "status" . Lens._String - if Json.httpRequestOk headerStatusCode && apiRequestOk apiStatus + putStrLn "End of GET request from news API" + if Http.isGETRequestOk headerStatusCode && apiRequestOk apiStatus then return (decode $ req ^. responseBody) else return $ Nothing diff --git a/src/Url.hs b/src/Url.hs index 742c05c..aa91f3d 100644 --- a/src/Url.hs +++ b/src/Url.hs @@ -1,3 +1,10 @@ +{-| +Module : Url + +Definition of the Url type +-} + + module Url ( Url @@ -6,7 +13,3 @@ module Url import Data.Text as Text type Url = Text - -encode = undefined - -decode = undefined diff --git a/src/User.hs b/src/User.hs index 7df3766..169d48f 100644 --- a/src/User.hs +++ b/src/User.hs @@ -1,3 +1,11 @@ +{-| +Module : User + +Definition of the 'User' data type along multiple functions concerning 'User' values. Creates +and uploads new users to database +-} + + {-# LANGUAGE OverloadedStrings #-} module User @@ -38,12 +46,15 @@ data Name = Name { , last :: Text } deriving (Show) +-- |The 'getFirstName' function takes a 'Name' and returns its 'first' name getFirstName :: Name -> Text getFirstName = first +-- |The 'getLastName' function takes a 'Name' and returns its 'last' name getLastName :: Name -> Text getLastName = last +-- |The 'newName' function takes a two strings concerning first, and last name, and returns a 'Name' newName :: String -> String -> Name newName first last = Name { first = Text.pack first @@ -64,18 +75,23 @@ collection = do let value' = M.fromJust value return $ Text.pack value' +-- |The 'getID' function takes a 'User' and returns its '_id' getID :: User -> ID getID = _id +-- |The 'getName' function takes a 'User' and returns its 'name' getName :: User -> Name getName user = name user +-- |The 'getEmail' function takes a 'User' and returns its 'email' address getEmail :: User -> Mail.Address getEmail = email +-- |The 'getLocation' function takes a 'User' and returns its geographic 'location' getLocation :: User -> Location.GeoLoc getLocation = location +-- |The 'getInterests' function takes a 'User' and returns its corresponding 'interests' getInterests :: User -> [Interest] getInterests = interests @@ -88,6 +104,8 @@ newUserData id name email location interests = User { , interests = interests } +-- |The 'newUser' function takes a information data about a user, and maybe returns a new 'User'. It first +-- creates a 'User' value, and then uploads it to database newUser :: Name -> Mail.Address -> Location.GeoLoc -> [Interest] -> IO (Maybe User) newUser name email location interests = case Mail.isAddressValid email of True -> do @@ -107,15 +125,18 @@ newUser name email location interests = case Mail.isAddressValid email of , "location" =: locationPost , "interests" =: interests] :: Bson.Document + putStrLn "Uploading new user data to DB..." collection <- collection pipe <- DB.open _id <- DB.insert pipe collection post DB.close pipe + putStrLn ("User uploaded correctly. ID = " ++ (show _id)) let user = newUserData _id name email location interests return $ Just user otherwise -> return $ Nothing +-- |The 'deleteUser' function takes a user's ID, and then deletes it from database deleteUser :: ID -> IO () deleteUser _id = do let selection = ["_id" =: _id] :: Bson.Document diff --git a/src/Utils.hs b/src/Utils.hs index 9371d0c..e2acfa6 100644 --- a/src/Utils.hs +++ b/src/Utils.hs @@ -1,3 +1,9 @@ +{-| +Module : Utils + +Definition of several library's utilities +-} + module Utils ( listOfTextsToListOfStrings @@ -10,22 +16,33 @@ module Utils import Data.Text as Text hiding (map) +-- |The 'listOfStringsToListOfText' function takes a list of strings and maps it to a list +-- of 'Text' listOfStringsToListOfText :: [String] -> [Text] listOfStringsToListOfText list = map Text.pack list +-- |The 'listOfTextsToListOfStrings' function takes a list of 'Text' and maps it to a list +-- of strings listOfTextsToListOfStrings :: [Text] -> [String] listOfTextsToListOfStrings list = map Text.unpack list +-- |The 'replaceCharByCharInString' function swaps a certain 'Char' value by a new 'Char' value +-- in a given string replaceCharByCharInString :: Char -> Char -> String -> String replaceCharByCharInString oldChar newChar str = map (\c -> if c == oldChar then newChar else c) str +-- |The 'connectListOfStrings' function connects strings, in a list of strings, by concatenating a given +-- string connectListOfStrings :: [String] -> String -> String connectListOfStrings (x : []) _ = x connectListOfStrings (x : xs) repl = x ++ repl ++ (connectListOfStrings xs repl) +-- |The 'stringToInteger' function takes a given number formatted in a 'String' value and converts it to an 'Integer' stringToInteger :: String -> Integer stringToInteger s = read s +-- |The 'handleNullValue' function handles a JSON null value: in case the input value is Nothing, it just returns +-- "Not available" as a sort of error; otherwise it just returns the given value handleNullValue :: Maybe a -> Either Text a handleNullValue (Just v) = Right v handleNullValue _ = Left $ Text.pack "Not available" diff --git a/src/Weather.hs b/src/Weather.hs index 9a9cd99..c78fa25 100644 --- a/src/Weather.hs +++ b/src/Weather.hs @@ -1,3 +1,9 @@ +{-| +Module : Weather + +Definition of 'Weather' data type. Retrieves current weather information from Open Weather Map API +-} + {-# LANGUAGE OverloadedStrings #-} module Weather @@ -14,7 +20,7 @@ import Config import Url import Location import Utils -import Json +import Http import Error as E import Data.Text as Text @@ -39,24 +45,18 @@ instance FromJSON Weather where humidity <- main .: "humidity" return (Weather temp pressure humidity) +-- |The 'getTemp' function takes a 'Weather' value and returns its 'temp' getTemp :: Weather -> Double getTemp = temp +-- |The 'getPressure' function takes a 'Weather' value and returns its 'pressure' getPressure :: Weather -> Int getPressure = pressure +-- |The 'getHumidity' function takes a 'Weather' value and returns its 'humidity' getHumidity :: Weather -> Int getHumidity = humidity --- getWindSpeed :: Weather -> Double --- getWindSpeed = windSpeed --- --- getRain :: Weather -> Int --- getRain = rain --- --- getSnow :: Weather -> Int --- getSnow = snow - endpoint :: IO Url endpoint = do value <- Config.getValue "api.owm.endpoint.current" @@ -77,23 +77,26 @@ apiRequestOk (Just n) = if n == 200 else False apiRequestOk _ = False +-- |The 'getCurrentWeatherFromGeoLoc' takes a geographic location and returns its current weather using +-- the Open Weather Map API using latitude and longitude from corresponding location getCurrentWeatherFromGeoLoc :: Location.GeoLoc -> IO (Maybe Weather) getCurrentWeatherFromGeoLoc geoLoc = do + putStrLn "Start of GET request from weather API..." endpoint <- endpoint key <- key let lat = Text.pack $ show $ Location.getLat geoLoc let long = Text.pack $ show $ Location.getLong geoLoc - putStrLn "Inside getCurrentWeatherFromGeoLoc 1" - let opts = defaults & param "APPID" .~ [key] + let opts = defaults & param "appid" .~ [key] & param "lat" .~ [lat] & param "lon" .~ [long] req <- getWith opts (Text.unpack endpoint) - putStrLn "Inside getCurrentWeatherFromGeoLoc 2" let headerStatusCode = req ^. responseStatus . statusCode let apiStatus = req ^? responseBody . Lens.key "cod" . Lens._Integer - if Json.httpRequestOk headerStatusCode && apiRequestOk apiStatus + putStrLn "End of GET request from weather API" + if Http.isGETRequestOk headerStatusCode && apiRequestOk apiStatus then return (decode $ req ^. responseBody) else return $ Nothing +-- |The 'kelvinToCelsius' function converts Kelvin to Celsius temperature kelvinToCelsius :: Double -> Double kelvinToCelsius k = k - 273.5 From 9cbae3e7e19ae67c4be031f88c25abed68c8a7c4 Mon Sep 17 00:00:00 2001 From: Luciano Perezzini Date: Mon, 8 Jan 2018 16:15:27 -0300 Subject: [PATCH 07/12] Use Http instead of Json --- src/Http.hs | 15 +++++++++++++++ src/Json.hs | 8 -------- 2 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 src/Http.hs delete mode 100644 src/Json.hs diff --git a/src/Http.hs b/src/Http.hs new file mode 100644 index 0000000..bb75e5a --- /dev/null +++ b/src/Http.hs @@ -0,0 +1,15 @@ +{-| +Module : Http + +Definition of functions to handle HTTP requests +-} + +module Http + ( + isGETRequestOk + ) where + +-- |The 'isGETRequestOk' returns whether a GET request got a 200 status code +isGETRequestOk :: Int -> Bool +isGETRequestOk 200 = True +isGETRequestOk _ = False diff --git a/src/Json.hs b/src/Json.hs deleted file mode 100644 index f3d2977..0000000 --- a/src/Json.hs +++ /dev/null @@ -1,8 +0,0 @@ -module Json - ( - httpRequestOk - ) where - -httpRequestOk :: Int -> Bool -httpRequestOk 200 = True -httpRequestOk _ = False From 77be32e856a800e5d523e93754c756591397ac83 Mon Sep 17 00:00:00 2001 From: Luciano Perezzini Date: Mon, 8 Jan 2018 16:16:17 -0300 Subject: [PATCH 08/12] Modify HTML template of a news article --- src/Html.hs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Html.hs b/src/Html.hs index 3d9413f..529c75f 100644 --- a/src/Html.hs +++ b/src/Html.hs @@ -53,26 +53,23 @@ articleToHtml article = let author = handleNull article News.getArticleAuthor title = handleNull article News.getArticleTitle description = handleNull article News.getArticleDescripton - url = News.getArticleUrl article + url = fromString $ Text.unpack $ News.getArticleUrl article :: AttributeValue urlToImage = handleNull article News.getArticleUrlToImage publishedAt = handleNull article News.getArticlePublishedAt in H.div $ do H.article $ do - h2 $ toHtml $ title - h3 $ toHtml $ description - p $ toHtml $ url + h3 $ a ! href url $ toHtml title + h5 $ toHtml $ sourceName + h4 $ toHtml $ description currentWeatherToHtml :: Weather -> Html currentWeatherToHtml weather = let - (temp, pressure, humidity) = (show $ Weather.kelvinToCelsius $ Weather.getTemp weather + (temp, pressure, humidity) = (show $ round $ Weather.kelvinToCelsius $ Weather.getTemp weather , show $ Weather.getPressure weather , show $ Weather.getHumidity weather) in H.div $ do - h4 "The current weather: " - ul $ do - li $ toHtml $ "Temperature: " ++ temp - li $ toHtml $ "Pressure: " ++ pressure - li $ toHtml $ "Humidity: " ++ humidity + h2 "The current weather: " + h3 $ toHtml $ "Temperature: " ++ temp ++ " °C; " ++ "pressure: " ++ pressure ++ " hPa; " ++ "humidity: " ++ humidity ++ " %" welcomeMailTemplate :: User -> Html welcomeMailTemplate user = docTypeHtml $ do From 3f24802abcb5622941b0fc23e1c2b9bbb8bfc9e8 Mon Sep 17 00:00:00 2001 From: Luciano Perezzini Date: Mon, 8 Jan 2018 16:16:26 -0300 Subject: [PATCH 09/12] Haddock --- src/Html.hs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Html.hs b/src/Html.hs index 529c75f..2fd7259 100644 --- a/src/Html.hs +++ b/src/Html.hs @@ -1,4 +1,10 @@ -{-# LANGUAGE OverloadedStrings, ExistentialQuantification #-} +{-| +Module : Html + +Definition of functions to render HTML output code +-} + +{-# LANGUAGE OverloadedStrings #-} module Html ( @@ -15,6 +21,7 @@ import Text.Blaze.Html.Renderer.String import Control.Monad (forM_) import Data.Text as Text hiding (unwords) +import Data.String import User import Location @@ -78,6 +85,7 @@ welcomeMailTemplate user = docTypeHtml $ do body $ do userInfoToHtml user +-- |The 'renderWelcomeMailTemplate' returns a string containing HTML code corresponding to a welcome-mail template renderWelcomeMailTemplate :: User -> String renderWelcomeMailTemplate user = renderHtml $ welcomeMailTemplate user @@ -96,10 +104,10 @@ dailyMailTemplate user news weather = let H.head $ do H.title "dailyHASK" body $ do - h2 $ toHtml $ userFirstName ++ ", the following news articles match your interests and were published today (" ++ totalHeader ++ ")" - -- h3 $ toHtml $ totalHeader currentWeatherToHtml weather + h2 $ toHtml $ userFirstName ++ ", the following news articles match your interests and were published today (" ++ totalHeader ++ ")" ul $ forM_ articles (li . articleToHtml) +-- |The 'renderDailyMailTemplate' returns a string containing HTML code corresponding to the daily-mail template renderDailyMailTemplate :: User -> News -> Weather -> String renderDailyMailTemplate user news weather = renderHtml $ dailyMailTemplate user news weather From 6d14aecf8c7bb9381e12c4ab6a15bd2369466890 Mon Sep 17 00:00:00 2001 From: Luciano Perezzini Date: Mon, 8 Jan 2018 16:16:44 -0300 Subject: [PATCH 10/12] Use Http instead of Json --- dailyHASK.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dailyHASK.cabal b/dailyHASK.cabal index 27770cb..3f6169f 100644 --- a/dailyHASK.cabal +++ b/dailyHASK.cabal @@ -57,7 +57,7 @@ library Url Mail Html - Json + Http News Weather Utils From 9d4ab50b7d94bd6bd29205b85a7718fe19a91ab2 Mon Sep 17 00:00:00 2001 From: Luciano Perezzini Date: Mon, 8 Jan 2018 16:17:19 -0300 Subject: [PATCH 11/12] Display msgs when executing --- app/Main.hs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/Main.hs b/app/Main.hs index 7c07d7d..905ce63 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -62,7 +62,9 @@ sendWelcomeMailToUser user = do let userEmailAddress = User.getEmail user conn <- Mail.connect Mail.auth conn + putStrLn "Sending welcome email to user..." Mail.send conn userEmailAddress "Welcome to dailyHASK" "plain text body" (Html.renderWelcomeMailTemplate user) + putStrLn "Welcome email sent" Mail.closeConnection conn return () @@ -96,17 +98,15 @@ doWork = let let userRecord = User _id name' email location' interests :: User news <- News.getNews interests - putStrLn "News articles retrieved from API..." currentWeather <- Weather.getCurrentWeatherFromGeoLoc $ User.getLocation userRecord - putStrLn "Weather information retrieved from API..." if M.isNothing news || M.isNothing currentWeather - then E.callError "Error. Main: couldn't retrive news articles. Aborting..." + then E.callError "Error. Main: couldn't retrive news articles or weather information. Aborting..." else let news' = M.fromJust news currentWeather' = M.fromJust currentWeather in do - Mail.send conn email "Your dailyHASK" "plain text body" (Html.renderDailyMailTemplate userRecord news' currentWeather') - putStrLn "Daily mail sent to user/s..." + Mail.send conn email "Your dailyHASK" "" (Html.renderDailyMailTemplate userRecord news' currentWeather') + putStrLn "Daily mail sent to user..." work :: [Bson.Document] -> SMTPConnection -> IO () work users conn = do @@ -125,9 +125,9 @@ doWork = let main :: IO () main = do - putStrLn ">> Select hour parameter to construct a cronjob" + putStrLn ">> Select hour parameter to construct cronjob" h <- getLine - putStrLn ">> Select minute parameter to construct a cronjob" + putStrLn ">> Select minute parameter to construct cronjob" m <- getLine main' h m @@ -142,7 +142,7 @@ main' h m = do else forever $ do now <- Date.getCurrentTimeFromServer when (scheduleMatches schedule now) doWork - threadDelay 60000000 -- delay 1 minute to skip schedule + threadDelay 60000000 -- delay 1 minute to skip schedule. TODO: find a much simpler way to delay, i.e: using criterion package where cronSpec = Text.pack (m ++ " " ++ h ++ " * * *") schedule = either (E.callError "Error at configuring cron schedule (it should not happen). Aborting...") id (parseCronSchedule cronSpec) From 8ee3b2b19df5046c8a0463f19fc438ecfc2c46d4 Mon Sep 17 00:00:00 2001 From: Luciano Perezzini Date: Mon, 8 Jan 2018 16:17:55 -0300 Subject: [PATCH 12/12] Add criterion package to estimate exec of functions (TODO) --- dailyHASK.cabal | 1 + 1 file changed, 1 insertion(+) diff --git a/dailyHASK.cabal b/dailyHASK.cabal index 3f6169f..95f9113 100644 --- a/dailyHASK.cabal +++ b/dailyHASK.cabal @@ -77,6 +77,7 @@ executable dailyHASK-exe , bson , cron , HaskellNet-SSL + , criterion other-modules: Paths_dailyHASK default-language: Haskell2010