1 ! Copyright (C) 2008 Bruno Deferrari, Doug Coleman, Slava Pestov.
2 ! See http://factorcode.org/license.txt for BSD license.
3 USING: concurrency.mailboxes kernel io.sockets io.encodings.8-bit calendar
4 accessors destructors namespaces io assocs arrays qualified fry
5 continuations threads strings classes combinators splitting hashtables
6 ascii irc.messages irc.messages.private ;
7 RENAME: join sequences => sjoin
8 EXCLUDE: sequences => join ;
11 ! ======================================
12 ! Setup and running objects
13 ! ======================================
15 : irc-port 6667 ; ! Default irc port
17 TUPLE: irc-profile server port nickname password ;
18 C: <irc-profile> irc-profile
20 TUPLE: irc-client profile stream in-messages out-messages join-messages
21 listeners is-running connect reconnect-time ;
22 : <irc-client> ( profile -- irc-client )
23 f <mailbox> <mailbox> <mailbox> H{ } clone f
24 [ <inet> latin1 <client> ] 15 seconds irc-client boa ;
26 TUPLE: irc-listener in-messages out-messages ;
27 TUPLE: irc-server-listener < irc-listener ;
28 TUPLE: irc-channel-listener < irc-listener name password timeout participants ;
29 TUPLE: irc-nick-listener < irc-listener name ;
30 SYMBOL: +server-listener+
37 : participant-mode ( n -- mode )
38 H{ { 64 +operator+ } { 43 +voice+ } { 0 +normal+ } } at ;
40 ! participant changed actions
46 : <irc-listener> ( -- irc-listener ) <mailbox> <mailbox> irc-listener boa ;
48 : <irc-server-listener> ( -- irc-server-listener )
49 <mailbox> <mailbox> irc-server-listener boa ;
51 : <irc-channel-listener> ( name -- irc-channel-listener )
52 [ <mailbox> <mailbox> ] dip f 60 seconds H{ } clone
53 irc-channel-listener boa ;
55 : <irc-nick-listener> ( name -- irc-nick-listener )
56 [ <mailbox> <mailbox> ] dip irc-nick-listener boa ;
58 ! ======================================
60 ! ======================================
62 TUPLE: participant-changed nick action ;
63 C: <participant-changed> participant-changed
65 SINGLETON: irc-listener-end ! send to a listener to stop its execution
66 SINGLETON: irc-end ! sent when the client isn't running anymore
67 SINGLETON: irc-disconnected ! sent when connection is lost
68 SINGLETON: irc-connected ! sent when connection is established
69 UNION: irc-broadcasted-message irc-end irc-disconnected irc-connected ;
71 : terminate-irc ( irc-client -- )
72 [ is-running>> ] keep and [
73 [ [ irc-end ] dip in-messages>> mailbox-put ]
74 [ [ f ] dip (>>is-running) ]
81 SYMBOL: current-irc-client
83 ! ======================================
85 ! ======================================
87 : irc> ( -- irc-client ) current-irc-client get ;
88 : irc-stream> ( -- stream ) irc> stream>> ;
89 : irc-write ( s -- ) irc-stream> stream-write ;
90 : irc-print ( s -- ) irc-stream> [ stream-print ] keep stream-flush ;
91 : listener> ( name -- listener/f ) irc> listeners>> at ;
93 : maybe-mailbox-get ( mailbox quot: ( irc-message -- ) -- )
94 [ dup mailbox-empty? [ drop yield ] ] dip '[ mailbox-get @ ] if ; inline
96 GENERIC: to-listener ( message obj -- )
98 M: string to-listener ( message string -- )
99 listener> [ +server-listener+ listener> ] unless*
100 [ to-listener ] [ drop ] if* ;
102 : unregister-listener ( name -- )
104 [ at [ irc-listener-end ] dip to-listener ]
108 M: irc-listener to-listener ( message irc-listener -- )
109 in-messages>> mailbox-put ;
111 : remove-participant ( nick channel -- )
112 listener> [ participants>> delete-at ] [ drop ] if* ;
114 : listeners-with-participant ( nick -- seq )
115 irc> listeners>> values
116 [ dup irc-channel-listener? [ participants>> key? ] [ 2drop f ] if ]
119 : remove-participant-from-all ( nick -- )
120 dup listeners-with-participant [ participants>> delete-at ] with each ;
122 : add-participant ( mode nick channel -- )
123 listener> [ participants>> set-at ] [ 2drop ] if* ;
127 : maybe-forward-join ( join -- )
128 [ prefix>> parse-name me? ] keep and
129 [ irc> join-messages>> mailbox-put ] when* ;
131 ! ======================================
132 ! IRC client messages
133 ! ======================================
136 "NICK " irc-write irc-print ;
140 "USER " irc-write irc-write
141 " hostname servername :irc.factor" irc-print ;
143 : /CONNECT ( server port -- stream )
144 irc> connect>> call drop ;
146 : /JOIN ( channel password -- )
148 [ [ " :" ] dip 3append ] when* irc-print ;
150 : /PART ( channel text -- )
151 [ "PART " irc-write irc-write ] dip
152 " :" irc-write irc-print ;
154 : /KICK ( channel who -- )
155 [ "KICK " irc-write irc-write ] dip
156 " " irc-write irc-print ;
158 : /PRIVMSG ( nick line -- )
159 [ "PRIVMSG " irc-write irc-write ] dip
160 " :" irc-write irc-print ;
162 : /ACTION ( nick line -- )
163 [ 1 , "ACTION " % % 1 , ] "" make /PRIVMSG ;
166 "QUIT :" irc-write irc-print ;
169 "PONG " irc-write irc-print ;
171 ! ======================================
172 ! Server message handling
173 ! ======================================
175 : me? ( string -- ? )
176 irc> profile>> nickname>> = ;
178 : irc-message-origin ( irc-message -- name )
179 dup name>> me? [ prefix>> parse-name ] [ name>> ] if ;
181 : broadcast-message-to-listeners ( message -- )
182 irc> listeners>> values [ to-listener ] with each ;
184 GENERIC: handle-participant-change ( irc-message -- )
186 M: join handle-participant-change ( join -- )
187 [ prefix>> parse-name +join+ <participant-changed> ]
188 [ trailing>> ] bi to-listener ;
190 M: part handle-participant-change ( part -- )
191 [ prefix>> parse-name +part+ <participant-changed> ]
192 [ channel>> ] bi to-listener ;
194 M: kick handle-participant-change ( kick -- )
195 [ who>> +part+ <participant-changed> ]
196 [ channel>> ] bi to-listener ;
198 M: quit handle-participant-change ( quit -- )
200 [ +part+ <participant-changed> ] [ listeners-with-participant ] bi
201 [ to-listener ] with each ;
203 GENERIC: handle-incoming-irc ( irc-message -- )
205 M: irc-message handle-incoming-irc ( irc-message -- )
206 +server-listener+ listener> [ to-listener ] [ drop ] if* ;
208 M: logged-in handle-incoming-irc ( logged-in -- )
209 name>> irc> profile>> (>>nickname) ;
211 M: ping handle-incoming-irc ( ping -- )
214 M: nick-in-use handle-incoming-irc ( nick-in-use -- )
215 name>> "_" append /NICK ;
217 M: privmsg handle-incoming-irc ( privmsg -- )
218 dup irc-message-origin to-listener ;
220 M: join handle-incoming-irc ( join -- )
221 { [ maybe-forward-join ]
222 [ dup trailing>> to-listener ]
223 [ [ drop +normal+ ] [ prefix>> parse-name ] [ trailing>> ] tri add-participant ]
224 [ handle-participant-change ]
227 M: part handle-incoming-irc ( part -- )
228 [ dup channel>> to-listener ]
229 [ [ prefix>> parse-name ] [ channel>> ] bi remove-participant ]
230 [ handle-participant-change ]
233 M: kick handle-incoming-irc ( kick -- )
234 { [ dup channel>> to-listener ]
235 [ [ who>> ] [ channel>> ] bi remove-participant ]
236 [ handle-participant-change ]
237 [ dup who>> me? [ unregister-listener ] [ drop ] if ]
240 M: quit handle-incoming-irc ( quit -- )
241 [ dup prefix>> parse-name listeners-with-participant
242 [ to-listener ] with each ]
243 [ prefix>> parse-name remove-participant-from-all ]
244 [ handle-participant-change ]
247 : >nick/mode ( string -- nick mode )
248 dup first "+@" member? [ unclip ] [ 0 ] if participant-mode ;
250 : names-reply>participants ( names-reply -- participants )
251 trailing>> [ blank? ] trim " " split
252 [ >nick/mode 2array ] map >hashtable ;
254 M: names-reply handle-incoming-irc ( names-reply -- )
255 [ names-reply>participants ] [ channel>> listener> ] bi [
257 [ [ f f <participant-changed> ] dip name>> to-listener ] bi
260 M: irc-broadcasted-message handle-incoming-irc ( irc-broadcasted-message -- )
261 broadcast-message-to-listeners ;
263 ! ======================================
264 ! Client message handling
265 ! ======================================
267 GENERIC: handle-outgoing-irc ( obj -- )
269 M: irc-message handle-outgoing-irc ( irc-message -- )
270 irc-message>client-line irc-print ;
272 ! ======================================
274 ! ======================================
276 : handle-reader-message ( irc-message -- )
277 irc> in-messages>> mailbox-put ;
281 : (handle-disconnect) ( -- )
283 [ [ irc-disconnected ] dip in-messages>> mailbox-put ]
284 [ dup reconnect-time>> sleep (connect-irc) ]
285 [ profile>> nickname>> /LOGIN ]
288 ! FIXME: do something with the exception, store somewhere to help debugging
289 : handle-disconnect ( error -- )
290 drop irc> is-running>> [ (handle-disconnect) ] when ;
292 : (reader-loop) ( -- )
294 |dispose stream-readln [
295 parse-irc-line handle-reader-message
301 : reader-loop ( -- ? )
302 [ (reader-loop) ] [ handle-disconnect ] recover t ;
304 : writer-loop ( -- ? )
305 irc> out-messages>> [ handle-outgoing-irc ] maybe-mailbox-get t ;
307 ! ======================================
309 ! ======================================
311 : in-multiplexer-loop ( -- ? )
312 irc> in-messages>> [ handle-incoming-irc ] maybe-mailbox-get t ;
314 : strings>privmsg ( name string -- privmsg )
315 privmsg new [ (>>trailing) ] keep [ (>>name) ] keep ;
317 : maybe-annotate-with-name ( name obj -- obj )
318 { { [ dup string? ] [ strings>privmsg ] }
319 { [ dup privmsg instance? ] [ swap >>name ] }
323 : listener-loop ( name -- ? )
325 out-messages>> [ maybe-annotate-with-name
326 irc> out-messages>> mailbox-put ] with
330 : spawn-irc-loop ( quot: ( -- ? ) name -- )
331 [ '[ irc> is-running>> [ @ ] [ f ] if ] ] dip
335 [ reader-loop ] "irc-reader-loop" spawn-irc-loop
336 [ writer-loop ] "irc-writer-loop" spawn-irc-loop
337 [ in-multiplexer-loop ] "in-multiplexer-loop" spawn-irc-loop ;
339 ! ======================================
340 ! Listener join request handling
341 ! ======================================
343 : set+run-listener ( name irc-listener -- )
344 over irc> listeners>> set-at
345 '[ , listener-loop ] "listener" spawn-irc-loop ;
347 GENERIC: (add-listener) ( irc-listener -- )
349 M: irc-channel-listener (add-listener) ( irc-channel-listener -- )
350 [ [ name>> ] [ password>> ] bi /JOIN ]
351 [ [ [ drop irc> join-messages>> ]
353 [ name>> '[ trailing>> , = ] ]
354 tri mailbox-get-timeout? trailing>> ] keep set+run-listener
357 M: irc-nick-listener (add-listener) ( irc-nick-listener -- )
358 [ name>> ] keep set+run-listener ;
360 M: irc-server-listener (add-listener) ( irc-server-listener -- )
361 [ +server-listener+ ] dip set+run-listener ;
363 GENERIC: (remove-listener) ( irc-listener -- )
365 M: irc-nick-listener (remove-listener) ( irc-nick-listener -- )
366 name>> unregister-listener ;
368 M: irc-channel-listener (remove-listener) ( irc-channel-listener -- )
369 [ [ name>> ] [ out-messages>> ] bi
370 [ [ part new ] dip >>channel ] dip mailbox-put ] keep
371 name>> unregister-listener ;
373 M: irc-server-listener (remove-listener) ( irc-server-listener -- )
374 drop +server-listener+ unregister-listener ;
376 : (connect-irc) ( irc-client -- )
377 [ profile>> [ server>> ] [ port>> ] bi /CONNECT ] keep
380 in-messages>> [ irc-connected ] dip mailbox-put ;
382 : with-irc-client ( irc-client quot: ( -- ) -- )
383 [ current-irc-client ] dip with-variable ; inline
387 : connect-irc ( irc-client -- )
389 [ (connect-irc) ] [ profile>> nickname>> /LOGIN ] bi
390 spawn-irc ] with-irc-client ;
392 : add-listener ( irc-listener irc-client -- )
393 swap '[ , (add-listener) ] with-irc-client ;
395 : remove-listener ( irc-listener irc-client -- )
396 swap '[ , (remove-listener) ] with-irc-client ;
398 : write-message ( message irc-listener -- ) out-messages>> mailbox-put ;
399 : read-message ( irc-listener -- message ) in-messages>> mailbox-get ;