heddit-0.0.1: src/Network/Reddit/Moderation.hs
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
-- |
-- Module : Network.Reddit.Moderation
-- Copyright : (c) 2021 Rory Tyler Hayford
-- License : BSD-3-Clause
-- Maintainer : rory.hayford@protonmail.com
-- Stability : experimental
-- Portability : GHC
--
-- Actions related to moderation. Assume that each action in this module requires
-- moderator privileges, unless stated otherwise
--
module Network.Reddit.Moderation
( -- * Item moderation
-- | These actions work on 'Item's, i.e either 'Comment's or 'Submission's.
-- This module also exports variants that take unwrapped 'SubmissionID's
-- and 'CommentID's to work with just one type of item (see below)
distinguishItem
, undistinguishItem
, removeItem
, sendRemovalMessage
, approveItem
, lockItem
, unlockItem
, ignoreItemReports
, unignoreItemReports
-- ** Removal reasons
, getRemovalReasons
, createRemovalReason
, updateRemovalReason
, deleteRemovalReason
-- ** Moderation listings
-- | Each of these retrieves a @Listing ItemID ModItem@. You can constrain
-- the type of reports by passing the appropriate 'ItemType' to the
-- paginator options
, getReports
, getModqueue
, getSpam
, getEdited
, getUnmoderated
, getModlog
-- ** Submission moderation
-- | Includes re-exports from "Network.Reddit.Submission"
, distinguishSubmission
, undistinguishSubmission
, approveSubmission
, lockSubmission
, unlockSubmission
, ignoreSubmissionReports
, unignoreSubmissionReports
, unmarkNSFW
, markNSFW
, setOC
, unsetOC
, setSpoiler
, unsetSpoiler
, stickySubmission
, unstickySubmission
, setSuggestedSort
-- ** Comment moderation
, showComment
, distinguishComment
, undistinguishComment
, approveComment
, lockComment
, unlockComment
, ignoreCommentReports
, unignoreCommentReports
-- ** Collections moderation
, createCollection
, deleteCollection
, addSubmissionToCollection
, removeSubmissionFromCollection
, reorderCollection
, updateCollectionDescription
, updateCollectionTitle
-- * Subreddit relationships
-- ** Moderators
, getModerators
, getModerator
, updateModerator
, removeModerator
, abdicateModerator
-- ** Mod invitations
, inviteModerator
, inviteModeratorWithPerms
, getInvitees
, getInvitee
, updateInvitation
, revokeInvitation
, acceptInvitation
-- ** Contributors
, getContributors
, getContributor
, addContributor
, removeContributor
, abdicateContributor
, getWikiContributors
, getWikiContributor
, addWikiContributor
, removeWikiContributor
-- ** Bans
, getBans
, getBan
, banUser
, unbanUser
, getWikibans
, getWikiban
, wikibanUser
, wikiUnbanUser
, getMuted
, getMutedUser
, unmuteUser
, muteUser
-- * Subreddit settings
, getSubredditSettings
, setSubredditSettings
-- * Subreddit rules
-- | To get a list of the current rules for a Subreddit,
-- an action which does not require moderator privileges,
-- see 'Network.Reddit.Actions.Subreddit.getSubredditRules'.
-- Also note that a subreddit may only configure up to 15
-- individual rules at a time, and that trying to add more may
-- raise an exception
, addSubredditRule
, deleteSubredditRule
, updateSubredditRule
, reorderSubredditRules
-- * Flair
, getFlairList
, getUserFlair
, setUserFlair
, setUserFlairs
, deleteUserFlair
, createFlairTemplate
, updateFlairTemplate
, createUserFlairTemplate
, createSubmissionFlairTemplate
, updateSubmissionFlairTemplate
, updateUserFlairTemplate
, deleteFlairTemplate
, clearUserFlairTemplates
, clearSubmissionFlairTemplates
, clearFlairTemplates
-- * Stylesheets, images and widgets
, getStylesheet
, updateStylesheet
-- ** Images
-- | Reddit only allows JPEG or PNG images in stylsheets, and further requires
-- that all -- uploaded images be less than 500Kb in size. Each action that
-- uploads an image file to stylesheets validates both of these constraints,
-- throwing a 'ClientException' in the event that they are not satisfied.
--
-- Note that most of the actions that delete images will appear to succeed
-- even if the named image does not exists
, uploadImage
, uploadHeader
, uploadMobileIcon
, uploadMobileHeader
, deleteImage
, deleteHeader
, deleteMobileIcon
, uploadBanner
, deleteBanner
, uploadBannerAdditional
, deleteBannerAdditional
, uploadBannerHover
, deleteBannerHover
-- * Wiki
, addWikiEditor
, removeWikiEditor
, getWikiPageSettings
, revertWikiPage
-- * Modmail
, getModmail
, getModmailWithOpts
, getModmailConversation
, getUnreadModmailCount
, replyToConversation
, archiveConversation
, unarchiveConversation
, highlightConversation
, unhighlightConversation
, markConversationsRead
, markConversationRead
, markConversationsUnread
, markConversationUnread
, bulkReadConversations
, muteModmailUser
, unmuteModmailUser
, createConversation
-- * Widgets
, deleteWidget
, updateWidget
, reorderWidgets
, addButtonWidget
, addCalendarWidget
, addCommunityListWidget
, addCustomWidget
, addImageWidget
, addMenuWidget
, addPostFlairWidget
, addTextAreaWidget
, uploadWidgetImage
-- * Emoji
, addEmoji
, deleteEmoji
, updateEmoji
, setCustomEmojiSize
-- * Misc
, getTraffic
-- * Types
, module M
) where
import Conduit
( (.|)
, runConduit
, withSourceFile
)
import Control.Monad ( void, when )
import Control.Monad.Catch
( MonadCatch(catch)
, MonadThrow(throwM)
)
import Data.Aeson
( FromJSON
, KeyValue((.=))
, ToJSON(toJSON)
, Value(..)
)
import Data.Bifunctor ( Bifunctor(bimap) )
import Data.Bool ( bool )
import Data.ByteString ( ByteString )
import qualified Data.ByteString.Lazy as LB
import Data.Conduit.Binary ( sinkLbs )
import qualified Data.Foldable as F
import Data.Foldable ( for_ )
import Data.Generics.Wrapped
import Data.HashMap.Strict ( HashMap )
import qualified Data.HashMap.Strict as HM
import Data.Ix ( Ix(inRange) )
import Data.List.Split ( chunksOf )
import Data.Maybe ( fromMaybe )
import Data.Sequence ( Seq((:<|)) )
import qualified Data.Text as T
import Data.Text ( Text )
import qualified Data.Text.Encoding as T
import Lens.Micro
import Network.HTTP.Client.MultipartFormData ( partBS, partFile )
import Network.Reddit.Internal
import Network.Reddit.Me
import Network.Reddit.Submission
import Network.Reddit.Types
import Network.Reddit.Types.Account
import Network.Reddit.Types.Comment
import Network.Reddit.Types.Emoji
import Network.Reddit.Types.Flair
import Network.Reddit.Types.Item
import Network.Reddit.Types.Moderation
import Network.Reddit.Types.Moderation as M
( Ban(Ban)
, BanNotes(BanNotes)
, ContentOptions(..)
, CrowdControlLevel(..)
, LanguageCode(..)
, ModAccount(ModAccount)
, ModAction(ModAction)
, ModActionID
, ModActionOpts(ModActionOpts)
, ModActionType(..)
, ModInvitee(ModInvitee)
, ModInviteeList(ModInviteeList)
, ModItem(..)
, ModItemOpts(ModItemOpts)
, ModPermission(..)
, Modmail(Modmail)
, ModmailAuthor(ModmailAuthor)
, ModmailConversation(ModmailConversation)
, ModmailID
, ModmailMessage(ModmailMessage)
, ModmailObjID(ModmailObjID)
, ModmailOpts(ModmailOpts)
, ModmailReply(ModmailReply)
, ModmailSort(..)
, ModmailState(..)
, MuteID(MuteID)
, MuteInfo(MuteInfo)
, NewConversation(NewConversation)
, NewRemovalReasonID
, RelID(RelID)
, RelInfo(RelInfo)
, RelInfoOpts(RelInfoOpts)
, RemovalMessage(RemovalMessage)
, RemovalReason(RemovalReason)
, RemovalReasonID
, RemovalType(..)
, S3ModerationLease(S3ModerationLease)
, SpamFilter(..)
, StructuredStyleImage(..)
, StyleImageAlignment(..)
, Stylesheet(Stylesheet)
, SubredditImage(SubredditImage)
, SubredditRelationship(..)
, SubredditSettings(SubredditSettings)
, SubredditType(..)
, Traffic(Traffic)
, TrafficStat(TrafficStat)
, Wikimode(..)
, defaultModmailOpts
, mkModmailReply
)
import Network.Reddit.Types.Subreddit
import Network.Reddit.Types.Widget
import Network.Reddit.Types.Wiki
import Network.Reddit.Utils
import qualified System.FilePath as FP
import Web.FormUrlEncoded
( Form
, ToForm(toForm)
)
import Web.HttpApiData ( ToHttpApiData(..) )
--Item moderation--------------------------------------------------------------
-- | Distinguish an item. See 'distinguishComment' for further comment-specific
-- options
distinguishItem :: MonadReddit m => Distinction -> ItemID -> m ()
distinguishItem how iid =
runAction_ defaultAPIAction
{ pathSegments = [ "api", "distinguish" ]
, method = POST
, requestData = mkTextFormData [ ("id", fullname iid)
, ("how", toQueryParam how)
]
}
-- | Remove the distinction from an item, also removing the sticky flag
-- for top-level comments
undistinguishItem :: MonadReddit m => ItemID -> m ()
undistinguishItem = distinguishItem Undistinguished
-- | Remove an item from the subreddit with an optional note to other mods.
-- Setting the @isSpam@ parameter to @True@ will entirely remove the item
-- from subreddit listings
removeItem
:: MonadReddit m
=> Maybe Body -- ^ A note for other mods. This is sent in second request
-- if @Just@
-> Bool -- ^ Spam flag. Will remove the item from all listings if @True@
-> ItemID
-> m ()
removeItem note isSpam iid = do
runAction_ defaultAPIAction
{ pathSegments = [ "api", "remove" ]
, method = POST
, requestData = mkTextFormData [ ("id", fullname iid)
, ("spam", toQueryParam isSpam)
]
}
for_ note $ \n ->
runAction_ defaultAPIAction
{ pathSegments =
[ "api", "v1", "modactions", "removal_reasons" ]
, method = POST
, requestData =
mkTextFormData [ ( "json"
, textObject [ "item_ids"
.= [ fullname iid ]
, "mod_note" .= n
]
)
]
}
-- | Send a removal message for an item. The precise action depends on the form
-- of 'RemovalType'
sendRemovalMessage :: MonadReddit m => RemovalMessage -> m ()
sendRemovalMessage rm@RemovalMessage { .. } =
runAction_ defaultAPIAction
{ pathSegments = [ "api", "v1", "modactions" ] <> [ getPath ]
, method = POST
, requestData = WithForm $ toForm rm
}
where
getPath = case itemID of
CommentItemID _ -> "removal_comment_message"
SubmissionItemID _ -> "removal_link_message"
approveItem, lockItem, unlockItem :: MonadReddit m => ItemID -> m ()
-- | Approve an item, reverting a removal and resetting its report counter
approveItem = withID "approve"
-- | Lock an item. See also 'unlockItem'
lockItem = withID "lock"
-- | Unlock an item
unlockItem = withID "unlock"
ignoreItemReports, unignoreItemReports :: MonadReddit m => ItemID -> m ()
-- | Prevent all future reports on this item from sending notifications or appearing
-- in moderation listings. See also 'unignoreItemReports', which reverses this action
ignoreItemReports = withID "ignore_reports"
-- | Re-allow the item to trigger notifications and appear in moderation listings
unignoreItemReports = withID "unignore_reports"
withID :: MonadReddit m => Text -> ItemID -> m ()
withID path iid =
runAction_ defaultAPIAction
{ pathSegments = [ "api", path ]
, method = POST
, requestData = mkTextFormData [ ("id", fullname iid) ]
}
-- | Get a list of 'RemovalReason's for the given subreddit
getRemovalReasons :: MonadReddit m => SubredditName -> m (Seq RemovalReason)
getRemovalReasons sname = runAction @RemovalReasonList r <&> wrappedTo
where
r = defaultAPIAction
{ pathSegments = [ "api", "v1", toUrlPiece sname, "removal_reasons" ]
}
-- | Create a new 'RemovalReason', returning the 'RemovalReasonID' of the newly
-- created reason
createRemovalReason
:: MonadReddit m => SubredditName -> Title -> Body -> m RemovalReasonID
createRemovalReason sname t m = runAction @NewRemovalReasonID r <&> wrappedTo
where
r = defaultAPIAction
{ pathSegments = [ "api", "v1", toUrlPiece sname, "removal_reasons" ]
, method = POST
, requestData = mkTextFormData [ ("title", t), ("message", m) ]
}
-- | Update a single 'RemovalReason'
updateRemovalReason :: MonadReddit m => SubredditName -> RemovalReason -> m ()
updateRemovalReason sname rr@RemovalReason { .. } =
runAction_ defaultAPIAction
{ pathSegments = [ "api"
, "v1"
, toUrlPiece sname
, "removal_reasons"
, toUrlPiece removalReasonID
]
, method = PUT
, requestData = WithForm $ toForm rr
}
-- | Delete the given removal reason
deleteRemovalReason
:: MonadReddit m => SubredditName -> RemovalReasonID -> m ()
deleteRemovalReason sname rrid =
runAction_ defaultAPIAction
{ pathSegments = [ "api"
, "v1"
, toUrlPiece sname
, "removal_reasons"
, toUrlPiece rrid
]
, method = DELETE
}
--Moderation listings----------------------------------------------------------
getReports, getModqueue, getSpam, getEdited, getUnmoderated
:: MonadReddit m
=> SubredditName
-> Paginator ItemID ModItem
-> m (Listing ItemID ModItem)
-- | Get the given subreddit\'s reported items
getReports = modItems "reports"
-- | Get the given subreddit\'s moderation queue
getModqueue = modItems "modqueue"
-- | Get the given subreddit\'s items marked as spam
getSpam = modItems "spam"
-- | Get the given subreddit\'s recently edited items
getEdited = modItems "edited"
-- | Get the given subreddit\'s unmoderated items
getUnmoderated = modItems "unmoderated"
modItems :: MonadReddit m
=> Text
-> SubredditName
-> Paginator ItemID ModItem
-> m (Listing ItemID ModItem)
modItems path sname paginator =
runAction defaultAPIAction
{ pathSegments = subAboutPath sname path
, requestData = paginatorToFormData paginator
}
-- | Get a log of moderator actions for the given subreddit
getModlog :: MonadReddit m
=> SubredditName
-> Paginator ModActionID ModAction
-> m (Listing ModActionID ModAction)
getModlog sname paginator =
runAction defaultAPIAction
{ pathSegments = subAboutPath sname "log"
, requestData = paginatorToFormData paginator
}
--Submission moderation--------------------------------------------------------
approveSubmission, lockSubmission, unlockSubmission
:: MonadReddit m => SubmissionID -> m ()
-- | Approve a submission. See 'approveItem'
approveSubmission = approveItem . SubmissionItemID
-- | Lock a submission. See 'lockItem'
lockSubmission = lockItem . SubmissionItemID
-- | Unlock a submission. See 'unlockItem'
unlockSubmission = unlockItem . SubmissionItemID
ignoreSubmissionReports, unignoreSubmissionReports
:: MonadReddit m => SubmissionID -> m ()
-- | Ignore reports for a submission. See 'ignoreItemReports'
ignoreSubmissionReports = ignoreItemReports . SubmissionItemID
-- | Resume reports for a submission. See 'unignoreItemReports'
unignoreSubmissionReports = unignoreItemReports . SubmissionItemID
-- | Distinguish a submission
distinguishSubmission :: MonadReddit m => Distinction -> SubmissionID -> m ()
distinguishSubmission how = distinguishItem how . SubmissionItemID
-- | Remove the distinction from a submission
undistinguishSubmission :: MonadReddit m => SubmissionID -> m ()
undistinguishSubmission = undistinguishItem . SubmissionItemID
-- | Sticky the submission in the subreddit
stickySubmission :: MonadReddit m
=> Bool -- ^ When @True@, this will set the submission as
-- the \"bottom\" sticky. Otherwise, the stickied
-- submission will go to the top slot
-> SubmissionID
-> m ()
stickySubmission = stickyUnsticky True
-- | Unsticky the submission in the subreddit
unstickySubmission :: MonadReddit m => SubmissionID -> m ()
unstickySubmission = stickyUnsticky False True
stickyUnsticky :: MonadReddit m => Bool -> Bool -> SubmissionID -> m ()
stickyUnsticky state bottom sid =
runAction_ defaultAPIAction
{ pathSegments = [ "api", "set_subreddit_sticky" ]
, method = POST
, requestData = mkTextFormData
$ [ ("id", fullname sid)
, ("state", toQueryParam state)
, ("api_type", "json")
]
<> bool [ ("num", "1") ] mempty bottom
}
-- | Set the suggested sort order for a submission
setSuggestedSort
:: MonadReddit m
=> Maybe ItemSort -- ^ If @Nothing@, will clear the existing sort
-> SubmissionID
-> m ()
setSuggestedSort isort sid =
runAction_ defaultAPIAction
{ pathSegments = [ "api", "set_suggested_sort" ]
, method = POST
, requestData =
mkTextFormData [ ("id", fullname sid)
, ( "sort"
, maybe "blank" toQueryParam isort
)
, ("api_type", "json")
]
}
--Comment moderation-----------------------------------------------------------
-- | Distinguish aa comment. If @True@, the @sticky@ param will set the comment
-- at the top of the page. This only applies to top-level comments; the flg is
-- otherwise ignored
distinguishComment :: MonadReddit m
=> Distinction
-> Bool -- ^ Sticky flag
-> CommentID
-> m ()
distinguishComment how sticky cid =
runAction_ defaultAPIAction
{ pathSegments = [ "api", "distinguish" ]
, method = POST
, requestData =
mkTextFormData [ ("id", fullname cid)
, ("how", toQueryParam how)
, ("sticky", toQueryParam sticky)
]
}
-- | Undistinguish a comment, also removing its sticky flag if applicable
undistinguishComment :: MonadReddit m => CommentID -> m ()
undistinguishComment = undistinguishItem . CommentItemID
approveComment, lockComment, unlockComment
:: MonadReddit m => CommentID -> m ()
-- | Approve a comment. See 'approveItem'
approveComment = approveItem . CommentItemID
-- | Lock a comment. See 'lockItem'
lockComment = lockItem . CommentItemID
-- | Unlock a comment. See 'unlockItem'
unlockComment = unlockItem . CommentItemID
ignoreCommentReports, unignoreCommentReports
:: MonadReddit m => CommentID -> m ()
-- | Ignore reports for a comment. See 'ignoreItemReports'
ignoreCommentReports = ignoreItemReports . CommentItemID
-- | Resume reports for a comment. See 'unignoreItemReports'
unignoreCommentReports = unignoreItemReports . CommentItemID
-- | Show a comment that has been \"collapsed\" by crowd-control
showComment :: MonadReddit m => CommentID -> m ()
showComment cid =
runAction_ defaultAPIAction
{ pathSegments = [ "api", "show_comment" ]
, method = POST
, requestData = mkTextFormData [ ("id", fullname cid) ]
}
-- | Create a new collection, returning the new 'Collection' upon success
createCollection :: MonadReddit m => NewCollection -> m Collection
createCollection nc =
runAction defaultAPIAction
{ pathSegments = collectionsPath "create_collection"
, method = POST
, requestData = WithForm $ toForm nc
}
-- | Delete the entire collection from the subreddit
deleteCollection :: MonadReddit m => CollectionID -> m ()
deleteCollection cid =
runAction_ defaultAPIAction
{ pathSegments = collectionsPath "delete_collection"
, method = POST
, requestData = mkTextFormData [ ("collection_id", cid) ]
}
-- | Add a submission to a collection
addSubmissionToCollection
:: MonadReddit m => CollectionID -> SubmissionID -> m ()
addSubmissionToCollection = collectionAddRemove "add_post_to_collection"
-- | Remove a submission from a collection
removeSubmissionFromCollection
:: MonadReddit m => CollectionID -> SubmissionID -> m ()
removeSubmissionFromCollection =
collectionAddRemove "remove_post_in_collection"
-- | Reorder the submissions that comprise the collection by providing a
-- container of 'SubmissionID's in the new intended order
reorderCollection
:: (MonadReddit m, Foldable t) => CollectionID -> t SubmissionID -> m ()
reorderCollection cid ss =
runAction_ defaultAPIAction
{ pathSegments = collectionsPath "reorder_collection"
, method = POST
, requestData = mkTextFormData [ ("collection_id", cid)
, ("link_ids", fullname ss)
]
}
-- | Update the description of the collection
updateCollectionDescription :: MonadReddit m => CollectionID -> Body -> m ()
updateCollectionDescription cid b =
runAction_ defaultAPIAction
{ pathSegments = collectionsPath "update_collection_description"
, method = POST
, requestData = mkTextFormData [ ("collection_id", cid)
, ("description", b)
]
}
-- | Update the title of the collection
updateCollectionTitle :: MonadReddit m => CollectionID -> Title -> m ()
updateCollectionTitle cid t =
runAction_ defaultAPIAction
{ pathSegments = collectionsPath "update_collection_title"
, method = POST
, requestData =
mkTextFormData [ ("collection_id", cid), ("title", t) ]
}
collectionAddRemove
:: MonadReddit m => PathSegment -> CollectionID -> SubmissionID -> m ()
collectionAddRemove path cid sid =
runAction_ defaultAPIAction
{ pathSegments = collectionsPath path
, method = POST
, requestData =
mkTextFormData [ ("collection_id", cid)
, ("link_fullname", fullname sid)
]
}
collectionsPath :: PathSegment -> [PathSegment]
collectionsPath path = [ "api", "v1", "collections", path ]
--Subreddit relationships------------------------------------------------------
-- | Get a list of information on all moderators for the given subreddit
getModerators :: MonadReddit m => SubredditName -> m (Seq ModAccount)
getModerators sname = runAction @ModList r <&> wrappedTo
where
r = defaultAPIAction { pathSegments = subAboutPath sname "moderators" }
-- | Get information about a single moderator, if such a moderator exists
getModerator
:: MonadReddit m => SubredditName -> Username -> m (Maybe ModAccount)
getModerator sname uname = do
mods <- runAction @ModList r <&> wrappedTo
case mods of
modInfo :<| _ -> pure $ Just modInfo
_ -> pure Nothing
where
r = defaultAPIAction
{ pathSegments = subAboutPath sname "moderators"
, requestData = mkTextFormData [ ("user", toQueryParam uname) ]
}
-- | Update the permissions granted to a current moderator
updateModerator
:: (MonadReddit m, Foldable t)
=> Maybe (t ModPermission)
-- ^ If @Nothing@, grants all permissions. If @Just@ and empty,
-- all permissions are revoked. Otherwise, each of the given container
-- of permissions is granted
-> SubredditName
-> Username
-> m ()
updateModerator = postUpdate Mod
-- | Revoke the given user\'s mod status
removeModerator :: MonadReddit m => SubredditName -> Username -> m ()
removeModerator = postUnfriend Mod
-- | Revoke the authenticated user\'s mod status in the given subreddit.
-- __Caution__!
abdicateModerator :: MonadReddit m => SubredditName -> m ()
abdicateModerator sname = do
Account { username } <- getMe
postUnfriend Mod sname username
-- | Invite a user to moderate the subreddit. This action will implicitly grant
-- the invitee all moderator permissions on the subreddit. To control which
-- specific set of permissions the invitee shall be allowed instead, see
-- 'inviteModeratorWithPerms'
inviteModerator :: MonadReddit m => SubredditName -> Username -> m ()
inviteModerator = invite $ mkTextForm [ ("permissions", "+all") ]
-- | Invite a user to moderate the subreddit with a specific set of permissions
inviteModeratorWithPerms
:: (MonadReddit m, Foldable t)
=> t ModPermission -- ^ If empty, no permissions are granted
-> SubredditName
-> Username
-> m ()
inviteModeratorWithPerms perms =
invite $ mkTextForm [ ("permissions", joinPerms perms) ]
invite :: MonadReddit m => Form -> SubredditName -> Username -> m ()
invite = postFriend ModInvitation
-- | Get a listing of users invited to moderate the subreddit. This endpoint only
-- returns 25 results at a time, and does not use the @Listing@ mechanism that
-- prevails elsewhere. You can paginate through all invitees by passing previous
-- 'ModInviteeList' results to subsequent invocations
getInvitees :: MonadReddit m
=> Maybe ModInviteeList
-- ^ A previously obtained 'ModInviteeList' that may contain
-- @before@ and @after@ fields to paginate through entries
-> SubredditName
-> m ModInviteeList
getInvitees mil sname =
runAction defaultAPIAction
{ pathSegments =
[ "api", "v1", toUrlPiece sname, "moderators_invited" ]
, requestData = WithForm $ maybe mempty toForm mil
}
-- | Get information about a single invited user
getInvitee
:: MonadReddit m => SubredditName -> Username -> m (Maybe ModInvitee)
getInvitee sname uname = do
ModInviteeList { invited } <- runAction r
case invited of
invitee :<| _ -> pure $ Just invitee
_ -> pure Nothing
where
r = defaultAPIAction
{ pathSegments =
[ "api", "v1", toUrlPiece sname, "moderators_invited" ]
, requestData = mkTextFormData [ ("username", toQueryParam uname) ]
}
-- | Update the permissions granted to the mod invitee
updateInvitation
:: (MonadReddit m, Foldable t)
=> Maybe (t ModPermission)
-- ^ If @Nothing@, grants all permissions. If @Just@ and empty,
-- all permissions are revoked. Otherwise, each of the given container
-- of permissions is granted
-> SubredditName
-> Username
-> m ()
updateInvitation = postUpdate ModInvitation
postUpdate :: (MonadReddit m, Foldable t)
=> SubredditRelationship
-> Maybe (t ModPermission)
-> SubredditName
-> Username
-> m ()
postUpdate ty ps sname uname =
runAction_ defaultAPIAction
{ pathSegments = subAPIPath sname "setpermissions"
, method = POST
, requestData = mkTextFormData [ ("name", toQueryParam uname)
, ("type", toQueryParam ty)
, ( "permissions"
, maybe "+all" joinPerms ps
)
, ("api_type", "json")
]
}
-- | Revoke an existing moderator invitation for the given user
revokeInvitation :: MonadReddit m => SubredditName -> Username -> m ()
revokeInvitation = postUnfriend ModInvitation
-- | Accept the invitation issued to the authenticated user to moderate the
-- given subreddit
acceptInvitation :: MonadReddit m => SubredditName -> m ()
acceptInvitation sname =
runAction_ defaultAPIAction
{ pathSegments = subAPIPath sname "accept_moderator_invitation"
, method = POST
, requestData = mkTextFormData [ ("api_type", "json") ]
}
-- | Get a list of contributors on the subreddit
getContributors :: MonadReddit m
=> SubredditName
-> Paginator RelID RelInfo
-> m (Listing RelID RelInfo)
getContributors = relListing Contributor
-- | Get a single contributor, if such a user exists
getContributor
:: MonadReddit m => SubredditName -> Username -> m (Maybe RelInfo)
getContributor = singleRel getContributors
-- | Give a user contributor status on the subreddit
addContributor :: MonadReddit m => SubredditName -> Username -> m ()
addContributor = postFriend Contributor mempty
-- | Remove a contributor from the subreddit
removeContributor :: MonadReddit m => SubredditName -> Username -> m ()
removeContributor = postUnfriend Contributor
-- | AbdicateModerator your contributor status on the given subreddit
abdicateContributor :: MonadReddit m => SubredditID -> m ()
abdicateContributor sid =
runAction_ defaultAPIAction
{ pathSegments = [ "api", "leavecontributor" ]
, method = POST
, requestData = mkTextFormData [ ("id", fullname sid) ]
}
-- | Get a list of wiki contributors on the subreddit
getWikiContributors :: MonadReddit m
=> SubredditName
-> Paginator RelID RelInfo
-> m (Listing RelID RelInfo)
getWikiContributors = relListing WikiContributor
-- | Get a single wiki contributor, if such a user exists
getWikiContributor
:: MonadReddit m => SubredditName -> Username -> m (Maybe RelInfo)
getWikiContributor = singleRel getWikiContributors
-- | Give a user wiki contributor privileges on the subreddit
addWikiContributor :: MonadReddit m => SubredditName -> Username -> m ()
addWikiContributor = postFriend WikiContributor mempty
-- | Revoke wiki contributor privileges on the subreddit
removeWikiContributor :: MonadReddit m => SubredditName -> Username -> m ()
removeWikiContributor = postUnfriend WikiContributor
-- | Get the banned users for a given subreddit
getBans :: MonadReddit m
=> SubredditName
-> Paginator RelID Ban
-> m (Listing RelID Ban)
getBans = relListing Banned
-- | Check to see if a given user is banned on a particular subreddit,
-- returning the details of the 'Ban' if so
getBan :: MonadReddit m => SubredditName -> Username -> m (Maybe Ban)
getBan = singleRel getBans
-- | Issue a ban against a user on the given subreddit, with the provided notes
-- and (optional) duration
banUser :: MonadReddit m => BanNotes -> SubredditName -> Username -> m ()
banUser ban = postFriend Banned (toForm ban)
-- | Remove an existing ban on a user
unbanUser :: MonadReddit m => SubredditName -> Username -> m ()
unbanUser = postUnfriend Banned
-- | Ban a user from participating in the wiki
wikibanUser :: MonadReddit m => BanNotes -> SubredditName -> Username -> m ()
wikibanUser ban = postFriend BannedFromWiki (toForm ban)
-- | Reverse an existing wiki ban for a user
wikiUnbanUser :: MonadReddit m => SubredditName -> Username -> m ()
wikiUnbanUser = postUnfriend BannedFromWiki
-- | Get a list of users banned on the subreddit wiki
getWikibans :: MonadReddit m
=> SubredditName
-> Paginator RelID RelInfo
-> m (Listing RelID RelInfo)
getWikibans = relListing BannedFromWiki
-- | Get information on a single user banned on the subreddit wiki, if such a ban
-- exists
getWikiban :: MonadReddit m => SubredditName -> Username -> m (Maybe RelInfo)
getWikiban = singleRel getWikibans
-- | Get a list of users muted on the subreddit wiki
getMuted :: MonadReddit m
=> SubredditName
-> Paginator MuteID MuteInfo
-> m (Listing MuteID MuteInfo)
getMuted = relListing Muted
-- | Get information on a single user muted on the subreddit wiki, if such a ban
-- exists
getMutedUser
:: MonadReddit m => SubredditName -> Username -> m (Maybe MuteInfo)
getMutedUser = singleRel getMuted
-- | Mute a single user on the subreddit
muteUser :: MonadReddit m => BanNotes -> SubredditName -> Username -> m ()
muteUser ban = postFriend Muted (toForm ban)
-- | Unmute a single user on the subreddit
unmuteUser :: MonadReddit m => SubredditName -> Username -> m ()
unmuteUser = postUnfriend Muted
postFriend :: MonadReddit m
=> SubredditRelationship
-> Form
-> SubredditName
-> Username
-> m ()
postFriend ty form sname uname =
runAction_ defaultAPIAction
{ pathSegments = subAPIPath sname "friend"
, method = POST
, requestData = WithForm
$ mkTextForm [ ("name", toQueryParam uname)
, ("type", toQueryParam ty)
, ("api_type", "json")
]
<> form
}
postUnfriend :: MonadReddit m
=> SubredditRelationship
-> SubredditName
-> Username
-> m ()
postUnfriend ty sname uname =
runAction_ defaultAPIAction
{ pathSegments = subAPIPath sname "unfriend"
, method = POST
, requestData = mkTextFormData [ ("name", toQueryParam uname)
, ("type", toQueryParam ty)
, ("api_type", "json")
]
}
relListing :: (MonadReddit m, FromJSON a, Paginable a, FromJSON t, Thing t)
=> SubredditRelationship
-> SubredditName
-> Paginator t a
-> m (Listing t a)
relListing ty sname paginator =
runAction defaultAPIAction
{ pathSegments = subAboutPath sname (toUrlPiece ty)
, requestData = paginatorToFormData paginator
}
singleRel :: forall m a t.
(MonadReddit m, Paginable a, PaginateOptions a ~ RelInfoOpts)
=> (SubredditName -> Paginator t a -> m (Listing t a))
-> SubredditName
-> Username
-> m (Maybe a)
singleRel action sname uname = do
Listing { children } <- action sname pag
case children of
child :<| _ -> pure $ Just child
_ -> pure Nothing
where
pag = (emptyPaginator @t @a)
{ opts = RelInfoOpts { username = Just uname } }
--Subreddit settings-----------------------------------------------------------
-- | Get the configured 'SubredditSettings' for a given subreddit
getSubredditSettings :: MonadReddit m => SubredditName -> m SubredditSettings
getSubredditSettings sname =
runAction defaultAPIAction { pathSegments = subAboutPath sname "edit" }
-- | Configure a subreddit with the provided 'SubredditSettings'
setSubredditSettings :: MonadReddit m => SubredditSettings -> m ()
setSubredditSettings ss = runAction_ r
where
r = defaultAPIAction
{ pathSegments = [ "api", "site_admin" ]
, method = POST
, requestData = WithForm $ toForm ss
}
--Subreddit rules--------------------------------------------------------------
-- | Add a rule to the subreddit. The newly created 'SubredditRule' is returned
-- upon success
addSubredditRule
:: MonadReddit m => SubredditName -> NewSubredditRule -> m SubredditRule
addSubredditRule sname nsr = runAction @PostedSubredditRule r <&> wrappedTo
where
r = defaultAPIAction
{ pathSegments = [ "api", "add_subreddit_rule" ]
, method = POST
, requestData = WithForm
$ mkTextForm [ ("r", toQueryParam sname), ("api_type", "json") ]
<> toForm nsr
}
-- | Delete the rule identified by the given name from the subreddit
deleteSubredditRule :: MonadReddit m => SubredditName -> Name -> m ()
deleteSubredditRule sname n =
runAction_ defaultAPIAction
{ pathSegments = [ "api", "remove_subreddit_rule" ]
, method = POST
, requestData = mkTextFormData [ ("r", toQueryParam sname)
, ("short_name", n)
]
}
-- | Update an existing subreddit rule. You must provide the @shortName@ of the
-- existing rule as a parameter in order for Reddit to identify the rule. The
-- @shortName@ can be changed by updating the 'SubredditRule' record, however
updateSubredditRule
:: MonadReddit m
=> SubredditName
-> Name
-- ^ The old name for the rule. This is required even if you are not
-- changing the name of the rule, as Reddit has no other data to
-- uniquely identify the rule
-> SubredditRule
-> m SubredditRule
updateSubredditRule sname oldName srule =
runAction @PostedSubredditRule r <&> wrappedTo
where
r = defaultAPIAction
{ pathSegments = [ "api", "update_subreddit_rule" ]
, method = POST
, requestData = WithForm
$ mkTextForm [ ("old_short_name", oldName)
, ("r", toQueryParam sname)
, ("api_type", "json")
]
<> toForm srule
}
-- | Reorder the subreddit rules
reorderSubredditRules
:: (MonadReddit m, Foldable t)
=> SubredditName
-> t Name
-- ^ The desired order of the rules. Must contain all of the @shortName@s
-- of currently configured 'SubredditRule's on the subreddit
-> m ()
reorderSubredditRules sname ns =
runAction_ defaultAPIAction
{ pathSegments = [ "api", "reorder_subreddit_rules" ]
, method = POST
, requestData =
mkTextFormData [ ("r", toQueryParam sname)
, ("new_rule_order", joinParams ns)
]
}
--Flair------------------------------------------------------------------------
-- | Get a list of usernames and the flair currently assigned to them
getFlairList :: MonadReddit m
=> SubredditName
-> Paginator UserID AssignedFlair
-> m (Listing UserID AssignedFlair)
getFlairList sname paginator = flairlistToListing
<$> runAction defaultAPIAction
{ pathSegments = subAPIPath sname "flairlist"
, requestData = paginatorToFormData paginator
}
-- | Get the 'UserFlair' that corresponds to a 'Username'
getUserFlair
:: MonadReddit m => SubredditName -> Username -> m (Maybe UserFlair)
getUserFlair sname uname = catch @_ @APIException action $ \case
JSONParseError _ _ -> pure Nothing
e -> throwM e
where
action = runAction @CurrentUserFlair r <&> Just . wrappedTo
r = defaultAPIAction
{ pathSegments = subAPIPath sname "flairselector"
, method = POST
, requestData = mkTextFormData [ ("name", toQueryParam uname) ]
}
-- | Set a user\'s flair. If the 'CSSClass' is provided in the 'FlairChoice', it
-- takes precedence over the 'FlairID' contained in that record
setUserFlair :: MonadReddit m => FlairSelection -> Username -> m ()
setUserFlair (FlairSelection FlairChoice { .. } txt sname) uname =
runAction_ route
where
route = case cssClass of
Just css -> baseRoute
{ pathSegments = subAPIPath sname "selectflair"
, requestData = WithForm
$ baseForm <> mkTextForm [ ("css_class", toQueryParam css) ]
}
Nothing -> baseRoute
{ pathSegments = subAPIPath sname "flair"
, requestData = WithForm
$ baseForm
<> mkTextForm [ ( "flair_template_id"
, toQueryParam templateID
)
]
}
baseForm = mkTextForm
$ [ ("name", toQueryParam uname) ]
<> foldMap pure (("text", ) <$> txt)
baseRoute = defaultAPIAction { method = POST }
-- | Set, update, or deleteSRImage the flair of multiple users at once, given a
-- container of 'AssignedFlair's
setUserFlairs :: (MonadReddit m, Foldable t)
=> SubredditName
-> t AssignedFlair
-> m (Seq FlairResult)
setUserFlairs sname afs = mconcat
<$> traverse (runAction . r) (chunksOf apiRequestLimit (F.toList afs))
where
r as = defaultAPIAction
{ pathSegments = subAPIPath sname "flaircsv"
, method = POST
, requestData =
mkTextFormData [ ("flair_csv", T.unlines $ mkRow <$> as) ]
}
mkRow AssignedFlair { .. } =
joinParams [ toQueryParam user
, maybe mempty toQueryParam text
, toQueryParam $ fromMaybe mempty cssClass
]
-- | Delete a user\'s flair on the given subreddit
deleteUserFlair :: MonadReddit m => SubredditName -> Username -> m ()
deleteUserFlair sname uname =
runAction_ defaultAPIAction
{ pathSegments = subAPIPath sname "deleteflair"
, method = POST
, requestData = mkTextFormData [ ("name", toQueryParam uname)
, ("api_type", "json")
]
}
-- | Create a new 'FlairTemplate' for either users or submissions, returning the
-- newly created template
createFlairTemplate :: MonadReddit m
=> FlairType
-> SubredditName
-> FlairTemplate
-> m FlairTemplate
createFlairTemplate fty sname tmpl = runAction
$ flairRoute (toForm tmpl) fty sname
-- | Create a new 'FlairTemplate' for users, returning the newly created template
createUserFlairTemplate
:: MonadReddit m => SubredditName -> FlairTemplate -> m FlairTemplate
createUserFlairTemplate = createFlairTemplate UserFlairType
-- | Create a new 'FlairTemplate' for submissions, returning the newly created
-- template
createSubmissionFlairTemplate
:: MonadReddit m => SubredditName -> FlairTemplate -> m FlairTemplate
createSubmissionFlairTemplate = createFlairTemplate SubmissionFlairType
-- | Update an existing 'FlairTemplate' for either users or submissions
updateFlairTemplate
:: MonadReddit m => FlairType -> SubredditName -> FlairTemplate -> m ()
updateFlairTemplate fty sname tmpl = runAction_ $ flairRoute form fty sname
where
form = toForm $ wrappedFrom @PostedFlairTemplate tmpl
-- | Update an existing 'FlairTemplate' for users
updateUserFlairTemplate
:: MonadReddit m => SubredditName -> FlairTemplate -> m ()
updateUserFlairTemplate = updateFlairTemplate UserFlairType
-- | Update an existing 'FlairTemplate' for submissions
updateSubmissionFlairTemplate
:: MonadReddit m => SubredditName -> FlairTemplate -> m ()
updateSubmissionFlairTemplate = updateFlairTemplate SubmissionFlairType
flairRoute :: Form -> FlairType -> SubredditName -> APIAction a
flairRoute form fty sname = defaultAPIAction
{ pathSegments = subAPIPath sname "flairtemplate_v2"
, method = POST
, requestData =
WithForm $ form <> mkTextForm [ ("flair_type", toQueryParam fty) ]
}
-- | Delete a user or submission flair template given its 'FlairID'
deleteFlairTemplate :: MonadReddit m => SubredditName -> FlairID -> m ()
deleteFlairTemplate sname ftid =
runAction_ defaultAPIAction
{ pathSegments = subAPIPath sname "deleteflairtemplate"
, method = POST
, requestData =
mkTextFormData [ ("flair_template_id", toQueryParam ftid)
]
}
-- | Clear all of the user flair templates on the subreddit
clearUserFlairTemplates :: MonadReddit m => SubredditName -> m ()
clearUserFlairTemplates = clearFlairTemplates UserFlairType
-- | Clear all of the user flair templates on the subreddit
clearSubmissionFlairTemplates :: MonadReddit m => SubredditName -> m ()
clearSubmissionFlairTemplates = clearFlairTemplates SubmissionFlairType
-- | Clear all of the user or submission flair templates on the subreddit
clearFlairTemplates :: MonadReddit m => FlairType -> SubredditName -> m ()
clearFlairTemplates fty sname =
runAction_ defaultAPIAction
{ pathSegments = subAPIPath sname "clearflairtemplates"
, method = POST
, requestData =
mkTextFormData [ ("flair_type", toQueryParam fty) ]
}
--Wikis------------------------------------------------------------------------
-- | Get the 'WikiPageSettings' for the subreddit\'s given wikipage
getWikiPageSettings
:: MonadReddit m => SubredditName -> WikiPageName -> m WikiPageSettings
getWikiPageSettings sname wpage =
runAction defaultAPIAction
{ pathSegments = [ "r"
, toUrlPiece sname
, "wiki"
, "settings"
, toQueryParam wpage
]
}
-- | Grant editing privileges to the given 'Username' on the subreddit\'s wikipage
addWikiEditor
:: MonadReddit m => SubredditName -> WikiPageName -> Username -> m ()
addWikiEditor = allowedEditor "add"
-- | Revoke the given 'Username'\'s editing privileges on the subreddit\'s wikipage
removeWikiEditor
:: MonadReddit m => SubredditName -> WikiPageName -> Username -> m ()
removeWikiEditor = allowedEditor "del"
allowedEditor :: MonadReddit m
=> Text
-> SubredditName
-> WikiPageName
-> Username
-> m ()
allowedEditor path sname wpage uname =
runAction_ defaultAPIAction
{ pathSegments = [ "r"
, toQueryParam sname
, "api"
, "wiki"
, "allowededitor"
, path
]
, method = POST
, requestData =
mkTextFormData [ ("page", toQueryParam wpage)
, ("username", toQueryParam uname)
]
}
-- | Revert the wikipage to the given revision
revertWikiPage :: MonadReddit m
=> SubredditName
-> WikiPageName
-> WikiRevisionID
-> m ()
revertWikiPage sname wpage wr =
runAction_ defaultAPIAction
{ pathSegments =
[ "r", toUrlPiece sname, "api", "wiki", "revert" ]
, method = POST
, requestData = mkTextFormData [ ("page", toQueryParam wpage)
, ("revision", toQueryParam wr)
]
}
--Stylesheets, images and widgets ----------------------------------------------
-- | Get the 'Stylesheet' that has been configured for the given subreddit
getStylesheet :: MonadReddit m => SubredditName -> m Stylesheet
getStylesheet sname =
runAction defaultAPIAction
{ pathSegments =
[ "r", toUrlPiece sname, "about", "stylesheet" ]
}
-- | Update a given subreddit\'s stylesheet with new contents, which must be
-- valid CSS
updateStylesheet :: MonadReddit m
=> SubredditName
-> Maybe Text -- ^ The reason for the change, if any
-> Text -- ^ The new contents of the stylesheet
-> m ()
updateStylesheet sname r contents =
runAction_ defaultAPIAction
{ pathSegments = subAPIPath sname "subreddit_stylesheet"
, method = POST
, requestData = mkTextFormData
$ [ ("stylesheet_contents", contents)
, ("op", "save")
, ("api_type", "json")
]
<> foldMap pure (("reason", ) <$> r)
}
uploadImage, uploadHeader
:: MonadReddit m => Text -> FilePath -> SubredditName -> m ()
-- | Upload an image file to add to the given subreddit\'s stylesheet
uploadImage = uploadSRImage "img"
-- | Upload the image header for the given subreddit\'s stylesheet
uploadHeader = uploadSRImage "header"
uploadMobileIcon, uploadMobileHeader
:: MonadReddit m => Text -> FilePath -> SubredditName -> m ()
-- | Upload a mobile icon for the given subreddit
uploadMobileIcon = uploadSRImage "icon"
-- | Upload the mobile header for the given subreddit
uploadMobileHeader = uploadSRImage "banner"
-- | Delete the named image from the given subreddit\'s stylesheet
deleteImage :: MonadReddit m => Text -> SubredditName -> m ()
deleteImage = deleteSRImage "img"
-- | Delete the named image from the given subreddit\'s stylesheet
deleteMobileIcon :: MonadReddit m => Text -> SubredditName -> m ()
deleteMobileIcon = deleteSRImage "icon"
-- | Delete header image from the given subreddit
deleteHeader :: MonadReddit m => SubredditName -> m ()
deleteHeader sname =
runAction_ defaultAPIAction
{ pathSegments = subAPIPath sname "delete_sr_header"
, method = POST
, requestData = mkTextFormData [ ("api_type", "json") ]
}
-- | Upload a banner for the subreddit (redesign only)
uploadBanner :: MonadReddit m => SubredditName -> FilePath -> m ()
uploadBanner sname fp = do
imgURL <- uploadS3Asset sname BannerBackground fp
updateStructuredStyles sname
$ mkTextForm [ (toQueryParam BannerBackground, imgURL) ]
-- | Delete the subreddit banner, even if it does not exist (redesign only)
deleteBanner :: MonadReddit m => SubredditName -> m ()
deleteBanner sname = updateStructuredStyles sname
$ mkTextForm [ (toQueryParam BannerBackground, mempty) ]
-- | Upload the additional image banner for the subreddit (redesign only)
uploadBannerAdditional
:: MonadReddit m
=> Maybe StyleImageAlignment
-> SubredditName
-> FilePath
-> m ()
uploadBannerAdditional sia sname fp = do
imgURL <- uploadS3Asset sname BannerAdditional fp
updateStructuredStyles sname . mkTextForm
$ [ (toQueryParam BannerAdditional, imgURL) ]
<> foldMap pure
(("bannerPositionedImagePosition", ) . toQueryParam <$> sia)
-- | Delete all additional banners, including the hover banner (redesign only)
deleteBannerAdditional :: MonadReddit m => SubredditName -> m ()
deleteBannerAdditional sname = updateStructuredStyles sname
$ mkTextForm [ (toQueryParam BannerAdditional, mempty)
, (toQueryParam BannerHover, mempty)
]
-- | Upload the banner hover image for the subreddit (redesign only)
uploadBannerHover :: MonadReddit m => SubredditName -> FilePath -> m ()
uploadBannerHover sname fp = do
imgURL <- uploadS3Asset sname BannerHover fp
updateStructuredStyles sname
$ mkTextForm [ (toQueryParam BannerHover, imgURL) ]
-- | Delete the subreddit banner hover image (redesign only)
deleteBannerHover :: MonadReddit m => SubredditName -> m ()
deleteBannerHover sname = updateStructuredStyles sname
$ mkTextForm [ (toQueryParam BannerHover, mempty) ]
uploadSRImage :: forall m.
MonadReddit m
=> ByteString
-> Text
-> FilePath
-> SubredditName
-> m ()
uploadSRImage ty name fp sname = withSourceFile @_ @m fp $ \bs -> do
img <- runConduit $ bs .| sinkLbs
imageType <- getImageType img
when (LB.length img > maxImageSize) . throwM
$ InvalidRequest "uploadSRImage: exceeded maximum image size"
runAction_ defaultAPIAction
{ pathSegments = subAPIPath sname "upload_sr_img"
, method = POST
, requestData =
WithMultipart [ partBS "img_type" imageType
-- This seems to work, but @partLBS img@ causes
-- reddit to freak out
, partFile "file" fp
, partBS "name" $ T.encodeUtf8 name
, partBS "upload_type" ty
, partBS "api_type" "json"
]
}
where
maxImageSize = 512000
getImageType = \case
bs
| LB.take 4 (LB.drop 6 bs) == "JFIF" -> pure "jpeg"
| LB.isPrefixOf "\137PNG\r\n\26\n" bs -> pure "png"
| otherwise -> throwM
$ InvalidRequest "uploadSRImage: Can't detect image type"
deleteSRImage :: MonadReddit m => Text -> Text -> SubredditName -> m ()
deleteSRImage path name sname =
runAction_ defaultAPIAction
{ pathSegments = subAPIPath sname $ "delete_sr_" <> path
, method = POST
, requestData = mkTextFormData [ ("img_name", name)
, ("api_type", "json")
]
}
uploadS3Asset :: MonadReddit m
=> SubredditName
-> StructuredStyleImage
-> FilePath
-> m URL
uploadS3Asset sname imageType fp = do
mimetype <- case FP.takeExtension fp of
ext
| ext `elem` [ ".jpeg", ".jpg" ] -> pure "image/jpeg"
| ext == ".png" -> pure "image/png"
| otherwise ->
throwM $ InvalidRequest "uploadS3Asset: invalid file type"
S3ModerationLease { .. }
<- runAction defaultAPIAction
{ pathSegments = [ "api"
, "v1"
, "style_asset_upload_s3"
, toUrlPiece sname
]
, method = POST
, requestData = mkTextFormData [ ( "filepath"
, toQueryParam
$ FP.takeFileName fp
)
, ("mimetype", mimetype)
, ( "imagetype"
, toQueryParam imageType
)
]
}
(url, ps) <- splitURL action
void . runActionWith_
=<< mkRequest url
defaultAPIAction
{ pathSegments = ps
, method = POST
, requestData = WithMultipart
$ HM.foldrWithKey mkParts
[ partFile "file" fp ]
fields
, rawJSON = False
}
pure $ T.intercalate "/" [ action, key ]
where
mkParts name value ps = partBS name (T.encodeUtf8 value) : ps
updateStructuredStyles :: MonadReddit m => SubredditName -> Form -> m ()
updateStructuredStyles sname form =
runAction_ defaultAPIAction
{ pathSegments =
[ "api", "v1", "structured_styles", toUrlPiece sname ]
, method = PATCH
, requestData = WithForm form
}
--Modmail----------------------------------------------------------------------
-- | Get all of the authenticated user\'s modmail. See 'getModmailWithOpts' in
-- order to control how modmail is sorted or filtered
getModmail :: MonadReddit m => m Modmail
getModmail =
runAction defaultAPIAction
{ pathSegments = modmailPath
, requestData = WithForm
$ toForm defaultModmailOpts { state = Just AllModmail }
}
-- | Get the authenticated user\'s modmail with the provided 'ModmailOpts'
getModmailWithOpts :: MonadReddit m => ModmailOpts -> m Modmail
getModmailWithOpts opts =
runAction defaultAPIAction
{ pathSegments = modmailPath
, requestData = WithForm $ toForm opts
}
-- | Get a single 'ModmailConversation' given its ID
getModmailConversation :: MonadReddit m => ModmailID -> m ModmailConversation
getModmailConversation m = runAction @ConversationDetails r <&> wrappedTo
where
r = defaultAPIAction { pathSegments = modmailPath <> [ toUrlPiece m ] }
-- | Get the number of unread modmail conversations according to conversation
-- state
getUnreadModmailCount :: MonadReddit m => m (HashMap ModmailState Word)
getUnreadModmailCount =
runAction defaultAPIAction
{ pathSegments = modmailPath <> [ "unread", "count" ] }
-- | Create a new 'ModmailConversation'
createConversation
:: MonadReddit m => NewConversation -> m ModmailConversation
createConversation nc = runAction @ConversationDetails r <&> wrappedTo
where
r = defaultAPIAction
{ pathSegments = modmailPath
, method = POST
, requestData = WithForm $ toForm nc
}
-- | Reply to the modmail conversation
replyToConversation
:: MonadReddit m => ModmailReply -> ModmailID -> m ModmailConversation
replyToConversation mr m = runAction @ConversationDetails r <&> wrappedTo
where
r = defaultAPIAction
{ pathSegments = modmailPath <> [ toUrlPiece m ]
, method = POST
, requestData = WithForm $ toForm mr
}
-- | Archive a modmail conversation
archiveConversation :: MonadReddit m => ModmailID -> m ()
archiveConversation m =
runAction_ defaultAPIAction
{ pathSegments = modmailPath <> [ toUrlPiece m, "archive" ]
, method = POST
}
-- | Archive a modmail conversation
unarchiveConversation :: MonadReddit m => ModmailID -> m ()
unarchiveConversation m =
runAction_ defaultAPIAction
{ pathSegments = modmailPath <> [ toUrlPiece m, "unarchive" ]
, method = POST
}
-- | Highlight a given conversation
highlightConversation :: MonadReddit m => ModmailID -> m ()
highlightConversation = highlightUnhighlight POST
-- | Unhighlight a given conversation
unhighlightConversation :: MonadReddit m => ModmailID -> m ()
unhighlightConversation = highlightUnhighlight DELETE
highlightUnhighlight :: MonadReddit m => Method -> ModmailID -> m ()
highlightUnhighlight method m =
runAction_ defaultAPIAction
{ pathSegments = modmailPath <> [ toUrlPiece m, "highlight" ]
, method
}
-- | Mark the conversations corresponding to a container of 'ModmailID's as read
markConversationsRead :: (Foldable t, MonadReddit m) => t ModmailID -> m ()
markConversationsRead = readUnread "read"
-- | Mark the conversation corresponding to a single 'ModmailID' as read
markConversationRead :: MonadReddit m => ModmailID -> m ()
markConversationRead m = markConversationsRead [ m ]
-- | Mark the conversations corresponding to a container of 'ModmailID's as unread
markConversationsUnread :: (Foldable t, MonadReddit m) => t ModmailID -> m ()
markConversationsUnread = readUnread "unread"
-- | Mark the conversation corresponding to a single 'ModmailID' as unread
markConversationUnread :: MonadReddit m => ModmailID -> m ()
markConversationUnread m = markConversationsUnread [ m ]
readUnread
:: (Foldable t, MonadReddit m) => PathSegment -> t ModmailID -> m ()
readUnread path ms =
runAction_ defaultAPIAction
{ pathSegments = modmailPath <> [ path ]
, method = POST
, requestData =
mkTextFormData [ ("conversationIds", joinParams ms) ]
}
-- | Mark all mail belonging to the subreddits as read, returning the 'ModmailID's
-- of the newly read conversations
bulkReadConversations :: (MonadReddit m, Foldable t)
=> Maybe ModmailState
-> t SubredditName
-> m (Seq ModmailID)
bulkReadConversations mms snames = runAction @BulkReadIDs r <&> wrappedTo
where
r = defaultAPIAction
{ pathSegments = modmailPath <> [ "bulk", "read" ]
, method = POST
, requestData = WithForm . mkTextForm
$ [ ("entity", joinParams snames) ]
<> foldMap pure (("state", ) . toQueryParam <$> mms)
}
-- | Mute the non-moderator user associated with the modmail conversation. Valid
-- durations for the @days@ parameter are 3, 7, and 28
muteModmailUser :: MonadReddit m => Word -> ModmailID -> m ()
muteModmailUser days m = do
when (days `notElem` [ 3, 7, 28 ]) . throwM
$ InvalidRequest --
"muteModmailUser: mute duration must be one of 3, 7, or 28"
runAction_ defaultAPIAction
{ pathSegments = modmailPath <> [ toUrlPiece m, "mute" ]
, method = POST
, requestData =
mkTextFormData [ ("num_hours", toQueryParam $ days * 24)
]
}
-- | Unmute the non-moderator user associated with the modmail conversation
unmuteModmailUser :: MonadReddit m => ModmailID -> m ()
unmuteModmailUser m =
runAction_ defaultAPIAction
{ pathSegments = modmailPath <> [ toUrlPiece m, "unmute" ]
, method = POST
}
modmailPath :: [PathSegment]
modmailPath = [ "api", "mod", "conversations" ]
--Widgets----------------------------------------------------------------------
-- | Delete a widget, given its ID
deleteWidget :: MonadReddit m => SubredditName -> WidgetID -> m ()
deleteWidget sname wid =
runAction_ defaultAPIAction
{ pathSegments =
subAPIPath sname "widget" <> [ toUrlPiece wid ]
, method = DELETE
}
-- | Reorder the widgets corresponding to a container of widget IDs in the given
-- section. At the moment, reddit does not allow for the 'Topbar' to be reordered.
-- If you attempt to reorder this section, you might receive an 'InvalidJSON'
-- exception
reorderWidgets :: (MonadReddit m, Foldable t)
=> Maybe WidgetSection
-> SubredditName
-> t WidgetID
-> m ()
reorderWidgets sm sname ws =
runAction_ defaultAPIAction
{ pathSegments =
subAPIPath sname "widget_order" <> [ toUrlPiece section ]
, method = PATCH
, requestData =
mkTextFormData [ ("json", textEncode $ F.toList ws)
, ("section", toQueryParam section)
]
}
where
section = fromMaybe Sidebar sm
-- | Update an existing widget, given its ID. You must wrap the widget type in
-- the appropriate 'Widget' constructors, as this action may be performed on
-- heterogeneous widget types. The update widget is returned upon success
updateWidget
:: MonadReddit m => SubredditName -> WidgetID -> Widget -> m Widget
updateWidget sname wid w =
runAction defaultAPIAction
{ pathSegments = subAPIPath sname "widget" <> [ toUrlPiece wid ]
, method = PUT
, requestData = mkTextFormData [ ("json", textEncode w) ]
}
-- | Add a button widget. Returns the created widget upon success. See the docs for
-- 'ButtonWidget' for the available options
addButtonWidget
:: MonadReddit m => SubredditName -> ButtonWidget -> m ButtonWidget
addButtonWidget = addNormalWidget
-- | Add a calendar widget, which requires an active Google account and public
-- calendar. Returns the created widget upon success. See the docs for
-- 'CalendarWidget' for the available options
addCalendarWidget
:: MonadReddit m
=> Maybe Body -- ^ A short description of the widget, in markdown
-> SubredditName
-> CalendarWidget
-> m CalendarWidget
addCalendarWidget = addDescribableWidget
-- | Add a community list widget. Returns the created widget upon success. See
-- the docs for 'CommunityListWidget' for the available options
addCommunityListWidget
:: MonadReddit m
=> Maybe Body -- ^ A short description of the widget, in markdown
-> SubredditName
-> CommunityListWidget
-> m CommunityListWidget
addCommunityListWidget = addDescribableWidget
-- | Add a custom widget. Returns the created widget upon success. See
-- the docs for 'CustomWidget' for the available options
addCustomWidget
:: MonadReddit m => SubredditName -> CustomWidget -> m CustomWidget
addCustomWidget = addNormalWidget
-- | Add an image widget. Returns the created widget upon success. See
-- the docs for 'ImageWidget' for the available options
addImageWidget
:: MonadReddit m => SubredditName -> ImageWidget -> m ImageWidget
addImageWidget = addNormalWidget
-- | Add a menu widget. Returns the created widget upon success. See
-- the docs for 'MenuWidget' for the available options
addMenuWidget :: MonadReddit m => SubredditName -> MenuWidget -> m MenuWidget
addMenuWidget = addNormalWidget
-- | Add a post flair widget. Returns the created widget upon success. See
-- the docs for 'PostFlairWidget' for the available options along with
-- 'mkPostFlairWidget'
addPostFlairWidget
:: MonadReddit m => SubredditName -> PostFlairWidget -> m PostFlairWidget
addPostFlairWidget = addNormalWidget
-- | Add a text area widget. Returns the created widget upon success. See
-- the docs for 'TextAreaWidget' for the available options as well as
-- 'mkTextAreaWidget'
addTextAreaWidget
:: MonadReddit m => SubredditName -> TextAreaWidget -> m TextAreaWidget
addTextAreaWidget = addNormalWidget
addNormalWidget
:: (MonadReddit m, ToJSON a, FromJSON a) => SubredditName -> a -> m a
addNormalWidget sname x =
runAction defaultAPIAction
{ pathSegments = subAPIPath sname "widget"
, method = POST
, requestData = mkTextFormData [ ("json", textEncode x) ]
}
addDescribableWidget
:: (MonadReddit m, ToJSON a, FromJSON a)
=> Maybe Body
-> SubredditName
-> a
-> m a
addDescribableWidget desc sname x =
runAction defaultAPIAction
{ pathSegments = subAPIPath sname "widget"
, method = POST
, requestData =
mkTextFormData [ ( "json"
, textEncode . describeWidget x
$ fromMaybe mempty desc
)
]
}
-- Certain 'Widget's can be given markdown-formatted description field. This
-- function injects the field into the widget\'s JSON 'Object' if applicable,
-- otherwise returning the value as-is
describeWidget :: ToJSON a => a -> Body -> Value
describeWidget widget (String -> desc) = case toJSON widget of
Object o -> Object $ HM.insert "description" desc o
v -> v
-- | Upload a widget image from a filepath. This returns the URL of the new image,
-- which is required for creating certain widgets
uploadWidgetImage :: MonadReddit m => SubredditName -> FilePath -> m UploadURL
uploadWidgetImage sname fp = do
S3ModerationLease { action, key }
<- uploadS3Image True (subAPIPath sname "widget_image_upload_s3") fp
pure . wrappedFrom $ T.intercalate "/" [ action, key ]
uploadS3Image :: MonadReddit m
=> Bool
-> [PathSegment]
-> FilePath
-> m S3ModerationLease
uploadS3Image rawJSON pathSegments fp = do
mimetype <- case FP.takeExtension fp of
ext
| ext `elem` [ ".jpeg", ".jpg" ] -> pure "image/jpeg"
| ext == ".png" -> pure "image/png"
| otherwise ->
throwM $ InvalidRequest "uploadS3Image: invalid file type"
s3@S3ModerationLease { .. }
<- runAction defaultAPIAction
{ pathSegments
, method = POST
, requestData = mkTextFormData [ ( "filepath"
, toQueryParam
$ FP.takeFileName fp
)
, ("mimetype", mimetype)
]
}
(url, ps) <- splitURL action
void . runActionWith_
=<< mkRequest url
defaultAPIAction
{ pathSegments = ps
, method = POST
, requestData = WithMultipart
$ HM.foldrWithKey mkParts
[ partFile "file" fp ]
fields
, rawJSON
}
pure s3
where
mkParts name value ps = partBS name (T.encodeUtf8 value) : ps
--Emoji------------------------------------------------------------------------
-- | Add a new emoji by uploading an image. See 'mkEmoji' to conveniently create
-- new 'Emoji's to add. Also note the restrictions on the filepath argument below,
-- which are not currently validated by this action. This action can also be used
-- to update the image for an existing emoji (see
-- 'Network.Reddit.Actions.Subreddit.getSubredditEmoji') to get a list of emojis
-- for a subreddit
addEmoji :: MonadReddit m
=> SubredditName
-> FilePath
-- ^ Must be an image in jpeg/png format, with maximum dimensions of
-- 128 x 128px and size of 64KB
-> Emoji
-> m ()
addEmoji sname fp emoji = do
S3ModerationLease { key }
<- uploadS3Image False (v1Path sname "emoji_asset_upload_s3.json") fp
runAction_ defaultAPIAction
{ pathSegments = v1Path sname "emoji.json"
, method = POST
, requestData = WithForm
$ toForm @NewEmoji (wrappedFrom emoji)
<> mkTextForm [ ("s3_key", toQueryParam key) ]
}
-- | Delete a single emoji and associated s3 image
deleteEmoji :: MonadReddit m => SubredditName -> EmojiName -> m ()
deleteEmoji sname ename =
runAction_ defaultAPIAction
{ pathSegments = v1Path sname "emoji" <> [ toUrlPiece ename ]
, method = DELETE
}
-- | Update an emoji. Only the boolean permissions fields will be sent. If you
-- would like to change the image associated with the emoji name, use 'addEmoji'
-- with an updated filepath
updateEmoji :: MonadReddit m => SubredditName -> Emoji -> m ()
updateEmoji sname emoji =
runAction_ defaultAPIAction
{ pathSegments = v1Path sname "emoji_permissions"
, method = POST
, requestData = WithForm $ toForm emoji
}
-- | Set the (h, w) dimensions for /all/ custom emojis on the subreddit. Both
-- dimensions must be between 16px and 40px. A @Nothing@ argument will disable
-- custom sizes
setCustomEmojiSize
:: MonadReddit m => SubredditName -> Maybe (Int, Int) -> m ()
setCustomEmojiSize sname = \case
Nothing -> runAction_ r { requestData = WithForm mempty }
Just ss@(h, w) -> case bimap inR inR ss of
(True, True) ->
runAction_ r
{ requestData =
mkTextFormData [ ("height", toQueryParam h)
, ("width", toQueryParam w)
]
}
_ -> throwM . InvalidRequest
$ "setCustomEmojiSize: Height and width must be between 16px and 40px"
where
inR = inRange (16, 40)
r = defaultAPIAction
{ pathSegments = v1Path sname "emoji_custom_size", method = POST }
v1Path :: ToHttpApiData a => a -> PathSegment -> [PathSegment]
v1Path sname path = [ "api", "v1", toUrlPiece sname ] <> [ path ]
--Misc-------------------------------------------------------------------------
-- | Get traffic statistics for the given subreddit
getTraffic :: MonadReddit m => SubredditName -> m Traffic
getTraffic sname =
runAction defaultAPIAction { pathSegments = subAboutPath sname "traffic" }