percy 0.0.6 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/README.md +34 -32
  2. data/VERSION +1 -1
  3. data/lib/percy.rb +187 -178
  4. metadata +13 -4
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Percy 0.0.6
1
+ # Percy 1.0.0
2
2
 
3
3
  ## Configuring and starting the bot
4
4
 
@@ -9,29 +9,31 @@
9
9
  require 'rubygems'
10
10
  require 'percy'
11
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
12
+ Percy.configure do |c|
13
+ c.server = 'chat.eu.freenode.net'
14
+ c.port = 6667
15
+ # c.password = 'password'
16
+ c.nick = 'Percyguy'
17
+ c.username = 'Percyguy'
18
+ c.verbose = true
19
+ c.logging = true
20
+ c.reconnect = true
21
+ c.reconnect_interval = 30
20
22
  end
21
23
 
22
- bot.connect
24
+ Percy.connect
23
25
 
24
26
  Start it with `ruby mybot.rb`.
25
27
 
26
28
  ## Handling Events
27
29
  ### Connect
28
- bot.on :connect do
30
+ Percy.on :connect do
29
31
  # ...
30
32
  end
31
33
  No variables.
32
34
 
33
35
  ### Channel message
34
- bot.on :channel, /^foo!/ do |env|
36
+ Percy.on :channel, /^foo!/ do |env|
35
37
  # ...
36
38
  end
37
39
  Variables:
@@ -43,7 +45,7 @@ env[:channel]<br />
43
45
  env[:message]</tt>
44
46
 
45
47
  ### Query message
46
- bot.on :query, /^bar!/ do |env|
48
+ Percy.on :query, /^bar!/ do |env|
47
49
  # ...
48
50
  end
49
51
  Variables:
@@ -54,7 +56,7 @@ env[:host]<br />
54
56
  env[:message]</tt>
55
57
 
56
58
  ### Join
57
- bot.on :join do |env|
59
+ Percy.on :join do |env|
58
60
  # ...
59
61
  end
60
62
  Variables:
@@ -65,7 +67,7 @@ env[:host]<br />
65
67
  env[:channel]</tt>
66
68
 
67
69
  ### Part
68
- bot.on :part do |env|
70
+ Percy.on :part do |env|
69
71
  # ...
70
72
  end
71
73
  Variables:
@@ -77,7 +79,7 @@ env[:channel]<br />
77
79
  env[:message]</tt>
78
80
 
79
81
  ### Quit
80
- bot.on :quit do |env|
82
+ Percy.on :quit do |env|
81
83
  # ...
82
84
  end
83
85
  Variables:
@@ -88,7 +90,7 @@ env[:host]<br />
88
90
  env[:message]</tt>
89
91
 
90
92
  ### Nickchange
91
- bot.on :nickchange do |env|
93
+ Percy.on :nickchange do |env|
92
94
  # ...
93
95
  end
94
96
  Variables:
@@ -99,7 +101,7 @@ env[:host]<br />
99
101
  env[:new_nick]</tt>
100
102
 
101
103
  ### Kick
102
- bot.on :kick do |env|
104
+ Percy.on :kick do |env|
103
105
  # ...
104
106
  end
105
107
  Variables:
@@ -111,58 +113,58 @@ env[:channel]<br />
111
113
  env[:victim]<br />
112
114
  env[:reason]</tt>
113
115
 
114
- ## Availabe Methods
116
+ ## Availabe Class Methods
115
117
 
116
- `raw(msg)`
118
+ `Percy.raw(msg)`
117
119
 
118
120
  Sends a raw message to the server.
119
121
 
120
- `message(recipient, msg)`
122
+ `Percy.message(recipient, msg)`
121
123
 
122
124
  Sends a message to a channel or an user.
123
125
 
124
- `notice(recipient, msg)`
126
+ `Percy.notice(recipient, msg)`
125
127
 
126
128
  Sends a notice to an user.
127
129
 
128
- `action(recipient, msg)`
130
+ `Percy.action(recipient, msg)`
129
131
 
130
132
  Performs an action (/me ...).
131
133
 
132
- `mode(recipient, option)`
134
+ `Percy.mode(recipient, option)`
133
135
 
134
136
  Sets a mode for a channel or an user.
135
137
 
136
- `channellimit(channel)`
138
+ `Percy.channellimit(channel)`
137
139
 
138
140
  Returns the channel limit of a channel (as integer if set, else (not set/timeout) false).
139
141
 
140
- `kick(channel, user, reason)`
142
+ `Percy.kick(channel, user, reason)`
141
143
 
142
144
  Kicks an user from a channel with a specific reason.
143
145
 
144
- `topic(channel, topic)`
146
+ `Percy.topic(channel, topic)`
145
147
 
146
148
  Sets the topic for a channel.
147
149
 
148
- `join(channel, password = nil)`
150
+ `Percy.join(channel, password = nil)`
149
151
 
150
152
  Joins a channel.
151
153
 
152
- `part(channel, msg)`
154
+ `Percy.part(channel, msg)`
153
155
 
154
156
  Parts a channel with a message.
155
157
 
156
- `quit(msg = nil)`
158
+ `Percy.quit(msg = nil)`
157
159
 
158
160
  Quits from the server with a message.
159
161
 
160
- `users_on(channel)`
162
+ `Percy.users_on(channel)`
161
163
 
162
164
  Returns an array of users from a channel (mode in front like: ['@percy', 'Peter_Parker', '+The_Librarian']) or false if timeout.
163
165
 
164
166
 
165
- `is_online(nick)`
167
+ `Percy.is_online(nick)`
166
168
 
167
169
  Returns a nickname as string if online, else false (not online/timeout)
168
170
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.6
1
+ 1.0.0
data/lib/percy.rb CHANGED
@@ -1,43 +1,73 @@
1
1
  $:.unshift File.expand_path(File.dirname(__FILE__))
2
2
 
3
+ require 'rubygems'
4
+ require 'eventmachine'
3
5
  require 'percylogger'
4
- require 'socket'
5
6
  require 'timeout'
6
7
  require 'thread'
7
8
 
8
9
  Thread.abort_on_exception = true
9
10
 
10
- class Percy
11
- VERSION = 'Percy 0.0.6 (http://github.com/tbuehlmann/percy)'
11
+ class Connection < EventMachine::Connection
12
+ include EventMachine::Protocols::LineText2
12
13
 
13
- Config = Struct.new(:server, :port, :password, :nick, :username, :verbose, :logging)
14
+ def connection_completed
15
+ Percy.raw "NICK #{Percy.config.nick}"
16
+ Percy.raw "USER #{Percy.config.nick} 0 * :#{Percy.config.username}"
17
+ Percy.raw "PASS #{Percy.config.password}" if Percy.config.password
18
+ end
14
19
 
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
20
+ def unbind
21
+ Percy.connected = false
22
+ Percy.traffic_logger.info('-- Percy disconnected') if Percy.traffic_logger
23
+ puts "#{Time.now.strftime('%d.%m.%Y %H:%M:%S')} -- Percy disconnected"
24
+ if Percy.config.reconnect
25
+ Percy.traffic_logger.info("-- Reconnecting in #{Percy.config.reconnect_interval} seconds") if Percy.traffic_logger
26
+ puts "#{Time.now.strftime('%d.%m.%Y %H:%M:%S')} -- Reconnecting in #{Percy.config.reconnect_interval} seconds"
27
+
28
+ EventMachine::add_timer(Percy.config.reconnect_interval) do
29
+ reconnect Percy.config.server, Percy.config.port
30
+ end
31
+ end
32
+ end
33
+
34
+ def receive_line(line)
35
+ Percy.parse line
36
+ end
37
+ end
38
+
39
+ class Percy
40
+ class << self
41
+ attr_reader :config
42
+ attr_accessor :traffic_logger, :connected
37
43
  end
38
44
 
39
- # configure block
40
- def configure(&block)
45
+ VERSION = 'Percy 1.0.0 (http://github.com/tbuehlmann/percy)'
46
+
47
+ Config = Struct.new(:server, :port, :password, :nick, :username, :verbose, :logging, :reconnect, :reconnect_interval)
48
+
49
+ @config = Config.new("localhost", 6667, nil, 'Percy', 'Percy', true, false, false, 30)
50
+
51
+ # helper variables for getting server return values
52
+ @observers = 0
53
+ @temp_socket = []
54
+
55
+ @connected = false
56
+
57
+ # user methods
58
+ @on_channel = []
59
+ @on_query = []
60
+ @on_connect = []
61
+ @on_join = []
62
+ @on_part = []
63
+ @on_quit = []
64
+ @on_nickchange = []
65
+ @on_kick = []
66
+
67
+ # observer synchronizer
68
+ @mutex_observer = Mutex.new
69
+
70
+ def self.configure(&block)
41
71
  block.call(@config)
42
72
 
43
73
  # logger
@@ -45,32 +75,84 @@ class Percy
45
75
  @error_logger = PercyLogger.new("#{PERCY_ROOT}/logs/error.log") if @config.logging
46
76
  end
47
77
 
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
78
+ # raw IRC messages
79
+ def self.raw(message)
80
+ @connection.send_data "#{message}\r\n"
81
+ @traffic_logger.info(">> #{message}") if @traffic_logg
82
+ puts "#{Time.now.strftime('%d.%m.%Y %H:%M:%S')} >> #{message}" if @config.verbose
53
83
  end
54
84
 
55
85
  # send a message
56
- def message(recipient, msg)
57
- raw "PRIVMSG #{recipient} :#{msg}"
86
+ def self.message(recipient, message)
87
+ self.raw "PRIVMSG #{recipient} :#{message}"
58
88
  end
59
89
 
60
90
  # send a notice
61
- def notice(recipient, msg)
62
- raw "NOTICE #{recipient} :#{msg}"
91
+ def self.notice(recipient, message)
92
+ self.raw "NOTICE #{recipient} :#{message}"
63
93
  end
64
94
 
65
95
  # set a mode
66
- def mode(recipient, option)
67
- raw "MODE #{recipient} #{option}"
96
+ def self.mode(recipient, option)
97
+ self.raw "MODE #{recipient} #{option}"
98
+ end
99
+
100
+ # kick a user
101
+ def self.kick(channel, user, reason)
102
+ if reason
103
+ self.raw "KICK #{channel} #{user} :#{reason}"
104
+ else
105
+ self.raw "KICK #{channel} #{user}"
106
+ end
107
+ end
108
+
109
+ # perform an action
110
+ def self.action(recipient, message)
111
+ self.raw "PRIVMSG #{recipient} :\001ACTION #{message}\001"
112
+ end
113
+
114
+ # set a topic
115
+ def self.topic(channel, topic)
116
+ self.raw "TOPIC #{channel} :#{topic}"
117
+ end
118
+
119
+ # joining a channel
120
+ def self.join(channel, password = nil)
121
+ if password
122
+ self.raw "JOIN #{channel} #{password}"
123
+ else
124
+ self.raw "JOIN #{channel}"
125
+ end
126
+ end
127
+
128
+ # parting a channel
129
+ def self.part(channel, message)
130
+ if msg
131
+ self.raw "PART #{channel} :#{message}"
132
+ else
133
+ self.raw 'PART'
134
+ end
135
+ end
136
+
137
+ # quitting
138
+ def self.quit(message = nil)
139
+ if message
140
+ self.raw "QUIT :#{message}"
141
+ else
142
+ self.raw 'QUIT'
143
+ end
144
+
145
+ @config.reconnect = false # so Percy does not reconnect after the socket has been closed
146
+ end
147
+
148
+ def self.nick
149
+ @config.nick
68
150
  end
69
151
 
70
152
  # returns all users on a specific channel
71
- def users_on(channel)
72
- add_observer
73
- raw "NAMES #{channel}"
153
+ def self.users_on(channel)
154
+ self.add_observer
155
+ self.raw "NAMES #{channel}"
74
156
 
75
157
  begin
76
158
  Timeout::timeout(10) do # try 10 seconds to retrieve the users of <channel>
@@ -92,14 +174,14 @@ class Percy
92
174
  rescue Timeout::Error
93
175
  return false
94
176
  ensure
95
- remove_observer
177
+ self.remove_observer
96
178
  end
97
179
  end
98
180
 
99
181
  # get the channel limit of a channel
100
- def channel_limit(channel)
101
- add_observer
102
- raw "MODE #{channel}"
182
+ def self.channel_limit(channel)
183
+ self.add_observer
184
+ self.raw "MODE #{channel}"
103
185
 
104
186
  begin
105
187
  Timeout::timeout(10) do # try 10 seconds to retrieve l mode of <channel>
@@ -121,14 +203,14 @@ class Percy
121
203
  rescue Timeout::Error
122
204
  return false
123
205
  ensure
124
- remove_observer
206
+ self.remove_observer
125
207
  end
126
208
  end
127
209
 
128
210
  # check whether an user is online
129
- def is_online(nick)
130
- add_observer
131
- raw "WHOIS #{nick}"
211
+ def self.is_online(nick)
212
+ self.add_observer
213
+ self.raw "WHOIS #{nick}"
132
214
 
133
215
  begin
134
216
  Timeout::timeout(10) do
@@ -152,60 +234,12 @@ class Percy
152
234
  rescue Timeout::Error
153
235
  return false
154
236
  ensure
155
- remove_observer
156
- end
157
- end
158
-
159
- # kick a user
160
- def kick(channel, user, reason)
161
- if reason
162
- raw "KICK #{channel} #{user} :#{reason}"
163
- else
164
- raw "KICK #{channel} #{user}"
165
- end
166
- end
167
-
168
- # perform an action
169
- def action(recipient, msg)
170
- raw "PRIVMSG #{recipient} :\001ACTION #{msg}\001"
171
- end
172
-
173
- # set a topic
174
- def topic(channel, topic)
175
- raw "TOPIC #{channel} :#{topic}"
176
- end
177
-
178
- # joining a channel
179
- def join(channel, password = nil)
180
- if password
181
- raw "JOIN #{channel} #{password}"
182
- else
183
- raw "JOIN #{channel}"
237
+ self.remove_observer
184
238
  end
185
239
  end
186
240
 
187
- # parting a channel
188
- def part(channel, msg)
189
- if msg
190
- raw "PART #{channel} :#{msg}"
191
- else
192
- raw 'PART'
193
- end
194
- end
195
-
196
- # quitting
197
- def quit(msg = nil)
198
- if msg
199
- raw "QUIT :#{msg}"
200
- else
201
- raw 'QUIT'
202
- end
203
-
204
- @running = false # so Percy does not reconnect after the socket has been closed
205
- end
206
-
207
241
  # on method
208
- def on(type = :channel, match = //, &block)
242
+ def self.on(type = :channel, match = //, &block)
209
243
  case type
210
244
  when :channel
211
245
  @on_channel << {:match => match, :proc => block}
@@ -227,22 +261,22 @@ class Percy
227
261
  end
228
262
 
229
263
  # add observer
230
- def add_observer
231
- @mutex.synchronize do
264
+ def self.add_observer
265
+ @mutex_observer.synchronize do
232
266
  @observers += 1
233
267
  end
234
268
  end
235
269
 
236
270
  # remove observer
237
- def remove_observer
238
- @mutex.synchronize do
271
+ def self.remove_observer
272
+ @mutex_observer.synchronize do
239
273
  @observers -= 1 # remove observer
240
274
  @temp_socket = [] if @observers == 0 # clear @temp_socket if no observers are active
241
275
  end
242
276
  end
243
277
 
244
- # parses incoming traffic
245
- def parse(type, env = nil)
278
+ # parses incoming traffic (types)
279
+ def self.parse_type(type, env = nil)
246
280
  case type
247
281
  when :connect
248
282
  @on_connect.each do |block|
@@ -284,17 +318,17 @@ class Percy
284
318
  when :query
285
319
  # version respones
286
320
  if env[:message] == "\001VERSION\001"
287
- notice env[:nick], "\001VERSION #{VERSION}\001"
321
+ self.notice env[:nick], "\001VERSION #{VERSION}\001"
288
322
  end
289
323
 
290
324
  # time response
291
325
  if env[:message] == "\001TIME\001"
292
- notice env[:nick], "\001TIME #{Time.now.strftime('%a %b %d %H:%M:%S %Y')}\001"
326
+ self.notice env[:nick], "\001TIME #{Time.now.strftime('%a %b %d %H:%M:%S %Y')}\001"
293
327
  end
294
328
 
295
329
  # ping response
296
330
  if env[:message] =~ /\001PING (\d+)\001/
297
- notice env[:nick], "\001PING #{$1}\001"
331
+ self.notice env[:nick], "\001PING #{$1}\001"
298
332
  end
299
333
 
300
334
  @on_query.each do |method|
@@ -396,77 +430,52 @@ class Percy
396
430
  end
397
431
  end
398
432
 
399
- def nick
400
- @config.nick
433
+ # connect!
434
+ def self.connect
435
+ @traffic_logger.info('-- Starting Percy') if @traffic_logger
436
+ puts "#{Time.now.strftime('%d.%m.%Y %H:%M:%S')} -- Starting Percy"
437
+
438
+ EventMachine::run do
439
+ @connection = EventMachine::connect(@config.server, @config.port, Connection)
440
+ end
401
441
  end
402
442
 
403
- # connect!
404
- def connect
405
- begin
406
- while @running
407
- @traffic_logger.info('-- Starting Percy') if @traffic_logger
408
- puts "#{Time.now.strftime('%d.%m.%Y %H:%M:%S')} -- Starting Percy"
409
-
410
-
411
- @socket = TCPSocket.open(@config.server, @config.port)
412
- raw "PASS #{@config.password}" if @config.password
413
- raw "NICK #{@config.nick}"
414
- raw "USER #{@config.nick} 0 * :#{@config.username}"
415
-
416
- while line = @socket.gets
417
- @traffic_logger.info("<< #{line.chomp}") if @traffic_logger
418
- puts "#{Time.now.strftime('%d.%m.%Y %H:%M:%S')} << #{line.chomp}" if @config.verbose
419
-
420
- case line.chomp
421
- when /^PING \S+$/
422
- raw line.chomp.gsub('PING', 'PONG')
423
-
424
- when /^:\S+ 376|422/
425
- parse(:connect)
426
-
427
- when /^:(\S+)!(\S+)@(\S+) PRIVMSG #(\S+) :/
428
- parse(:channel, :nick => $1, :user => $2, :host => $3, :channel => "##{$4}", :message => $')
429
-
430
- when /^:(\S+)!(\S+)@(\S+) PRIVMSG \S+ :/
431
- parse(:query, :nick => $1, :user => $2, :host => $3, :message => $')
432
-
433
- when /^:(\S+)!(\S+)@(\S+) JOIN :*(\S+)$/
434
- parse(:join, :nick => $1, :user => $2, :host => $3, :channel => $4)
435
-
436
- when /^:(\S+)!(\S+)@(\S+) PART (\S+)/
437
- parse(:part, :nick => $1, :user => $2, :host => $3, :channel => $4, :message => $'.sub(' :', ''))
438
-
439
- when /^:(\S+)!(\S+)@(\S+) QUIT/
440
- parse(:quit, :nick => $1, :user => $2, :host => $3, :message => $'.sub(' :', ''))
441
-
442
- when /^:(\S+)!(\S+)@(\S+) NICK :/
443
- parse(:nickchange, :nick => $1, :user => $2, :host => $3, :new_nick => $')
444
-
445
- when /^:(\S+)!(\S+)@(\S+) KICK (\S+) (\S+) :/
446
- parse(:kick, :nick => $1, :user => $2, :host => $3, :channel => $4, :victim => $5, :reason => $')
447
- end
448
-
449
- if @observers > 0
450
- @temp_socket << line.chomp
451
- end
452
- end
453
-
454
- @traffic_logger.info('-- Percy disconnected') if @traffic_logger
455
- puts "#{Time.now.strftime('%d.%m.%Y %H:%M:%S')} -- Percy disconnected"
456
- @connected = false
457
- end
458
- rescue => e
459
- @error_logger.error(e.message)
460
- e.backtrace.each do |line|
461
- @error_logger.error(line)
462
- end
463
-
464
- @traffic_logger.info('-- Percy disconnected') if @traffic_logger
465
- puts "#{Time.now.strftime('%d.%m.%Y %H:%M:%S')} -- Percy disconnected"
466
- @connected = false
467
- ensure
468
- @traffic_logger.file.close if @traffic_logger
469
- @error_logger.file.close if @error_logger
443
+ # parsing incoming traffic
444
+ def self.parse(message)
445
+ @traffic_logger.info("<< #{message.chomp}") if @traffic_logger
446
+ puts "#{Time.now.strftime('%d.%m.%Y %H:%M:%S')} << #{message.chomp}" if @config.verbose
447
+
448
+ case message.chomp
449
+ when /^PING \S+$/
450
+ self.raw message.chomp.gsub('PING', 'PONG')
451
+
452
+ when /^:\S+ 376|422/
453
+ self.parse_type(:connect)
454
+
455
+ when /^:(\S+)!(\S+)@(\S+) PRIVMSG #(\S+) :/
456
+ self.parse_type(:channel, :nick => $1, :user => $2, :host => $3, :channel => "##{$4}", :message => $')
457
+
458
+ when /^:(\S+)!(\S+)@(\S+) PRIVMSG \S+ :/
459
+ self.parse_type(:query, :nick => $1, :user => $2, :host => $3, :message => $')
460
+
461
+ when /^:(\S+)!(\S+)@(\S+) JOIN :*(\S+)$/
462
+ self.parse_type(:join, :nick => $1, :user => $2, :host => $3, :channel => $4)
463
+
464
+ when /^:(\S+)!(\S+)@(\S+) PART (\S+)/
465
+ self.parse_type(:part, :nick => $1, :user => $2, :host => $3, :channel => $4, :message => $'.sub(' :', ''))
466
+
467
+ when /^:(\S+)!(\S+)@(\S+) QUIT/
468
+ self.parse_type(:quit, :nick => $1, :user => $2, :host => $3, :message => $'.sub(' :', ''))
469
+
470
+ when /^:(\S+)!(\S+)@(\S+) NICK :/
471
+ self.parse_type(:nickchange, :nick => $1, :user => $2, :host => $3, :new_nick => $')
472
+
473
+ when /^:(\S+)!(\S+)@(\S+) KICK (\S+) (\S+) :/
474
+ self.parse_type(:kick, :nick => $1, :user => $2, :host => $3, :channel => $4, :victim => $5, :reason => $')
475
+ end
476
+
477
+ if @observers > 0
478
+ @temp_socket << message.chomp
470
479
  end
471
480
  end
472
481
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: percy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - "Tobias B\xC3\xBChlmann"
@@ -9,10 +9,19 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-12-13 00:00:00 +01:00
12
+ date: 2009-12-21 00:00:00 +01:00
13
13
  default_executable:
14
- dependencies: []
15
-
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: eventmachine
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.12.10
24
+ version:
16
25
  description: Percy is an IRC bot framework inspired by isaac with various changes.
17
26
  email: tobias.buehlmann@gmx.de
18
27
  executables: []