net-irc 0.0.3 → 0.0.4

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.
@@ -0,0 +1,227 @@
1
+ class Net::IRC::Client
2
+ include Net::IRC
3
+ include Constants
4
+
5
+ attr_reader :host, :port, :opts
6
+ attr_reader :prefix, :channels
7
+
8
+ def initialize(host, port, opts={})
9
+ @host = host
10
+ @port = port
11
+ @opts = OpenStruct.new(opts)
12
+ @log = @opts.logger || Logger.new($stdout)
13
+ @channels = {
14
+ # "#channel" => {
15
+ # :modes => [],
16
+ # :users => [],
17
+ # }
18
+ }
19
+ @channels.extend(MonitorMixin)
20
+ end
21
+
22
+ # Connect to server and start loop.
23
+ def start
24
+ @socket = TCPSocket.open(@host, @port)
25
+ on_connected
26
+ post PASS, @opts.pass if @opts.pass
27
+ post NICK, @opts.nick
28
+ post USER, @opts.user, "0", "*", @opts.real
29
+ while l = @socket.gets
30
+ begin
31
+ @log.debug "RECEIVE: #{l.chomp}"
32
+ m = Message.parse(l)
33
+ next if on_message(m) === true
34
+ name = "on_#{(COMMANDS[m.command.upcase] || m.command).downcase}"
35
+ send(name, m) if respond_to?(name)
36
+ rescue Exception => e
37
+ warn e
38
+ warn e.backtrace.join("\r\t")
39
+ raise
40
+ rescue Message::InvalidMessage
41
+ @log.error "MessageParse: " + l.inspect
42
+ end
43
+ end
44
+ rescue IOError
45
+ ensure
46
+ finish
47
+ end
48
+
49
+ # Close connection to server.
50
+ def finish
51
+ begin
52
+ @socket.close
53
+ rescue
54
+ end
55
+ on_disconnected
56
+ end
57
+
58
+ # Catch all messages.
59
+ # If this method return true, aother callback will not be called.
60
+ def on_message(m)
61
+ end
62
+
63
+ # Default RPL_WELCOME callback.
64
+ # This sets @prefix from the message.
65
+ def on_rpl_welcome(m)
66
+ @prefix = Prefix.new(m[1][/\S+$/])
67
+ end
68
+
69
+ # Default PING callback. Response PONG.
70
+ def on_ping(m)
71
+ post PONG, @prefix ? @prefix.nick : ""
72
+ end
73
+
74
+ # For managing channel
75
+ def on_rpl_namreply(m)
76
+ type = m[1]
77
+ channel = m[2]
78
+ init_channel(channel)
79
+
80
+ @channels.synchronize do
81
+ m[3].split(/\s+/).each do |u|
82
+ _, mode, nick = *u.match(/^([@+]?)(.+)/)
83
+
84
+ @channels[channel][:users] << nick
85
+ @channels[channel][:users].uniq!
86
+
87
+ case mode
88
+ when "@" # channel operator
89
+ @channels[channel][:modes] << [:o, nick]
90
+ when "+" # voiced (under moderating mode)
91
+ @channels[channel][:modes] << [:v, nick]
92
+ end
93
+ end
94
+
95
+ case type
96
+ when "@" # secret
97
+ @channels[channel][:modes] << [:s, nil]
98
+ when "*" # private
99
+ @channels[channel][:modes] << [:p, nil]
100
+ when "=" # public
101
+ end
102
+
103
+ @channels[channel][:modes].uniq!
104
+ end
105
+ end
106
+
107
+ # For managing channel
108
+ def on_part(m)
109
+ nick = m.prefix.nick
110
+ channel = m[0]
111
+ init_channel(channel)
112
+
113
+ @channels.synchronize do
114
+ info = @channels[channel]
115
+ if info
116
+ info[:users].delete(nick)
117
+ info[:modes].delete_if {|u|
118
+ u[1] == nick
119
+ }
120
+ end
121
+ end
122
+ end
123
+
124
+ # For managing channel
125
+ def on_quit(m)
126
+ nick = m.prefix.nick
127
+
128
+ @channels.synchronize do
129
+ @channels.each do |channel, info|
130
+ info[:users].delete(nick)
131
+ info[:modes].delete_if {|u|
132
+ u[1] == nick
133
+ }
134
+ end
135
+ end
136
+ end
137
+
138
+ # For managing channel
139
+ def on_kick(m)
140
+ users = m[1].split(/,/)
141
+
142
+ @channels.synchronize do
143
+ m[0].split(/,/).each do |chan|
144
+ init_channel(chan)
145
+ info = @channels[chan]
146
+ if info
147
+ users.each do |nick|
148
+ info[:users].delete(nick)
149
+ info[:modes].delete_if {|u|
150
+ u[1] == nick
151
+ }
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ # For managing channel
159
+ def on_join(m)
160
+ nick = m.prefix.nick
161
+ channel = m[0]
162
+
163
+ @channels.synchronize do
164
+ init_channel(channel)
165
+
166
+ @channels[channel][:users] << nick
167
+ @channels[channel][:users].uniq!
168
+ end
169
+ end
170
+
171
+ # For managing channel
172
+ def on_mode(m)
173
+ channel = m[0]
174
+ @channels.synchronize do
175
+ init_channel(channel)
176
+
177
+ mode = Message::ModeParser::RFC1459::Channel.parse(m)
178
+ mode[:negative].each do |m|
179
+ @channels[channel][:modes].delete(m)
180
+ end
181
+
182
+ mode[:positive].each do |m|
183
+ @channels[channel][:modes] << m
184
+ end
185
+
186
+ @channels[channel][:modes].uniq!
187
+ [mode[:negative], mode[:positive]]
188
+ end
189
+ end
190
+
191
+ # For managing channel
192
+ def init_channel(channel)
193
+ @channels[channel] ||= {
194
+ :modes => [],
195
+ :users => [],
196
+ }
197
+ end
198
+
199
+ # Do nothing.
200
+ # This is for avoiding error on calling super.
201
+ # So you can always call super at subclass.
202
+ def method_missing(name, *args)
203
+ end
204
+
205
+ # Call when socket connected.
206
+ def on_connected
207
+ end
208
+
209
+ # Call when socket closed.
210
+ def on_disconnected
211
+ end
212
+
213
+ private
214
+
215
+ # Post message to server.
216
+ #
217
+ # include Net::IRC::Constants
218
+ # post PRIVMSG, "#channel", "foobar"
219
+ def post(command, *params)
220
+ m = Message.new(nil, command, params.map {|s|
221
+ s ? s.gsub(/[\r\n]/, " ") : ""
222
+ })
223
+
224
+ @log.debug "SEND: #{m.to_s.chomp}"
225
+ @socket << m
226
+ end
227
+ end # Client
@@ -0,0 +1,214 @@
1
+ module Net::IRC::Constants # :nodoc:
2
+ RPL_WELCOME = '001'
3
+ RPL_YOURHOST = '002'
4
+ RPL_CREATED = '003'
5
+ RPL_MYINFO = '004'
6
+ RPL_BOUNCE = '005'
7
+ RPL_USERHOST = '302'
8
+ RPL_ISON = '303'
9
+ RPL_AWAY = '301'
10
+ RPL_UNAWAY = '305'
11
+ RPL_NOWAWAY = '306'
12
+ RPL_WHOISUSER = '311'
13
+ RPL_WHOISSERVER = '312'
14
+ RPL_WHOISOPERATOR = '313'
15
+ RPL_WHOISIDLE = '317'
16
+ RPL_ENDOFWHOIS = '318'
17
+ RPL_WHOISCHANNELS = '319'
18
+ RPL_WHOWASUSER = '314'
19
+ RPL_ENDOFWHOWAS = '369'
20
+ RPL_LISTSTART = '321'
21
+ RPL_LIST = '322'
22
+ RPL_LISTEND = '323'
23
+ RPL_UNIQOPIS = '325'
24
+ RPL_CHANNELMODEIS = '324'
25
+ RPL_NOTOPIC = '331'
26
+ RPL_TOPIC = '332'
27
+ RPL_INVITING = '341'
28
+ RPL_SUMMONING = '342'
29
+ RPL_INVITELIST = '346'
30
+ RPL_ENDOFINVITELIST = '347'
31
+ RPL_EXCEPTLIST = '348'
32
+ RPL_ENDOFEXCEPTLIST = '349'
33
+ RPL_VERSION = '351'
34
+ RPL_WHOREPLY = '352'
35
+ RPL_ENDOFWHO = '315'
36
+ RPL_NAMREPLY = '353'
37
+ RPL_ENDOFNAMES = '366'
38
+ RPL_LINKS = '364'
39
+ RPL_ENDOFLINKS = '365'
40
+ RPL_BANLIST = '367'
41
+ RPL_ENDOFBANLIST = '368'
42
+ RPL_INFO = '371'
43
+ RPL_ENDOFINFO = '374'
44
+ RPL_MOTDSTART = '375'
45
+ RPL_MOTD = '372'
46
+ RPL_ENDOFMOTD = '376'
47
+ RPL_YOUREOPER = '381'
48
+ RPL_REHASHING = '382'
49
+ RPL_YOURESERVICE = '383'
50
+ RPL_TIME = '391'
51
+ RPL_USERSSTART = '392'
52
+ RPL_USERS = '393'
53
+ RPL_ENDOFUSERS = '394'
54
+ RPL_NOUSERS = '395'
55
+ RPL_TRACELINK = '200'
56
+ RPL_TRACECONNECTING = '201'
57
+ RPL_TRACEHANDSHAKE = '202'
58
+ RPL_TRACEUNKNOWN = '203'
59
+ RPL_TRACEOPERATOR = '204'
60
+ RPL_TRACEUSER = '205'
61
+ RPL_TRACESERVER = '206'
62
+ RPL_TRACESERVICE = '207'
63
+ RPL_TRACENEWTYPE = '208'
64
+ RPL_TRACECLASS = '209'
65
+ RPL_TRACERECONNECT = '210'
66
+ RPL_TRACELOG = '261'
67
+ RPL_TRACEEND = '262'
68
+ RPL_STATSLINKINFO = '211'
69
+ RPL_STATSCOMMANDS = '212'
70
+ RPL_ENDOFSTATS = '219'
71
+ RPL_STATSUPTIME = '242'
72
+ RPL_STATSOLINE = '243'
73
+ RPL_UMODEIS = '221'
74
+ RPL_SERVLIST = '234'
75
+ RPL_SERVLISTEND = '235'
76
+ RPL_LUSERCLIENT = '251'
77
+ RPL_LUSEROP = '252'
78
+ RPL_LUSERUNKNOWN = '253'
79
+ RPL_LUSERCHANNELS = '254'
80
+ RPL_LUSERME = '255'
81
+ RPL_ADMINME = '256'
82
+ RPL_ADMINLOC1 = '257'
83
+ RPL_ADMINLOC2 = '258'
84
+ RPL_ADMINEMAIL = '259'
85
+ RPL_TRYAGAIN = '263'
86
+ ERR_NOSUCHNICK = '401'
87
+ ERR_NOSUCHSERVER = '402'
88
+ ERR_NOSUCHCHANNEL = '403'
89
+ ERR_CANNOTSENDTOCHAN = '404'
90
+ ERR_TOOMANYCHANNELS = '405'
91
+ ERR_WASNOSUCHNICK = '406'
92
+ ERR_TOOMANYTARGETS = '407'
93
+ ERR_NOSUCHSERVICE = '408'
94
+ ERR_NOORIGIN = '409'
95
+ ERR_NORECIPIENT = '411'
96
+ ERR_NOTEXTTOSEND = '412'
97
+ ERR_NOTOPLEVEL = '413'
98
+ ERR_WILDTOPLEVEL = '414'
99
+ ERR_BADMASK = '415'
100
+ ERR_UNKNOWNCOMMAND = '421'
101
+ ERR_NOMOTD = '422'
102
+ ERR_NOADMININFO = '423'
103
+ ERR_FILEERROR = '424'
104
+ ERR_NONICKNAMEGIVEN = '431'
105
+ ERR_ERRONEUSNICKNAME = '432'
106
+ ERR_NICKNAMEINUSE = '433'
107
+ ERR_NICKCOLLISION = '436'
108
+ ERR_UNAVAILRESOURCE = '437'
109
+ ERR_USERNOTINCHANNEL = '441'
110
+ ERR_NOTONCHANNEL = '442'
111
+ ERR_USERONCHANNEL = '443'
112
+ ERR_NOLOGIN = '444'
113
+ ERR_SUMMONDISABLED = '445'
114
+ ERR_USERSDISABLED = '446'
115
+ ERR_NOTREGISTERED = '451'
116
+ ERR_NEEDMOREPARAMS = '461'
117
+ ERR_ALREADYREGISTRED = '462'
118
+ ERR_NOPERMFORHOST = '463'
119
+ ERR_PASSWDMISMATCH = '464'
120
+ ERR_YOUREBANNEDCREEP = '465'
121
+ ERR_YOUWILLBEBANNED = '466'
122
+ ERR_KEYSET = '467'
123
+ ERR_CHANNELISFULL = '471'
124
+ ERR_UNKNOWNMODE = '472'
125
+ ERR_INVITEONLYCHAN = '473'
126
+ ERR_BANNEDFROMCHAN = '474'
127
+ ERR_BADCHANNELKEY = '475'
128
+ ERR_BADCHANMASK = '476'
129
+ ERR_NOCHANMODES = '477'
130
+ ERR_BANLISTFULL = '478'
131
+ ERR_NOPRIVILEGES = '481'
132
+ ERR_CHANOPRIVSNEEDED = '482'
133
+ ERR_CANTKILLSERVER = '483'
134
+ ERR_RESTRICTED = '484'
135
+ ERR_UNIQOPPRIVSNEEDED = '485'
136
+ ERR_NOOPERHOST = '491'
137
+ ERR_UMODEUNKNOWNFLAG = '501'
138
+ ERR_USERSDONTMATCH = '502'
139
+ RPL_SERVICEINFO = '231'
140
+ RPL_ENDOFSERVICES = '232'
141
+ RPL_SERVICE = '233'
142
+ RPL_NONE = '300'
143
+ RPL_WHOISCHANOP = '316'
144
+ RPL_KILLDONE = '361'
145
+ RPL_CLOSING = '362'
146
+ RPL_CLOSEEND = '363'
147
+ RPL_INFOSTART = '373'
148
+ RPL_MYPORTIS = '384'
149
+ RPL_STATSCLINE = '213'
150
+ RPL_STATSNLINE = '214'
151
+ RPL_STATSILINE = '215'
152
+ RPL_STATSKLINE = '216'
153
+ RPL_STATSQLINE = '217'
154
+ RPL_STATSYLINE = '218'
155
+ RPL_STATSVLINE = '240'
156
+ RPL_STATSLLINE = '241'
157
+ RPL_STATSHLINE = '244'
158
+ RPL_STATSSLINE = '244'
159
+ RPL_STATSPING = '246'
160
+ RPL_STATSBLINE = '247'
161
+ RPL_STATSDLINE = '250'
162
+ ERR_NOSERVICEHOST = '492'
163
+
164
+ PASS = 'PASS'
165
+ NICK = 'NICK'
166
+ USER = 'USER'
167
+ OPER = 'OPER'
168
+ MODE = 'MODE'
169
+ SERVICE = 'SERVICE'
170
+ QUIT = 'QUIT'
171
+ SQUIT = 'SQUIT'
172
+ JOIN = 'JOIN'
173
+ PART = 'PART'
174
+ TOPIC = 'TOPIC'
175
+ NAMES = 'NAMES'
176
+ LIST = 'LIST'
177
+ INVITE = 'INVITE'
178
+ KICK = 'KICK'
179
+ PRIVMSG = 'PRIVMSG'
180
+ NOTICE = 'NOTICE'
181
+ MOTD = 'MOTD'
182
+ LUSERS = 'LUSERS'
183
+ VERSION = 'VERSION'
184
+ STATS = 'STATS'
185
+ LINKS = 'LINKS'
186
+ TIME = 'TIME'
187
+ CONNECT = 'CONNECT'
188
+ TRACE = 'TRACE'
189
+ ADMIN = 'ADMIN'
190
+ INFO = 'INFO'
191
+ SERVLIST = 'SERVLIST'
192
+ SQUERY = 'SQUERY'
193
+ WHO = 'WHO'
194
+ WHOIS = 'WHOIS'
195
+ WHOWAS = 'WHOWAS'
196
+ KILL = 'KILL'
197
+ PING = 'PING'
198
+ PONG = 'PONG'
199
+ ERROR = 'ERROR'
200
+ AWAY = 'AWAY'
201
+ REHASH = 'REHASH'
202
+ DIE = 'DIE'
203
+ RESTART = 'RESTART'
204
+ SUMMON = 'SUMMON'
205
+ USERS = 'USERS'
206
+ WALLOPS = 'WALLOPS'
207
+ USERHOST = 'USERHOST'
208
+ ISON = 'ISON'
209
+ end
210
+
211
+ Net::IRC::COMMANDS = Net::IRC::Constants.constants.inject({}) {|r,i| # :nodoc:
212
+ r.update(Net::IRC::Constants.const_get(i) => i)
213
+ }
214
+
@@ -0,0 +1,100 @@
1
+ class Net::IRC::Message
2
+ include Net::IRC
3
+
4
+ class InvalidMessage < Net::IRC::IRCException; end
5
+
6
+ attr_reader :prefix, :command, :params
7
+
8
+ # Parse string and return new Message.
9
+ # If the string is invalid message, this method raises Net::IRC::Message::InvalidMessage.
10
+ def self.parse(str)
11
+ _, prefix, command, *rest = *PATTERN::MESSAGE_PATTERN.match(str)
12
+ raise InvalidMessage, "Invalid message: #{str.dump}" unless _
13
+
14
+ case
15
+ when rest[0] && !rest[0].empty?
16
+ middle, trailer, = *rest
17
+ when rest[2] && !rest[2].empty?
18
+ middle, trailer, = *rest[2, 2]
19
+ when rest[1]
20
+ params = []
21
+ trailer = rest[1]
22
+ when rest[3]
23
+ params = []
24
+ trailer = rest[3]
25
+ else
26
+ params = []
27
+ end
28
+
29
+ params ||= middle.split(/ /)[1..-1]
30
+ params << trailer if trailer
31
+
32
+ new(prefix, command, params)
33
+ end
34
+
35
+ def initialize(prefix, command, params)
36
+ @prefix = Prefix.new(prefix.to_s)
37
+ @command = command
38
+ @params = params
39
+ end
40
+
41
+ # Same as @params[n].
42
+ def [](n)
43
+ @params[n]
44
+ end
45
+
46
+ # Iterate params.
47
+ def each(&block)
48
+ @params.each(&block)
49
+ end
50
+
51
+ # Stringfy message to raw IRC message.
52
+ def to_s
53
+ str = ""
54
+
55
+ str << ":#{@prefix} " unless @prefix.empty?
56
+ str << @command
57
+
58
+ if @params
59
+ f = false
60
+ @params.each do |param|
61
+ str << " "
62
+ if !f && (param.size == 0 || / / =~ param || /^:/ =~ param)
63
+ str << ":#{param}"
64
+ f = true
65
+ else
66
+ str << param
67
+ end
68
+ end
69
+ end
70
+
71
+ str << "\x0D\x0A"
72
+
73
+ str
74
+ end
75
+ alias to_str to_s
76
+
77
+ # Same as params.
78
+ def to_a
79
+ @params
80
+ end
81
+
82
+ # If the message is CTCP, return true.
83
+ def ctcp?
84
+ message = @params[1]
85
+ message[0] == 1 && message[message.length-1] == 1
86
+ end
87
+
88
+ def inspect
89
+ '#<%s:0x%x prefix:%s command:%s params:%s>' % [
90
+ self.class,
91
+ self.object_id,
92
+ @prefix,
93
+ @command,
94
+ @params.inspect
95
+ ]
96
+ end
97
+
98
+ autoload :ModeParser, "net/irc/message/modeparser"
99
+ end # Message
100
+