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.
- data/README.markdown +5 -2
- data/lib/rhuidean.rb +10 -1
- data/lib/rhuidean/client.rb +95 -62
- data/lib/rhuidean/methods.rb +7 -1
- data/lib/rhuidean/numeric.rb +1 -1
- data/lib/rhuidean/stateful_channel.rb +237 -0
- data/lib/rhuidean/stateful_client.rb +330 -0
- data/lib/rhuidean/stateful_user.rb +117 -0
- data/rakefile +6 -4
- data/test/tc_client.rb +6 -8
- data/test/ts_rhuidean.rb +2 -4
- metadata +7 -4
data/README.markdown
CHANGED
|
@@ -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
|
-
|
|
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
|
---------------
|
data/lib/rhuidean.rb
CHANGED
|
@@ -5,7 +5,16 @@
|
|
|
5
5
|
# Copyright (c) 2003-2010 Eric Will <rakaur@malkier.net>
|
|
6
6
|
#
|
|
7
7
|
|
|
8
|
-
|
|
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
|
data/lib/rhuidean/client.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
#
|
|
444
|
+
# Represent ourselves in a string.
|
|
413
445
|
# ---
|
|
414
|
-
#
|
|
415
|
-
# returns:: +self+
|
|
446
|
+
# returns:: our nickname and object ID
|
|
416
447
|
#
|
|
417
|
-
def
|
|
418
|
-
@
|
|
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
|
-
|
|
483
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/rhuidean/methods.rb
CHANGED
|
@@ -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
|
|
data/lib/rhuidean/numeric.rb
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
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/'
|
data/test/tc_client.rb
CHANGED
|
@@ -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
|
-
|
|
55
|
-
str = "rhuidean-#{
|
|
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
|
-
|
|
65
|
+
c.thread = Thread.new { c.io_loop }
|
|
67
66
|
|
|
68
|
-
|
|
67
|
+
sleep(1) until worked
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
assert(true, welcome)
|
|
69
|
+
assert(worked)
|
|
72
70
|
end
|
|
73
71
|
end
|
|
74
72
|
|
data/test/ts_rhuidean.rb
CHANGED
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
|
-
-
|
|
8
|
-
|
|
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
|
|
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
|