{-# 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 () "" 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"