percy 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/LICENSE +7 -0
  2. data/README.rdoc +80 -0
  3. data/lib/percy.rb +450 -0
  4. data/lib/percylogger.rb +67 -0
  5. metadata +58 -0
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2009 Tobias Bühlmann
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,80 @@
1
+ = Percy 0.0.3
2
+
3
+ == Configuring the bot
4
+
5
+ === mybot.rb
6
+ $:.unshift "#{File.expand_path(File.dirname(__FILE__))}/lib"
7
+ PERCY_ROOT = File.expand_path(File.dirname(__FILE__))
8
+
9
+ require 'rubygems'
10
+ require 'percy'
11
+
12
+ bot = Percy.new
13
+
14
+ bot.configure do |c|
15
+ c.server = 'chat.eu.freenode.net'
16
+ c.port = 6667
17
+ c.nick = 'Percyguy'
18
+ c.verbose = true
19
+ c.logging = true
20
+ end
21
+
22
+ Start it with <tt>ruby mybot.rb</tt>.
23
+
24
+ == Events
25
+ Handle events with:
26
+ bot.on :connect do
27
+ # join channels
28
+ # do things
29
+ end
30
+
31
+ bot.on :channel, /^foo!/ do |env|
32
+ # available variables: env[:nick], env[:user], env[:host], env[:channel], env[:message]
33
+ end
34
+
35
+ bot.on :query, /^bar!/ do |env|
36
+ # available variables: env[:nick], env[:user], env[:host], env[:message]
37
+ end
38
+
39
+ bot.on :join do |env|
40
+ # available variables: env[:nick], env[:user], env[:host], env[:channel]
41
+ end
42
+
43
+ bot.on :part do |env|
44
+ # available variables: env[:nick], env[:user], env[:host], env[:channel], env[:message]
45
+ end
46
+
47
+ bot.on :quit do |env|
48
+ # available variables: env[:nick], env[:user], env[:host], env[:message]
49
+ end
50
+
51
+ bot.on :nickchange do |env|
52
+ # available variables: env[:nick], env[:user], env[:host], env[:new_nick]
53
+ end
54
+
55
+ bot.on :kick do |env|
56
+ # available variables: env[:nick], env[:user], env[:host], env[:new_nick], env[:channel], env[:victim]
57
+ end
58
+
59
+ == Methods availabe
60
+ * message(recipient, msg)
61
+ * notice(recipient, msg)
62
+ * action(recipient, msg)
63
+ * mode(recipient, option)
64
+ * channellimit(channel)
65
+ * kick(channel, user, reason)
66
+ * topic(channel, topic)
67
+ * join(channel, password = nil)
68
+ * part(channel, msg)
69
+ * quit(msg = nil)
70
+ * users_on(channel)
71
+ * is_online(nick)
72
+
73
+ == License
74
+ Copyright (c) 2009 Tobias Bühlmann
75
+
76
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
77
+
78
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
79
+
80
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/lib/percy.rb ADDED
@@ -0,0 +1,450 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__))
2
+
3
+ require 'percylogger'
4
+ require 'socket'
5
+ require 'timeout'
6
+ require 'thread'
7
+
8
+ Thread.abort_on_exception = true
9
+
10
+ class Percy
11
+ VERSION = 'Percy 0.0.3 (http://github.com/tbuehlmann/percy)'
12
+
13
+ Config = Struct.new(:server, :port, :password, :nick, :username, :verbose, :logging)
14
+
15
+ def initialize
16
+ @config = Config.new("localhost", 6667, nil, 'Percy', 'Percy', true, false)
17
+
18
+ # helper variables for getting server return values
19
+ @observers = 0
20
+ @temp_socket = []
21
+
22
+ # user methods
23
+ @on_channel = []
24
+ @on_query = []
25
+ @on_connect = []
26
+ @on_join = []
27
+ @on_part = []
28
+ @on_quit = []
29
+ @on_nickchange = []
30
+ @on_kick = []
31
+
32
+ # observer synchronizer
33
+ @mutex = Mutex.new
34
+
35
+ # running variable (provisional solution for rejoining at netsplit)
36
+ @running = true
37
+ end
38
+
39
+ # configure block
40
+ def configure(&block)
41
+ block.call(@config)
42
+
43
+ # logger
44
+ @traffic_logger = PercyLogger.new("#{PERCY_ROOT}/logs/traffic.log") if @config.logging
45
+ @error_logger = PercyLogger.new("#{PERCY_ROOT}/logs/error.log") if @config.logging
46
+ end
47
+
48
+ # raw irc messages
49
+ def raw(msg)
50
+ @socket.puts "#{msg}\r\n"
51
+ @traffic_logger.info(">> #{msg}") if @traffic_logger
52
+ puts "#{Time.now.strftime('%d.%m.%Y %H:%M:%S')} >> #{msg}" if @config.verbose
53
+ end
54
+
55
+ # send a message
56
+ def message(recipient, msg)
57
+ raw "PRIVMSG #{recipient} :#{msg}"
58
+ end
59
+
60
+ # send a notice
61
+ def notice(recipient, msg)
62
+ raw "NOTICE #{recipient} :#{msg}"
63
+ end
64
+
65
+ # set a mode
66
+ def mode(recipient, option)
67
+ raw "MODE #{recipient} #{option}"
68
+ end
69
+
70
+ # get the channellimit of a channel
71
+ def channellimit(channel)
72
+ add_observer
73
+ raw "MODE #{channel}"
74
+
75
+ begin
76
+ Timeout::timeout(10) do # try 10 seconds to retrieve l mode of <channel>
77
+ loop do
78
+ @temp_socket.each do |line|
79
+ if line =~ /^:(\S+) 324 (\S+) #{channel} .*l.* (\d+)/
80
+ return $3.to_i
81
+ end
82
+ end
83
+ sleep 0.5
84
+ end
85
+ end
86
+ rescue Timeout::Error
87
+ return false
88
+ ensure
89
+ remove_observer
90
+ end
91
+ end
92
+
93
+ # check whether an user is online
94
+ def is_online(nick)
95
+ add_observer
96
+ raw "WHOIS #{nick}"
97
+
98
+ begin
99
+ Timeout::timeout(10) do
100
+ loop do
101
+ @temp_socket.each do |line|
102
+ if line =~ /^:(\S+) 311 (\S+) (#{nick}) /i
103
+ return $3
104
+ elsif line =~ /^:\S+ 401 \S+ #{nick} /i
105
+ return false
106
+ end
107
+ end
108
+ sleep 0.5
109
+ end
110
+ end
111
+ rescue Timeout::Error
112
+ return false
113
+ ensure
114
+ remove_observer
115
+ end
116
+ end
117
+
118
+ # kick a user
119
+ def kick(channel, user, reason)
120
+ if reason
121
+ raw "KICK #{channel} #{user} :#{reason}"
122
+ else
123
+ raw "KICK #{channel} #{user}"
124
+ end
125
+ end
126
+
127
+ # perform an action
128
+ def action(recipient, msg)
129
+ raw "PRIVMSG #{recipient} :\001ACTION #{msg}\001"
130
+ end
131
+
132
+ # set a topic
133
+ def topic(channel, topic)
134
+ raw "TOPIC #{channel} :#{topic}"
135
+ end
136
+
137
+ # joining a channel
138
+ def join(channel, password = nil)
139
+ if password
140
+ raw "JOIN #{channel} #{password}"
141
+ else
142
+ raw "JOIN #{channel}"
143
+ end
144
+ end
145
+
146
+ # parting a channel
147
+ def part(channel, msg)
148
+ if msg
149
+ raw "PART #{channel} :#{msg}"
150
+ else
151
+ raw 'PART'
152
+ end
153
+ end
154
+
155
+ # quitting
156
+ def quit(msg = nil)
157
+ if msg
158
+ raw "QUIT :#{msg}"
159
+ else
160
+ raw 'QUIT'
161
+ end
162
+
163
+ @running = false # so Percy does not reconnect after the socket has been closed
164
+ end
165
+
166
+ # on method
167
+ def on(type = :channel, match = //, &block)
168
+ case type
169
+ when :channel
170
+ @on_channel << {:match => match, :proc => block}
171
+ when :connect
172
+ @on_connect << block
173
+ when :query
174
+ @on_query << {:match => match, :proc => block}
175
+ when :join
176
+ @on_join << block
177
+ when :part
178
+ @on_part << block
179
+ when :quit
180
+ @on_quit << block
181
+ when :nickchange
182
+ @on_nickchange << block
183
+ when :kick
184
+ @on_kick << block
185
+ end
186
+ end
187
+
188
+ # add observer
189
+ def add_observer
190
+ @mutex.synchronize do
191
+ @observers += 1
192
+ end
193
+ end
194
+
195
+ # remove observer
196
+ def remove_observer
197
+ @mutex.synchronize do
198
+ @observers -= 1 # remove observer
199
+ @temp_socket = [] if @observers == 0 # clear @temp_socket if no observers are active
200
+ end
201
+ end
202
+
203
+ # returns all users on a specific channel
204
+ def users_on(channel)
205
+ add_observer
206
+ raw "NAMES #{channel}"
207
+
208
+ begin
209
+ Timeout::timeout(10) do # try 10 seconds to retrieve the users of <channel>
210
+ loop do
211
+ @temp_socket.each do |line|
212
+ if line =~ /^:(\S+) 353 (\S+) = #{channel} :/
213
+ return $'.split(' ')
214
+ end
215
+ end
216
+ sleep 0.5
217
+ end
218
+ end
219
+ rescue Timeout::Error
220
+ return false
221
+ ensure
222
+ remove_observer
223
+ end
224
+ end
225
+
226
+ # parses incoming traffic
227
+ def parse(type, env = nil)
228
+ case type
229
+ when :connect
230
+ @on_connect.each do |block|
231
+ Thread.new do
232
+ begin
233
+ unless @connected
234
+ @connected = true
235
+ block.call
236
+ end
237
+ rescue => e
238
+ if @error_logger
239
+ @error_logger.error(e.message)
240
+ e.backtrace.each do |line|
241
+ @error_logger.error(line)
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
247
+
248
+ when :channel
249
+ @on_channel.each do |method|
250
+ if env[:message] =~ method[:match]
251
+ Thread.new do
252
+ begin
253
+ method[:proc].call(env)
254
+ rescue => e
255
+ if @error_logger
256
+ @error_logger.error(e.message)
257
+ e.backtrace.each do |line|
258
+ @error_logger.error(line)
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end
264
+ end
265
+
266
+ when :query
267
+ # version respones
268
+ if env[:message] == "\001VERSION\001"
269
+ notice env[:nick], "\001VERSION #{VERSION}\001"
270
+ end
271
+
272
+ # time response
273
+ if env[:message] == "\001TIME\001"
274
+ notice env[:nick], "\001TIME #{Time.now.strftime('%a %b %d %H:%M:%S %Y')}\001"
275
+ end
276
+
277
+ # ping response
278
+ if env[:message] =~ /\001PING (\d+)\001/
279
+ notice env[:nick], "\001PING #{$1}\001"
280
+ end
281
+
282
+ @on_query.each do |method|
283
+ if env[:message] =~ method[:match]
284
+ Thread.new do
285
+ begin
286
+ method[:proc].call(env)
287
+ rescue => e
288
+ if @error_logger
289
+ @error_logger.error(e.message)
290
+ e.backtrace.each do |line|
291
+ @error_logger.error(line)
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end
297
+ end
298
+
299
+ when :join
300
+ @on_join.each do |block|
301
+ Thread.new do
302
+ begin
303
+ block.call(env)
304
+ rescue => e
305
+ if @error_logger
306
+ @error_logger.error(e.message)
307
+ e.backtrace.each do |line|
308
+ @error_logger.error(line)
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end
314
+
315
+ when :part
316
+ @on_part.each do |block|
317
+ Thread.new do
318
+ begin
319
+ block.call(env)
320
+ rescue => e
321
+ if @error_logger
322
+ @error_logger.error(e.message)
323
+ e.backtrace.each do |line|
324
+ @error_logger.error(line)
325
+ end
326
+ end
327
+ end
328
+ end
329
+ end
330
+
331
+ when :quit
332
+ @on_quit.each do |block|
333
+ Thread.new do
334
+ begin
335
+ block.call(env)
336
+ rescue => e
337
+ if @error_logger
338
+ @error_logger.error(e.message)
339
+ e.backtrace.each do |line|
340
+ @error_logger.error(line)
341
+ end
342
+ end
343
+ end
344
+ end
345
+ end
346
+
347
+ when :nickchange
348
+ @on_nickchange.each do |block|
349
+ Thread.new do
350
+ begin
351
+ block.call(env)
352
+ rescue => e
353
+ if @error_logger
354
+ @error_logger.error(e.message)
355
+ e.backtrace.each do |line|
356
+ @error_logger.error(line)
357
+ end
358
+ end
359
+ end
360
+ end
361
+ end
362
+
363
+ when :kick
364
+ @on_kick.each do |block|
365
+ Thread.new do
366
+ begin
367
+ block.call(env)
368
+ rescue => e
369
+ if @error_logger
370
+ @error_logger.error(e.message)
371
+ e.backtrace.each do |line|
372
+ @error_logger.error(line)
373
+ end
374
+ end
375
+ end
376
+ end
377
+ end
378
+ end
379
+ end
380
+
381
+ # connect!
382
+ def connect
383
+ begin
384
+ while @running
385
+ @traffic_logger.info('-- Starting Percy') if @traffic_logger
386
+ puts "#{Time.now.strftime('%d.%m.%Y %H:%M:%S')} -- Starting Percy"
387
+
388
+
389
+ @socket = TCPSocket.open(@config.server, @config.port)
390
+ raw "PASS #{@config.password}" if @config.password
391
+ raw "NICK #{@config.nick}"
392
+ raw "USER #{@config.nick} 0 * :#{@config.username}"
393
+
394
+ while line = @socket.gets
395
+ @traffic_logger.info("<< #{line.chomp}") if @traffic_logger
396
+ puts "#{Time.now.strftime('%d.%m.%Y %H:%M:%S')} << #{line.chomp}" if @config.verbose
397
+
398
+ case line.chomp
399
+ when /^PING \S+$/
400
+ raw line.chomp.gsub('PING', 'PONG')
401
+
402
+ when /^:(\S+) (376|422)/
403
+ parse(:connect)
404
+
405
+ when /^:(\S+)!(\S+)@(\S+) PRIVMSG #(\S+) :/
406
+ parse(:channel, :nick => $1, :user => $2, :host => $3, :channel => "##{$4}", :message => $')
407
+
408
+ when /^:(\S+)!(\S+)@(\S+) PRIVMSG (\S+) :/
409
+ parse(:query, :nick => $1, :user => $2, :host => $3, :message => $')
410
+
411
+ when /^:(\S+)!(\S+)@(\S+) JOIN :*(\S+)$/
412
+ parse(:join, :nick => $1, :user => $2, :host => $3, :channel => $4)
413
+
414
+ when /^:(\S+)!(\S+)@(\S+) PART (\S+)/
415
+ parse(:part, :nick => $1, :user => $2, :host => $3, :channel => $4, :message => $'.sub(' :', ''))
416
+
417
+ when /^:(\S+)!(\S+)@(\S+) QUIT/
418
+ parse(:quit, :nick => $1, :user => $2, :host => $3, :message => $'.sub(' :', ''))
419
+
420
+ when /^:(\S+)!(\S+)@(\S+) NICK :/
421
+ parse(:nickchange, :nick => $1, :user => $2, :host => $3, :new_nick => $')
422
+
423
+ when /^:(\S+)!(\S+)@(\S+) KICK (\S+) (\S+) :/
424
+ parse(:kick, :nick => $1, :user => $2, :host => $3, :channel => $4, :victim => $5, :reason => $')
425
+ end
426
+
427
+ if @observers > 0
428
+ @temp_socket << line.chomp
429
+ end
430
+ end
431
+
432
+ @traffic_logger.info('-- Percy disconnected') if @traffic_logger
433
+ puts "#{Time.now.strftime('%d.%m.%Y %H:%M:%S')} -- Percy disconnected"
434
+ @connected = false
435
+ end
436
+ rescue => e
437
+ @error_logger.error(e.message)
438
+ e.backtrace.each do |line|
439
+ @error_logger.error(line)
440
+ end
441
+
442
+ @traffic_logger.info('-- Percy disconnected') if @traffic_logger
443
+ puts "#{Time.now.strftime('%d.%m.%Y %H:%M:%S')} -- Percy disconnected"
444
+ @connected = false
445
+ ensure
446
+ @traffic_logger.file.close if @traffic_logger
447
+ @error_logger.file.close if @error_logger
448
+ end
449
+ end
450
+ end
@@ -0,0 +1,67 @@
1
+ class PercyLogger
2
+ require 'thread'
3
+
4
+ DEBUG = 0
5
+ INFO = 1
6
+ WARN = 2
7
+ ERROR = 3
8
+ FATAL = 4
9
+ UNKNOWN = 5
10
+ LEVEL = ['DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL', 'UNKNOWN']
11
+
12
+ attr_accessor :level, :time_format, :file
13
+
14
+ def initialize(filename = 'log.log', level = DEBUG, time_format = '%d.%m.%Y %H:%M:%S')
15
+ @filename = filename
16
+ @level = level
17
+ @time_format = time_format
18
+ @mutex = Mutex.new
19
+
20
+ unless File.exist?(@filename)
21
+ unless File.directory?(File.dirname(@filename))
22
+ Dir.mkdir(File.dirname(@filename))
23
+ end
24
+ File.new(@filename, 'w+')
25
+ end
26
+
27
+ @file = File.open(@filename, 'w+')
28
+ @file.sync = true
29
+ end
30
+
31
+ def write(severity, message)
32
+ begin
33
+ if severity >= @level
34
+ @mutex.synchronize do
35
+ @file.puts "#{LEVEL[severity]} #{Time.now.strftime(@time_format)} #{message}"
36
+ end
37
+ end
38
+ rescue => e
39
+ puts e.message
40
+ puts e.backtrace.join('\n')
41
+ end
42
+ end
43
+
44
+ def debug(message)
45
+ write DEBUG, message
46
+ end
47
+
48
+ def info(message)
49
+ write INFO, message
50
+ end
51
+
52
+ def warn(message)
53
+ write WARN, message
54
+ end
55
+
56
+ def error(message)
57
+ write ERROR, message
58
+ end
59
+
60
+ def fatal(message)
61
+ write FATAL, message
62
+ end
63
+
64
+ def unknown(message)
65
+ write UNKNOWN, message
66
+ end
67
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: percy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - "Tobias B\xC3\xBChlmann"
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-12-01 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Percy is an IRC bot framework inspired by isaac with various changes.
17
+ email: tobias.buehlmann@gmx.de
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - lib/percy.rb
26
+ - lib/percylogger.rb
27
+ - README.rdoc
28
+ - LICENSE
29
+ has_rdoc: true
30
+ homepage: http://github.com/tbuehlmann/percy
31
+ licenses: []
32
+
33
+ post_install_message:
34
+ rdoc_options: []
35
+
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: "0"
43
+ version:
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: "0"
49
+ version:
50
+ requirements: []
51
+
52
+ rubyforge_project:
53
+ rubygems_version: 1.3.5
54
+ signing_key:
55
+ specification_version: 3
56
+ summary: IRC bot framework (inspired by isaac)
57
+ test_files: []
58
+