1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
|
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TupleSections #-}
-- |
-- Module : Network.IRC.Conduit.Internal
-- Copyright : (c) 2016 Michael Walker
-- License : MIT
-- Maintainer : Michael Walker <mike@barrucadu.co.uk>
-- Stability : experimental
-- Portability : BangPatterns, DeriveFunctor, OverloadedStrings, RankNTypes, TupleSections
--
-- Internal IRC conduit types and utilities. This module is NOT
-- considered to form part of the public interface of this library.
module Network.IRC.Conduit.Internal where
import Control.Applicative ((<$>))
import Control.Arrow ((&&&))
import Data.ByteString (ByteString, isSuffixOf, singleton,
unpack)
import Data.Char (ord)
import Data.Conduit (ConduitM, await, yield)
import Data.Maybe (isJust, listToMaybe)
import Data.Monoid ((<>))
import Data.Profunctor (Choice)
import Data.String (fromString)
import Network.IRC.CTCP (CTCPByteString, getUnderlyingByteString,
orCTCP)
import Text.Read (readMaybe)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as B8
import qualified Network.IRC as I
-- * Internal Lens synonyms
-- | See @<http://hackage.haskell.org/package/lens/docs/Control-Lens-Lens.html#t:Lens Control.Lens.Lens.Lens>@.
type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
-- | A @<http://hackage.haskell.org/package/lens/docs/Control-Lens-Type.html#t:Simple Simple>@ 'Lens'.
type Lens' s a = Lens s s a a
-- | See @<http://hackage.haskell.org/package/lens/docs/Control-Lens-Prism.html#t:Prism Control.Lens.Prism.Prism>@.
type Prism s t a b = forall p f. (Choice p, Applicative f) => p a (f b) -> p s (f t)
-- | A @<http://hackage.haskell.org/package/lens/docs/Control-Lens-Type.html#t:Simple Simple>@ 'Prism'.
type Prism' s a = Prism s s a a
-- *Conduits
-- |Split up incoming bytestrings into new lines.
chunked :: Monad m => ConduitM ByteString ByteString m ()
chunked = chunked' ""
where
chunked' !leftover = do
-- Wait for a value from upstream
val <- await
case val of
Just val' ->
let
carriage = fromIntegral $ fromEnum '\r'
newline = fromIntegral $ fromEnum '\n'
-- Split on '\n's, removing any stray '\r's (line endings
-- are usually '\r\n's, but this isn't certain).
bytes = B.filter (/=carriage) $ leftover <> val'
splitted = B.split newline bytes
-- If the last chunk ends with a '\n', then we have a
-- complete message at the end, and can yield it
-- immediately. Otherwise, store the partial message to
-- prepend to the next bytestring received.
(toyield, remainder)
| singleton newline `isSuffixOf` bytes = (splitted, "")
| otherwise = init &&& last $ splitted
in do
-- Yield all complete and nonempty messages, and loop.
mapM_ yield $ filter (not . B.null) toyield
chunked' remainder
Nothing -> return ()
-- *Type synonyms
type ChannelName a = a
type NickName a = a
type ServerName a = a
type Reason a = Maybe a
type IsModeSet = Bool
type ModeFlag a = a
type ModeArg a = a
type NumericArg a = a
-- |The target of a message. Will be a nick or channel name.
type Target a = a
type IrcEvent = Event ByteString
type IrcSource = Source ByteString
type IrcMessage = Message ByteString
-- *Messages
-- |A decoded IRC message + source.
data Event a = Event
{ _raw :: ByteString
-- ^The message as a bytestring.
, _source :: Source a
-- ^The source of the message (user, channel, or server).
, _message :: Message a
-- ^The decoded message. This will never be a 'RawMsg'.
}
deriving (Eq, Functor, Show)
-- |The source of an IRC message.
data Source a = User (NickName a)
-- ^The message comes directly from a user.
| Channel (ChannelName a) (NickName a)
-- ^The message comes from a user in a channel.
| Server (ServerName a)
-- ^The message comes directly from the server.
deriving (Eq, Functor, Show)
-- |A decoded IRC message.
data Message a = Privmsg (Target a) (Either CTCPByteString a)
-- ^A message, either from a user or to a channel the
-- client is in. CTCPs are distinguished by starting
-- and ending with a \\001 (SOH).
| Notice (Target a) (Either CTCPByteString a)
-- ^Like a privmsg, but should not provoke an automatic
-- response.
| Nick (NickName a)
-- ^Someone has updated their nick.
| Join (ChannelName a)
-- ^Someone has joined a channel.
| Part (ChannelName a) (Reason a)
-- ^Someone has left a channel.
| Quit (Reason a)
-- ^Someone has left the network.
| Mode (Target a) IsModeSet [ModeFlag a] [ModeArg a]
-- ^Someone has set some channel modes or user modes.
| Topic (ChannelName a) a
-- ^Someone has set the topic of a channel.
| Invite (ChannelName a) (NickName a)
-- ^The client has been invited to a channel.
| Kick (ChannelName a) (NickName a) (Reason a)
-- ^Someone has been kicked from a channel.
| Ping (ServerName a) (Maybe (ServerName a))
-- ^The client has received a server ping, and should
-- send a pong asap.
| Pong (ServerName a)
-- ^A pong sent to the named server.
| Numeric Int [NumericArg a]
-- ^One of the many server numeric responses.
| RawMsg a
-- ^Never produced by decoding, but can be used to send
-- arbitrary bytestrings to the IRC server. Naturally,
-- this should only be used when you are confident that
-- the produced bytestring will be a valid IRC message.
deriving (Eq, Functor, Show)
-- *Decoding messages
fromByteString :: ByteString -> Either ByteString IrcEvent
fromByteString bs = maybe (Left bs) (Right . uncurry (Event bs)) (attemptDecode bs)
-- |Attempt to decode a ByteString into a message, returning a Nothing
-- if either the source or the message can't be determined.
attemptDecode :: ByteString -> Maybe (IrcSource, IrcMessage)
attemptDecode bs = I.decode bs >>= decode'
where
decode' msg = case msg of
-- Disambiguate PRIVMSG and NOTICE source by checking the first
-- character of the target
I.Message (Just (I.NickName n _ _)) "PRIVMSG" [t, m] | isChan t -> Just (Channel t n, privmsg t m)
| otherwise -> Just (User n, privmsg t m)
I.Message (Just (I.NickName n _ _)) "NOTICE" [t, m] | isChan t -> Just (Channel t n, notice t m)
| otherwise -> Just (User n, notice t m)
I.Message (Just (I.NickName n _ _)) "NICK" [n'] -> Just (User n, Nick n')
I.Message (Just (I.NickName n _ _)) "JOIN" [c] -> Just (Channel c n, Join c)
I.Message (Just (I.NickName n _ _)) "PART" (c:r) -> Just (Channel c n, Part c $ listToMaybe r)
I.Message (Just (I.NickName n _ _)) "QUIT" r -> Just (User n, Quit $ listToMaybe r)
I.Message (Just (I.NickName n _ _)) "KICK" (c:u:r) -> Just (Channel c n, Kick c u $ listToMaybe r)
I.Message (Just (I.NickName n _ _)) "INVITE" [_, c] -> Just (User n, Invite c n)
I.Message (Just (I.NickName n _ _)) "TOPIC" [c, t] -> Just (Channel c n, Topic c t)
I.Message (Just (I.NickName n _ _)) "MODE" (t:fs:as) | n == t -> (User n,) <$> mode t fs as
| otherwise -> (Channel t n,) <$> mode t fs as
I.Message (Just (I.Server s)) "PING" (s1:s2) -> Just (Server s, Ping s1 $ listToMaybe s2)
I.Message Nothing "PING" (s1:s2) -> Just (Server s1, Ping s1 $ listToMaybe s2)
I.Message (Just (I.Server s)) n args | isNumeric n -> (Server s,) <$> numeric n args
_ -> Nothing
-- An IRC channel name can start with '#', '&', '+', or '!', all
-- of which have different meanings. However, most servers only
-- support '#'.
isChan t = B.take 1 t `elem` ["#", "&", "+", "!"]
-- Check if the message looks like a ctcp or not, and produce the appropriate message type.
privmsg t = Privmsg t . (Right `orCTCP` Left)
notice t = Notice t . (Right `orCTCP` Left)
-- Decode a set of mode changes
mode t fs as = case unpack fs of
(f:fs') | f == fromIntegral (ord '+') -> Just $ Mode t True (map singleton fs') as
| f == fromIntegral (ord '-') -> Just $ Mode t False (map singleton fs') as
_ -> Nothing
-- Parse the number in a numeric response
isNumeric = isJust . (readMaybe :: String -> Maybe Int) . B8.unpack
numeric n args = flip Numeric args <$> readMaybe (B8.unpack n)
-- *Encoding messages
-- |Encode an IRC message into a single bytestring suitable for
-- sending to the server.
toByteString :: IrcMessage -> ByteString
toByteString (Privmsg t (Left ctcpbs)) = mkMessage "PRIVMSG" [t, getUnderlyingByteString ctcpbs]
toByteString (Privmsg t (Right bs)) = mkMessage "PRIVMSG" [t, bs]
toByteString (Notice t (Left ctcpbs)) = mkMessage "NOTICE" [t, getUnderlyingByteString ctcpbs]
toByteString (Notice t (Right bs)) = mkMessage "NOTICE" [t, bs]
toByteString (Nick n) = mkMessage "NICK" [n]
toByteString (Join c) = mkMessage "JOIN" [c]
toByteString (Part c (Just r)) = mkMessage "PART" [c, r]
toByteString (Part c Nothing) = mkMessage "PART" [c]
toByteString (Quit (Just r)) = mkMessage "QUIT" [r]
toByteString (Quit Nothing) = mkMessage "QUIT" []
toByteString (Mode t True ms as) = mkMessage "MODE" $ t : ("+" <> B.concat ms) : as
toByteString (Mode t False ms as) = mkMessage "MODE" $ t : ("-" <> B.concat ms) : as
toByteString (Invite c n) = mkMessage "INVITE" [c, n]
toByteString (Topic c bs) = mkMessage "TOPIC" [c, bs]
toByteString (Kick c n (Just r)) = mkMessage "KICK" [c, n, r]
toByteString (Kick c n Nothing) = mkMessage "KICK" [c, n]
toByteString (Ping s1 (Just s2)) = mkMessage "PING" [s1, s2]
toByteString (Ping s1 Nothing) = mkMessage "PING" [s1]
toByteString (Pong s) = mkMessage "PONG" [s]
toByteString (Numeric n as) = mkMessage (fromString $ show n) as
toByteString (RawMsg bs) = bs
mkMessage :: ByteString -> [ByteString] -> ByteString
mkMessage cmd = I.encode . I.Message Nothing cmd
-- |Construct a raw message.
rawMessage :: ByteString
-- ^The command
-> [ByteString]
-- ^The arguments
-> IrcMessage
rawMessage cmd = RawMsg . mkMessage cmd
|