percy 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/percy/irc.rb ADDED
@@ -0,0 +1,508 @@
1
+ require 'ostruct'
2
+ require 'timeout'
3
+ require 'thread'
4
+ require 'percy/connection'
5
+ Percy.autoload :'PercyLogger', 'percy/percylogger'
6
+
7
+ module Percy
8
+ class IRC
9
+ class << self
10
+ attr_reader :config
11
+ attr_accessor :traffic_logger, :connected
12
+ end
13
+
14
+ @config = OpenStruct.new({:server => 'localhost',
15
+ :port => 6667,
16
+ :nick => 'Percy',
17
+ :username => 'Percy',
18
+ :verbose => true,
19
+ :logging => false,
20
+ :reconnect => true,
21
+ :reconnect_interval => 30})
22
+
23
+ # helper variables for getting server return values
24
+ @observers = 0
25
+ @temp_socket = []
26
+
27
+ @connected = false
28
+
29
+ # user methods
30
+ @events = Hash.new []
31
+ @listened_types = [:connect, :channel, :query, :join, :part, :quit, :nickchange, :kick] # + 3-digit numbers
32
+
33
+ # observer synchronizer
34
+ @mutex_observer = Mutex.new
35
+
36
+ def self.configure(&block)
37
+ block.call(@config)
38
+
39
+ # logger
40
+ if @config.logging
41
+ @traffic_logger = PercyLogger.new(Pathname.new(PERCY_ROOT).join('logs').expand_path, 'traffic.log')
42
+ @error_logger = PercyLogger.new(Pathname.new(PERCY_ROOT).join('logs').expand_path, 'error.log')
43
+ end
44
+ end
45
+
46
+ # raw IRC messages
47
+ def self.raw(message)
48
+ @connection.send_data "#{message}\r\n"
49
+ @traffic_logger.info(">> #{message}") if @traffic_logger
50
+ puts "#{Time.now.strftime('%d.%m.%Y %H:%M:%S')} >> #{message}" if @config.verbose
51
+ end
52
+
53
+ # send a message
54
+ def self.message(recipient, message)
55
+ self.raw "PRIVMSG #{recipient} :#{message}"
56
+ end
57
+
58
+ # send a notice
59
+ def self.notice(recipient, message)
60
+ self.raw "NOTICE #{recipient} :#{message}"
61
+ end
62
+
63
+ # set a mode
64
+ def self.mode(recipient, option)
65
+ self.raw "MODE #{recipient} #{option}"
66
+ end
67
+
68
+ # kick a user
69
+ def self.kick(channel, user, reason)
70
+ if reason
71
+ self.raw "KICK #{channel} #{user} :#{reason}"
72
+ else
73
+ self.raw "KICK #{channel} #{user}"
74
+ end
75
+ end
76
+
77
+ # perform an action
78
+ def self.action(recipient, message)
79
+ self.raw "PRIVMSG #{recipient} :\001ACTION #{message}\001"
80
+ end
81
+
82
+ # set a topic
83
+ def self.topic(channel, topic)
84
+ self.raw "TOPIC #{channel} :#{topic}"
85
+ end
86
+
87
+ # joining a channel
88
+ def self.join(channel, password = nil)
89
+ if password
90
+ self.raw "JOIN #{channel} #{password}"
91
+ else
92
+ self.raw "JOIN #{channel}"
93
+ end
94
+ end
95
+
96
+ # parting a channel
97
+ def self.part(channel, message)
98
+ if msg
99
+ self.raw "PART #{channel} :#{message}"
100
+ else
101
+ self.raw 'PART'
102
+ end
103
+ end
104
+
105
+ # quitting
106
+ def self.quit(message = nil)
107
+ if message
108
+ self.raw "QUIT :#{message}"
109
+ else
110
+ self.raw 'QUIT'
111
+ end
112
+
113
+ @config.reconnect = false # so Percy does not reconnect after the socket has been closed
114
+ end
115
+
116
+ def self.nick
117
+ @config.nick
118
+ end
119
+
120
+ # returns all users on a specific channel as array: ['Foo', 'bar', 'The_Librarian']
121
+ def self.users_on(channel)
122
+ actual_length = self.add_observer
123
+ self.raw "NAMES #{channel}"
124
+ channel = Regexp.escape(channel)
125
+
126
+ begin
127
+ Timeout::timeout(30) do # try 30 seconds to retrieve the users of <channel>
128
+ start = actual_length
129
+ ending = @temp_socket.length
130
+ users = []
131
+
132
+ loop do
133
+ for line in start..ending do
134
+ case @temp_socket[line]
135
+ when /^:\S+ 353 .+ #{channel} :/i
136
+ users << $'.split(' ')
137
+ when /^:\S+ 366 .+ #{channel}/i
138
+ return users.flatten.uniq.map { |element| element.gsub(/[!@%+]/, '') } # removing all modes
139
+ end
140
+ end
141
+
142
+ sleep 0.25
143
+ start = ending
144
+ ending = @temp_socket.length
145
+ end
146
+ end
147
+ rescue Timeout::Error
148
+ return []
149
+ ensure
150
+ self.remove_observer
151
+ end
152
+ end
153
+
154
+ # returns all users on a specific channel as array (with status): ['@Foo', '+bar', 'The_Librarian', '!Frank']
155
+ def self.users_with_status_on(channel)
156
+ actual_length = self.add_observer
157
+ self.raw "NAMES #{channel}"
158
+ channel = Regexp.escape(channel)
159
+
160
+ begin
161
+ Timeout::timeout(30) do # try 30 seconds to retrieve the users of <channel>
162
+ start = actual_length
163
+ ending = @temp_socket.length
164
+ users = []
165
+
166
+ loop do
167
+ for line in start..ending do
168
+ case @temp_socket[line]
169
+ when /^:\S+ 353 .+ #{channel} :/i
170
+ users << $'.split(' ')
171
+ when /^:\S+ 366 .+ #{channel}/i
172
+ return users.flatten.uniq
173
+ end
174
+ end
175
+
176
+ sleep 0.25
177
+ start = ending
178
+ ending = @temp_socket.length
179
+ end
180
+ end
181
+ rescue Timeout::Error
182
+ return []
183
+ ensure
184
+ self.remove_observer
185
+ end
186
+ end
187
+
188
+ # get the channel limit of a channel
189
+ def self.channel_limit(channel)
190
+ actual_length = self.add_observer
191
+ self.raw "MODE #{channel}"
192
+
193
+ begin
194
+ Timeout::timeout(10) do # try 10 seconds to retrieve l mode of <channel>
195
+ start = actual_length
196
+ ending = @temp_socket.length
197
+ channel = Regexp.escape(channel)
198
+
199
+ loop do
200
+ for line in start..ending do
201
+ if @temp_socket[line] =~ /^:\S+ 324 \S+ #{channel} .*l.* (\d+)/
202
+ return $1.to_i
203
+ end
204
+ end
205
+
206
+ sleep 0.25
207
+ start = ending
208
+ ending = @temp_socket.length
209
+ end
210
+ end
211
+ rescue Timeout::Error
212
+ return false
213
+ ensure
214
+ self.remove_observer
215
+ end
216
+ end
217
+
218
+ # check whether an user is online
219
+ def self.is_online(nick)
220
+ actual_length = self.add_observer
221
+ self.raw "WHOIS #{nick}"
222
+
223
+ begin
224
+ Timeout::timeout(10) do
225
+ start = actual_length
226
+ ending = @temp_socket.length
227
+ nick = Regexp.escape(nick)
228
+
229
+ loop do
230
+ for line in start..ending do
231
+ if @temp_socket[line] =~ /^:\S+ 311 \S+ (#{nick}) /i
232
+ return $1
233
+ elsif @temp_socket[line] =~ /^:\S+ 401 \S+ #{nick} /i
234
+ return false
235
+ end
236
+ end
237
+
238
+ sleep 0.25
239
+ start = ending
240
+ ending = @temp_socket.length
241
+ end
242
+ end
243
+ rescue Timeout::Error
244
+ return false
245
+ ensure
246
+ self.remove_observer
247
+ end
248
+ end
249
+
250
+ # on method
251
+ def self.on(type = :channel, match = //, &block)
252
+ unless @listened_types.include?(type) || type =~ /^\d\d\d$/
253
+ raise ArgumentError, "#{type} is not a supported type"
254
+ end
255
+
256
+ @events[type] = [] if @events[type].empty? # @events' default value is [], but it's not possible to add elements to it (weird!)
257
+ case type
258
+ when :channel || :query
259
+ @events[type] << {:match => match, :proc => block}
260
+ else
261
+ @events[type] << block
262
+ end
263
+ end
264
+
265
+ # connect!
266
+ def self.connect
267
+ @traffic_logger.info('-- Starting Percy') if @traffic_logger
268
+ puts "#{Time.now.strftime('%d.%m.%Y %H:%M:%S')} -- Starting Percy"
269
+
270
+ EventMachine::run do
271
+ @connection = EventMachine::connect(@config.server, @config.port, Connection)
272
+ end
273
+ end
274
+
275
+ private
276
+
277
+ # add observer
278
+ def self.add_observer
279
+ @mutex_observer.synchronize do
280
+ @observers += 1
281
+ end
282
+
283
+ return @temp_socket.length - 1 # so the loop knows where to begin to search for patterns
284
+ end
285
+
286
+ # remove observer
287
+ def self.remove_observer
288
+ @mutex_observer.synchronize do
289
+ @observers -= 1 # remove observer
290
+ @temp_socket = [] if @observers == 0 # clear @temp_socket if no observers are active
291
+ end
292
+ end
293
+
294
+ # parses incoming traffic (types)
295
+ def self.parse_type(type, env = nil)
296
+ case type
297
+ when /^\d\d\d$/
298
+ if @events[type]
299
+ @events[type].each do |block|
300
+ Thread.new do
301
+ begin
302
+ block.call(env)
303
+ rescue => e
304
+ if @error_logger
305
+ @error_logger.error(e.message)
306
+ e.backtrace.each do |line|
307
+ @error_logger.error(line)
308
+ end
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end
314
+
315
+ # :connect
316
+ if type =~ /^376|422$/
317
+ @events[:connect].each do |block|
318
+ Thread.new do
319
+ begin
320
+ unless @connected
321
+ @connected = true
322
+ block.call
323
+ end
324
+ rescue => e
325
+ if @error_logger
326
+ @error_logger.error(e.message)
327
+ e.backtrace.each do |line|
328
+ @error_logger.error(line)
329
+ end
330
+ end
331
+ end
332
+ end
333
+ end
334
+ end
335
+
336
+ when :channel
337
+ @events[type].each do |method|
338
+ if env[:message] =~ method[:match]
339
+ Thread.new do
340
+ begin
341
+ method[:proc].call(env)
342
+ rescue => e
343
+ if @error_logger
344
+ @error_logger.error(e.message)
345
+ e.backtrace.each do |line|
346
+ @error_logger.error(line)
347
+ end
348
+ end
349
+ end
350
+ end
351
+ end
352
+ end
353
+
354
+ when :query
355
+ # version respones
356
+ if env[:message] == "\001VERSION\001"
357
+ self.notice env[:nick], "\001VERSION #{VERSION}\001"
358
+ end
359
+
360
+ # time response
361
+ if env[:message] == "\001TIME\001"
362
+ self.notice env[:nick], "\001TIME #{Time.now.strftime('%a %b %d %H:%M:%S %Y')}\001"
363
+ end
364
+
365
+ # ping response
366
+ if env[:message] =~ /\001PING (\d+)\001/
367
+ self.notice env[:nick], "\001PING #{$1}\001"
368
+ end
369
+
370
+ @events[type].each do |method|
371
+ if env[:message] =~ method[:match]
372
+ Thread.new do
373
+ begin
374
+ method[:proc].call(env)
375
+ rescue => e
376
+ if @error_logger
377
+ @error_logger.error(e.message)
378
+ e.backtrace.each do |line|
379
+ @error_logger.error(line)
380
+ end
381
+ end
382
+ end
383
+ end
384
+ end
385
+ end
386
+
387
+ when :join
388
+ @events[type].each do |block|
389
+ Thread.new do
390
+ begin
391
+ block.call(env)
392
+ rescue => e
393
+ if @error_logger
394
+ @error_logger.error(e.message)
395
+ e.backtrace.each do |line|
396
+ @error_logger.error(line)
397
+ end
398
+ end
399
+ end
400
+ end
401
+ end
402
+
403
+ when :part
404
+ @events[type].each do |block|
405
+ Thread.new do
406
+ begin
407
+ block.call(env)
408
+ rescue => e
409
+ if @error_logger
410
+ @error_logger.error(e.message)
411
+ e.backtrace.each do |line|
412
+ @error_logger.error(line)
413
+ end
414
+ end
415
+ end
416
+ end
417
+ end
418
+
419
+ when :quit
420
+ @events[type].each do |block|
421
+ Thread.new do
422
+ begin
423
+ block.call(env)
424
+ rescue => e
425
+ if @error_logger
426
+ @error_logger.error(e.message)
427
+ e.backtrace.each do |line|
428
+ @error_logger.error(line)
429
+ end
430
+ end
431
+ end
432
+ end
433
+ end
434
+
435
+ when :nickchange
436
+ @events[type].each do |block|
437
+ Thread.new do
438
+ begin
439
+ block.call(env)
440
+ rescue => e
441
+ if @error_logger
442
+ @error_logger.error(e.message)
443
+ e.backtrace.each do |line|
444
+ @error_logger.error(line)
445
+ end
446
+ end
447
+ end
448
+ end
449
+ end
450
+
451
+ when :kick
452
+ @events[type].each do |block|
453
+ Thread.new do
454
+ begin
455
+ block.call(env)
456
+ rescue => e
457
+ if @error_logger
458
+ @error_logger.error(e.message)
459
+ e.backtrace.each do |line|
460
+ @error_logger.error(line)
461
+ end
462
+ end
463
+ end
464
+ end
465
+ end
466
+ end
467
+ end
468
+
469
+ # parsing incoming traffic
470
+ def self.parse(message)
471
+ @traffic_logger.info("<< #{message.chomp}") if @traffic_logger
472
+ puts "#{Time.now.strftime('%d.%m.%Y %H:%M:%S')} << #{message.chomp}" if @config.verbose
473
+
474
+ case message.chomp
475
+ when /^PING \S+$/
476
+ self.raw message.chomp.gsub('PING', 'PONG')
477
+
478
+ when /^:\S+ (\d\d\d) /
479
+ self.parse_type($1, :params => $')
480
+
481
+ when /^:(\S+)!(\S+)@(\S+) PRIVMSG #(\S+) :/
482
+ self.parse_type(:channel, :nick => $1, :user => $2, :host => $3, :channel => "##{$4}", :message => $')
483
+
484
+ when /^:(\S+)!(\S+)@(\S+) PRIVMSG \S+ :/
485
+ self.parse_type(:query, :nick => $1, :user => $2, :host => $3, :message => $')
486
+
487
+ when /^:(\S+)!(\S+)@(\S+) JOIN :*(\S+)$/
488
+ self.parse_type(:join, :nick => $1, :user => $2, :host => $3, :channel => $4)
489
+
490
+ when /^:(\S+)!(\S+)@(\S+) PART (\S+)/
491
+ self.parse_type(:part, :nick => $1, :user => $2, :host => $3, :channel => $4, :message => $'.sub(' :', ''))
492
+
493
+ when /^:(\S+)!(\S+)@(\S+) QUIT/
494
+ self.parse_type(:quit, :nick => $1, :user => $2, :host => $3, :message => $'.sub(' :', ''))
495
+
496
+ when /^:(\S+)!(\S+)@(\S+) NICK :/
497
+ self.parse_type(:nickchange, :nick => $1, :user => $2, :host => $3, :new_nick => $')
498
+
499
+ when /^:(\S+)!(\S+)@(\S+) KICK (\S+) (\S+) :/
500
+ self.parse_type(:kick, :nick => $1, :user => $2, :host => $3, :channel => $4, :victim => $5, :reason => $')
501
+ end
502
+
503
+ if @observers > 0
504
+ @temp_socket << message.chomp
505
+ end
506
+ end
507
+ end
508
+ end
@@ -0,0 +1,72 @@
1
+ require 'thread'
2
+ autoload :FileUtils, 'fileutils'
3
+
4
+ module Percy
5
+ class PercyLogger
6
+ DEBUG = 0
7
+ INFO = 1
8
+ WARN = 2
9
+ ERROR = 3
10
+ FATAL = 4
11
+ UNKNOWN = 5
12
+ LEVEL = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL', 'UNKNOWN']
13
+
14
+ attr_accessor :level, :time_format, :file
15
+
16
+ def initialize(dirpath = Pathname.new($0).dirname.join('logs').expand_path, filename = 'log.log', level = DEBUG, time_format = '%d.%m.%Y %H:%M:%S')
17
+ @dirpath = dirpath
18
+ @pathname = dirpath.join(filename)
19
+ @level = level
20
+ @time_format = time_format
21
+ @mutex = Mutex.new
22
+
23
+ unless @pathname.exist?
24
+ unless @dirpath.directory?
25
+ FileUtils.mkdir_p @dirpath
26
+ end
27
+
28
+ File.new(@pathname, 'w+')
29
+ end
30
+
31
+ @file = File.open(@pathname, 'a+')
32
+ @file.sync = true
33
+ end
34
+
35
+ def write(severity, message)
36
+ begin
37
+ if severity >= @level
38
+ @mutex.synchronize do
39
+ @file.puts "#{LEVEL[severity]} #{Time.now.strftime(@time_format)} #{message}"
40
+ end
41
+ end
42
+ rescue => e
43
+ puts e.message
44
+ puts e.backtrace.join('\n')
45
+ end
46
+ end
47
+
48
+ def debug(message)
49
+ write DEBUG, message
50
+ end
51
+
52
+ def info(message)
53
+ write INFO, message
54
+ end
55
+
56
+ def warn(message)
57
+ write WARN, message
58
+ end
59
+
60
+ def error(message)
61
+ write ERROR, message
62
+ end
63
+
64
+ def fatal(message)
65
+ write FATAL, message
66
+ end
67
+
68
+ def unknown(message)
69
+ write UNKNOWN, message
70
+ end
71
+ end
72
+ end