packages feed

heddit-0.1: src/Network/Reddit/Comment.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ViewPatterns #-}

-- |
-- Module      : Network.Reddit.Comment
-- Copyright   : (c) 2021 Rory Tyler Hayford
-- License     : BSD-3-Clause
-- Maintainer  : rory.hayford@protonmail.com
-- Stability   : experimental
-- Portability : GHC
--
module Network.Reddit.Comment
    (  -- * Reading comments
      getComments
    , getComment
    , getCommentByURL
    , withReplies
    , loadMoreComments
    , loadMoreCommentsDef
    , unsaveComment
    , saveComment
      -- * Creating, editing, and deleting
    , deleteComment
    , editComment
    , replyToComment
    , getNewComments
    , setCommentReplies
      -- * Voting
      -- $vote
    , upvoteComment
    , downvoteComment
    , unvoteComment
      -- * Awarding
    , awardComment
    , gildComment
      -- * Misc
    , reportComment
      -- * Types
    , module M
    ) where

import           Control.Monad.Catch             ( MonadThrow(throwM) )

import           Data.Generics.Wrapped           ( wrappedTo )
import           Data.Sequence                   ( Seq((:<|)) )
import qualified Data.Sequence                   as Seq

import           Lens.Micro

import           Network.Reddit.Internal
import           Network.Reddit.Item
import           Network.Reddit.Types
import           Network.Reddit.Types.Comment
import           Network.Reddit.Types.Comment    as M
                 ( ChildComment(..)
                 , Comment(Comment)
                 , CommentID(CommentID)
                 , MoreComments(MoreComments)
                 )
import           Network.Reddit.Types.Submission
import           Network.Reddit.Types.Subreddit
import           Network.Reddit.Utils

import           Web.FormUrlEncoded              ( ToForm(toForm) )
import           Web.HttpApiData                 ( ToHttpApiData(toQueryParam)
                                                 )

-- | Get the 'Comment's corresponding to a container of 'CommentID's
getComments :: (MonadReddit m, Foldable t)
            => ItemOpts 
            -> t CommentID
            -> m (Seq Comment)
getComments = getMany

-- | Get information on a single 'CommentID'. Throws an exception if no such
-- 'Comment' exists
getComment :: MonadReddit m => CommentID -> m Comment
getComment cid = getComments defaultItemOpts [ cid ] >>= \case
    comment :<| _ -> pure comment
    _             -> throwM $ InvalidResponse "getComment: No results"

-- | Get a comment based on its permalink URL, which must be of the form
-- https:\/\/{www.}reddit.com\/r\/\<SUB\>\/comments\/\<SID\>\/\<NAME\>/\<ID\>
getCommentByURL :: MonadReddit m => URL -> m Comment
getCommentByURL url = getComment =<< commentIDFromURL url

-- | Get new 'Comment's, either for the site as a whole or for a single subreddit,
-- given its 'SubredditName'
getNewComments :: MonadReddit m
               => Maybe SubredditName
               -> Paginator CommentID Comment
               -> m (Listing CommentID Comment)
getNewComments sname paginator =
    runAction defaultAPIAction
              { requestData  = paginatorToFormData paginator
              , pathSegments = [ "comments" ]
                    & maybe id (\s -> (<>) [ "r", toQueryParam s ]) sname
              }

-- | Update a 'Comment' to include its 'ChildComment's, returning the updated
-- 'Comment'. This will probably be necessary if the original 'Comment' was obtained
-- by getting a 'Username'\'s or 'Subreddit'\'s comments, etc...
withReplies :: MonadReddit m => ItemOpts -> Comment -> m Comment
withReplies ItemOpts { .. } Comment { .. } =
    runAction @WithReplies r <&> wrappedTo
  where
    r = defaultAPIAction
        { pathSegments =
              [ "comments", toQueryParam linkID, "_", toQueryParam commentID ]
        , requestData  = mkTextFormData
              $ [ ("context", "100") ] -- asking for extra context
              <> foldMap pure (("sort", ) . toQueryParam <$> itemSort)
        }

-- | Save a comment
saveComment :: MonadReddit m => CommentID -> m ()
saveComment = save . CommentItemID

-- | Unsave a comment
unsaveComment :: MonadReddit m => CommentID -> m ()
unsaveComment = unsave . CommentItemID

-- | Delete a comment that the currently authenticated user has authored, given its
-- 'CommentID'
deleteComment :: MonadReddit m => CommentID -> m ()
deleteComment = delete . CommentItemID

-- | Edit a comment given its 'CommentID', receving an updated 'Comment' in response
editComment :: MonadReddit m => CommentID -> Body -> m Comment
editComment (CommentItemID -> cid) txt = edit cid txt >>= \case
    CommentItem c    -> pure c
    SubmissionItem _ -> throwM
        $ InvalidResponse "editComment: Expected a Comment, got a Submission"

-- | Reply to a comment given its 'CommentID', returning the newly created 'Comment'
replyToComment :: MonadReddit m => CommentID -> Body -> m Comment
replyToComment = reply . CommentItemID

-- | Enable/disable inbox replies for a comment
setCommentReplies :: MonadReddit m => Bool -> CommentID -> m ()
setCommentReplies p (CommentItemID -> cid) = setInboxReplies p cid

-- | Upvote a comment.
upvoteComment :: MonadReddit m => CommentID -> m ()
upvoteComment = vote Upvote . CommentItemID

-- | Downvote a comment.
downvoteComment :: MonadReddit m => CommentID -> m ()
downvoteComment = vote Downvote . CommentItemID

-- | Remove an existing vote on a comment.
unvoteComment :: MonadReddit m => CommentID -> m ()
unvoteComment = vote Unvote . CommentItemID

-- | Grant an 'Award' to a comment. See 'mkAward' for creating an award with
-- default values, as well as the pattern synonyms for common awards
awardComment :: MonadReddit m => Award -> CommentID -> m AwardingsSummary
awardComment a = award a . CommentItemID

-- | Convenience action to anonymously give 'Gold' to the author of a comment,
-- without a message. See 'awardComment' if you need more fine-grained control
gildComment :: MonadReddit m => CommentID -> m AwardingsSummary
gildComment = gild . CommentItemID

-- | Report a comment to the subreddit\'s mods
reportComment :: MonadReddit m => Report -> CommentID -> m ()
reportComment r = report r . CommentItemID

{- HLINT ignore "Use mconcat" -}
-- | Transform 'MoreComments', loading the actual comments they refer to, up to
-- the limit passed in (pass 'Nothing' for no limit). If 'CommentID's still remain
-- from the original 'MoreComments', they will be returned in a new 'MoreComments'
-- inserted into the resulting sequence of 'ChildComment's, along with an updated
-- count
loadMoreComments
    :: forall m.
    MonadReddit m
    => Maybe Word
    -> ItemOpts 
    -> SubmissionID
    -> MoreComments
    -> m (Seq ChildComment)
loadMoreComments limitM opts sid MoreComments { .. } = foldr (<>) mempty
    <$> traverse fetchMore (Seq.chunksOf 100 toFetch)
    <&> (<> more) -- appending this way, after the fold, will put @more@ at the end
  where
    fetchMore :: Seq CommentID -> m (Seq ChildComment)
    fetchMore cids = runAction @LoadedChildren r <&> wrappedTo
      where
        r = defaultAPIAction
            { pathSegments = [ "api", "morechildren" ]
            , method       = POST
            , requestData  = WithForm
                  $ toForm opts
                  <> mkTextForm [ ("link_id", fullname sid)
                                , ("api_type", "json")
                                , ("children", joinParams cids)
                                ]
            }

    more = case remaining of
        Seq.Empty -> mempty
        cids      -> Seq.singleton . More . MoreComments cids
            $ count - fromIntegral limit

    (toFetch, remaining) = Seq.splitAt limit childIDs

    limit = maybe (length childIDs) fromIntegral limitM

-- | A version of 'loadMoreComments' with default parameters for the limit
-- (@Nothing@) and options ('defaultItemOpts')
loadMoreCommentsDef
    :: MonadReddit m => SubmissionID -> MoreComments -> m (Seq ChildComment)
loadMoreCommentsDef = loadMoreComments Nothing defaultItemOpts
--
-- $vote
-- __Note__: According to Reddit\'s API rules:
--
-- votes must be cast by humans. That is, API clients proxying a human's
-- action one-for-one are OK, but bots deciding how to vote on content or amplifying
-- a human's vote are not. See the reddit rules for more details on what constitutes
-- vote cheating.
--