cinch 0.1
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.rdoc +120 -0
- data/Rakefile +65 -0
- data/lib/cinch.rb +19 -0
- data/lib/cinch/base.rb +236 -0
- data/lib/cinch/irc.rb +44 -0
- data/lib/cinch/irc/message.rb +121 -0
- data/lib/cinch/irc/parser.rb +125 -0
- data/lib/cinch/irc/socket.rb +291 -0
- data/spec/helper.rb +8 -0
- data/spec/irc/helper.rb +8 -0
- data/spec/irc/message_spec.rb +61 -0
- data/spec/irc/parser_spec.rb +107 -0
- data/spec/irc/socket_spec.rb +90 -0
- data/spec/options_spec.rb +40 -0
- metadata +93 -0
@@ -0,0 +1,121 @@
|
|
1
|
+
module Cinch
|
2
|
+
module IRC
|
3
|
+
|
4
|
+
# == Author
|
5
|
+
# * Lee Jarvis - ljjarvis@gmail.com
|
6
|
+
#
|
7
|
+
# == Description
|
8
|
+
# IRC::Message is a nicely encapsulated IRC message object. Used directly by
|
9
|
+
# IRC::Parser#parse_servermessage and sent to every plugin defined. It does
|
10
|
+
# not do any parsing of itself, that's all down to the parser
|
11
|
+
#
|
12
|
+
# == See
|
13
|
+
# * Cinch::IRC::Parser#parse_servermessage
|
14
|
+
#
|
15
|
+
# TODO: Add more documentation
|
16
|
+
class Message
|
17
|
+
|
18
|
+
# Message prefix
|
19
|
+
attr_reader :prefix
|
20
|
+
|
21
|
+
# Message command (PRIVMSG, JOIN, KICK, etc)
|
22
|
+
attr_reader :command
|
23
|
+
|
24
|
+
# Message params
|
25
|
+
attr_reader :params
|
26
|
+
|
27
|
+
# Message symbol (lowercase command, ie. :privmsg, :join, :kick)
|
28
|
+
attr_reader :symbol
|
29
|
+
|
30
|
+
# The raw string passed to ::new
|
31
|
+
attr_reader :raw
|
32
|
+
|
33
|
+
# Hash with message attributes
|
34
|
+
attr_reader :data
|
35
|
+
|
36
|
+
# Message text
|
37
|
+
attr_reader :text
|
38
|
+
|
39
|
+
# Arguments parsed from a rule
|
40
|
+
attr_accessor :args
|
41
|
+
|
42
|
+
# The IRC::Socket object (or nil)
|
43
|
+
attr_accessor :irc
|
44
|
+
|
45
|
+
# Invoked directly from IRC::Parser#parse_servermessage
|
46
|
+
def initialize(raw, prefix, command, params)
|
47
|
+
@raw = raw
|
48
|
+
@prefix = prefix
|
49
|
+
@command = command
|
50
|
+
@params = params
|
51
|
+
@text = params.last unless params.empty?
|
52
|
+
|
53
|
+
@symbol = command.downcase.to_sym
|
54
|
+
@data = {}
|
55
|
+
@args = {}
|
56
|
+
@irc = nil
|
57
|
+
end
|
58
|
+
|
59
|
+
# Access attribute
|
60
|
+
def [](var)
|
61
|
+
@data[var.to_sym]
|
62
|
+
end
|
63
|
+
|
64
|
+
# Add a new attribute (stored in @data)
|
65
|
+
def add(var, val)
|
66
|
+
@data[var.to_sym] = val
|
67
|
+
end
|
68
|
+
alias []= add
|
69
|
+
|
70
|
+
# Remove an attribute
|
71
|
+
def delete(var)
|
72
|
+
var = var.to_sym
|
73
|
+
return unless @data.key?(var)
|
74
|
+
@data.delete(var)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Check if our message was sent privately
|
78
|
+
def private?
|
79
|
+
!@data[:channel]
|
80
|
+
end
|
81
|
+
|
82
|
+
# Add the nick/user/host attributes
|
83
|
+
def apply_user(nick, user, host)
|
84
|
+
@data[:nick] = nick
|
85
|
+
@data[:user] = user
|
86
|
+
@data[:host] = host
|
87
|
+
end
|
88
|
+
|
89
|
+
def reply(text)
|
90
|
+
recipient = data[:channel] || data[:nick]
|
91
|
+
@irc.privmsg(recipient, text)
|
92
|
+
end
|
93
|
+
|
94
|
+
def answer(text)
|
95
|
+
return unless data[:channel]
|
96
|
+
@irc.privmsg(data[:channel], "#{data[:nick]}: #{text}")
|
97
|
+
end
|
98
|
+
|
99
|
+
def action(text)
|
100
|
+
reply("\001ACTION #{text}\001")
|
101
|
+
end
|
102
|
+
|
103
|
+
# The raw IRC message
|
104
|
+
def to_s
|
105
|
+
raw
|
106
|
+
end
|
107
|
+
|
108
|
+
# Catch methods and check if they exist as keys in
|
109
|
+
# the attribute hash
|
110
|
+
def method_missing(meth, *args, &blk) # :nodoc:
|
111
|
+
if @data.key?(meth)
|
112
|
+
@data[meth]
|
113
|
+
else
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module Cinch
|
2
|
+
module IRC
|
3
|
+
|
4
|
+
# == Author
|
5
|
+
# * Lee Jarvis - ljjarvis@gmail.com
|
6
|
+
#
|
7
|
+
# == Description
|
8
|
+
# Parse incoming IRC lines and extract data, returning a nicely
|
9
|
+
# encapsulated Cinch::IRC::Message
|
10
|
+
#
|
11
|
+
# == Example
|
12
|
+
# require 'cinch/irc/parser'
|
13
|
+
# include Cinch::IRC::Parser
|
14
|
+
#
|
15
|
+
# message = parse(":foo!bar@myhost.com PRIVMSG #mychan :ding dong!")
|
16
|
+
#
|
17
|
+
# message.class #=> Cinch::IRC::Message
|
18
|
+
# message.command #=> PRIVMSG
|
19
|
+
# message.nick #=> foo
|
20
|
+
# message.channel #=> #mychan
|
21
|
+
# message.text #=> ding dong!
|
22
|
+
class Parser
|
23
|
+
|
24
|
+
# A hash holding all of our patterns
|
25
|
+
attr_reader :patterns
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
@patterns = {}
|
29
|
+
setup_patterns
|
30
|
+
end
|
31
|
+
|
32
|
+
# Add a new pattern
|
33
|
+
def add_pattern(key, pattern)
|
34
|
+
raise ArgumentError, "Pattern is not a regular expression" unless pattern.is_a?(Regexp)
|
35
|
+
@patterns[key.to_sym] = pattern
|
36
|
+
end
|
37
|
+
|
38
|
+
# Remove a pattern
|
39
|
+
def remove_pattern(key)
|
40
|
+
key = key.to_sym
|
41
|
+
@patterns.delete(key) if @patterns.key?(key)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Helper for our patterns Hash
|
45
|
+
def pattern(key)
|
46
|
+
@patterns[key]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Set up some default patterns used directly by this class
|
50
|
+
def setup_patterns
|
51
|
+
add_pattern :letter, /[a-zA-Z]/
|
52
|
+
add_pattern :hex, /[\dA-Fa-f]/
|
53
|
+
|
54
|
+
add_pattern :ip4addr, /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/
|
55
|
+
add_pattern :ip6addr, /[\dA-Fa-f](?::[\dA-Fa-f]){7}|0:0:0:0:0:(?:0|[Ff]{4}):#{pattern(:ip4addr)}/
|
56
|
+
add_pattern :hostaddr, /#{pattern(:ip4addr)}|#{pattern(:ip6addr)}/
|
57
|
+
add_pattern :shortname, /[A-Za-z0-9][A-Za-z0-9-]*/
|
58
|
+
add_pattern :hostname, /#{pattern(:shortname)}(?:\.#{pattern(:shortname)})*/
|
59
|
+
add_pattern :host, /#{pattern(:hostname)}|#{pattern(:hostaddr)}/
|
60
|
+
|
61
|
+
add_pattern :user, /[^\x00\x10\x0D\x20@]+/
|
62
|
+
add_pattern :nick, /[A-Za-z\[\]\\`_^{|}][A-Za-z\d\[\]\\`_^{|}-]{0,19}/
|
63
|
+
|
64
|
+
add_pattern :userhost, /(#{pattern(:nick)})(?:(?:!(#{pattern(:user)}))?@(#{pattern(:host)}))?/
|
65
|
+
|
66
|
+
add_pattern :channel, /(?:[#+&]|![A-Z\d]{5})[^\x00\x07\x10\x0D\x20,:]/
|
67
|
+
|
68
|
+
# Server message parsing patterns
|
69
|
+
add_pattern :prefix, /(?:(\S+)\x20)?/
|
70
|
+
add_pattern :command, /([A-Za-z]+|\d{3})/
|
71
|
+
add_pattern :middle, /[^\x00\x20\r\n:][^\x00\x20\r\n]*/
|
72
|
+
add_pattern :trailing, /[^\x00\r\n]*/
|
73
|
+
add_pattern :params, /(?:((?:#{pattern(:middle)}){0,14}(?::?#{pattern(:trailing)})?))/
|
74
|
+
add_pattern :message, /\A#{pattern(:prefix)}#{pattern(:command)}#{pattern(:params)}\Z/
|
75
|
+
|
76
|
+
add_pattern :params_scan, /(?!:)([^\x00\x20\r\n:]+)|:([^\x00\r\n]*)/
|
77
|
+
end
|
78
|
+
private :setup_patterns
|
79
|
+
|
80
|
+
# Parse the incoming raw IRC string and return
|
81
|
+
# a nicely formatted IRC::Message
|
82
|
+
def parse_servermessage(raw)
|
83
|
+
raise ArgumentError, raw unless matches = raw.match(pattern(:message))
|
84
|
+
|
85
|
+
prefix, command, parameters = matches.captures
|
86
|
+
|
87
|
+
params = []
|
88
|
+
parameters.scan(pattern(:params_scan)) {|a, c| params << (a || c) }
|
89
|
+
|
90
|
+
m = IRC::Message.new(raw, prefix, command, params)
|
91
|
+
|
92
|
+
if prefix && userhost = parse_userhost(prefix)
|
93
|
+
m.apply_user(*userhost)
|
94
|
+
|
95
|
+
unless m.params.empty?
|
96
|
+
m[:recipient] = m.params.first
|
97
|
+
m[:channel] = m[:recipient] if valid_channel?(m[:recipient])
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
m # Return our IRC::Message
|
102
|
+
end
|
103
|
+
alias :parse :parse_servermessage
|
104
|
+
|
105
|
+
# Parse the prefix returned from the server
|
106
|
+
# and return an Array of [nick, user, host] or
|
107
|
+
# nil if no match is found
|
108
|
+
def parse_userhost(prefix)
|
109
|
+
if matches = prefix.match(pattern(:userhost))
|
110
|
+
matches.captures
|
111
|
+
else
|
112
|
+
nil
|
113
|
+
end
|
114
|
+
end
|
115
|
+
alias :extract_userhost :parse_userhost
|
116
|
+
|
117
|
+
# Check if a string is a valid channel
|
118
|
+
def valid_channel?(str)
|
119
|
+
!str.match(pattern(:channel)).nil?
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
@@ -0,0 +1,291 @@
|
|
1
|
+
module Cinch
|
2
|
+
module IRC
|
3
|
+
# == Author
|
4
|
+
# * Lee Jarvis - ljjarvis@gmail.com
|
5
|
+
#
|
6
|
+
# == Description
|
7
|
+
# This class has been directly take from the irc-socket library. Original documentation
|
8
|
+
# for this class can be found {here}[http://rdoc.injekt.net/irc-socket].
|
9
|
+
#
|
10
|
+
# IRCSocket is an IRC wrapper around a TCPSocket. It implements all of the major
|
11
|
+
# commands laid out in {RFC 2812}[http://irchelp.org/irchelp/rfc/rfc2812.txt].
|
12
|
+
# All these commands are available as instance methods of an IRCSocket Object.
|
13
|
+
#
|
14
|
+
# == Example
|
15
|
+
# irc = IRCSocket.new('irc.freenode.org')
|
16
|
+
# irc.connect
|
17
|
+
#
|
18
|
+
# if irc.connected?
|
19
|
+
# irc.nick "HulkHogan"
|
20
|
+
# irc.user "Hulk", 0, "*", "I am Hulk Hogan"
|
21
|
+
#
|
22
|
+
# while line = irc.read
|
23
|
+
#
|
24
|
+
# # Join a channel after MOTD
|
25
|
+
# if line.split[1] == '376'
|
26
|
+
# irc.join "#mychannel"
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# puts "Received: #{line}"
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# === Block Form
|
34
|
+
# IRCSocket.new('irc.freenode.org') do |irc|
|
35
|
+
# irc.nick "SpongeBob"
|
36
|
+
# irc.user "Spongey", 0, "*", "Square Pants"
|
37
|
+
#
|
38
|
+
# puts irc.read
|
39
|
+
# end
|
40
|
+
class Socket
|
41
|
+
|
42
|
+
# The server our socket is connected to
|
43
|
+
attr_reader :server
|
44
|
+
|
45
|
+
# The port our socket is connected on
|
46
|
+
attr_reader :port
|
47
|
+
|
48
|
+
# The TCPSocket instance
|
49
|
+
attr_reader :socket
|
50
|
+
|
51
|
+
# Creates a new IRCSocket and automatically connects
|
52
|
+
#
|
53
|
+
# === Example
|
54
|
+
# irc = IRCSocket.open('irc.freenode.org')
|
55
|
+
#
|
56
|
+
# while data = irc.read
|
57
|
+
# puts data
|
58
|
+
# end
|
59
|
+
def self.open(server, port=6667)
|
60
|
+
irc = new(server, port)
|
61
|
+
irc.connect
|
62
|
+
irc
|
63
|
+
end
|
64
|
+
|
65
|
+
# Create a new IRCSocket to connect to +server+ on +port+. Defaults to port 6667.
|
66
|
+
# If an optional code block is given, it will be passed an instance of the IRCSocket.
|
67
|
+
# NOTE: Using the block form does not mean the socket will send the applicable QUIT
|
68
|
+
# command to leave the IRC server. You must send this yourself.
|
69
|
+
def initialize(server, port=6667)
|
70
|
+
@server = server
|
71
|
+
@port = port
|
72
|
+
|
73
|
+
@socket = nil
|
74
|
+
@connected = false
|
75
|
+
|
76
|
+
if block_given?
|
77
|
+
connect
|
78
|
+
yield self
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Check if our socket is alive and connected to an IRC server
|
83
|
+
def connected?
|
84
|
+
@connected
|
85
|
+
end
|
86
|
+
alias connected connected?
|
87
|
+
|
88
|
+
# Connect to an IRC server, returns true on a successful connection, or
|
89
|
+
# raises otherwise
|
90
|
+
def connect
|
91
|
+
@socket = TCPSocket.new(server, port)
|
92
|
+
rescue Interrupt
|
93
|
+
raise
|
94
|
+
rescue Exception
|
95
|
+
raise
|
96
|
+
else
|
97
|
+
@connected = true
|
98
|
+
end
|
99
|
+
|
100
|
+
# Read the next line in from the server. If no arguments are passed
|
101
|
+
# the line will have the CRLF chomp'ed. Returns nil if no data could be read
|
102
|
+
def read(chompstr="\r\n")
|
103
|
+
if data = @socket.gets("\r\n")
|
104
|
+
data.chomp!(chompstr) if chompstr
|
105
|
+
data
|
106
|
+
else
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
rescue IOError
|
110
|
+
nil
|
111
|
+
end
|
112
|
+
|
113
|
+
# Write to our Socket and append CRLF
|
114
|
+
def write(data)
|
115
|
+
@socket.write(data + "\r\n")
|
116
|
+
rescue IOError
|
117
|
+
raise
|
118
|
+
end
|
119
|
+
|
120
|
+
# Sugar for #write
|
121
|
+
def raw(*args) # :nodoc:
|
122
|
+
args.last.insert(0, ':') unless args.last.nil?
|
123
|
+
write args.join(' ').strip
|
124
|
+
end
|
125
|
+
|
126
|
+
# More sugar
|
127
|
+
def write_optional(command, *optional) # :nodoc:
|
128
|
+
command = "#{command} #{optional.join(' ')}" if optional
|
129
|
+
write(command.strip)
|
130
|
+
end
|
131
|
+
private :raw, :write_optional
|
132
|
+
|
133
|
+
# Send PASS command
|
134
|
+
def pass(password)
|
135
|
+
write("PASS #{password}")
|
136
|
+
end
|
137
|
+
|
138
|
+
# Send NICK command
|
139
|
+
def nick(nickname)
|
140
|
+
write("NICK #{nickname}")
|
141
|
+
end
|
142
|
+
|
143
|
+
# Send USER command
|
144
|
+
def user(user, mode, unused, realname)
|
145
|
+
write("USER #{user} #{mode} #{unused} :#{realname}")
|
146
|
+
end
|
147
|
+
|
148
|
+
# Send OPER command
|
149
|
+
def oper(name, password)
|
150
|
+
write("OPER #{name} #{password}")
|
151
|
+
end
|
152
|
+
|
153
|
+
# Send the MODE command.
|
154
|
+
# Should probably implement a better way of doing this
|
155
|
+
def mode(channel, *modes)
|
156
|
+
write("MODE #{channel} #{modes.join(' ')}")
|
157
|
+
end
|
158
|
+
|
159
|
+
# Send QUIT command
|
160
|
+
def quit(message=nil)
|
161
|
+
raw("QUIT", message)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Send JOIN command - Join a channel with given password
|
165
|
+
def join(channel, password=nil)
|
166
|
+
write("JOIN #{channel}")
|
167
|
+
end
|
168
|
+
|
169
|
+
# Send PART command
|
170
|
+
def part(channel, message=nil)
|
171
|
+
raw("PART", channel, message)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Send TOPIC command
|
175
|
+
def topic(channel, topic=nil)
|
176
|
+
raw("TOPIC", channel, topic)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Send NAMES command
|
180
|
+
def names(*channels)
|
181
|
+
write("NAMES #{channels.join(',') unless channels.empty?}")
|
182
|
+
end
|
183
|
+
|
184
|
+
# Send LIST command
|
185
|
+
def list(*channels)
|
186
|
+
write("LIST #{channels.join(',') unless channels.empty?}")
|
187
|
+
end
|
188
|
+
|
189
|
+
# Send INVITE command
|
190
|
+
def invite(nickname, channel)
|
191
|
+
write("INVITE #{nickname} #{channel}")
|
192
|
+
end
|
193
|
+
|
194
|
+
# Send KICK command
|
195
|
+
def kick(channel, user, comment=nil)
|
196
|
+
raw("KICK", channel, user, comment)
|
197
|
+
end
|
198
|
+
|
199
|
+
# Send PRIVMSG command
|
200
|
+
def privmsg(target, message)
|
201
|
+
write("PRIVMSG #{target} :#{message}")
|
202
|
+
end
|
203
|
+
|
204
|
+
# Send NOTICE command
|
205
|
+
def notice(target, message)
|
206
|
+
write("NOTICE #{target} :#{message}")
|
207
|
+
end
|
208
|
+
|
209
|
+
# Send MOTD command
|
210
|
+
def motd(target=nil)
|
211
|
+
write_optional("MOTD", target)
|
212
|
+
end
|
213
|
+
|
214
|
+
# Send VERSION command
|
215
|
+
def version(target=nil)
|
216
|
+
write_optional("VERSION", target)
|
217
|
+
end
|
218
|
+
|
219
|
+
# Send STATS command
|
220
|
+
def stats(*params)
|
221
|
+
write_optional("STATS", params)
|
222
|
+
end
|
223
|
+
|
224
|
+
# Send TIME command
|
225
|
+
def time(target=nil)
|
226
|
+
write_optional("TIME", target)
|
227
|
+
end
|
228
|
+
|
229
|
+
# Send INFO command
|
230
|
+
def info(target=nil)
|
231
|
+
write_optional("INFO", target)
|
232
|
+
end
|
233
|
+
|
234
|
+
# Send SQUERY command
|
235
|
+
def squery(target, message)
|
236
|
+
write("SQUERY #{target} :#{message}")
|
237
|
+
end
|
238
|
+
|
239
|
+
# Send WHO command
|
240
|
+
def who(*params)
|
241
|
+
write_optional("WHO", params)
|
242
|
+
end
|
243
|
+
|
244
|
+
# Send WHOIS command
|
245
|
+
def whois(*params)
|
246
|
+
write_optional("WHOIS", params)
|
247
|
+
end
|
248
|
+
|
249
|
+
# Send WHOWAS command
|
250
|
+
def whowas(*params)
|
251
|
+
write_optional("WHOWAS", params)
|
252
|
+
end
|
253
|
+
|
254
|
+
# Send KILL command
|
255
|
+
def kill(user, message)
|
256
|
+
write("KILL #{user} :#{message}")
|
257
|
+
end
|
258
|
+
|
259
|
+
# Send PING command
|
260
|
+
def ping(server)
|
261
|
+
write("PING #{server}")
|
262
|
+
end
|
263
|
+
|
264
|
+
# Send PONG command
|
265
|
+
def pong(server)
|
266
|
+
write("PONG #{server}")
|
267
|
+
end
|
268
|
+
|
269
|
+
# Send AWAY command
|
270
|
+
def away(message=nil)
|
271
|
+
raw("AWAY", message)
|
272
|
+
end
|
273
|
+
|
274
|
+
# Send USERS command
|
275
|
+
def users(target=nil)
|
276
|
+
write_optional("USERS", target)
|
277
|
+
end
|
278
|
+
|
279
|
+
# Send USERHOST command
|
280
|
+
def userhost(*users)
|
281
|
+
write("USERHOST #{users.join(' ')}")
|
282
|
+
end
|
283
|
+
|
284
|
+
# Close our socket instance
|
285
|
+
def close
|
286
|
+
@socket.close if connected?
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|