packages feed

VKHS 1.8.4 → 1.9

raw patch · 8 files changed

+182/−126 lines, 8 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Web.VKHS.API.Base: apiRf :: (FromJSON b, MonadAPI m x s) => MethodName -> MethodArgs -> (b -> Either String a) -> API m x a
+ Web.VKHS.API.Simple: apiSimpleF :: (MonadAPI m x s, FromJSON b) => MethodName -> [(String, Text)] -> (b -> Either String a) -> API m x a
+ Web.VKHS.Imports: thPutStrLn :: Handle -> Text -> IO ()
+ Web.VKHS.Imports: tputStrLn :: Text -> IO ()

Files

CHANGELOG.md view
@@ -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
README.md view
@@ -8,7 +8,7 @@ (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 @@   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` 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 @@     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 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 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 API calls+-------------------- -Performing custom API calls----------------------------+`vkq` allows user to call arbitrary API method. The generic interface is as follows: -vkq allows user to call arbitrary API method. The format 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 -    Usage: vkq call [--verbose] [--req-per-sec N] [--interactive] [--appid APPID]-                    [--user USER] [--pass PASS] [-a ACCESS_TOKEN] METHOD PARAMS+    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  -For example, lets call ausio.search method to get some Beatles records:+The session may look like the following: -    $ vkq call group.search q=Beatles --pretty+    $ vkq api "messages.send" "user_id=111111,message=\"test\""  --pretty+    bd7da7e9cfb4cc12c0a49093173ca8785c7d6c918f00edb7315bb8526f5f372f1174b643e50e1a47d35da -      { "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"-        },-    ...+    $ 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 @@     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 =========
VKHS.cabal view
@@ -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
app/vkq/Main.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} @@ -49,20 +50,21 @@   } 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 @@         <*> 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 @@ 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 @@   -} -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 @@    |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 (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"
src/Web/VKHS.hs view
@@ -156,6 +156,14 @@                       <> "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 @@   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'
src/Web/VKHS/API/Base.hs view
@@ -136,11 +136,12 @@  -- | 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,10 +150,23 @@         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
src/Web/VKHS/API/Simple.hs view
@@ -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.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 @@         _ -> 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 @@     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 @@     ]     (\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 @@  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
src/Web/VKHS/Imports.hs view
@@ -22,7 +22,6 @@   , 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.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 @@  tshow :: (Show a) => a -> Text tshow = tpack . show++tputStrLn = Data.Text.IO.putStrLn+thPutStrLn = Data.Text.IO.hPutStrLn+