packages feed

discord-haskell 1.6.1 → 1.7.0

raw patch · 17 files changed

+108/−104 lines, 17 filesdep +mtldep +unliftioPVP ok

version bump matches the API change (PVP)

Dependencies added: mtl, unliftio

API changes (from Hackage documentation)

+ Discord: type DiscordHandler = ReaderT DiscordHandle IO
+ Discord.Internal.Types.Channel: [parentId] :: Channel -> ParentId
+ Discord.Internal.Types.Prelude: type ParentId = Snowflake
- Discord: RunDiscordOpts :: Text -> (DiscordHandle -> IO ()) -> IO () -> (DiscordHandle -> Event -> IO ()) -> (Text -> IO ()) -> Bool -> RunDiscordOpts
+ Discord: RunDiscordOpts :: Text -> DiscordHandler () -> IO () -> (Event -> DiscordHandler ()) -> (Text -> IO ()) -> Bool -> RunDiscordOpts
- Discord: [discordOnEvent] :: RunDiscordOpts -> DiscordHandle -> Event -> IO ()
+ Discord: [discordOnEvent] :: RunDiscordOpts -> Event -> DiscordHandler ()
- Discord: [discordOnStart] :: RunDiscordOpts -> DiscordHandle -> IO ()
+ Discord: [discordOnStart] :: RunDiscordOpts -> DiscordHandler ()
- Discord: readCache :: DiscordHandle -> IO Cache
+ Discord: readCache :: DiscordHandler Cache
- Discord: restCall :: (FromJSON a, Request (r a)) => DiscordHandle -> r a -> IO (Either RestCallErrorCode a)
+ Discord: restCall :: (FromJSON a, Request (r a)) => r a -> DiscordHandler (Either RestCallErrorCode a)
- Discord: sendCommand :: DiscordHandle -> GatewaySendable -> IO ()
+ Discord: sendCommand :: GatewaySendable -> DiscordHandler ()
- Discord: stopDiscord :: DiscordHandle -> IO ()
+ Discord: stopDiscord :: DiscordHandler ()
- Discord.Internal.Types.Channel: ChannelGuildCategory :: ChannelId -> GuildId -> Channel
+ Discord.Internal.Types.Channel: ChannelGuildCategory :: ChannelId -> Text -> GuildId -> Channel
- Discord.Internal.Types.Channel: ChannelText :: ChannelId -> GuildId -> Text -> Integer -> [Overwrite] -> Integer -> Bool -> Text -> Maybe MessageId -> Channel
+ Discord.Internal.Types.Channel: ChannelText :: ChannelId -> GuildId -> Text -> Integer -> [Overwrite] -> Integer -> Bool -> Text -> Maybe MessageId -> ParentId -> Channel

Files

README.md view
@@ -7,9 +7,10 @@  import Control.Monad (when) import Data.Text (isPrefixOf, toLower, Text)-import Control.Concurrent (threadDelay) import qualified Data.Text.IO as TIO +import UnliftIO+ import Discord import Discord.Types import qualified Discord.Requests as R@@ -21,12 +22,12 @@                                             , discordOnEvent = eventHandler }                      TIO.putStrLn userFacingError -eventHandler :: DiscordHandle -> Event -> IO ()-eventHandler dis event = case event of+eventHandler :: Event -> DiscordHandler ()+eventHandler event = case event of        MessageCreate m -> when (not (fromBot m) && isPing (messageText m)) $ do-               _ <- restCall dis (R.CreateReaction (messageChannel m, messageId m) "eyes")+               _ <- restCall (R.CreateReaction (messageChannel m, messageId m) "eyes")                threadDelay (4 * 10^6)-               _ <- restCall dis (R.CreateMessage (messageChannel m) "Pong!")+               _ <- restCall (R.CreateMessage (messageChannel m) "Pong!")                pure ()        _ -> pure () @@ -94,7 +95,7 @@ `CreateEmbed` has a `Default` instance, so you only need to specify the fields you use:  ```haskell-_ <- restCall dis (R.CreateMessageEmbed <channel_id> "Pong!" $+_ <- restCall (R.CreateMessageEmbed <channel_id> "Pong!" $         def { createEmbedTitle = "Pong Embed"             , createEmbedImage = Just $ CreateEmbedImageUpload <bytestring>             , createEmbedThumbnail = Just $ CreateEmbedImageUrl
discord-haskell.cabal view
@@ -1,7 +1,7 @@ cabal-version:       2.0 name:                discord-haskell -- library version is also noted at src/Discord/Rest/Prelude.hs-version:             1.6.1+version:             1.7.0 description:         Functions and data types to write discord bots.                      Official discord docs <https://discord.com/developers/docs/reference>.                      .@@ -32,6 +32,7 @@                        -threaded   build-depends:       base                      , text+                     , unliftio                      , discord-haskell  library@@ -86,4 +87,5 @@                      , vector                      , websockets                      , wuss-+                     , mtl+                     , unliftio
examples/ping-pong.hs view
@@ -1,11 +1,12 @@ {-# LANGUAGE OverloadedStrings #-}  -- allows "strings" to be Data.Text  import Control.Monad (when, forM_)-import Control.Concurrent (threadDelay)-import Data.Char (toLower) import qualified Data.Text as T import qualified Data.Text.IO as TIO +import UnliftIO (liftIO)+import UnliftIO.Concurrent+ import Discord import Discord.Types import qualified Discord.Requests as R@@ -22,34 +23,34 @@   -- open ghci and run  [[ :info RunDiscordOpts ]] to see available fields   t <- runDiscord $ def { discordToken = tok                         , discordOnStart = startHandler-                        , discordOnEnd = putStrLn "Ended"+                        , discordOnEnd = liftIO $ putStrLn "Ended"                         , discordOnEvent = eventHandler                         , discordOnLog = \s -> TIO.putStrLn s >> TIO.putStrLn ""                         }-  threadDelay (1 `div` 10 * 10^6)+  threadDelay (1 `div` 10 * 10^(6 :: Int))   TIO.putStrLn t  -- If the start handler throws an exception, discord-haskell will gracefully shutdown --     Use place to execute commands you know you want to complete-startHandler :: DiscordHandle -> IO ()-startHandler dis = do-  Right partialGuilds <- restCall dis R.GetCurrentUserGuilds+startHandler :: DiscordHandler ()+startHandler = do+  Right partialGuilds <- restCall R.GetCurrentUserGuilds    forM_ partialGuilds $ \pg -> do-    Right guild <- restCall dis $ R.GetGuild (partialGuildId pg)-    Right chans <- restCall dis $ R.GetGuildChannels (guildId guild)+    Right guild <- restCall $ R.GetGuild (partialGuildId pg)+    Right chans <- restCall $ R.GetGuildChannels (guildId guild)     case filter isTextChannel chans of-      (c:_) -> do _ <- restCall dis $ R.CreateMessage (channelId c) "Hello! I will reply to pings with pongs"+      (c:_) -> do _ <- restCall $ R.CreateMessage (channelId c) "Hello! I will reply to pings with pongs"                   pure ()       _ -> pure ()  -- If an event handler throws an exception, discord-haskell will continue to run-eventHandler :: DiscordHandle -> Event -> IO ()-eventHandler dis event = case event of+eventHandler :: Event -> DiscordHandler ()+eventHandler event = case event of       MessageCreate m -> when (not (fromBot m) && isPing m) $ do-        _ <- restCall dis (R.CreateReaction (messageChannel m, messageId m) "eyes")-        threadDelay (4 * 10^6)-        _ <- restCall dis (R.CreateMessage (messageChannel m) "Pong!")+        _ <- restCall (R.CreateReaction (messageChannel m, messageId m) "eyes")+        threadDelay (4 * 10^(6 :: Int))+        _ <- restCall (R.CreateMessage (messageChannel m) "Pong!")         pure ()       _ -> pure () @@ -61,4 +62,4 @@ fromBot m = userIsBot (messageAuthor m)  isPing :: Message -> Bool-isPing = ("ping" `T.isPrefixOf`) . T.map toLower . messageText+isPing = ("ping" `T.isPrefixOf`) . T.toLower . messageText
src/Discord.hs view
@@ -9,6 +9,8 @@   , readCache   , stopDiscord +  , DiscordHandler+   , DiscordHandle   , Cache(..)   , RestCallErrorCode(..)@@ -18,37 +20,36 @@   ) where  import Prelude hiding (log)-import Control.Monad (forever, void)-import Control.Concurrent (forkIO, threadDelay, ThreadId, killThread)-import Control.Concurrent.Async (race)-import Control.Exception.Safe (try, finally, IOException, SomeException)-import Control.Concurrent.Chan-import Control.Concurrent.MVar+import Control.Monad.Reader import Data.Aeson (FromJSON) import Data.Default (Default, def) import qualified Data.Text as T import qualified Data.Text.Encoding as TE +import UnliftIO (race, try, finally, SomeException, IOException)+import UnliftIO.Concurrent+ import Discord.Handle import Discord.Internal.Rest import Discord.Internal.Rest.User (UserRequest(GetCurrentUser)) import Discord.Internal.Gateway +type DiscordHandler = ReaderT DiscordHandle IO  data RunDiscordOpts = RunDiscordOpts   { discordToken :: T.Text-  , discordOnStart :: DiscordHandle -> IO ()+  , discordOnStart :: DiscordHandler ()   , discordOnEnd :: IO ()-  , discordOnEvent :: DiscordHandle -> Event -> IO ()+  , discordOnEvent :: Event -> DiscordHandler ()   , discordOnLog :: T.Text -> IO ()   , discordForkThreadForEvents :: Bool   }  instance Default RunDiscordOpts where   def = RunDiscordOpts { discordToken = ""-                       , discordOnStart = \_ -> pure ()+                       , discordOnStart = pure ()                        , discordOnEnd = pure ()-                       , discordOnEvent = \_ _-> pure ()+                       , discordOnEvent = \_ -> pure ()                        , discordOnLog = \_ -> pure ()                        , discordForkThreadForEvents = True                        }@@ -56,10 +57,10 @@ runDiscord :: RunDiscordOpts -> IO T.Text runDiscord opts = do   log <- newChan-  logId <- startLogger (discordOnLog opts) log-  (cache, cacheId) <- startCacheThread log-  (rest, restId) <- startRestThread (Auth (discordToken opts)) log-  (gate, gateId) <- startGatewayThread (Auth (discordToken opts)) cache log+  logId <- liftIO $ startLogger (discordOnLog opts) log+  (cache, cacheId) <- liftIO $ startCacheThread log+  (rest, restId) <- liftIO $ startRestThread (Auth (discordToken opts)) log+  (gate, gateId) <- liftIO $ startGatewayThread (Auth (discordToken opts)) cache log    libE <- newEmptyMVar @@ -76,19 +77,19 @@                                  ]                              } -  finally (runDiscordLoop opts handle)-          (discordOnEnd opts >> stopDiscord handle)+  finally (runDiscordLoop handle opts)+          (discordOnEnd opts >> runReaderT stopDiscord handle) -runDiscordLoop :: RunDiscordOpts -> DiscordHandle -> IO T.Text-runDiscordLoop opts handle = do-  resp <- writeRestCall (discordHandleRestChan handle) GetCurrentUser+runDiscordLoop :: DiscordHandle -> RunDiscordOpts -> IO T.Text+runDiscordLoop handle opts = do+  resp <- liftIO $ writeRestCall (discordHandleRestChan handle) GetCurrentUser   case resp of     Left (RestCallInternalErrorCode c e1 e2) -> libError $              "HTTP Error Code " <> T.pack (show c) <> " " <> TE.decodeUtf8 e1                                                    <> " " <> TE.decodeUtf8 e2     Left (RestCallInternalHttpException e) -> libError ("HTTP Exception -  " <> T.pack (show e))     Left (RestCallInternalNoParse _ _) -> libError "Couldn't parse GetCurrentUser"-    _ -> do me <- try (discordOnStart opts handle)+    _ -> do me <- liftIO . runReaderT (try $ discordOnStart opts) $ handle             case me of               Left (e :: SomeException) -> libError ("discordOnStart handler stopped on an exception:\n\n" <> T.pack (show e))               Right _ -> loop@@ -104,7 +105,7 @@                Right (Right event) -> do                  let action = if discordForkThreadForEvents opts then void . forkIO                                                                  else id-                 action $ do me <- try (discordOnEvent opts handle event)+                 action $ do me <- liftIO . runReaderT (try $ discordOnEvent opts event) $ handle                              case me of                                Left (e :: SomeException) -> writeChan (discordHandleLog handle)                                          ("discord-haskell stopped on an exception:\n\n" <> T.pack (show e))@@ -117,44 +118,50 @@   deriving (Show, Eq, Ord)  -- | Execute one http request and get a response-restCall :: (FromJSON a, Request (r a)) => DiscordHandle -> r a -> IO (Either RestCallErrorCode a)-restCall h r = do empty <- isEmptyMVar (discordHandleLibraryError h)-                  if not empty-                  then pure (Left (RestCallErrorCode 400 "Library Stopped Working" ""))-                  else do-                      resp <- writeRestCall (discordHandleRestChan h) r-                      case resp of-                        Right x -> pure (Right x)-                        Left (RestCallInternalErrorCode c e1 e2) ->-                          pure (Left (RestCallErrorCode c (TE.decodeUtf8 e1) (TE.decodeUtf8 e2)))-                        Left (RestCallInternalHttpException _) ->-                          threadDelay (10 * 10^6) >> restCall h r-                        Left (RestCallInternalNoParse err dat) -> do-                          let formaterr = T.pack ("Parse Exception " <> err <> " for " <> show dat)-                          writeChan (discordHandleLog h) formaterr-                          pure (Left (RestCallErrorCode 400 "Library Stopped Working" formaterr))+restCall :: (FromJSON a, Request (r a)) => r a -> DiscordHandler (Either RestCallErrorCode a)+restCall r = do h <- ask+                empty <- isEmptyMVar (discordHandleLibraryError h)+                if not empty+                then pure (Left (RestCallErrorCode 400 "Library Stopped Working" ""))+                else do+                    resp <- liftIO $ writeRestCall (discordHandleRestChan h) r+                    case resp of+                      Right x -> pure (Right x)+                      Left (RestCallInternalErrorCode c e1 e2) ->+                        pure (Left (RestCallErrorCode c (TE.decodeUtf8 e1) (TE.decodeUtf8 e2)))+                      Left (RestCallInternalHttpException _) ->+                        threadDelay (10 * 10^(6 :: Int)) >> restCall r+                      Left (RestCallInternalNoParse err dat) -> do+                        let formaterr = T.pack ("Parse Exception " <> err <> " for " <> show dat)+                        writeChan (discordHandleLog h) formaterr+                        pure (Left (RestCallErrorCode 400 "Library Stopped Working" formaterr))  -- | Send a GatewaySendable, but not Heartbeat, Identify, or Resume-sendCommand :: DiscordHandle -> GatewaySendable -> IO ()-sendCommand h e = case e of-                    Heartbeat _ -> pure ()-                    Identify {} -> pure ()-                    Resume {} -> pure ()-                    _ -> writeChan (snd (discordHandleGateway h)) e+sendCommand :: GatewaySendable -> DiscordHandler ()+sendCommand e = do+  h <- ask+  case e of+    Heartbeat _ -> pure ()+    Identify {} -> pure ()+    Resume {} -> pure ()+    _ -> writeChan (snd (discordHandleGateway h)) e  -- | Access the current state of the gateway cache-readCache :: DiscordHandle -> IO Cache-readCache h = do merr <- readMVar (snd (discordHandleCache h))-                 case merr of-                   Left (c, _) -> pure c-                   Right c -> pure c+readCache :: DiscordHandler Cache+readCache = do+  h <- ask+  merr <- readMVar (snd (discordHandleCache h))+  case merr of+    Left (c, _) -> pure c+    Right c -> pure c   -- | Stop all the background threads-stopDiscord :: DiscordHandle -> IO ()-stopDiscord h = do _ <- tryPutMVar (discordHandleLibraryError h) "Library has closed"-                   threadDelay (10^6 `div` 10)-                   mapM_ (killThread . toId) (discordHandleThreads h)+stopDiscord :: DiscordHandler ()+stopDiscord = do h <- ask+                 _ <- tryPutMVar (discordHandleLibraryError h) "Library has closed"+                 threadDelay (10^(6 :: Int) `div` 10)+                 mapM_ (killThread . toId) (discordHandleThreads h)   where toId t = case t of                    DiscordHandleThreadIdRest a -> a                    DiscordHandleThreadIdGateway a -> a
src/Discord/Internal/Gateway/Cache.hs view
@@ -4,7 +4,6 @@ module Discord.Internal.Gateway.Cache where  import Prelude hiding (log)-import Data.Monoid ((<>)) import Control.Monad (forever) import Control.Concurrent.MVar import Control.Concurrent.Chan
src/Discord/Internal/Gateway/EventLoop.hs view
@@ -13,7 +13,6 @@ import Control.Concurrent.Chan import Control.Concurrent (threadDelay, killThread, forkIO) import Control.Exception.Safe (try, finally, handle, SomeException)-import Data.Monoid ((<>)) import Data.IORef import Data.Aeson (eitherDecode, encode) import qualified Data.Text as T@@ -90,7 +89,7 @@                       startEventStream (ConnData conn seshID auth events) interval seqID userSend log                   Right (InvalidSession retry) -> do                       t <- getRandomR (1,5)-                      threadDelay (t * 10^6)+                      threadDelay (t * (10^(6 :: Int)))                       pure $ if retry                              then ConnReconnect (Auth tok) seshID seqID                              else ConnStart@@ -104,7 +103,7 @@                       pure ConnClosed           case next :: Either SomeException ConnLoopState of             Left _ -> do t <- getRandomR (3,20)-                         threadDelay (t * 10^6)+                         threadDelay (t * (10^(6 :: Int)))                          writeChan log ("gateway - trying to reconnect after " <> T.pack (show retries)                                                <> " failures")                          loop (ConnReconnect (Auth tok) seshID seqID) (retries + 1)@@ -130,7 +129,7 @@  heartbeat :: Chan GatewaySendable -> Int -> IORef Integer -> IO () heartbeat send interval seqKey = do-  threadDelay (1 * 10^6)+  threadDelay (1 * 10^(6 :: Int))   forever $ do     num <- readIORef seqKey     writeChan send (Heartbeat num)@@ -208,7 +207,7 @@ sendableLoop :: Connection -> Sendables -> IO () sendableLoop conn sends = forever $ do   -- send a ~120 events a min by delaying-  threadDelay (round (10^6 * (62 / 120)))+  threadDelay $ round ((10^(6 :: Int)) * (62 / 120) :: Double)   let e :: Either GatewaySendable GatewaySendable -> GatewaySendable       e = either id id   payload <- e <$> race (readChan (userSends sends)) (readChan (gatewaySends sends))
src/Discord/Internal/Rest/Channel.hs view
@@ -19,7 +19,6 @@  import Data.Aeson import Data.Emoji (unicodeByName)-import Data.Monoid (mempty, (<>)) import qualified Data.Text as T import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL
src/Discord/Internal/Rest/Emoji.hs view
@@ -13,7 +13,6 @@   ) where  import Data.Aeson-import Data.Monoid (mempty, (<>)) import Codec.Picture import Network.HTTP.Req ((/:)) import qualified Network.HTTP.Req as R
src/Discord/Internal/Rest/Guild.hs view
@@ -21,7 +21,6 @@   import Data.Aeson-import Data.Monoid (mempty, (<>)) import Network.HTTP.Req ((/:)) import qualified Network.HTTP.Req as R import qualified Data.Text as T
src/Discord/Internal/Rest/HTTP.hs view
@@ -11,7 +11,6 @@   ) where  import Prelude hiding (log)-import Data.Semigroup ((<>))  import Control.Monad.IO.Class (liftIO) import Control.Concurrent (threadDelay)@@ -101,8 +100,8 @@   let body   = R.responseBody resp       code   = R.responseStatusCode resp       status = R.responseStatusMessage resp-      global = (Just "true" ==) $ readMaybeBS =<< R.responseHeader resp "X-RateLimit-Global"-      remain = fromMaybe 1 $ readMaybeBS =<< R.responseHeader resp "X-RateLimit-Remaining"+      global = (Just ("true" :: String) ==) $ readMaybeBS =<< R.responseHeader resp "X-RateLimit-Global"+      remain = fromMaybe 1 $ readMaybeBS =<< R.responseHeader resp "X-RateLimit-Remaining" :: Integer       reset = withDelta . fromMaybe 10 $ readMaybeBS =<< R.responseHeader resp "X-RateLimit-Reset-After"        withDelta :: Double -> POSIXTime
src/Discord/Internal/Rest/Invite.hs view
@@ -10,7 +10,6 @@   ( InviteRequest(..)   ) where -import Data.Monoid (mempty) import Network.HTTP.Req ((/:)) import qualified Network.HTTP.Req as R import qualified Data.Text as T
src/Discord/Internal/Rest/Prelude.hs view
@@ -9,7 +9,6 @@ import Prelude hiding (log) import Control.Exception.Safe (throwIO) import Control.Monad.IO.Class (MonadIO, liftIO)-import Data.Monoid ((<>)) import qualified Data.Text as T import qualified Data.Text.Encoding as TE @@ -25,7 +24,7 @@   where   -- | https://discord.com/developers/docs/reference#user-agent   -- Second place where the library version is noted-  agent = "DiscordBot (https://github.com/aquarial/discord-haskell, 1.6.1)"+  agent = "DiscordBot (https://github.com/aquarial/discord-haskell, 1.7.0)"  -- Append to an URL infixl 5 //
src/Discord/Internal/Rest/User.hs view
@@ -14,7 +14,6 @@  import Data.Aeson import Codec.Picture-import Data.Monoid (mempty, (<>)) import Network.HTTP.Req ((/:)) import qualified Network.HTTP.Req as R import qualified Data.Text as T
src/Discord/Internal/Rest/Webhook.hs view
@@ -14,7 +14,6 @@   ) where  import Data.Aeson-import Data.Monoid (mempty, (<>)) import qualified Data.Text as T import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL
src/Discord/Internal/Types/Channel.hs view
@@ -7,7 +7,6 @@ import Control.Applicative (empty) import Data.Aeson import Data.Aeson.Types (Parser)-import Data.Monoid ((<>)) import Data.Text (Text) import Data.Time.Clock import qualified Data.Text as T@@ -31,6 +30,7 @@       , channelTopic       :: T.Text      -- ^ The topic of the channel. (0 - 1024 chars).       , channelLastMessage :: Maybe MessageId   -- ^ The id of the last message sent in the                                                 --   channel+      , parentId           :: ParentId    -- ^ The id of the parent channel (category)       }   | ChannelNews       { channelId          :: ChannelId@@ -75,6 +75,7 @@       }   | ChannelGuildCategory       { channelId          :: ChannelId+      , channelName        :: T.Text       , channelGuild       :: GuildId       } deriving (Show, Eq, Ord) @@ -92,6 +93,7 @@                      <*> o .:? "nsfw" .!= False                      <*> o .:? "topic" .!= ""                      <*> o .:? "last_message_id"+                     <*> o .:  "parent_id"       1 ->         ChannelDirectMessage <$> o .:  "id"                              <*> o .:  "recipients"@@ -111,6 +113,7 @@                        <*> o .:? "last_message_id"       4 ->         ChannelGuildCategory <$> o .: "id"+                             <*> o .:  "name"                              <*> o .:? "guild_id" .!= 0       5 ->         ChannelNews <$> o .:  "id"@@ -141,6 +144,7 @@               , ("permission_overwrites",   toJSON <$> pure channelPermissions)               , ("topic",   toJSON <$> pure channelTopic)               , ("last_message_id",  toJSON <$> channelLastMessage)+              , ("parent_id",  toJSON <$> pure parentId)               ] ]   toJSON ChannelNews{..} = object [(name,value) | (name, Just value) <-               [ ("id",     toJSON <$> pure channelId)@@ -182,6 +186,7 @@               ] ]   toJSON ChannelGuildCategory{..} = object [(name,value) | (name, Just value) <-               [ ("id",     toJSON <$> pure channelId)+              , ("name", toJSON <$> pure channelName)               , ("guild_id", toJSON <$> pure channelGuild)               ] ] 
src/Discord/Internal/Types/Gateway.hs view
@@ -8,12 +8,12 @@ import System.Info  import qualified Data.Text as T-import Data.Monoid ((<>)) import Data.Time (UTCTime) import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) import Data.Aeson import Data.Aeson.Types import Data.Maybe (fromMaybe)+import Data.Functor import Text.Read (readMaybe)  import Discord.Internal.Types.Prelude@@ -140,16 +140,14 @@   toJSON (UpdateStatus (UpdateStatusOpts since game status afk)) = object [       "op" .= (3 :: Int)     , "d"  .= object [-        "since" .= case since of Nothing -> Nothing-                                 Just s -> Just ((10^6) * (utcTimeToPOSIXSeconds s))+        "since" .= (since <&> \s -> 1000 * utcTimeToPOSIXSeconds s) -- takes UTCTime and returns unix time (in milliseconds)       , "afk" .= afk       , "status" .= statusString status-      , "game" .= case game of Nothing -> Nothing-                               Just a -> Just $ object [-                                           "name" .= activityName a-                                         , "type" .= (fromEnum $ activityType a :: Int)-                                         , "url" .= activityUrl a-                                         ]+      , "game" .= (game <&> \a -> object [+                                "name" .= activityName a+                              , "type" .= (fromEnum $ activityType a :: Int)+                              , "url" .= activityUrl a+                              ])       ]     ]   toJSON (UpdateStatusVoice (UpdateStatusVoiceOpts guild channel mute deaf)) =
src/Discord/Internal/Types/Prelude.hs view
@@ -11,7 +11,6 @@ import Data.Time.Clock import qualified Data.Text as T import Data.Time.Clock.POSIX-import Data.Monoid ((<>)) import Control.Monad (mzero)  import Data.Functor.Compose (Compose(Compose, getCompose))@@ -54,6 +53,7 @@ type RoleId = Snowflake type IntegrationId = Snowflake type WebhookId = Snowflake+type ParentId = Snowflake  -- | Gets a creation date from a snowflake. snowflakeCreationDate :: Snowflake -> UTCTime