summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSergeyMironov <>2017-06-30 21:11:00 (GMT)
committerhdiff <hdiff@hdiff.luite.com>2017-06-30 21:11:00 (GMT)
commita4a3205836c9f80ef9d530b6ae18954173179907 (patch)
treebbca2b711183ae3ae84e6315b86df6d30da7538a
parent84af287100b61dc468f102ec3ec1a3eeec1ae34b (diff)
version 1.91.9
-rw-r--r--CHANGELOG.md18
-rw-r--r--README.md122
-rw-r--r--VKHS.cabal2
-rw-r--r--app/vkq/Main.hs88
-rw-r--r--src/Web/VKHS.hs17
-rw-r--r--src/Web/VKHS/API/Base.hs20
-rw-r--r--src/Web/VKHS/API/Simple.hs44
-rw-r--r--src/Web/VKHS/Imports.hs11
8 files changed, 189 insertions, 133 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78e5089..fe249d6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,17 @@
-TODO
-----
-* Decrypt 'RepeatedForm' errors
-* Show capchas to users if required
-* Re-implement VK monad as a Free monad special case
-* Runhaskell: handle some standard command line arguments
-* Support storing access-tokens in a temp file
+Version 1.9
+-----------
+
+* Disable Audio API due to upstream changes
+* Handle re-login conditions
+* Handle too-many-requests condition
+* Stop re-exporting `Prelude`, `Imports` provides aliases for popular
+ `Data.Text` functions
+* Cache access token in file
+* Minor fixes
Version 1.7.2
-------------
+
* Initial support for runhaskell mode
Version 1.7.1
diff --git a/README.md b/README.md
index fab4440..6c81378 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ token, it is possible to call various API methods to -query audio files-
(disabled by VK) or retrieve wall messages.
Features
-========
+--------
* Provides access to VK API. Interface options include: VK monad and `vkq` command
line tool.
@@ -20,15 +20,20 @@ Features
copied into `runhaskell` scripts and tweaked according to ones need.
* No more dependencies on curlhs/taglib.
-Issues
-======
+ToDo
+----
+* ~~Decrypt 'RepeatedForm' errors~~
+* ~~Support storing access-tokens in a temp file~~
* Still no support for captchas, one probably should hack `defaultSupervisor`
and add them.
-* Network connection timeout is not handled by the coroutine supervisor.
+* Re-implement VK monad as a Free monad special case
+* Runhaskell: handle some standard command line arguments
* Minor issues here and there. Use `git grep FIXME` to find them
* File uploading still not functioning.
-* Lots grammatical mistakes. Any corrections will be kindly accepted.
+* Network connection timeout is not handled by the coroutine supervisor.
+* Enhance the way `vkq` accepts arguments, support multy-line messages.
+* Grammatical mistakes. Any corrections will be kindly accepted.
Installing
==========
@@ -90,8 +95,7 @@ VKQ command line application
`vkq` is a command line tool which demonstrates API usage. It can be used for
logging in, -downloading music- and reading wall messages. Call `vkq --help` or
-`vkq --help [command]` to read online help.
-
+`vkq command --help` to read online help.
Logging in to VK
----------------
@@ -107,58 +111,85 @@ file or passed to future instances directly using `-a` argument.
d8a41221616ef5ba19537125dc0349bad9d529fa15314ad765911726fe98b15185ac41a7ca2c62f3bf4b9
$ export VKQ_ACCESS_TOKEN=d785932b871f096bd73aac6a35d7a7c469dd788d796463a871e5beb5c61bc6c96788ec2
-Alternatively, using `--eval` option
+Alternatively, result may be achieved using `--eval` option
$ eval `vkq login user@mail.org pass123 --eval`
#### Saving access token to file
- $ vkq login --access-token-file=.access-token
-
-VKQ will ask for email/password and cache the access token into a file. Newer
-versions of VKHS have `--access-token-flag` option enabled by default. Set it
-to empty value to disable the caching feature.
-
-
-Performing custom API calls
----------------------------
+VKQ will cache the access token into a file. Newer versions of VKHS have
+`--access-token-flag` option enabled by default. Set it to empty value to
+disable the caching.
-vkq allows user to call arbitrary API method. The format is as follows:
- Usage: vkq call [--verbose] [--req-per-sec N] [--interactive] [--appid APPID]
- [--user USER] [--pass PASS] [-a ACCESS_TOKEN] METHOD PARAMS
-
-
-For example, lets call ausio.search method to get some Beatles records:
-
- $ vkq call group.search q=Beatles --pretty
-
- { "response": [
- 614751,
- {
- "lyrics_id": "6604412",
- "url": "http://cs1-36v4.vk-cdn.net/p16/59674dd8717db2.mp3?extra=k0s2ja3l6pq6aIDOEW5y5XUCs2--JLX9wZpzOT3iuSnZPR-DNhJSF075NUhICB_szMOKKlVJFFlqLlg691q6cKhwiGZgTRU1oAimXzXY396cfNAHnotc8--7w-0xnvoPK6qVoI8",
- "aid": 85031440,
- "title": "Twist and Shout ",
- "genre": 1,
- "owner_id": 9559206,
- "duration": 156,
- "artist": "The Beatles"
- },
- ...
+Performing API calls
+--------------------
+`vkq` allows user to call arbitrary API method. The generic interface is as follows:
+
+ $ vkq api --help
+ Usage: vkq api [--verbose] [--req-per-sec N] [--interactive] [--appid APPID]
+ [--user USER] [--pass PASS] [-a ACCESS_TOKEN]
+ [--access-token-file FILE] METHOD PARAMS [--pretty]
+ Call VK API method
+
+ Available options:
+ --verbose Be verbose
+ --req-per-sec N Max number of requests per second
+ --interactive Allow interactive queries
+ --appid APPID Application ID, defaults to VKHS
+ --user USER User name or email
+ --pass PASS User password
+ -a ACCESS_TOKEN Access token. Honores VKQ_ACCESS_TOKEN environment
+ variable
+ --access-token-file FILE Filename to store actual access token, should be used
+ to pass its value between sessions
+ METHOD Method name
+ PARAMS Method arguments, KEY=VALUE[,KEY2=VALUE2[,,,]]
+ --pretty Pretty print resulting JSON
+ -h,--help Show this help text
+
+
+The session may look like the following:
+
+ $ vkq api "messages.send" "user_id=111111,message=\"test\"" --pretty
+ bd7da7e9cfb4cc12c0a49093173ca8785c7d6c918f00edb7315bb8526f5f372f1174b643e50e1a47d35da
+
+ $ vkq api "users.get" ""
+ {"response":[{"first_name":"Сергей","uid":222222,"last_name":"Миронов"}]}
+
+ $ vkq api "messages.send" "user_id=333333,message=Hi theree!"
+ {"response":57505}
+
+ $ vkq api "groups.search" "q=Haskell"
+ $ vkq api "groups.search" "q=Haskell" --pretty
+ {
+ "response": [
+ 30,
+ {
+ "screen_name": "ml_mat_asm",
+ "photo": "https://pp.userapi.com/c638217/v638217626/54113/v5Ib71-dDzo.jpg",
+ "is_closed": 0,
+ "photo_medium": "https://pp.userapi.com/c638217/v638217626/54112/Nu_si987vOc.jpg",
+ "name": "Matlab | Assembler | MathCAD | Haskell | Prolog",
+ "photo_big": "https://pp.userapi.com/c638217/v638217626/54111/HGnUbgUorVU.jpg",
+ "gid": 78651325,
+ "is_admin": 0,
+ "is_member": 0,
+ "type": "page"
+ },
+ ...
+ }
VKHS library/runhaskell mode
============================
-Starting from 1.7.2 the library supports RunHaskell-mode. Consider the
-following example:
+Starting from 1.7.2 the library supports runhaskell-mode.
#!/usr/bin/env runhaskell
{-# LANGUAGE RecordWildCards #-}
- import Prelude ()
import Web.VKHS
import Web.VKHS.Imports
@@ -166,12 +197,13 @@ following example:
main = runVK_ defaultOptions $ do
Sized cnt cs <- getCountries
forM_ cs $ \Country{..} -> do
- liftIO $ putStrLn co_title
+ liftIO $ tputStrLn co_title
When executed, the program asks for login/password and outputs list of countries
known to VK. `getCountries` and several other methods are defined in
-`Web.VKHS.API.Simple`. `vkq` application may be used as a more elaborated
-example.
+`Web.VKHS.API.Simple`.
+
+The distribuption contains `./app/runhaskell` folder with a couple of examples.
Debugging
=========
diff --git a/VKHS.cabal b/VKHS.cabal
index cc31486..b3c0cef 100644
--- a/VKHS.cabal
+++ b/VKHS.cabal
@@ -1,6 +1,6 @@
name: VKHS
-version: 1.8.4
+version: 1.9
synopsis: Provides access to Vkontakte social network via public API
description:
Provides access to Vkontakte API methods. Library requires no interaction
diff --git a/app/vkq/Main.hs b/app/vkq/Main.hs
index 8a1421e..1f9310c 100644
--- a/app/vkq/Main.hs
+++ b/app/vkq/Main.hs
@@ -1,4 +1,5 @@
{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
@@ -49,20 +50,21 @@ data PhotoOptions = PhotoOptions {
} deriving(Show)
data Options
- = Login GenericOptions LoginOptions
- | API GenericOptions APIOptions
- | Music GenericOptions MusicOptions
- | UserQ GenericOptions UserOptions
- | WallQ GenericOptions WallOptions
- | GroupQ GenericOptions GroupOptions
- | DBQ GenericOptions DBOptions
- | Photo GenericOptions PhotoOptions
+ = Login { genOpts :: GenericOptions, loginOpts :: LoginOptions }
+ | API { genOpts :: GenericOptions, apiOpts :: APIOptions }
+ | Music { genOpts :: GenericOptions, musicOpts :: MusicOptions }
+ | UserQ { genOpts :: GenericOptions, userOpts :: UserOptions }
+ | WallQ { genOpts :: GenericOptions, wallOpts :: WallOptions }
+ | GroupQ { genOpts :: GenericOptions, groupOpts :: GroupOptions }
+ | DBQ { genOpts :: GenericOptions, dbOpts :: DBOptions }
+ | Photo { genOpts :: GenericOptions, photoOpts :: PhotoOptions }
deriving(Show)
toMaybe :: (Functor f) => f String -> f (Maybe String)
toMaybe = fmap (\s -> if s == "" then Nothing else Just s)
-opts m =
+-- FIXME support --version flag
+optdesc m =
let
-- genericOptions_ :: Parser GenericOptions
@@ -80,7 +82,7 @@ opts m =
<*> ppass
<*> strOption (short 'a' <> m <> metavar "ACCESS_TOKEN" <>
help ("Access token. Honores " ++ env_access_token ++ " environment variable"))
- <*> strOption (long "access-token-file" <> value "" <> metavar "FILE" <>
+ <*> strOption (long "access-token-file" <> value (l_access_token_file defaultOptions) <> metavar "FILE" <>
help ("Filename to store actual access token, should be used to pass its value between sessions"))
genericOptions = genericOptions_
@@ -165,8 +167,8 @@ opts m =
main :: IO ()
main = ( do
m <- maybe (value "") (value) <$> lookupEnv env_access_token
- o <- execParser (info (helper <*> opts m) (fullDesc <> header "VKontakte social network tool"))
- r <- runExceptT (cmd o)
+ o <- execParser (info (helper <*> optdesc m) (fullDesc <> header "VKontakte social network tool"))
+ r <- runVK (genOpts o) (cmd o)
case r of
Left err -> do
hPutStrLn stderr err
@@ -187,25 +189,25 @@ main = ( do
-}
-cmd :: Options -> ExceptT Text IO ()
+cmd :: (MonadLogin (m (R m x)) (R m x) s, MonadAPI m x s) => Options -> m (R m x) ()
-- Login
cmd (Login go LoginOptions{..}) = do
- AccessToken{..} <- runLogin go
+ at@AccessToken{..} <- login
case l_eval of
True -> liftIO $ putStrLn $ Text.pack $ printf "export %s=%s\n" env_access_token at_access_token
- False -> liftIO $ putStrLn $ Text.pack at_access_token
+ False -> do
+ modifyAccessToken at
+ liftIO $ putStrLn $ Text.pack at_access_token
-- API / CALL
cmd (API go APIOptions{..}) = do
- runAPI go $ do
- x <- apiJ a_method (map (id *** tpack) $ splitFragments "," "=" a_args)
- if a_pretty
- then do
- liftIO $ putStrLn $ jsonEncodePretty x
- else
- liftIO $ putStrLn $ jsonEncode x
- return ()
+ x <- apiJ a_method (map (id *** tpack) $ splitFragments "," "=" a_args)
+ if a_pretty
+ then do
+ liftIO $ putStrLn $ jsonEncodePretty x
+ else
+ liftIO $ putStrLn $ jsonEncode x
cmd (Music go@GenericOptions{..} mo@MusicOptions{..}) = do
error "VK disabled audio API since 2016/11."
@@ -215,23 +217,19 @@ cmd (GroupQ go (GroupOptions{..}))
|not (null g_search_string) = do
- runAPI go $ do
+ (Sized cnt grs) <- groupSearch (tpack g_search_string)
- (Sized cnt grs) <- groupSearch (tpack g_search_string)
-
- forM_ grs $ \gr -> do
- liftIO $ printf "%s\n" (gr_format g_output_format gr)
+ forM_ grs $ \gr -> do
+ liftIO $ printf "%s\n" (gr_format g_output_format gr)
cmd (DBQ go (DBOptions{..}))
|db_countries = do
- runAPI go $ do
-
- (Sized cnt cs) <- getCountries
+ (Sized cnt cs) <- getCountries
- forM_ cs $ \Country{..} -> do
- liftIO $ Text.putStrLn $ Text.concat [ tshow co_int, "\t", co_title]
+ forM_ cs $ \Country{..} -> do
+ liftIO $ Text.putStrLn $ Text.concat [ tshow co_int, "\t", co_title]
|db_cities = do
error "not implemented"
@@ -239,21 +237,19 @@ cmd (DBQ go (DBOptions{..}))
cmd (Photo go PhotoOptions{..})
|p_listAlbums = do
- runAPI go $ do
- (Sized cnt als) <- getAlbums Nothing
- forM_ als $ \Album{..} -> do
- liftIO $ Text.putStrLn $ Text.concat [ tshow al_id, "\t", al_title]
+ (Sized cnt als) <- getAlbums Nothing
+ forM_ als $ \Album{..} -> do
+ liftIO $ Text.putStrLn $ Text.concat [ tshow al_id, "\t", al_title]
|p_uploadServer = do
- runAPI go $ do
- (Sized cnt als) <- getAlbums Nothing
- let album = [a | a <- als, al_id a == -7]
- case album of
- [a] -> do
- PhotoUploadServer{..} <- getPhotoUploadServer a
- liftIO $ Text.putStrLn pus_upload_url
- _ ->
- error "Ivalid album"
+ (Sized cnt als) <- getAlbums Nothing
+ let album = [a | a <- als, al_id a == -7]
+ case album of
+ [a] -> do
+ PhotoUploadServer{..} <- getPhotoUploadServer a
+ liftIO $ Text.putStrLn pus_upload_url
+ _ ->
+ error "Ivalid album"
|otherwise = do
error "invalid command line arguments"
diff --git a/src/Web/VKHS.hs b/src/Web/VKHS.hs
index c4f8125..8425ffe 100644
--- a/src/Web/VKHS.hs
+++ b/src/Web/VKHS.hs
@@ -156,6 +156,14 @@ defaultSupervisor = go where
<> "high-level wrappers `apiSimpleH` / `apiSimpleHM`"
lift $ throwError res_desc
+ RepeatedForm Form{..} k -> do
+ alert $ "Failed to complete login procedure. Last seen form is\n"
+ <> "\n"
+ <> printForm "\t" form
+ <> "\n"
+ <> "You may try to obtain more details by setting --verbose flag and/or checking the 'latest.html' file"
+ lift $ throwError res_desc
+
_ -> do
alert $ "Unsupervised error: " <> res_desc
lift $ throwError res_desc
@@ -175,13 +183,12 @@ runAPI go@GenericOptions{..} m = do
s <- initialState go
flip evalStateT s $ do
- at <- readInitialAccessToken >>= \case
+ readInitialAccessToken >>= \case
Nothing ->
- defaultSupervisor (login >>= return . Fine)
- Just at ->
- pure at
+ return ()
+ Just at -> do
+ modifyAccessToken at
- modifyAccessToken at
defaultSupervisor (m >>= return . Fine)
-- | Run the VK monad @m@ using generic options @go@ and 'defaultSupervisor'
diff --git a/src/Web/VKHS/API/Base.hs b/src/Web/VKHS/API/Base.hs
index 34050b6..5eba16b 100644
--- a/src/Web/VKHS/API/Base.hs
+++ b/src/Web/VKHS/API/Base.hs
@@ -136,11 +136,12 @@ api m args = do
-- | Invoke the request, in case of failure, escalate the probelm to the
-- supervisor. The superwiser has a chance to change the arguments
-apiR :: (Aeson.FromJSON a, MonadAPI m x s)
+apiRf :: (Aeson.FromJSON b, MonadAPI m x s)
=> MethodName -- ^ API method name
-> MethodArgs -- ^ API method arguments
+ -> (b -> Either String a)
-> API m x a
-apiR m0 args0 = go (ReExec m0 args0) where
+apiRf m0 args0 flt = go (ReExec m0 args0) where
go action = do
j <- do
case action of
@@ -149,13 +150,26 @@ apiR m0 args0 = go (ReExec m0 args0) where
ReParse j -> do
pure j
case parseJSON j of
- (Right (Response _ a)) -> return a
+ (Right (Response _ b)) -> do
+ case flt b of
+ Right a -> return a
+ Left e -> do
+ recovery <- raise (CallFailure (m0, args0, j, e))
+ go recovery
(Left e) -> do
recovery <- raise (CallFailure (m0, args0, j, e))
go recovery
-- | Invoke the request, in case of failure, escalate the probelm to the
-- supervisor. The superwiser has a chance to change the arguments
+apiR :: (Aeson.FromJSON a, MonadAPI m x s)
+ => MethodName -- ^ API method name
+ -> MethodArgs -- ^ API method arguments
+ -> API m x a
+apiR m0 args0 = apiRf m0 args0 Right
+
+-- | Invoke the request, in case of failure, escalate the probelm to the
+-- supervisor. The superwiser has a chance to change the arguments
apiHM :: forall m x a s . (Aeson.FromJSON a, MonadAPI m x s)
=> MethodName -- ^ API method name
-> MethodArgs -- ^ API method arguments
diff --git a/src/Web/VKHS/API/Simple.hs b/src/Web/VKHS/API/Simple.hs
index d1e0be9..2dea388 100644
--- a/src/Web/VKHS/API/Simple.hs
+++ b/src/Web/VKHS/API/Simple.hs
@@ -8,21 +8,20 @@
-- #!/usr/bin/env runhaskell
-- {-# LANGUAGE RecordWildCards #-}
-- {-# LANGUAGE OverloadedStrings #-}
-
--- import Prelude ()
+--
-- import Web.VKHS
-- import Web.VKHS.Imports
-
+--
-- main :: IO ()
-- main = runVK_ defaultOptions $ do
-- Sized cnt gs <- groupSearch "Котики"
-- forM_ gs $ \gr@GroupRecord{..} -> do
--- liftIO $ putStrLn gr_name
--- liftIO $ putStrLn "--------------"
+-- liftIO $ tputStrLn gr_name
+-- liftIO $ tputStrLn "--------------"
-- Sized wc ws <- getGroupWall gr
-- forM_ ws $ \WallRecord{..} -> do
--- liftIO $ putStrLn wr_text
--- liftIO $ putStrLn "--------------"
+-- liftIO $ tputStrLn wr_text
+-- liftIO $ tputStrLn "--------------"
-- @
--
-- See more scripts under @./app/runhaskell@ folder
@@ -33,7 +32,6 @@
{-# LANGUAGE ScopedTypeVariables #-}
module Web.VKHS.API.Simple where
-import Prelude()
import qualified Data.Text as Text
import qualified Data.ByteString.Char8 as BS
import qualified Data.Aeson as Aeson
@@ -48,13 +46,17 @@ import Web.VKHS.API.Base
import Web.VKHS.API.Types
max_count = 1000
+
+-- | We are using API v5.44 by default
ver = "5.44"
+apiSimpleF nm args f = apiRf nm (("v",ver):args) f
apiSimple nm args = apiR nm (("v",ver):args)
apiSimpleH nm args handler = apiH nm (("v",ver):args) handler
apiSimpleHM nm args handler = apiHM nm (("v",ver):args) handler
apiVer nm args = api nm (("v",ver):args)
+-- | This function demonstrates pure-functional error handling
groupSearch :: (MonadAPI m x s) => Text -> API m x (Sized [GroupRecord])
groupSearch q =
fmap (sortBy (compare `on` gr_members_count)) <$> do
@@ -68,12 +70,10 @@ groupSearch q =
_ -> Nothing
)
-
getCountries :: (MonadAPI m x s) => API m x (Sized [Country])
getCountries =
fmap (sortBy (compare `on` co_title)) <$> do
- resp_data <$> do
- apiR "database.getCountries" $
+ apiSimple "database.getCountries"
[("v",ver),
("need_all", "1"),
("count", tpack (show max_count))
@@ -88,6 +88,8 @@ getCities Country{..} mq =
maybe [] (\q -> [("q",q)]) mq
-- | See [https://vk.com/dev/wall.get]
+--
+-- This function demonstrates monadic error handling
getGroupWall :: forall m x s . (MonadAPI m x s) => GroupRecord -> API m x (Sized [WallRecord])
getGroupWall GroupRecord{..} =
apiSimpleHM "wall.get"
@@ -96,8 +98,10 @@ getGroupWall GroupRecord{..} =
]
(\ErrorRecord{..} ->
case er_code of
- AccessDenied -> return (Just $ Sized 0 [])
- _ -> return Nothing
+ AccessDenied -> do
+ return (Just $ Sized 0 [])
+ _ -> do
+ return Nothing
:: API m x (Maybe (Sized [WallRecord])))
-- TODO: Take User as argument for more type-safety
@@ -113,19 +117,15 @@ getAlbums muid =
getPhotoUploadServer :: (MonadAPI m x s) => Album -> API m x PhotoUploadServer
getPhotoUploadServer Album{..} =
- resp_data <$> do
- api "photos.getUploadServer" $
- [("album_id", tshow al_id)
- ]
+ apiSimple "photos.getUploadServer" [("album_id", tshow al_id)]
getCurrentUser :: (MonadAPI m x s) => API m x UserRecord
getCurrentUser = do
- Response{..} <- apiVer "users.get" []
- users <- pure resp_data
- case (length users == 1) of
- False -> terminate (JSONParseFailure' resp_json "getCurrentUser: expecting single UserRecord")
- True -> return (head users)
+ apiSimpleF "users.get" [] $ \users ->
+ case (length users == 1) of
+ False -> Left "getCurrentUser: array with one user record"
+ True -> Right (head users)
-- * FIXME fix setUserPhoto, it is not actually working
diff --git a/src/Web/VKHS/Imports.hs b/src/Web/VKHS/Imports.hs
index e3e8666..1d5f813 100644
--- a/src/Web/VKHS/Imports.hs
+++ b/src/Web/VKHS/Imports.hs
@@ -22,7 +22,6 @@ module Web.VKHS.Imports (
, module Data.Data
, module Data.Scientific
, module Text.Printf
- , module Prelude
, module Text.Show.Pretty
, module Text.Read
) where
@@ -47,9 +46,9 @@ import Data.Function (on)
import Data.Text (Text(..), pack, unpack)
import Data.Text.IO (putStrLn, hPutStrLn)
import Data.List (head, length, sortBy, (++))
-import Prelude (error, Integer, FilePath, (==), (.), Show(..), String,
- ($), IO(..), Bool(..), compare, Ordering(..),
- Read(..))
+-- import Prelude (error, Integer, FilePath, (==), (.), Show(..), String,
+-- ($), IO(..), Bool(..), compare, Ordering(..),
+-- Read(..), error, undefined)
import Text.Printf
import Text.Show.Pretty
import Text.Read (readMaybe)
@@ -61,3 +60,7 @@ tunpack = unpack
tshow :: (Show a) => a -> Text
tshow = tpack . show
+
+tputStrLn = Data.Text.IO.putStrLn
+thPutStrLn = Data.Text.IO.hPutStrLn
+