percy 1.3.0 → 1.4.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/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