discord-haskell-voice 2.2.2 → 2.3.0
raw patch · 12 files changed
+84/−56 lines, 12 filesdep +microlensdep +microlens-thdep −lensdep ~conduitdep ~discord-haskellPVP ok
version bump matches the API change (PVP)
Dependencies added: microlens, microlens-th
Dependencies removed: lens
Dependency ranges changed: conduit, discord-haskell
API changes (from Hackage documentation)
- Discord.Internal.Types.VoiceUDP: _MalformedPacket :: Prism' VoiceUDPPacket ByteString
- Discord.Internal.Types.VoiceUDP: _SpeakingData :: Prism' VoiceUDPPacket ByteString
- Discord.Internal.Types.VoiceUDP: _SpeakingDataEncrypted :: Prism' VoiceUDPPacket (ByteString, ByteString)
- Discord.Internal.Types.VoiceUDP: _SpeakingDataEncryptedExtra :: Prism' VoiceUDPPacket (ByteString, ByteString)
- Discord.Internal.Types.VoiceUDP: _UnknownPacket :: Prism' VoiceUDPPacket ByteString
- Discord.Internal.Types.VoiceWebsocket: _ClientDisconnect :: Prism' VoiceWebsocketReceivable UserId
- Discord.Internal.Types.VoiceWebsocket: _HeartbeatAck :: Prism' VoiceWebsocketReceivable Int
- Discord.Internal.Types.VoiceWebsocket: _ParseError :: Prism' VoiceWebsocketReceivable Text
- Discord.Internal.Types.VoiceWebsocket: _Reconnect :: Prism' VoiceWebsocketReceivable ()
- Discord.Internal.Types.VoiceWebsocket: _Resumed :: Prism' VoiceWebsocketReceivable ()
- Discord.Internal.Types.VoiceWebsocket: _SpeakingR :: Prism' VoiceWebsocketReceivable SpeakingPayload
- Discord.Internal.Types.VoiceWebsocket: _UnknownOPCode :: Prism' VoiceWebsocketReceivable (Integer, Object)
+ Discord.Voice: playYouTubeWith :: String -> (String -> [String]) -> String -> String -> Voice ()
+ Discord.Voice: playYouTubeWith' :: String -> (String -> [String]) -> String -> String -> ConduitT ByteString ByteString (ResourceT DiscordHandler) () -> Voice ()
- Discord.Internal.Types.VoiceUDP: _IPDiscovery :: Prism' VoiceUDPPacket (Integer, Text, Integer)
+ Discord.Internal.Types.VoiceUDP: _IPDiscovery :: Traversal' VoiceUDPPacket (Integer, Text, Integer)
- Discord.Internal.Types.VoiceWebsocket: _Hello :: Prism' VoiceWebsocketReceivable Int
+ Discord.Internal.Types.VoiceWebsocket: _Hello :: Traversal' VoiceWebsocketReceivable Int
- Discord.Internal.Types.VoiceWebsocket: _Ready :: Prism' VoiceWebsocketReceivable ReadyPayload
+ Discord.Internal.Types.VoiceWebsocket: _Ready :: Traversal' VoiceWebsocketReceivable ReadyPayload
- Discord.Internal.Types.VoiceWebsocket: _SessionDescription :: Prism' VoiceWebsocketReceivable (Text, [Word8])
+ Discord.Internal.Types.VoiceWebsocket: _SessionDescription :: Traversal' VoiceWebsocketReceivable (Text, [Word8])
Files
- ChangeLog.md +7/−0
- README.md +16/−15
- discord-haskell-voice.cabal +13/−10
- examples/BasicMusicBot.hs +2/−1
- src/Discord/Internal/Types/VoiceCommon.hs +1/−1
- src/Discord/Internal/Types/VoiceUDP.hs +6/−2
- src/Discord/Internal/Types/VoiceWebsocket.hs +15/−2
- src/Discord/Internal/Voice.hs +7/−6
- src/Discord/Internal/Voice/CommonUtils.hs +1/−1
- src/Discord/Internal/Voice/UDPLoop.hs +3/−3
- src/Discord/Internal/Voice/WebsocketLoop.hs +11/−15
- src/Discord/Voice.hs +2/−0
ChangeLog.md view
@@ -2,6 +2,13 @@ ## Unreleased changes ++## 2.3.0++- Export `playYouTubeWith` and `playYoUTubeWith'` from `Discord.Voice`.+- Update `discord-haskell` dependency bounds to `>= 1.12.0 && <= 1.13.0`.+- Migrate from `lens` to `microlens`, following the `opus` package doing the same.+ ## 2.2.2 - Update `discord-haskell` dependency to 1.12.0
README.md view
@@ -1,7 +1,7 @@ # discord-haskell-voice --+[](https://hackage.haskell.org/package/discord-haskell-voice)+[](https://hackage.haskell.org/package/discord-haskell) Welcome to `discord-haskell-voice`! This library provides you with a high-level interface for interacting with Discord's Voice API, building on top of the@@ -26,7 +26,7 @@ rickrollHalfVolume :: Channel -> DiscordHandler () rickrollHalfVolume c@(ChannelVoice {}) = do void $ runVoice $ do- join (channelGuild c) (channelId c) + join (channelGuild c) (channelId c) let halfAmplitude = awaitForever $ \current -> yield $ round $ fromIntegral current * 0.5 playYouTube' "rickroll" $ packInt16C .| halfAmplitude .| unpackInt16C@@ -36,17 +36,18 @@ ## Requirements - The library uses [`saltine`](https://github.com/tel/saltine) for encryption-and decryption of audio packets. This requires the appropriate libraries to be-installed on your system. See their README for information.+and decryption of audio packets. This requires libsodium to be installed on+your system. See their README for information. - The library requires Opus libraries to be installed on your system. The `libopus-dev` package available on package repositories should be sufficient-on most \*nix systems. Windows is unexplored yet (WSL works).+on most \*nix systems. The `opus` brew package suffices on Mac. Windows+is unexplored yet (WSL works). - If you are to use any variants of `playFile`, `playYouTube`, you will need FFmpeg installed. To specify a custom executable name, see the `-With` function variants. - If you are to use any variants of `playYouTube`, you will additionally need youtube-dl installed. This is used to get the stream URL to pass to FFmpeg. To-specify a custom executable name, use `playYouTubeWith`.+specify a custom executable name (such as yt-dlp), use `playYouTubeWith`. ## Features @@ -54,8 +55,8 @@ - Can join/leave Discord voice channels. It is possible to join multiple of them simultaneously (one per sever) and stream different contents to each.-- It is also possible for many voice channels (across many servers) and play the-same content, radio/subscriber-style.+- It is also possible to join many voice channels (across many servers) and play+the same content, radio/subscriber-style. - You can play arbitrary PCM audio, arbitrary audio (with FFmpeg), and arbitrary internet audio (with youtube-dl). - You can transform audio arbitrarily using Conduit.@@ -84,8 +85,8 @@ # --- myproject.cabal <truncated> build-depends: base >=4.7 && <5- , discord-haskell ==1.12.0- , discord-haskell-voice ==2.2.2+ , discord-haskell ==1.13.0+ , discord-haskell-voice ==2.3.0 ``` ### Stack@@ -95,16 +96,16 @@ ``` # --- stack.yaml <truncated> extra-deps:-- discord-haskell-1.12.0-- discord-haskell-voice-2.2.2+- discord-haskell-1.13.0+- discord-haskell-voice-2.3.0 ``` ``` # --- package.yaml <truncated> dependencies: - base >= 4.7 && < 5-- discord-haskell == 1.12.0-- discord-haskell-voice == 2.2.2+- discord-haskell == 1.13.0+- discord-haskell-voice == 2.3.0 ``` ## Documentation
discord-haskell-voice.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: discord-haskell-voice-version: 2.2.2+version: 2.3.0 synopsis: Voice support for discord-haskell. description: Supplementary library to discord-haskell. See the project README on GitHub for more information. <https://github.com/yutotakano/discord-haskell-voice> category: Network@@ -49,9 +49,10 @@ , base >=4.7 && <5 , binary ==0.8.* , bytestring >=0.10.12.0 && <0.11- , conduit ==1.3.4.1- , discord-haskell ==1.12.0- , lens >=4.19.2 && <5+ , conduit ==1.3.4.2+ , discord-haskell >=1.12.0 && <=1.13.0+ , microlens >=0.4.13.0+ , microlens-th >=0.4.3.10 , mtl ==2.2.2 , network >=3.1.1.1 && <3.2 , opus ==0.1.0.0@@ -79,10 +80,11 @@ , base >=4.7 && <5 , binary ==0.8.* , bytestring >=0.10.12.0 && <0.11- , conduit ==1.3.4.1- , discord-haskell ==1.12.0+ , conduit ==1.3.4.2+ , discord-haskell >=1.12.0 && <=1.13.0 , discord-haskell-voice- , lens >=4.19.2 && <5+ , microlens >=0.4.13.0+ , microlens-th >=0.4.3.10 , mtl ==2.2.2 , network >=3.1.1.1 && <3.2 , optparse-applicative >=0.15.1.0 && <0.17@@ -113,10 +115,11 @@ , base >=4.7 && <5 , binary ==0.8.* , bytestring >=0.10.12.0 && <0.11- , conduit ==1.3.4.1- , discord-haskell ==1.12.0+ , conduit ==1.3.4.2+ , discord-haskell >=1.12.0 && <=1.13.0 , discord-haskell-voice- , lens >=4.19.2 && <5+ , microlens >=0.4.13.0+ , microlens-th >=0.4.3.10 , mtl ==2.2.2 , network >=3.1.1.1 && <3.2 , opus ==0.1.0.0
examples/BasicMusicBot.hs view
@@ -106,7 +106,8 @@ let adjustVolume = awaitForever $ \current -> do v' <- liftIO $ readTVarIO volume yield $ round $ fromIntegral current * (fromIntegral v' / 100)- playYouTube' x $ packInt16C .| adjustVolume .| unpackInt16C+ playYouTubeWith' "ffmpeg" defaultFFmpegArgs "yt-dlp" x $+ packInt16C .| adjustVolume .| unpackInt16C case result of Left e -> liftIO $ print e >> pure ()
src/Discord/Internal/Types/VoiceCommon.hs view
@@ -32,7 +32,7 @@ import Control.Concurrent ( Chan, MVar, ThreadId ) import Control.Concurrent.BoundedChan qualified as Bounded import Control.Exception.Safe ( Exception, MonadMask, MonadCatch, MonadThrow )-import Control.Lens ( makeFields )+import Lens.Micro.TH ( makeFields ) import Control.Monad.Except import Control.Monad.Reader import Data.ByteString qualified as B
src/Discord/Internal/Types/VoiceUDP.hs view
@@ -26,7 +26,7 @@ -} module Discord.Internal.Types.VoiceUDP where -import Control.Lens ( makePrisms )+import Lens.Micro import Data.Binary.Get import Data.Binary.Put import Data.Binary@@ -47,6 +47,10 @@ | MalformedPacket BL.ByteString deriving (Show, Eq) +_IPDiscovery :: Traversal' VoiceUDPPacket (Integer, T.Text, Integer)+_IPDiscovery f (IPDiscovery ssrc ip port) = (\(a, b, c) -> IPDiscovery a b c) <$> f (ssrc, ip, port)+_IPDiscovery f packet = pure packet+ data VoiceUDPPacketHeader = Header Word8 Word8 Word16 Word32 Word32 @@ -107,4 +111,4 @@ putLazyByteString a put (MalformedPacket a) = putLazyByteString a -$(makePrisms ''VoiceUDPPacket)+-- $(makePrisms ''VoiceUDPPacket)
src/Discord/Internal/Types/VoiceWebsocket.hs view
@@ -27,7 +27,7 @@ module Discord.Internal.Types.VoiceWebsocket where import Control.Applicative ( (<|>) )-import Control.Lens ( makePrisms )+import Lens.Micro import Data.Aeson import Data.Aeson.Types import Data.Text qualified as T@@ -50,6 +50,19 @@ | Reconnect -- Internal use deriving (Show, Eq) +_Ready :: Traversal' VoiceWebsocketReceivable ReadyPayload+_Ready f (Ready rp) = Ready <$> f rp+_Ready f rp = pure rp++_SessionDescription :: Traversal' VoiceWebsocketReceivable (T.Text, [Word8])+_SessionDescription f (SessionDescription t bytes) = uncurry SessionDescription <$> f (t, bytes)+_SessionDescription f sd = pure sd++_Hello :: Traversal' VoiceWebsocketReceivable Int+_Hello f (Hello a) = Hello <$> f a+_Hello f a = pure a++ data VoiceWebsocketSendable = Identify IdentifyPayload -- Opcode 0 | SelectProtocol SelectProtocolPayload -- Opcode 1@@ -196,4 +209,4 @@ ] ] -$(makePrisms ''VoiceWebsocketReceivable)+-- $(makePrisms ''VoiceWebsocketReceivable)
src/Discord/Internal/Voice.hs view
@@ -50,7 +50,8 @@ ) import Control.Concurrent.BoundedChan qualified as Bounded import Control.Exception.Safe ( finally, bracket, throwTo, catch, throwIO )-import Control.Lens+import Lens.Micro+import Lens.Micro.Extras (view) import Control.Monad.Reader ( ask, liftIO, runReaderT ) import Control.Monad.Except ( runExceptT, throwError ) import Control.Monad.Trans ( lift )@@ -149,8 +150,8 @@ -- There is no way to prevent it, so as a consequence, the bot may -- linger in the voice call for a few minutes after the bot program is -- killed.- mapMOf_ (traverse . guildId) (\x -> updateStatusVoice x Nothing False False) finalState- mapMOf_ (traverse . websocket . _1) (liftIO . killWkThread) finalState+ traverseOf_ (traverse . guildId) (\x -> updateStatusVoice x Nothing False False) finalState+ traverseOf_ (traverse . websocket . _1) (liftIO . killWkThread) finalState pure result @@ -303,7 +304,7 @@ updateSpeakingStatus micStatus = do h <- (^. voiceHandles) <$> ask handles <- liftIO $ readMVar h- flip (mapMOf_ traverse) handles $ \handle ->+ flip (traverseOf_ traverse) handles $ \handle -> liftIO $ writeChan (handle ^. websocket . _2 . _2) $ Speaking $ SpeakingPayload { speakingPayloadMicrophone = micStatus , speakingPayloadSoundshare = False@@ -383,9 +384,9 @@ encoder <- liftIO $ opusEncoderCreate enCfg loop encoder where- enCfg = _EncoderConfig # (opusSR48k, True, app_audio)+ enCfg = mkEncoderConfig opusSR48k True app_audio -- 1275 is the max bytes an opus 20ms frame can have- streamCfg = _StreamConfig # (enCfg, 48*20, 1276)+ streamCfg = mkStreamConfig enCfg (48*20) 1276 loop encoder = await >>= \case Nothing -> do -- Send at least 5 blank frames (20ms * 5 = 100 ms)
src/Discord/Internal/Voice/CommonUtils.hs view
@@ -24,7 +24,7 @@ import Control.Concurrent import Control.Concurrent.Async ( race )-import Control.Lens+import Lens.Micro import Data.Text qualified as T import Data.Time.Clock.POSIX import Data.Time
src/Discord/Internal/Voice/UDPLoop.hs view
@@ -47,7 +47,7 @@ ) import Control.Concurrent.BoundedChan qualified as Bounded import Control.Exception.Safe ( handle, SomeException, finally, try, bracket )-import Control.Lens+import Lens.Micro import Control.Monad.IO.Class ( MonadIO ) import Data.Binary ( encode, decode ) import Data.ByteString.Lazy qualified as BL@@ -288,8 +288,8 @@ decodeOpusData :: B.ByteString -> IO B.ByteString decodeOpusData bytes = do- let deCfg = _DecoderConfig # (opusSR48k, True)- let deStreamCfg = _DecoderStreamConfig # (deCfg, 48*20, 0)+ let deCfg = mkDecoderConfig opusSR48k True+ let deStreamCfg = mkDecoderStreamConfig deCfg (48*20) 0 decoder <- opusDecoderCreate deCfg decoded <- opusDecode decoder deStreamCfg bytes pure decoded
src/Discord/Internal/Voice/WebsocketLoop.hs view
@@ -48,7 +48,7 @@ , readMVar ) import Control.Exception.Safe ( try, tryAsync, SomeException, finally, handle )-import Control.Lens+import Lens.Micro import Control.Monad ( forever, guard ) import Control.Monad.Except ( runExceptT, ExceptT (ExceptT), lift ) import Control.Monad.IO.Class ( liftIO )@@ -125,9 +125,7 @@ -- @opts@ to report back to runVoice, so it can be killed in the -- future. websocketFsm WSStart retries udpInfo = do- -- Use of tryAsync (unsafe, as it catches asynchronous exceptions) is- -- justified here, since it will only log, then go to WSClosed.- next <- tryAsync $ connect (opts ^. endpoint) $ \conn -> do+ next <- try $ connect (opts ^. endpoint) $ \conn -> do (libSends, sendTid) <- flip (setupSendLoop conn) log $ opts ^. wsHandle . _2 result <- flip finally (killThread sendTid) $ runExceptT $ do@@ -216,18 +214,16 @@ -- Connection is now closed. case next :: Either SomeException WSState of Left e -> do- (✍!) log $ "could not connect due to an exception: " <>+ (✍!) log $ "connection terminated due to a synchronous exception: " <> (tshow e) writeChan (opts ^. wsHandle . _1) $ Left $ VoiceWebsocketCouldNotConnect- "could not connect due to an exception"+ "connection terminated due to a synchronous exception" websocketFsm WSClosed 0 udpInfo Right n -> websocketFsm n 0 udpInfo websocketFsm WSResume retries udpInfo = do- -- Use of tryAsync (unsafe, as it catches asynchronous exceptions) is- -- justified here, since it will only log, then go to WSClosed.- next <- tryAsync $ connect (opts ^. endpoint) $ \conn -> do+ next <- try $ connect (opts ^. endpoint) $ \conn -> do (libSends, sendTid) <- flip (setupSendLoop conn) log $ opts ^. wsHandle . _2 helloPacket <- getPayload conn case helloPacket of@@ -247,26 +243,26 @@ Right (Discord.Internal.Types.VoiceWebsocket.Resumed) -> do -- use the previous UDP launch options since it's not resent udpLaunchOpts <- readMVar udpInfo- + -- Pass not the MVar but the raw options, since -- there's no writing to be done. finally (eventStream conn opts interval udpLaunchOpts libSends log) $ (killThread heartGenTid >> killThread sendTid) Right p -> do- (✍!) log $ "First packet after Resume not " <> + (✍!) log $ "First packet after Resume not " <> "Opcode 9 Resumed: " <> (tshow p) pure WSClosed Right p -> do (✍!) log $ "First packet not Opcode 8 Hello: " <> (tshow p) pure WSClosed - -- Connection is now closed. case next :: Either SomeException WSState of- Left _ -> do- (✍!) log $ "could not resume, retrying after 5 seconds"+ Left e -> do+ (✍!) log $ "could not resume due to a synchronous exception: " <>+ (tshow e) <> ", retrying after 5 seconds" threadDelay $ 5 * (10^(6 :: Int)) websocketFsm WSResume (retries + 1) udpInfo- Right n -> websocketFsm n 1 udpInfo+ Right n -> websocketFsm n retries udpInfo -- | Create the library-specific sending packets Chan, and then create the -- thread for eternally sending contents in the said Chan, as well as the
src/Discord/Voice.hs view
@@ -56,6 +56,8 @@ , playFileWith' , playYouTube , playYouTube'+ , playYouTubeWith+ , playYouTubeWith' , defaultFFmpegArgs ) where