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.
@@ -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
+