rhuidean 0.2.7 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,10 +24,13 @@ and it also offers clever ways of accomplishing basic and advanced tasks.
24
24
  The current active development/maintenance is done by:
25
25
 
26
26
  - rakaur, Eric Will <rakaur@malkier.net>
27
+
28
+ Almost all of the testing was also done by me, with help from:
29
+
27
30
  - sycobuny, Stephen Belcher <sycobuny@malkier.net>
31
+ - dKingston, Michael Rodriguez <dkingston@malkier.net>
28
32
 
29
- Almost all of the testing was also done by us. I (rakaur) wrote all of the base
30
- code, and sycobuny has expanded on it with more in-depth classes.
33
+ And others on irc.malkier.net.
31
34
 
32
35
  2. INSTALLATION
33
36
  ---------------
@@ -5,7 +5,16 @@
5
5
  # Copyright (c) 2003-2010 Eric Will <rakaur@malkier.net>
6
6
  #
7
7
 
8
- # Import required rhuidean modules.
8
+ module Rhuidean
9
+ # Version number
10
+ V_MAJOR = 1
11
+ V_MINOR = 0
12
+ V_PATCH = 0
13
+
14
+ VERSION = "#{V_MAJOR}.#{V_MINOR}.#{V_PATCH}"
15
+ end
16
+
17
+ # Import required app modules
9
18
  %w(client event methods numeric timer).each do |m|
10
19
  require 'rhuidean/' + m
11
20
  end
@@ -5,16 +5,14 @@
5
5
  # Copyright (c) 2003-2010 Eric Will <rakaur@malkier.net>
6
6
  #
7
7
 
8
- # Import required Ruby modules.
8
+ # Import required Ruby modules
9
9
  %w(logger socket).each { |m| require m }
10
10
 
11
11
  module IRC
12
12
 
13
13
  # The IRC::Client class acts as an abstract interface to the IRC protocol.
14
14
  class Client
15
- ##
16
- # constants
17
- VERSION = '0.2.7'
15
+ include Rhuidean # Version info and such
18
16
 
19
17
  ##
20
18
  # instance attributes
@@ -22,7 +20,7 @@ class Client
22
20
  :nickname, :username, :realname, :bind_to
23
21
 
24
22
  # Our TCPSocket.
25
- attr_reader :socket, :channels
23
+ attr_reader :socket
26
24
 
27
25
  # A simple Exeption class.
28
26
  class Error < Exception
@@ -58,9 +56,6 @@ class Client
58
56
  @dead = false
59
57
  @connected = false
60
58
 
61
- # List of channels we're on
62
- @channels = []
63
-
64
59
  # Received data waiting to be parsed.
65
60
  @recvq = []
66
61
 
@@ -119,15 +114,6 @@ class Client
119
114
  # Track our nickname...
120
115
  on(:NICK) { |m| @nickname = m.target if m.origin_nick == @nickname }
121
116
 
122
- # Track channels
123
- on(:JOIN) { |m| @channels << m.target if m.origin_nick == @nickname }
124
-
125
- on(:PART) do |m|
126
- @channels.delete(m.target) if m.origin_nick == @nickname
127
- end
128
-
129
- on(:KICK) { |m| @channels.delete(m.target) if m.params[0] == @nickname }
130
-
131
117
  self
132
118
  end
133
119
 
@@ -197,7 +183,7 @@ class Client
197
183
  ret = nil # Dead
198
184
  end
199
185
 
200
- unless ret
186
+ if not ret or ret.empty?
201
187
  @eventq.post(:dead)
202
188
  return
203
189
  end
@@ -236,6 +222,9 @@ class Client
236
222
  end
237
223
  rescue Errno::EAGAIN
238
224
  retry
225
+ rescue Exception
226
+ @eventq.post(:dead)
227
+ return
239
228
  end
240
229
  end
241
230
 
@@ -302,10 +291,53 @@ class Client
302
291
  self
303
292
  end
304
293
 
294
+ #
295
+ # Logs a regular message.
296
+ # ---
297
+ # message:: the string to log
298
+ # returns:: +self+
299
+ #
300
+ def log(message)
301
+ @logger.info(caller[0].split('/')[-1]) { message } if @logger
302
+ end
303
+
304
+ #
305
+ # Logs a debug message.
306
+ # ---
307
+ # message:: the string to log
308
+ # returns:: +self+
309
+ #
310
+ def debug(message)
311
+ return unless @logger
312
+
313
+ @logger.debug(caller[0].split('/')[-1]) { message } if @debug
314
+ end
315
+
305
316
  ######
306
317
  public
307
318
  ######
308
319
 
320
+ #
321
+ # Sets the logging object to use.
322
+ # If it quacks like a Logger object, it should work.
323
+ # ---
324
+ # logger:: the Logger to use
325
+ # returns:: +self+
326
+ #
327
+ def logger=(logger)
328
+ @logger = logger
329
+
330
+ # Set to false/nil to disable logging...
331
+ return unless @logger
332
+
333
+ @logger.progname = 'irc'
334
+ @logger.datetime_format = '%b %d %H:%M:%S '
335
+
336
+ # We only have 'logging' and 'debugging', so just set the
337
+ # object to show all levels. I might change this someday.
338
+ @logger.level = Logger::DEBUG
339
+ end
340
+
309
341
  #
310
342
  # Registers Event handlers with our EventQueue.
311
343
  # ---
@@ -327,7 +359,7 @@ class Client
327
359
  end
328
360
 
329
361
  #
330
- # Schedules input/output and runs the EventQueue.
362
+ # Schedules input/output and runs the +EventQueue+.
331
363
  # ---
332
364
  # returns:: never, thread dies on +:exit+
333
365
  #
@@ -409,46 +441,12 @@ class Client
409
441
  end
410
442
 
411
443
  #
412
- # Logs a regular message.
444
+ # Represent ourselves in a string.
413
445
  # ---
414
- # message:: the string to log
415
- # returns:: +self+
446
+ # returns:: our nickname and object ID
416
447
  #
417
- def log(message)
418
- @logger.info(caller[0].split('/')[-1]) { message } if @logger
419
- end
420
-
421
- #
422
- # Logs a debug message.
423
- # ---
424
- # message:: the string to log
425
- # returns:: +self+
426
- #
427
- def debug(message)
428
- return unless @logger
429
-
430
- @logger.debug(caller[0].split('/')[-1]) { message } if @debug
431
- end
432
-
433
- #
434
- # Sets the logging object to use.
435
- # If it quacks like a Logger object, it should work.
436
- # ---
437
- # logger:: the Logger to use
438
- # returns:: +self+
439
- #
440
- def logger=(logger)
441
- @logger = logger
442
-
443
- # Set to false/nil to disable logging...
444
- return unless @logger
445
-
446
- @logger.progname = 'irc'
447
- @logger.datetime_format = '%b %d %H:%M:%S '
448
-
449
- # We only have 'logging' and 'debugging', so just set the
450
- # object to show all levels. I might change this someday.
451
- @logger.level = Logger::DEBUG
448
+ def to_s
449
+ "#{@nickname}:#{self.object_id}"
452
450
  end
453
451
 
454
452
  #
@@ -479,8 +477,23 @@ class Message
479
477
  # style of (char *origin, char *target, char *parv[]) in C.
480
478
  #
481
479
  def initialize(client, raw, origin, target, params)
482
- @client, @ctcp, @origin = client, nil, origin
483
- @params, @raw, @target = params, raw, target
480
+ # The IRC::Client that processed this message
481
+ @client = client
482
+
483
+ # If this is a CTCP, the type of CTCP
484
+ @ctcp = nil
485
+
486
+ # The originator of the message. Sometimes server, sometimes n!u@h
487
+ @origin = origin
488
+
489
+ # A space-tokenized array of anything after a colon
490
+ @params = params
491
+
492
+ # The full string from the IRC server
493
+ @raw = raw
494
+
495
+ # Usually the intended recipient; usually a user or channel
496
+ @target = target
484
497
 
485
498
  # Is the origin a user? Let's make this a little more simple...
486
499
  if m = ORIGIN_RE.match(@origin)
@@ -498,19 +511,39 @@ class Message
498
511
  public
499
512
  ######
500
513
 
514
+ #
515
+ # Was the message sent to a channel?
516
+ # ---
517
+ # returns:: +true+ or +false+
518
+ #
501
519
  def to_channel?
502
520
  %w(# & !).include?(@target[0])
503
521
  end
504
522
 
505
- def is_ctcp?
523
+ #
524
+ # Was the message formatted as a CTCP message?
525
+ # ---
526
+ # returns:: +true+ or +false+
527
+ #
528
+ def ctcp?
506
529
  @ctcp
507
530
  end
508
531
 
509
- def is_action?
532
+ #
533
+ # Was the message formatted as a CTCP action?
534
+ # ---
535
+ # returns:: +true+ or +false+
536
+ #
537
+ def action?
510
538
  @ctcp == :action
511
539
  end
512
540
 
513
- def is_dcc?
541
+ #
542
+ # Was the message formatted as a DCC notice?
543
+ # ---
544
+ # returns:: +true+ or +false+
545
+ #
546
+ def dcc?
514
547
  @ctcp == :dcc
515
548
  end
516
549
  end
@@ -7,6 +7,12 @@
7
7
 
8
8
  module IRC
9
9
 
10
+ #
11
+ # These methods are shortcuts for sending data to the IRC
12
+ # server. You can use `raw` to do any of them, or even add
13
+ # a string directly to `@sendq` if you really want. I'm sure
14
+ # I haven't thought of everything here.
15
+ #
10
16
  class Client
11
17
  ######
12
18
  public
@@ -68,7 +74,7 @@ class Client
68
74
  end
69
75
 
70
76
  # Sends an IRC MODE command.
71
- def mode(target, mode)
77
+ def mode(target, mode = '')
72
78
  @sendq << "MODE #{target} #{mode}"
73
79
  end
74
80
 
@@ -18,7 +18,7 @@ RPL_WELCOME = :'001'
18
18
  RPL_YOURHOST = :'002'
19
19
  RPL_CREATED = :'003'
20
20
  RPL_MYINFO = :'004'
21
- RPL_BOUNCE = :'005'
21
+ RPL_ISUPPORT = :'005'
22
22
 
23
23
  # ERRORS
24
24
  ERR_NOSUCHNICK = :'401'
@@ -0,0 +1,237 @@
1
+ #
2
+ # rhuidean: a small, lightweight IRC client library
3
+ # lib/rhuidean/stateful_channel.rb: state-keeping IRC channel
4
+ #
5
+ # Copyright (c) 2003-2010 Eric Will <rakaur@malkier.net>
6
+ #
7
+
8
+ # Import required app modules
9
+ %w(stateful_client stateful_user).each { |m| require 'rhuidean/' + m }
10
+
11
+ module IRC
12
+
13
+ ##
14
+ # Represents a channel on IRC.
15
+ #
16
+ class StatefulChannel
17
+
18
+ ##
19
+ # instance attributes
20
+ attr_reader :modes, :name, :users
21
+
22
+ ##
23
+ # Creates a new +StatefulChannel+.
24
+ # The channel has an +EventQueue+, but it really points to the EventQueue of
25
+ # the +StatefulClient+ that created it. This kind of breaks OOP,
26
+ # but it allows the Channel to post relevant events back to the client,
27
+ # like mode changes.
28
+ # ---
29
+ # name:: the name of the channel
30
+ # client:: the +IRC::Client+ that sees us
31
+ # returns:: +self+
32
+ #
33
+ def initialize(name, client)
34
+ # The Client we belong to
35
+ @client = client
36
+
37
+ # The channel's key
38
+ @key = nil
39
+
40
+ # The channel's user limit
41
+ @limit = 0
42
+
43
+ # The channel's modes
44
+ @modes = []
45
+
46
+ # The name of the channel, including the prefix
47
+ @name = name
48
+
49
+ # The list of StatefulUsers on the channel keyed by nickname
50
+ @users = IRCHash.new(@client.casemapping)
51
+ end
52
+
53
+ ######
54
+ public
55
+ ######
56
+
57
+ #
58
+ # Represent ourselves in a string.
59
+ # ---
60
+ # returns:: our name
61
+ #
62
+ def to_s
63
+ @name
64
+ end
65
+
66
+ #
67
+ # Add a user to our userlist.
68
+ # This also adds us to the user's channel list.
69
+ # ---
70
+ # user:: the +StatefulUser+ to add
71
+ # returns:: +self+
72
+ #
73
+ def add_user(user)
74
+ @users[user.nickname] = user
75
+ user.join_channel(self)
76
+
77
+ self
78
+ end
79
+
80
+ #
81
+ # Remove a user from our userlist.
82
+ # This also removes us from the user's channel list.
83
+ # ---
84
+ # user:: a +StatefulUser+ or the name of one
85
+ # returns:: +self+ (nil on catastrophic failure)
86
+ #
87
+ def delete_user(user)
88
+ if user.class == String
89
+ @users[user].part_channel(self)
90
+ @users.delete(user)
91
+
92
+ self
93
+ elsif user.class == StatefulUser
94
+ user.part_channel(self)
95
+ @users.delete(user.nickname)
96
+
97
+ self
98
+ else
99
+ nil
100
+ end
101
+ end
102
+
103
+ STATUS_MODES = { 'o' => :oper,
104
+ 'v' => :voice }
105
+
106
+ LIST_MODES = { 'b' => :ban,
107
+ 'e' => :except,
108
+ 'I' => :invex }
109
+
110
+ PARAM_MODES = { 'l' => :limited,
111
+ 'k' => :keyed }
112
+
113
+ BOOL_MODES = { 'i' => :invite_only,
114
+ 'm' => :moderated,
115
+ 'n' => :no_external,
116
+ 'p' => :private,
117
+ 's' => :secret,
118
+ 't' => :topic_lock }
119
+
120
+ #
121
+ # Parse a mode string.
122
+ # Update channel state for modes we know, and fire off events.
123
+ # ---
124
+ # m:: the IRC::Message object
125
+ # modes:: the mode string
126
+ # params:: an array of the params tokenized by space
127
+ # returns:: nothing of consequence...
128
+ #
129
+ def parse_modes(m, modes, params)
130
+ mode = nil # :add or :del
131
+
132
+ modes.each_char do |c|
133
+ flag, param = nil
134
+
135
+ if c == '+'
136
+ mode = :add
137
+ next
138
+ elsif c == '-'
139
+ mode = :del
140
+ next
141
+ end
142
+
143
+ # Status modes
144
+ if STATUS_MODES.include?(c)
145
+ flag = STATUS_MODES[c]
146
+ param = params.shift
147
+
148
+ # Status modes from RPL_ISUPPORT
149
+ elsif @client.status_modes.keys.include?(c)
150
+ flag = c.to_sym
151
+ param = params.shift
152
+
153
+ # List modes
154
+ elsif LIST_MODES.include?(c)
155
+ flag = LIST_MODES[c]
156
+ param = params.shift
157
+
158
+ # List modes from RPL_ISUPPORT
159
+ elsif @client.channel_modes[:list].include?(c)
160
+ flag = c.to_sym
161
+ param = params.shift
162
+
163
+ # Always has a param (some send the key, some send '*')
164
+ elsif c == 'k'
165
+ flag = :keyed
166
+ param = params.shift
167
+ @key = mode == :add ? param : nil
168
+
169
+ # Has a param when +, doesn't when -
170
+ elsif c == 'l'
171
+ flag = :limited
172
+ param = params.shift if mode == :add
173
+ @limit = mode == :add ? param : 0
174
+
175
+ # Always has a param from RPL_ISUPPORT
176
+ elsif @client.channel_modes[:always].include?(c)
177
+ flag = c.to_sym
178
+ param = params.shift
179
+
180
+ # Has a parm when +, doesn't when - from RPL_ISUPPORT
181
+ elsif @client.channel_modes[:set].include?(c)
182
+ flag = c.to_sym
183
+ param = params.shift if mode == :add
184
+
185
+ # The rest, no param
186
+ elsif BOOL_MODES.include?(c)
187
+ flag = BOOL_MODES[c]
188
+
189
+ # The rest, no param from RPL_ISUPPORT
190
+ elsif @client.channel_modes[:bool].include?(c)
191
+ flag = c.to_sym
192
+ end
193
+
194
+ # Add non-status and non-list modes to the channel's modes
195
+ unless junk_cmode?(c)
196
+ if mode == :add
197
+ @modes << flag
198
+ else
199
+ @modes.delete(flag)
200
+ end
201
+ end
202
+
203
+ # Update status modes for users
204
+ if status_mode?(c)
205
+ if mode == :add
206
+ @users[param].add_status_mode(flag, self)
207
+ elsif mode == :del
208
+ @users[param].delete_status_mode(flag, self)
209
+ end
210
+ end
211
+
212
+ # And send out events for everything
213
+ event = "mode_#{flag.to_s}".to_sym
214
+ @client.eventq.post(event, m, mode, param)
215
+ end
216
+ end
217
+
218
+ #######
219
+ private
220
+ #######
221
+
222
+ def status_mode?(modechar)
223
+ return true if STATUS_MODES.include?(modechar)
224
+ return true if @client.status_modes.keys.include?(modechar)
225
+ return false
226
+ end
227
+
228
+ def junk_cmode?(modechar)
229
+ return true if STATUS_MODES.include?(modechar)
230
+ return true if LIST_MODES.include?(modechar)
231
+ return true if @client.channel_modes[:list].include?(modechar)
232
+ return true if @client.status_modes.keys.include?(modechar)
233
+ return false
234
+ end
235
+ end
236
+
237
+ end # module IRC
@@ -0,0 +1,330 @@
1
+ #
2
+ # rhuidean: a small, lightweight IRC client library
3
+ # lib/rhuidean/stateful_client.rb: state-keeping IRC::Client
4
+ #
5
+ # Copyright (c) 2003-2010 Eric Will <rakaur@malkier.net>
6
+ #
7
+
8
+ # Import required app modules
9
+ %w(stateful_channel stateful_user).each { |m| require 'rhuidean/' + m }
10
+
11
+ module IRC
12
+
13
+ ##
14
+ # A +StatefulClient+ builds on +Client+ and tracks everything
15
+ # it does. This is useful for getting a head start on a bot.
16
+ #
17
+ class StatefulClient < Client
18
+
19
+ ##
20
+ # instance attributes
21
+ attr_reader :casemapping, :channels, :channel_modes, :eventq, :status_modes
22
+ attr_reader :users
23
+
24
+ ##
25
+ # Creates a new +StatefulClient+.
26
+ # ---
27
+ # returns:: +self+
28
+ #
29
+ def initialize
30
+ # StatefulChannels keyed by channel name
31
+ @channels = IRCHash.new(:rfc)
32
+
33
+ # Known channel types, from RPL_ISUPPORT
34
+ @channel_types = %w(# &)
35
+
36
+ # Additional channel modes from RPL_ISUPPORT
37
+ @channel_modes = {}
38
+
39
+ # Additional status modes we get from RPL_ISUPPORT
40
+ @status_modes = {}
41
+
42
+ # StatefulUsers we know about, keyed by nickname
43
+ @users = IRCHash.new(:rfc)
44
+
45
+ super
46
+ end
47
+
48
+ ######
49
+ public
50
+ ######
51
+
52
+ #
53
+ # Adds a +StatefulUser+ to our known-users list.
54
+ # ---
55
+ # user:: a +StatefulUser+
56
+ # returns:: +self+
57
+ #
58
+ def add_user(user)
59
+ @users[user.nickname] = user
60
+
61
+ self
62
+ end
63
+
64
+ #
65
+ # Removes a +StatefulUser+ from our known-users list, usually
66
+ # so it can die and be eaten by the GC.
67
+ # ---
68
+ # user:: either a +StatefulUser+ or the name of one
69
+ # returns:: +self+ (nil on catestrophic failure)
70
+ #
71
+ def delete_user(user)
72
+ if user.class == String
73
+ @users.delete(user)
74
+ self
75
+ elsif user.class == StatefulUser
76
+ @users.delete(user.nickname)
77
+ self
78
+ else
79
+ nil
80
+ end
81
+ end
82
+
83
+ #######
84
+ private
85
+ #######
86
+
87
+ PREFIX_RE = /^\((\w+)\)(.*)$/
88
+
89
+ # Set up our event handlers
90
+ def set_default_handlers
91
+ on(:dead) do
92
+ @channels.clear
93
+ @users.clear
94
+ end
95
+
96
+ on(Numeric::RPL_ISUPPORT) { |m| do_rpl_isupport(m) }
97
+
98
+ on(:JOIN) { |m| do_join(m) }
99
+ on(:PART) { |m| do_part(m) }
100
+ on(:NICK) { |m| do_nick(m) }
101
+ on(:KICK) { |m| do_kick(m) }
102
+ on(:QUIT) { |m| do_quit(m) }
103
+
104
+ # Parse and sync channel modes
105
+ on(:MODE) do |m|
106
+ next unless @channel_types.include?(m.target[0])
107
+ @channels[m.target].parse_modes(m, m.params[0], m.params[1..-1])
108
+ end
109
+
110
+ # Parse reply from MODE
111
+ on(Numeric::RPL_CHANNELMODEIS) do |m|
112
+ @channels[m.params[0]].parse_modes(m, m.params[1], m.params[2..-1])
113
+ end
114
+
115
+ # Sync current users in channel
116
+ on(Numeric::RPL_NAMEREPLY) { |m| do_rpl_namereply(m) }
117
+
118
+ super
119
+ end
120
+
121
+ # Parse RPL_ISUPPORT to make us smarter!
122
+ def do_rpl_isupport(m)
123
+ supported = []
124
+
125
+ m.params.each { |param| supported << param.split('=') }
126
+
127
+ supported.each do |name, value|
128
+ case name
129
+
130
+ # CASEMAPPING=rfc1459
131
+ when 'CASEMAPPING'
132
+ if value == "rfc1459"
133
+ @casemapping = :rfc
134
+ @channels = IRCHash.new(:rfc)
135
+ @users = IRCHash.new(:rfc)
136
+ elsif value == "ascii"
137
+ @casemapping = :ascii
138
+ @channels = IRCHash.new(:ascii)
139
+ @users = IRCHash.new(:ascii)
140
+ end
141
+
142
+ # CHANMODES=eIb,k,l,imnpst
143
+ # Fields are: list param, always param, param when +, no param
144
+ when 'CHANMODES'
145
+ listp, alwaysp, setp, nop = value.split(',')
146
+
147
+ @channel_modes[:list] = listp.split('')
148
+ @channel_modes[:always] = alwaysp.split('')
149
+ @channel_modes[:set] = setp.split('')
150
+ @channel_modes[:bool] = nop.split('')
151
+
152
+ # CHANTYPES=&#
153
+ when 'CHANTYPES'
154
+ @channel_types = value.split('')
155
+
156
+ # PREFIX=(ov)@+
157
+ when 'PREFIX'
158
+ m = PREFIX_RE.match(value)
159
+ modes, prefixes = m[1], m[2]
160
+
161
+ modes = modes.split('')
162
+ prefixes = prefixes.split('')
163
+
164
+ modes.each_with_index do |m, i|
165
+ @status_modes[m] = prefixes[i]
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ def do_join(m)
172
+ # Track channels
173
+ if m.origin_nick == @nickname
174
+ @channels[m.target] = StatefulChannel.new(m.target, self)
175
+ mode(m.target) # Get the channel modes
176
+ else
177
+ nick = m.origin_nick
178
+ user = @users[nick] || StatefulUser.new(nick, self)
179
+ @users[user.nickname] ||= user
180
+
181
+ @channels[m.target].add_user(user)
182
+ debug("join: #{user} -> #{m.target}")
183
+ end
184
+ end
185
+
186
+ def do_part(m)
187
+ # Track channels
188
+ if m.origin_nick == @nickname
189
+ chan = @channels[m.target]
190
+
191
+ # We can't see if they're in the channel we parted
192
+ chan.users.each { |n, user| user.part_channel(chan) }
193
+
194
+ # If we have users in no channels they must die
195
+ @users.delete_if { |n, user| user.channels.empty? }
196
+
197
+ @channels.delete(chan.name)
198
+
199
+ debug("parted: #{chan.name}")
200
+ else
201
+ user = @users[m.origin_nick]
202
+
203
+ @channels[m.target].delete_user(user)
204
+ debug("part: #{user.nickname} -> #{m.origin_nick}")
205
+
206
+ delete_user(user) if user.channels.empty?
207
+ end
208
+ end
209
+
210
+ def do_nick(m)
211
+ # Get the user object
212
+ user = @users[m.origin_nick]
213
+
214
+ # Rename it, rekey it, and delete the old one
215
+ user.nickname = m.target
216
+ @users[m.target] = user
217
+ @users.delete(m.origin_nick)
218
+
219
+ # We have to rekey it on channel lists, too
220
+ user.channels.each do |name, channel|
221
+ channel.users[m.target] = user
222
+ channel.users.delete(m.origin_nick)
223
+ end
224
+
225
+ debug("nick: #{m.origin_nick} -> #{m.target}")
226
+ end
227
+
228
+ def do_kick(m)
229
+ # Track channels
230
+ if m.params[0] == @nickname
231
+ chan = @channels[m.target]
232
+
233
+ # We can't see if they're in the channel we got kicked from
234
+ chan.users.each { |n, user| user.part_channel(chan) }
235
+
236
+ # If we have users in no channels they must die
237
+ @users.delete_if { |n, user| user.channels.empty? }
238
+
239
+ @channels.delete(chan.name)
240
+
241
+ debug("kicked: #{chan.name}")
242
+ else
243
+ user = @users[m.params[0]]
244
+
245
+ @channels[m.target].delete_user(user)
246
+ debug("kick: #{user.nickname} -> #{m.origin_nick}")
247
+
248
+ delete_user(user) if user.channels.empty?
249
+ end
250
+ end
251
+
252
+ def do_quit(m)
253
+ if m.origin_nick == @nickname
254
+ @eventq.post(:dead)
255
+ else
256
+ user = @users[m.origin_nick]
257
+
258
+ user.channels.each { |name, chan| chan.delete_user(user) }
259
+
260
+ delete_user(user)
261
+ debug("quit: #{user.nickname}")
262
+ end
263
+ end
264
+
265
+ #
266
+ # In the case of multiple status modes, we assume the ircd sends only
267
+ # the uppermost (i.e.: @ when @+). My testing, even with stupid ircds
268
+ # with a thousand modes, seems to support this.
269
+ #
270
+ def do_rpl_namereply(m)
271
+ chan = @channels[m.params[1]]
272
+ names = m.params[2..-1]
273
+ names[0] = names[0][1..-1] # Get rid of leading ':'
274
+ modes = @status_modes.keys
275
+ prefixes = @status_modes.values
276
+ name_re = /^([#{prefixes}])*(.+)/
277
+
278
+ names.each do |name|
279
+ m = name_re.match(name)
280
+ user = @users[m[2]] || StatefulUser.new(m[2], self)
281
+ prefix = m[1]
282
+
283
+ @users[user.nickname] ||= user
284
+
285
+ if prefix == '@'
286
+ user.add_status_mode(:oper, chan)
287
+
288
+ elsif prefix == '+'
289
+ user.add_status_mode(:voice, chan)
290
+
291
+ elsif prefixes.include?(prefix)
292
+ smode = modes[prefixes.find_index(prefix)]
293
+ user.add_status_mode(smode.to_sym, chan)
294
+ end
295
+
296
+ chan.add_user(user)
297
+ debug("names: #{user} -> #{chan}")
298
+ end
299
+ end
300
+ end
301
+
302
+ end # module IRC
303
+
304
+ # So we don't have to do @channels[irc_downcase(name)] constantly
305
+ class IRCHash < Hash
306
+ def initialize(casemapping)
307
+ @casemapping = casemapping
308
+
309
+ super()
310
+ end
311
+
312
+ def [](key)
313
+ key = irc_downcase(key)
314
+ super(key)
315
+ end
316
+
317
+ def []=(key, value)
318
+ key = irc_downcase(key)
319
+ super(key, value)
320
+ end
321
+
322
+ def irc_downcase(string)
323
+ if @casemapping == :rfc
324
+ string.downcase.tr('{}|^', '[]\\~')
325
+ else
326
+ string
327
+ end
328
+ end
329
+ end
330
+
@@ -0,0 +1,117 @@
1
+ #
2
+ # rhuidean: a small, lightweight IRC client library
3
+ # lib/rhuidean/stateful_user.rb: state-keeping IRC user
4
+ #
5
+ # Copyright (c) 2003-2010 Eric Will <rakaur@malkier.net>
6
+ #
7
+
8
+ # Import required app modules
9
+ %w(stateful_client stateful_channel).each { |m| require 'rhuidean/' + m }
10
+
11
+ module IRC
12
+
13
+ #
14
+ # Represents a user on IRC. Each nickname should only have one of
15
+ # these objects, no matter how many channels they're in. If we can't
16
+ # see them in any channels they disappear.
17
+ #
18
+ class StatefulUser
19
+
20
+ ##
21
+ # instance attributes
22
+ attr_reader :channels, :modes
23
+ attr_accessor :nickname
24
+
25
+ ##
26
+ # Creates a new +StatefulUser+.
27
+ # ---
28
+ # nickname:: the user's nickname, as a string
29
+ # client:: the +IRC::Client+ that sees us
30
+ # returns::+ self+
31
+ #
32
+ def initialize(nickname, client)
33
+ # The Client we belong to
34
+ @client = client
35
+
36
+ # StatefulChannels we're on, keyed by name
37
+ @channels = IRCHash.new(@client.casemapping)
38
+
39
+ # Status modes on channels, keyed by channel name (:oper, :voice)
40
+ @modes = {}
41
+
42
+ # The user's nickname
43
+ @nickname = nickname
44
+ end
45
+
46
+ ######
47
+ public
48
+ ######
49
+
50
+ #
51
+ # Represent ourselves in a string.
52
+ # ---
53
+ # returns:: our nickname
54
+ #
55
+ def to_s
56
+ @nickname
57
+ end
58
+
59
+ #
60
+ # Add a channel to our joined-list.
61
+ # ---
62
+ # channel:: the +StatefulChannel+ to add
63
+ # returns:: +self+
64
+ #
65
+ def join_channel(channel)
66
+ @channels[channel.name] = channel
67
+
68
+ self
69
+ end
70
+
71
+ #
72
+ # Remove a channel from our joined-list.
73
+ # Also clears our status modes for that channel.
74
+ # ---
75
+ # channel:: the +StatefulChannel+ to remove
76
+ # returns:: +self+
77
+ #
78
+ def part_channel(channel)
79
+ @modes.delete(channel.name)
80
+ @channels.delete(channel.name)
81
+
82
+ self
83
+ end
84
+
85
+ #
86
+ # Give us a status mode on a channel.
87
+ # ---
88
+ # flag:: +Symbol+ representing a mode flag
89
+ # channel:: either a +StatefulChannel+ or the name of one
90
+ # returns:: +self+
91
+ #
92
+ def add_status_mode(flag, channel)
93
+ if channel.class == StatefulChannel then channel = channel.name end
94
+ (@modes[channel] ||= []) << flag
95
+
96
+ self
97
+ end
98
+
99
+ #
100
+ # Take away a status mode on a channel.
101
+ # ---
102
+ # flag:: +Symbol+ representing a mode flag
103
+ # channel:: either a +StatefulChannel+ or the name of one
104
+ # returns:: +self+
105
+ #
106
+ def delete_status_mode(flag, channel)
107
+ if channel.class == StatefulChannel then channel = channel.name end
108
+ return unless @modes[channel]
109
+
110
+ @modes[channel].delete(flag)
111
+
112
+ self
113
+ end
114
+ end
115
+
116
+ end # module IRC
117
+
data/rakefile CHANGED
@@ -10,7 +10,10 @@
10
10
  require 'rake/' + m
11
11
  end
12
12
 
13
- VER = '0.2.7'
13
+ $: << Dir.getwd
14
+ $: << 'lib'
15
+
16
+ require 'lib/rhuidean'
14
17
 
15
18
  #
16
19
  # Default task - unit tests.
@@ -20,7 +23,6 @@ VER = '0.2.7'
20
23
  task :default => [:test]
21
24
 
22
25
  Rake::TestTask.new do |t|
23
- t.libs << 'test'
24
26
  t.test_files = %w(test/ts_rhuidean.rb)
25
27
  end
26
28
 
@@ -46,7 +48,7 @@ PKG_FILES = FileList['README.markdown', 'rakefile',
46
48
 
47
49
  Rake::PackageTask.new('package') do |p|
48
50
  p.name = 'rhuidean'
49
- p.version = VER
51
+ p.version = Rhuidean::VERSION
50
52
  p.need_tar = false
51
53
  p.need_zip = false
52
54
  p.package_files = PKG_FILES
@@ -54,7 +56,7 @@ end
54
56
 
55
57
  spec = Gem::Specification.new do |s|
56
58
  s.name = 'rhuidean'
57
- s.version = VER
59
+ s.version = Rhuidean::VERSION
58
60
  s.author = 'Eric Will'
59
61
  s.email = 'rakaur@malkier.net'
60
62
  s.homepage = 'http://github.com/rakaur/rhuidean/'
@@ -51,24 +51,22 @@ class TestClient < Test::Unit::TestCase
51
51
  assert_match(/^rhuidean\d+$/, c.nickname)
52
52
  assert_equal('rhuidean', c.username)
53
53
 
54
- welcome = false
55
- str = "rhuidean-#{IRC::Client::VERSION} [#{RUBY_PLATFORM}]"
54
+ worked = false
55
+ str = "rhuidean-#{Rhuidean::VERSION} [#{RUBY_PLATFORM}]"
56
56
 
57
57
  assert_nothing_raised do
58
- c.on(IRC::Numeric::RPL_WELCOME) { welcome = true }
59
-
60
58
  c.on(IRC::Numeric::RPL_ENDOFMOTD) do
61
59
  c.join('#malkier')
62
60
  c.privmsg('#malkier', str)
61
+ worked = true
63
62
  end
64
63
  end
65
64
 
66
- assert_nothing_raised { c.connect }
65
+ c.thread = Thread.new { c.io_loop }
67
66
 
68
- t = Thread.new { c.io_loop }
67
+ sleep(1) until worked
69
68
 
70
- sleep(1) until welcome
71
- assert(true, welcome)
69
+ assert(worked)
72
70
  end
73
71
  end
74
72
 
@@ -5,11 +5,9 @@
5
5
  # Copyright (c) 2003-2010 Eric Will <rakaur@malkier.net>
6
6
  #
7
7
 
8
- $: << 'lib'
8
+ $: << Dir.getwd
9
9
 
10
10
  require 'test/unit'
11
-
12
- require 'rhuidean'
13
-
11
+ require 'lib/rhuidean'
14
12
  require 'test/tc_client.rb'
15
13
 
metadata CHANGED
@@ -3,10 +3,10 @@ name: rhuidean
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
+ - 1
6
7
  - 0
7
- - 2
8
- - 7
9
- version: 0.2.7
8
+ - 0
9
+ version: 1.0.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Eric Will
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-10-25 00:00:00 -04:00
17
+ date: 2010-11-10 00:00:00 -05:00
18
18
  default_executable:
19
19
  dependencies: []
20
20
 
@@ -35,6 +35,9 @@ files:
35
35
  - lib/rhuidean/event.rb
36
36
  - lib/rhuidean/methods.rb
37
37
  - lib/rhuidean/numeric.rb
38
+ - lib/rhuidean/stateful_channel.rb
39
+ - lib/rhuidean/stateful_client.rb
40
+ - lib/rhuidean/stateful_user.rb
38
41
  - test/tc_client.rb
39
42
  - test/ts_rhuidean.rb
40
43
  has_rdoc: true