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/README.md +67 -28
- data/VERSION +1 -1
- data/examples/github_blog.rb +2 -1
- data/examples/is_online_checker.rb +10 -10
- data/lib/percy/connection.rb +35 -0
- data/lib/percy/formatting.rb +28 -0
- data/lib/percy/irc.rb +508 -0
- data/lib/percy/percylogger.rb +72 -0
- data/lib/percy.rb +7 -542
- data/percy.gemspec +6 -3
- metadata +7 -4
- data/lib/percylogger.rb +0 -71
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
|