VKHS-1.9.2: app/vkq/Main.hs
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Main where
import Prelude hiding(putStrLn)
import Control.Monad.Except
import Control.Exception(SomeException(..),handle)
import System.Environment
import System.FilePath(splitExtension,replaceExtension,(</>))
import System.Directory(getTemporaryDirectory,doesFileExist)
import System.Exit
import System.IO(stderr,Handle,openBinaryFile,openBinaryTempFile,IOMode(..))
import Options.Applicative
import Text.RegexPR
import Text.Parsec ((<|>),(<?>))
import qualified Text.Parsec as Parsec
import qualified Text.Parsec.String as Parsec
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import qualified Data.ByteString.Char8 as BS
-- import Web.VKHS.Types
-- import Web.VKHS.Error
-- import Web.VKHS.Client as Client
-- import Web.VKHS.Monad hiding (catch)
-- import Web.VKHS.API as API
import Web.VKHS.Imports
import Web.VKHS
import Util
env_access_token = "VKQ_ACCESS_TOKEN"
data MusicOptions = MusicOptions {
m_list_music :: Bool
, m_search_string :: String
, m_name_format :: String
, m_output_format :: String
, m_out_dir :: Maybe String
, m_records_id :: [String]
, m_skip_existing :: Bool
} deriving(Show)
-- Open file. Return filename and handle. Don't open file if it exists
openFileMR :: MusicOptions -> MusicRecord -> IO (FilePath, Maybe Handle)
openFileMR mo@MusicOptions{..} mr@MusicRecord{..} =
case m_out_dir of
Nothing -> do
let (_,ext) = splitExtension (mr_url_str)
temp <- getTemporaryDirectory
(fp,h) <- openBinaryTempFile temp ("vkqmusic"++ext)
return (fp, Just h)
Just odir -> do
let (_,ext) = splitExtension mr_url_str
let name = mr_format m_output_format mr
let name' = replaceExtension name (takeWhile (/='?') ext)
let fp = (odir </> name')
e <- doesFileExist fp
case (e && m_skip_existing) of
True -> do
return (fp,Nothing)
False -> do
handle (\(_::SomeException) -> do
Text.hPutStrLn stderr ("Failed to open file " <> tpack fp)
return (fp, Nothing)
) $ do
h <- openBinaryFile fp WriteMode
return (fp,Just h)
data UserOptions = UserOptions {
u_queryString :: String
} deriving(Show)
data WallOptions = WallOptions {
w_woid :: String
} deriving(Show)
data GroupOptions = GroupOptions {
g_search_string :: String
, g_output_format :: String
} deriving(Show)
-- | Options to query VK database
data DBOptions = DBOptions {
db_countries :: Bool
, db_cities :: Bool
} deriving(Show)
data APIOptions = APIOptions {
a_method :: String
, a_args :: [String]
, a_pretty :: Bool
} deriving(Show)
data LoginOptions = LoginOptions {
l_eval :: Bool
} deriving(Show)
data PhotoOptions = PhotoOptions {
p_listAlbums :: Bool
, p_uploadServer :: Bool
, p_setUserPhoto :: Bool
, p_userPhoto :: String
} deriving(Show)
data Options
= 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)
-- * FIXME support --version flag
optdesc m =
let
-- genericOptions_ :: Parser GenericOptions
genericOptions_ puser ppass = GenericOptions
<$> (pure $ o_login_host defaultOptions)
<*> (pure $ o_api_host defaultOptions)
<*> (pure $ o_port defaultOptions)
<*> flag Normal Debug (long "verbose" <> help "Be verbose")
<*> (pure $ o_use_https defaultOptions)
<*> fmap (read . tunpack) (tpack <$> strOption (value (show $ o_max_request_rate_per_sec defaultOptions) <> long "req-per-sec" <> metavar "N" <> help "Max number of requests per second"))
<*> flag True False (long "interactive" <> help "Allow interactive queries")
<*> (AppID <$> strOption (long "appid" <> metavar "APPID" <> value "3128877" <> help "Application ID, defaults to VKHS" ))
<*> puser
<*> ppass
<*> strOption (short 'a' <> m <> metavar "ACCESS_TOKEN" <>
help ("Access token. Honores " ++ env_access_token ++ " environment variable"))
<*> 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"))
<*> strOption (long "cookies-file" <> value (l_cookies_file defaultOptions) <> metavar "FILE" <>
help "Filename to stote cookies, should be used to store cookies between sessions")
genericOptions = genericOptions_
(strOption (value "" <> long "user" <> metavar "USER" <> help "User name or email"))
(strOption (value "" <> long "pass" <> metavar "PASS" <> help "User password"))
genericOptions_login = genericOptions_
(argument str (metavar "USER" <> help "User name or email" <> value ""))
(argument str (metavar "PASS" <> help "User password" <> value ""))
api_cmd = (info (API <$> genericOptions <*> (APIOptions
<$> argument str (metavar "METHOD" <> help "Method name")
<*> many (argument str (metavar "PARAMS" <> help "Method arguments, KEY=VALUE[,KEY2=VALUE2[,,,]]"))
<*> switch (long "pretty" <> help "Pretty print resulting JSON")))
( progDesc "Call VK API method" ))
in hsubparser (
command "login" (info (Login <$> genericOptions_login <*> (LoginOptions
<$> flag False True (long "eval" <> help "Print in shell-friendly format")
))
( progDesc "Login and print access token" ))
<> command "call" api_cmd
<> command "api" api_cmd
<> command "music" (info ( Music <$> genericOptions <*> (MusicOptions
<$> switch (long "list" <> short 'l' <> help "List music files")
<*> strOption
( metavar "STR"
<> long "query" <> short 'q' <> value [] <> help "Query string")
<*> strOption
( metavar "FORMAT"
<> short 'f'
<> value "%i %U\t%t"
<> help "Listing format, supported tags: %i %o %a %t %d %u"
)
<*> strOption
( metavar "FORMAT"
<> short 'F'
<> value "%i_%a_%t"
<> help ("Output format, supported tags:" ++ (listTags mr_tags))
)
<*> toMaybe (strOption (metavar "DIR" <> short 'o' <> help "Output directory" <> value ""))
<*> many (argument str (metavar "RECORD_ID" <> help "Download records"))
<*> flag False True (long "skip-existing" <> help "Don't download existing files")
))
( progDesc "List or download music files"))
<> command "user" (info ( UserQ <$> genericOptions <*> (UserOptions
<$> strOption (long "query" <> short 'q' <> help "String to query")
))
( progDesc "Extract various user information"))
<> command "wall" (info ( WallQ <$> genericOptions <*> (WallOptions
<$> strOption (long "id" <> short 'i' <> help "Owner id")
))
( progDesc "Extract wall information"))
<> command "group" (info ( GroupQ <$> genericOptions <*> (GroupOptions
<$> strOption (long "query" <> short 'q' <> value [] <> help "Group search string")
<*> strOption
( metavar "FORMAT"
<> short 'F'
<> value "%i %m %n %u"
<> help ("Output format, supported tags:" ++ (listTags gr_tags))
)
))
( progDesc "Extract groups information"))
<> command "db" (info ( DBQ <$> genericOptions <*> (DBOptions
<$> flag False True (long "list-countries" <> help "List known countries")
<*> flag False True (long "list-cities" <> help "List known cities")
))
( progDesc "Extract generic DB information"))
<> command "photo" (info ( Photo <$> genericOptions <*> (PhotoOptions
<$> flag False True (long "list-albums" <> help "List Albums")
<*> flag False True (long "upload-server" <> help "Get upload server")
<*> flag False True (long "set-user-photo" <> help "Set user photo")
<*> strOption (long "photo" <> help "User photo to set" <> value "")
))
( progDesc "Photo-related queries"))
)
main :: IO ()
main = ( do
m <- maybe (value "") (value) <$> lookupEnv env_access_token
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 (printVKError err)
exitFailure
Right _ -> do
return ()
)`catch` (\(e::SomeException) -> do
putStrLn $ Text.pack (show e)
exitFailure
)
{-
____ _ ___
/ ___| | |_ _|
| | | | | |
| |___| |___ | |
\____|_____|___|
-}
cmd :: (MonadAPI m x s) => Options -> m (R m x) ()
-- Login
cmd (Login go LoginOptions{..}) = do
at@AccessToken{..} <- login
case l_eval of
True -> liftIO $ putStrLn $ Text.pack $ printf "export %s=%s\n" env_access_token at_access_token
False -> do
liftIO $ putStrLn $ Text.pack at_access_token
-- API / CALL
cmd (API go APIOptions{..}) =
let
word :: Parsec.Parser String
word = Parsec.try (do
c <- Parsec.oneOf "\"'"
ret <- Parsec.many $ Parsec.satisfy (/=c)
Parsec.char c
return ret)
Parsec.<|>
Parsec.many (Parsec.satisfy (not . flip elem (" \"=" :: String)))
term :: Parsec.Parser (String,String)
term = (,) <$> word <*> ((Parsec.char '=') *> word) <?> "term"
res :: Either Parsec.ParseError [(String,String)]
res = foldr (liftA2 (:)) (pure [])
$ flip map a_args
$ Parsec.runParser term () "<cmdline>"
in do
case res of
Left err -> error $ "error parsing command line arguments: " <> show err
Right pairs -> do
x <- api a_method (map (id *** tpack) pairs)
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."
-- Query groups files
cmd (GroupQ go (GroupOptions{..}))
|not (null g_search_string) = do
grs <- groupSearch (tpack g_search_string)
forM_ grs $ \gr -> do
liftIO $ printf "%s\n" (gr_format g_output_format gr)
cmd (DBQ go (DBOptions{..}))
|db_countries = do
cs <- getCountries
forM_ cs $ \Country{..} -> do
liftIO $ Text.putStrLn $ Text.concat [ tshow $ coid_id $ co_coid, "\t", co_title]
|db_cities = do
error "not implemented"
cmd (Photo go PhotoOptions{..})
|p_listAlbums = do
(Sized cnt als) <- getAlbums Nothing
forM_ als $ \Album{..} -> do
liftIO $ Text.putStrLn $ Text.concat [ tshow al_id, "\t", al_title]
|p_uploadServer = 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"
|p_setUserPhoto = do
user <- getCurrentUser
setUserPhoto user p_userPhoto
|otherwise = do
error "invalid command line arguments"