{-# LANGUAGE GADTs #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE InstanceSigs #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE OverloadedStrings #-} -- | Provides actions for Channel API interactions module Discord.Internal.Rest.Channel ( ChannelRequest(..) , MessageDetailedOpts(..) , AllowedMentions(..) , ReactionTiming(..) , MessageTiming(..) , ChannelInviteOpts(..) , ModifyChannelOpts(..) , ChannelPermissionsOpts(..) , GroupDMAddRecipientOpts(..) , ChannelPermissionsOptsType(..) , StartThreadOpts(..) , StartThreadNoMessageOpts(..) , ListThreads(..) ) where import Data.Aeson import Data.Default (Default, def) import Data.Emoji (unicodeByName) import qualified Data.Text as T import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL import Network.HTTP.Client (RequestBody (RequestBodyBS)) import Network.HTTP.Client.MultipartFormData (partFileRequestBody, partBS) import Network.HTTP.Req ((/:)) import qualified Network.HTTP.Req as R import Discord.Internal.Rest.Prelude import Discord.Internal.Types import Control.Monad (join) instance Request (ChannelRequest a) where majorRoute = channelMajorRoute jsonRequest = channelJsonRequest -- | Data constructor for requests. See data ChannelRequest a where -- | Gets a channel by its id. GetChannel :: ChannelId -> ChannelRequest Channel -- | Edits channels options. ModifyChannel :: ChannelId -> ModifyChannelOpts -> ChannelRequest Channel -- | Deletes a channel if its id doesn't equal to the id of guild. DeleteChannel :: ChannelId -> ChannelRequest Channel -- | Gets a messages from a channel with limit of 100 per request. GetChannelMessages :: ChannelId -> (Int, MessageTiming) -> ChannelRequest [Message] -- | Gets a message in a channel by its id. GetChannelMessage :: (ChannelId, MessageId) -> ChannelRequest Message -- | Sends a message to a channel. CreateMessage :: ChannelId -> T.Text -> ChannelRequest Message -- | Sends a message with granular controls. CreateMessageDetailed :: ChannelId -> MessageDetailedOpts -> ChannelRequest Message -- | Add an emoji reaction to a message. ID must be present for custom emoji CreateReaction :: (ChannelId, MessageId) -> T.Text -> ChannelRequest () -- | Remove a Reaction this bot added DeleteOwnReaction :: (ChannelId, MessageId) -> T.Text -> ChannelRequest () -- | Remove a Reaction someone else added DeleteUserReaction :: (ChannelId, MessageId) -> UserId -> T.Text -> ChannelRequest () -- | Deletes all reactions of a single emoji on a message DeleteSingleReaction :: (ChannelId, MessageId) -> T.Text -> ChannelRequest () -- | List of users that reacted with this emoji GetReactions :: (ChannelId, MessageId) -> T.Text -> (Int, ReactionTiming) -> ChannelRequest [User] -- | Delete all reactions on a message DeleteAllReactions :: (ChannelId, MessageId) -> ChannelRequest () -- | Edits a message content. EditMessage :: (ChannelId, MessageId) -> MessageDetailedOpts -> ChannelRequest Message -- | Deletes a message. DeleteMessage :: (ChannelId, MessageId) -> ChannelRequest () -- | Deletes a group of messages. BulkDeleteMessage :: (ChannelId, [MessageId]) -> ChannelRequest () -- | Edits a permission overrides for a channel. EditChannelPermissions :: ChannelId -> OverwriteId -> ChannelPermissionsOpts -> ChannelRequest () -- | Gets all instant invites to a channel. GetChannelInvites :: ChannelId -> ChannelRequest Object -- | Creates an instant invite to a channel. CreateChannelInvite :: ChannelId -> ChannelInviteOpts -> ChannelRequest Invite -- | Deletes a permission override from a channel. DeleteChannelPermission :: ChannelId -> OverwriteId -> ChannelRequest () -- | Sends a typing indicator a channel which lasts 10 seconds. TriggerTypingIndicator :: ChannelId -> ChannelRequest () -- | Gets all pinned messages of a channel. GetPinnedMessages :: ChannelId -> ChannelRequest [Message] -- | Pins a message. AddPinnedMessage :: (ChannelId, MessageId) -> ChannelRequest () -- | Unpins a message. DeletePinnedMessage :: (ChannelId, MessageId) -> ChannelRequest () -- | Adds a recipient to a Group DM using their access token GroupDMAddRecipient :: ChannelId -> GroupDMAddRecipientOpts -> ChannelRequest () -- | Removes a recipient from a Group DM GroupDMRemoveRecipient :: ChannelId -> UserId -> ChannelRequest () -- | Start a thread from a message StartThreadFromMessage :: ChannelId -> MessageId -> StartThreadOpts -> ChannelRequest Channel -- | Start a thread without a message StartThreadNoMessage :: ChannelId -> StartThreadNoMessageOpts -> ChannelRequest Channel -- | Join a thread JoinThread :: ChannelId -> ChannelRequest () -- | Add a thread member AddThreadMember :: ChannelId -> UserId -> ChannelRequest () -- | Leave a thread LeaveThread :: ChannelId -> ChannelRequest () -- | Remove a thread member RemoveThreadMember :: ChannelId -> UserId -> ChannelRequest () -- | Get a thread member GetThreadMember :: ChannelId -> UserId -> ChannelRequest ThreadMember -- | List the thread members ListThreadMembers :: ChannelId -> ChannelRequest [ThreadMember] -- | List public archived threads in the given channel. Optionally before a -- given time, and optional maximum number of threads. Returns the threads, -- thread members, and whether there are more to collect. -- Requires the READ_MESSAGE_HISTORY permission. ListPublicArchivedThreads :: ChannelId -> (Maybe UTCTime, Maybe Integer) -> ChannelRequest ListThreads -- | List private archived threads in the given channel. Optionally before a -- given time, and optional maximum number of threads. Returns the threads, -- thread members, and whether there are more to collect. -- Requires both the READ_MESSAGE_HISTORY and MANAGE_THREADS permissions. ListPrivateArchivedThreads :: ChannelId -> (Maybe UTCTime, Maybe Integer) -> ChannelRequest ListThreads -- | List joined private archived threads in the given channel. Optionally -- before a given time, and optional maximum number of threads. Returns the -- threads, thread members, and whether there are more to collect. -- Requires both the READ_MESSAGE_HISTORY and MANAGE_THREADS permissions. ListJoinedPrivateArchivedThreads :: ChannelId -> (Maybe UTCTime, Maybe Integer) -> ChannelRequest ListThreads -- | Data constructor for CreateMessageDetailed requests. data MessageDetailedOpts = MessageDetailedOpts { messageDetailedContent :: T.Text , messageDetailedTTS :: Bool , messageDetailedEmbeds :: Maybe [CreateEmbed] , messageDetailedFile :: Maybe (T.Text, B.ByteString) , messageDetailedAllowedMentions :: Maybe AllowedMentions , messageDetailedReference :: Maybe MessageReference , messageDetailedComponents :: Maybe [ComponentActionRow] , messageDetailedStickerIds :: Maybe [StickerId] } deriving (Show, Read, Eq, Ord) instance Default MessageDetailedOpts where def = MessageDetailedOpts { messageDetailedContent = "" , messageDetailedTTS = False , messageDetailedEmbeds = Nothing , messageDetailedFile = Nothing , messageDetailedAllowedMentions = Nothing , messageDetailedReference = Nothing , messageDetailedComponents = Nothing , messageDetailedStickerIds = Nothing } -- | Data constructor for GetReaction requests data ReactionTiming = BeforeReaction MessageId | AfterReaction MessageId | LatestReaction deriving (Show, Read, Eq, Ord) reactionTimingToQuery :: ReactionTiming -> R.Option 'R.Https reactionTimingToQuery t = case t of (BeforeReaction snow) -> "before" R.=: show snow (AfterReaction snow) -> "after" R.=: show snow (LatestReaction) -> mempty -- | Data constructor for GetChannelMessages requests. See data MessageTiming = AroundMessage MessageId | BeforeMessage MessageId | AfterMessage MessageId | LatestMessages deriving (Show, Read, Eq, Ord) messageTimingToQuery :: MessageTiming -> R.Option 'R.Https messageTimingToQuery t = case t of (AroundMessage snow) -> "around" R.=: show snow (BeforeMessage snow) -> "before" R.=: show snow (AfterMessage snow) -> "after" R.=: show snow (LatestMessages) -> mempty data ChannelInviteOpts = ChannelInviteOpts { channelInviteOptsMaxAgeSeconds :: Maybe Integer , channelInviteOptsMaxUsages :: Maybe Integer , channelInviteOptsIsTemporary :: Maybe Bool , channelInviteOptsDontReuseSimilarInvite :: Maybe Bool } deriving (Show, Read, Eq, Ord) instance ToJSON ChannelInviteOpts where toJSON ChannelInviteOpts{..} = object [(name, val) | (name, Just val) <- [("max_age", toJSON <$> channelInviteOptsMaxAgeSeconds), ("max_uses", toJSON <$> channelInviteOptsMaxUsages), ("temporary", toJSON <$> channelInviteOptsIsTemporary), ("unique", toJSON <$> channelInviteOptsDontReuseSimilarInvite) ] ] data ModifyChannelOpts = ModifyChannelOpts { modifyChannelName :: Maybe T.Text , modifyChannelPosition :: Maybe Integer , modifyChannelTopic :: Maybe T.Text , modifyChannelNSFW :: Maybe Bool , modifyChannelBitrate :: Maybe Integer , modifyChannelUserRateLimit :: Maybe Integer , modifyChannelPermissionOverwrites :: Maybe [Overwrite] , modifyChannelParentId :: Maybe ChannelId , modifyChannelDefaultAutoArchive :: Maybe Integer , modifyChannelThreadArchived :: Maybe Bool , modifyChannelThreadAutoArchive :: Maybe Integer , modifyChannelThreadLocked :: Maybe Bool , modifyChannelThreadInvitiable :: Maybe Bool } deriving (Show, Read, Eq, Ord) instance Default ModifyChannelOpts where def = ModifyChannelOpts Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing instance ToJSON ModifyChannelOpts where toJSON ModifyChannelOpts{..} = object [(name, val) | (name, Just val) <- [("name", toJSON <$> modifyChannelName), ("position", toJSON <$> modifyChannelPosition), ("topic", toJSON <$> modifyChannelTopic), ("nsfw", toJSON <$> modifyChannelNSFW), ("bitrate", toJSON <$> modifyChannelBitrate), ("user_limit", toJSON <$> modifyChannelUserRateLimit), ("permission_overwrites", toJSON <$> modifyChannelPermissionOverwrites), ("parent_id", toJSON <$> modifyChannelParentId), ("default_auto_archive_duration", toJSON <$> modifyChannelDefaultAutoArchive), ("archived", toJSON <$> modifyChannelThreadArchived), ("auto_archive_duration", toJSON <$> modifyChannelThreadAutoArchive), ("locked", toJSON <$> modifyChannelThreadLocked), ("invitable", toJSON <$> modifyChannelThreadInvitiable) ] ] data ChannelPermissionsOpts = ChannelPermissionsOpts { channelPermissionsOptsAllow :: Integer , channelPermissionsOptsDeny :: Integer , channelPermissionsOptsType :: ChannelPermissionsOptsType } deriving (Show, Read, Eq, Ord) data ChannelPermissionsOptsType = ChannelPermissionsOptsUser | ChannelPermissionsOptsRole deriving (Show, Read, Eq, Ord) instance ToJSON ChannelPermissionsOptsType where toJSON t = case t of ChannelPermissionsOptsUser -> String "member" ChannelPermissionsOptsRole -> String "role" instance ToJSON ChannelPermissionsOpts where toJSON (ChannelPermissionsOpts a d t) = object [ ("allow", toJSON a ) , ("deny", toJSON d) , ("type", toJSON t)] -- | https://discord.com/developers/docs/resources/channel#group-dm-add-recipient data GroupDMAddRecipientOpts = GroupDMAddRecipientOpts { groupDMAddRecipientUserToAdd :: UserId , groupDMAddRecipientUserToAddNickName :: T.Text , groupDMAddRecipientGDMJoinAccessToken :: T.Text } deriving (Show, Read, Eq, Ord) data StartThreadOpts = StartThreadOpts { startThreadName :: T.Text , startThreadAutoArchive :: Maybe Integer -- ^ can be one of 60, 1440, 4320, 10080 , startThreadRateLimit :: Maybe Integer } deriving (Show, Read, Eq, Ord) instance ToJSON StartThreadOpts where toJSON StartThreadOpts{..} = object [ (name, value) | (name, Just value) <- [ ("name", toJSON <$> pure startThreadName) , ("auto_archive_duration", toJSON <$> startThreadAutoArchive) , ("rate_limit_per_user", toJSON <$> startThreadRateLimit) ] ] data StartThreadNoMessageOpts = StartThreadNoMessageOpts { startThreadNoMessageBaseOpts :: StartThreadOpts , startThreadNoMessageType :: Integer -- ^ 10, 11, or 12 (https://discord.com/developers/docs/resources/channel#channel-object-channel-types) , startThreadNoMessageInvitable :: Maybe Bool } deriving (Show, Read, Eq, Ord) instance ToJSON StartThreadNoMessageOpts where toJSON StartThreadNoMessageOpts{..} = object [ (name, value) | (name, Just value) <- [ ("name", toJSON <$> pure (startThreadName startThreadNoMessageBaseOpts)) , ("auto_archive_duration", toJSON <$> (startThreadAutoArchive startThreadNoMessageBaseOpts)) , ("rate_limit_per_user", toJSON <$> (startThreadRateLimit startThreadNoMessageBaseOpts)) , ("type", toJSON <$> pure startThreadNoMessageType) , ("invitable", toJSON <$> startThreadNoMessageInvitable) ] ] data ListThreads = ListThreads { listThreadsThreads :: [Channel] , listThreadsMembers :: [ThreadMember] , listThreadsHasMore :: Bool -- ^ whether there is more data to retrieve } deriving (Show, Read, Eq, Ord) instance ToJSON ListThreads where toJSON ListThreads{..} = object [ ("threads", toJSON listThreadsThreads) , ("members", toJSON listThreadsMembers) , ("has_more", toJSON listThreadsHasMore) ] instance FromJSON ListThreads where parseJSON = withObject "ListThreads" $ \o -> ListThreads <$> o .: "threads" <*> o .: "members" <*> o .: "has_more" channelMajorRoute :: ChannelRequest a -> String channelMajorRoute c = case c of (GetChannel chan) -> "get_chan " <> show chan (ModifyChannel chan _) -> "mod_chan " <> show chan (DeleteChannel chan) -> "mod_chan " <> show chan (GetChannelMessages chan _) -> "msg " <> show chan (GetChannelMessage (chan, _)) -> "get_msg " <> show chan (CreateMessage chan _) -> "msg " <> show chan (CreateMessageDetailed chan _) -> "msg " <> show chan (CreateReaction (chan, _) _) -> "add_react " <> show chan (DeleteOwnReaction (chan, _) _) -> "react " <> show chan (DeleteUserReaction (chan, _) _ _) -> "react " <> show chan (DeleteSingleReaction (chan, _) _) -> "react " <> show chan (GetReactions (chan, _) _ _) -> "react " <> show chan (DeleteAllReactions (chan, _)) -> "react " <> show chan (EditMessage (chan, _) _) -> "get_msg " <> show chan (DeleteMessage (chan, _)) -> "get_msg " <> show chan (BulkDeleteMessage (chan, _)) -> "del_msgs " <> show chan (EditChannelPermissions chan _ _) -> "perms " <> show chan (GetChannelInvites chan) -> "invites " <> show chan (CreateChannelInvite chan _) -> "invites " <> show chan (DeleteChannelPermission chan _) -> "perms " <> show chan (TriggerTypingIndicator chan) -> "tti " <> show chan (GetPinnedMessages chan) -> "pins " <> show chan (AddPinnedMessage (chan, _)) -> "pin " <> show chan (DeletePinnedMessage (chan, _)) -> "pin " <> show chan (GroupDMAddRecipient chan _) -> "groupdm " <> show chan (GroupDMRemoveRecipient chan _) -> "groupdm " <> show chan (StartThreadFromMessage chan _ _) -> "thread " <> show chan (StartThreadNoMessage chan _) -> "thread " <> show chan (JoinThread chan) -> "thread " <> show chan (AddThreadMember chan _) -> "thread " <> show chan (LeaveThread chan) -> "thread " <> show chan (RemoveThreadMember chan _) -> "thread " <> show chan (GetThreadMember chan _) -> "thread " <> show chan (ListThreadMembers chan) -> "thread " <> show chan (ListPublicArchivedThreads chan _) -> "thread " <> show chan (ListPrivateArchivedThreads chan _) -> "thread " <> show chan (ListJoinedPrivateArchivedThreads chan _) -> "thread " <> show chan cleanupEmoji :: T.Text -> T.Text cleanupEmoji emoji = let noAngles = T.replace "<" "" (T.replace ">" "" emoji) byName = T.pack <$> unicodeByName (T.unpack (T.replace ":" "" emoji)) in case (byName, T.stripPrefix ":" noAngles) of (Just e, _) -> e (_, Just a) -> "custom:" <> a (_, Nothing) -> noAngles channels :: R.Url 'R.Https channels = baseUrl /: "channels" channelJsonRequest :: ChannelRequest r -> JsonRequest channelJsonRequest c = case c of (GetChannel chan) -> Get (channels // chan) mempty (ModifyChannel chan patch) -> Patch (channels // chan) (pure (R.ReqBodyJson patch)) mempty (DeleteChannel chan) -> Delete (channels // chan) mempty (GetChannelMessages chan (n,timing)) -> let n' = max 1 (min 100 n) options = "limit" R.=: n' <> messageTimingToQuery timing in Get (channels // chan /: "messages") options (GetChannelMessage (chan, msg)) -> Get (channels // chan /: "messages" // msg) mempty (CreateMessage chan msg) -> let content = ["content" .= msg] body = pure $ R.ReqBodyJson $ object content in Post (channels // chan /: "messages") body mempty (CreateMessageDetailed chan msgOpts) -> let fileUpload = messageDetailedFile msgOpts filePart = ( case fileUpload of Nothing -> [] Just f -> [ partFileRequestBody "file" (T.unpack $ fst f) (RequestBodyBS $ snd f) ] ) ++ join (maybe [] (maybeEmbed . Just <$>) (messageDetailedEmbeds msgOpts)) payloadData = object $ [ "content" .= messageDetailedContent msgOpts , "tts" .= messageDetailedTTS msgOpts ] ++ [ name .= value | (name, Just value) <- [ ("embeds", toJSON . (createEmbed <$>) <$> messageDetailedEmbeds msgOpts) , ("allowed_mentions", toJSON <$> messageDetailedAllowedMentions msgOpts) , ("message_reference", toJSON <$> messageDetailedReference msgOpts) , ("components", toJSON <$> messageDetailedComponents msgOpts) , ("sticker_ids", toJSON <$> messageDetailedStickerIds msgOpts) ] ] payloadPart = partBS "payload_json" $ BL.toStrict $ encode payloadData body = R.reqBodyMultipart (payloadPart : filePart) in Post (channels // chan /: "messages") body mempty (CreateReaction (chan, msgid) emoji) -> let e = cleanupEmoji emoji in Put (channels // chan /: "messages" // msgid /: "reactions" /: e /: "@me" ) R.NoReqBody mempty (DeleteOwnReaction (chan, msgid) emoji) -> let e = cleanupEmoji emoji in Delete (channels // chan /: "messages" // msgid /: "reactions" /: e /: "@me" ) mempty (DeleteUserReaction (chan, msgid) uID emoji) -> let e = cleanupEmoji emoji in Delete (channels // chan /: "messages" // msgid /: "reactions" /: e // uID ) mempty (DeleteSingleReaction (chan, msgid) emoji) -> let e = cleanupEmoji emoji in Delete (channels // chan /: "messages" // msgid /: "reactions" /: e) mempty (GetReactions (chan, msgid) emoji (n, timing)) -> let e = cleanupEmoji emoji n' = max 1 (min 100 n) options = "limit" R.=: n' <> reactionTimingToQuery timing in Get (channels // chan /: "messages" // msgid /: "reactions" /: e) options (DeleteAllReactions (chan, msgid)) -> Delete (channels // chan /: "messages" // msgid /: "reactions" ) mempty -- copied from CreateMessageDetailed, should be outsourced to function probably (EditMessage (chan, msg) msgOpts) -> let fileUpload = messageDetailedFile msgOpts filePart = ( case fileUpload of Nothing -> [] Just f -> [ partFileRequestBody "file" (T.unpack $ fst f) (RequestBodyBS $ snd f) ] ) ++ join (maybe [] (maybeEmbed . Just <$>) (messageDetailedEmbeds msgOpts)) payloadData = object $ [ "content" .= messageDetailedContent msgOpts , "tts" .= messageDetailedTTS msgOpts ] ++ [ name .= value | (name, Just value) <- [ ("embeds", toJSON . (createEmbed <$>) <$> messageDetailedEmbeds msgOpts) , ("allowed_mentions", toJSON <$> messageDetailedAllowedMentions msgOpts) , ("message_reference", toJSON <$> messageDetailedReference msgOpts) , ("components", toJSON <$> messageDetailedComponents msgOpts) , ("sticker_ids", toJSON <$> messageDetailedStickerIds msgOpts) ] ] payloadPart = partBS "payload_json" $ BL.toStrict $ encode payloadData body = R.reqBodyMultipart (payloadPart : filePart) in Patch (channels // chan /: "messages" // msg) body mempty (DeleteMessage (chan, msg)) -> Delete (channels // chan /: "messages" // msg) mempty (BulkDeleteMessage (chan, msgs)) -> let body = pure . R.ReqBodyJson $ object ["messages" .= msgs] in Post (channels // chan /: "messages" /: "bulk-delete") body mempty (EditChannelPermissions chan perm patch) -> Put (channels // chan /: "permissions" // perm) (R.ReqBodyJson patch) mempty (GetChannelInvites chan) -> Get (channels // chan /: "invites") mempty (CreateChannelInvite chan patch) -> Post (channels // chan /: "invites") (pure (R.ReqBodyJson patch)) mempty (DeleteChannelPermission chan perm) -> Delete (channels // chan /: "permissions" // perm) mempty (TriggerTypingIndicator chan) -> Post (channels // chan /: "typing") (pure R.NoReqBody) mempty (GetPinnedMessages chan) -> Get (channels // chan /: "pins") mempty (AddPinnedMessage (chan, msg)) -> Put (channels // chan /: "pins" // msg) R.NoReqBody mempty (DeletePinnedMessage (chan, msg)) -> Delete (channels // chan /: "pins" // msg) mempty (GroupDMAddRecipient chan (GroupDMAddRecipientOpts uid nick tok)) -> Put (channels // chan // chan /: "recipients" // uid) (R.ReqBodyJson (object [ ("access_token", toJSON tok) , ("nick", toJSON nick)])) mempty (GroupDMRemoveRecipient chan userid) -> Delete (channels // chan // chan /: "recipients" // userid) mempty (StartThreadFromMessage chan mid sto) -> Post (channels // chan /: "messages" // mid /: "threads") (pure $ R.ReqBodyJson $ toJSON sto) mempty (StartThreadNoMessage chan sto) -> Post (channels // chan /: "messages" /: "threads") (pure $ R.ReqBodyJson $ toJSON sto) mempty (JoinThread chan) -> Put (channels // chan /: "thread-members" /: "@me") R.NoReqBody mempty (AddThreadMember chan uid) -> Put (channels // chan /: "thread-members" // uid) R.NoReqBody mempty (LeaveThread chan) -> Delete (channels // chan /: "thread-members" /: "@me") mempty (RemoveThreadMember chan uid) -> Delete (channels // chan /: "thread-members" // uid) mempty (GetThreadMember chan uid) -> Get (channels // chan /: "thread-members" // uid) mempty (ListThreadMembers chan) -> Get (channels // chan /: "thread-members") mempty (ListPublicArchivedThreads chan (time, lim)) -> Get (channels // chan /: "threads" /: "archived" /: "public") (maybe mempty ("limit" R.=:) lim <> maybe mempty ("before" R.=:) time) (ListPrivateArchivedThreads chan (time, lim)) -> Get (channels // chan /: "threads" /: "archived" /: "private") (maybe mempty ("limit" R.=:) lim <> maybe mempty ("before" R.=:) time) (ListJoinedPrivateArchivedThreads chan (time, lim)) -> Get (channels // chan /: "users" /: "@me" /: "threads" /: "archived" /: "private") (maybe mempty ("limit" R.=:) lim <> maybe mempty ("before" R.=:) time)