packages feed

rtorrent-rpc-0.3.0.0: Network/RTorrent/Action/Internals.hs

{-# LANGUAGE TypeOperators, TypeFamilies, RankNTypes #-}
{-# LANGUAGE OverloadedStrings #-}

{-|
Module      : Action.Internals
Copyright   : (c) Kai Lindholm, 2014, 2025
License     : MIT
Maintainer  : megantti@gmail.com
Stability   : experimental

-}

module Network.RTorrent.Action.Internals (
      Action (..)
    , simpleAction
    , pureAction

    , sequenceActions
    , (<+>)

    , Param (..)
    , ActionB (..)

    , AllAction (..)
    , allToMulti
) where

import Control.Applicative
import Control.Monad
import Control.Monad.Except (throwError)

import Data.Monoid
import Data.Traversable hiding (mapM)

import qualified Data.Map as M
import qualified Data.Vector as V
import qualified Data.Text as T

import Network.RTorrent.Command.Internals
import Network.RTorrent.Priority
import Network.RTorrent.Value

-- | A type for actions that can act on different things like torrents and files.
--
-- @a@ is the return type.
data Action i a = Action {
          actionCmds :: V.Vector (T.Text, V.Vector Param)                
          -- ^ Commands and parameters
        , actionParse :: forall m. (Monad m, MonadFail m) => Value -> m a 
          -- ^ Value parser
        , actionIndex :: i
          -- ^ Index at which the action is executed.
    }

-- | Wrapper to get monoid and applicative instances.
newtype ActionB i a = ActionB { runActionB :: i -> Action i a}

-- | A simple action that can be used when constructing new ones.
-- For example, 
--
-- @
-- getTorrentDir :: TorrentId -> TorrentAction Text
-- getTorrentDir = simpleAction "d.directory" []
--
-- setTorrentDir :: Text -> TorrentId -> TorrentAction Int
-- setTorrentDir dir = simpleAction "d.directory.set" [PString dir]
-- @
--
-- A list of commands can be found in 
--
-- <https://github.com/rakshasa/rtorrent/wiki/rTorrent-0.9-Comprehensive-Command-list-%28WIP%29>
--
-- but to get a proper explanation for the commands a dive into the source code of RTorrent is possibly needed.
simpleAction :: RpcType a =>
       T.Text
    -> [Param]
    -> i
    -> Action i a
simpleAction cmd params = Action (V.singleton (cmd, V.fromList params)) parseValue

instance Functor (Action i) where
    fmap f (Action cmds p fid) = Action cmds (fmap f . p) fid

instance Functor (ActionB i) where
    fmap f = ActionB . (fmap f .) . runActionB

instance Applicative (ActionB i) where
    pure a = ActionB $ Action V.empty (const (pure a))

    (ActionB a) <*> (ActionB b) = ActionB $ \tid -> let
        arrayd 1 x = V.head x
        arrayd _ x = ValueArray x

        parse :: (Monad m, MonadFail m) => (Value -> m (a -> b)) -> (Value -> m a) -> Value -> m b
        parse parseA parseB arr = do
            (valsA, valsB) <- V.splitAt len <$> getArray arr
            parseA (arrayd (length cmdsA) valsA)
              <*> parseB (arrayd (length cmdsB) valsB)
        len = length cmdsA
        Action cmdsA pA _ = a tid
        Action cmdsB pB _ = b tid
      in Action (cmdsA <> cmdsB) (parse pA pB) tid

instance Semigroup a => Semigroup (ActionB i a) where
    (<>) = liftA2 (<>)

instance Monoid a => Monoid (ActionB i a) where
    mempty = pure mempty

instance RpcType i => Command (Action i a) where
    type Ret (Action i a) = a

    commandCall (Action cmds _ tid) =
        RTMethodCall
        . V.map (\(cmd, params) ->
               (cmd, V.cons (toValue tid) (V.map toValue params)))
        $ cmds
    commandValue (Action cmds parse _) = parse <=< deconstructArray
      where
        deconstructArray = if length cmds > 1 then return else single

    levels (Action cmds _ _) = length cmds

-- | Parameters for actions.
data Param =
    PString T.Text
  | PInt Int
  | PBool Bool
  | PTorrentPriority TorrentPriority
  | PFilePriority FilePriority
  | PFilter [T.Text]

instance Show Param where
    show (PString str) = show str
    show (PInt i) = show i
    show (PBool b) = show b
    show (PTorrentPriority p) = show (fromEnum p)
    show (PFilePriority p) = show (fromEnum p)
    show (PFilter f) = show f

instance RpcType Param where
    toValue (PString str) = toValue str
    toValue (PBool b) = toValue b
    toValue (PInt i) = toValue i
    toValue (PTorrentPriority p) = toValue p
    toValue (PFilePriority p) = toValue p
    toValue (PFilter p) = ValueArray . V.fromList . map ValueString $ p

    fromValue _ = throwError "No fromValue for Params"

-- | Sequence multiple actions, for example with @f = []@ or @f = Vector@.
sequenceActions :: Traversable f => f (i -> Action i a) -> i -> Action i (f a)
sequenceActions = runActionB . traverse ActionB

-- | An action that does nothing but return the value.
pureAction :: a -> i -> Action i a
pureAction a = Action V.empty (const (return a))

infixr 6 <+>
-- | Combine two actions to get a new one.
(<+>) :: (i -> Action i a) -> (i -> Action i b) -> i -> Action i (a :*: b)
a <+> b = runActionB $ (:*:) <$> ActionB a <*> ActionB b

data AllAction i a = AllAction 
    i  -- ^ Dummy index
    T.Text -- ^ Function call
    (V.Vector Param) -- ^ Parameters 
    (i -> Action i a) -- ^ Action at each index

makeMultiCallStr :: [(String, [Param])] -> [String]
makeMultiCallStr = ("" :)
              . map (\(cmd, params) -> cmd ++ "=" ++ makeList params)
  where
    makeList :: Show a => [a] -> String
    makeList params = ('{' :) . go params $ "}"
      where
        go :: Show a => [a] -> ShowS
        go [x] = shows x
        go (x:xs) = shows x . (',' :) . go xs
        go [] = id

makeMultiCall :: V.Vector (T.Text, V.Vector Param) -> V.Vector T.Text
makeMultiCall = V.map (\(cmd, params) -> cmd <> "=" <> makeList params)
  where
    makeList :: Show a => V.Vector a -> T.Text
    makeList = T.cons '{' . flip T.snoc '}' . T.intercalate "," . V.toList . V.map (T.pack . show)

-- | Turn an 'AllAction' to a regular 'Action'.
allToMulti :: AllAction i a -> j -> Action j (V.Vector a)
allToMulti (AllAction emptyId multicall filt action) j =
    Action {
        actionCmds = V.singleton (multicall,
                (filt <>)
                . V.map PString $ makeMultiCall cmds),
        actionParse = let 
            deconstructArray = if length cmds > 1 then return else single
            in mapM (parse <=< deconstructArray)
                <=< getArray
        ,
        actionIndex = j
    }
  where
    Action cmds parse _ = action emptyId

instance Command (AllAction i a) where
    type Ret (AllAction i a) = V.Vector a
    commandCall (AllAction emptyId multicall filt action) =
                      mkRTMethodCall multicall
                    . (V.map toValue filt <>)
                    . V.map ValueString
                    . makeMultiCall
                    $ cmds
      where
        Action cmds _ _ = action emptyId

    commandValue (AllAction emptyId _ filt action) =
        mapM (parse
              <=< deconstructArray)
            <=< getArray
            <=< single
      where
        Action cmds parse _ = action emptyId
        deconstructArray = if length cmds > 1 then return else single