bbrowning-ponder 0.0.2.1
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/LICENSE +7 -0
- data/README.md +334 -0
- data/examples/echo.rb +25 -0
- data/examples/github_blog.rb +31 -0
- data/examples/redis_last_seen.rb +140 -0
- data/lib/ponder.rb +22 -0
- data/lib/ponder/async_irc.rb +117 -0
- data/lib/ponder/callback.rb +42 -0
- data/lib/ponder/connection.rb +37 -0
- data/lib/ponder/delegate.rb +11 -0
- data/lib/ponder/filter.rb +7 -0
- data/lib/ponder/formatting.rb +30 -0
- data/lib/ponder/irc.rb +110 -0
- data/lib/ponder/logger/blind_io.rb +11 -0
- data/lib/ponder/logger/twoflogger.rb +93 -0
- data/lib/ponder/logger/twoflogger18.rb +93 -0
- data/lib/ponder/thaum.rb +233 -0
- data/lib/ponder/version.rb +3 -0
- data/ponder.gemspec +38 -0
- data/test/test_async_irc.rb +108 -0
- data/test/test_callback.rb +54 -0
- data/test/test_helper.rb +3 -0
- data/test/test_irc.rb +130 -0
- metadata +107 -0
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'thread'
|
3
|
+
autoload :FileUtils, 'fileutils'
|
4
|
+
|
5
|
+
module Ponder
|
6
|
+
module Logger
|
7
|
+
class Twoflogger
|
8
|
+
attr_accessor :level, :levels, :time_format
|
9
|
+
|
10
|
+
def initialize(destination = Ponder.root.join('logs', 'log.log'), level = :debug, time_format = '%Y-%m-%d %H:%M:%S', levels = {:debug => 0, :info => 1, :warn => 2, :error => 3, :fatal => 4, :unknown => 5})
|
11
|
+
@level = level
|
12
|
+
@time_format = time_format
|
13
|
+
@levels = levels
|
14
|
+
@queue = Queue.new
|
15
|
+
@mutex = Mutex.new
|
16
|
+
@running = false
|
17
|
+
|
18
|
+
define_level_shorthand_methods
|
19
|
+
self.log_dev = destination
|
20
|
+
end
|
21
|
+
|
22
|
+
def start_logging
|
23
|
+
@running = true
|
24
|
+
@thread = Thread.new do
|
25
|
+
begin
|
26
|
+
while @running do
|
27
|
+
write(@queue.pop)
|
28
|
+
end
|
29
|
+
ensure
|
30
|
+
@log_dev.close if @log_dev.is_a?(File)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def stop_logging
|
36
|
+
@running = false
|
37
|
+
end
|
38
|
+
|
39
|
+
def log_dev=(destination)
|
40
|
+
stop_logging
|
41
|
+
|
42
|
+
if destination.is_a?(Pathname)
|
43
|
+
unless destination.exist?
|
44
|
+
unless destination.dirname.directory?
|
45
|
+
FileUtils.mkdir_p destination.dirname
|
46
|
+
end
|
47
|
+
|
48
|
+
File.new(destination, 'w+')
|
49
|
+
end
|
50
|
+
@log_dev = File.open(destination, 'a+')
|
51
|
+
@log_dev.sync = true
|
52
|
+
elsif destination.is_a?(IO)
|
53
|
+
@log_dev = destination
|
54
|
+
else
|
55
|
+
raise TypeError, 'need a Pathname or IO'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def define_level_shorthand_methods
|
62
|
+
@levels.each_pair do |level_name, severity|
|
63
|
+
self.class.send(:define_method, level_name, Proc.new { |*messages| queue(severity, *messages) })
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def queue(severity, *messages)
|
68
|
+
raise(ArgumentError, 'Need a message') if messages.empty?
|
69
|
+
raise(ArgumentError, 'Need messages that respond to #to_s') if messages.any? { |message| !message.respond_to?(:to_s) }
|
70
|
+
|
71
|
+
if severity >= @levels[@level]
|
72
|
+
message_hashes = messages.map { |message| {:severity => severity, :message => message} }
|
73
|
+
|
74
|
+
@mutex.synchronize do
|
75
|
+
message_hashes.each do |hash|
|
76
|
+
@queue << hash
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def write(*message_hashes)
|
83
|
+
begin
|
84
|
+
message_hashes.each do |hash|
|
85
|
+
@log_dev.puts "#{@levels.index(hash[:severity])} #{Time.now.strftime(@time_format)} #{hash[:message]}"
|
86
|
+
end
|
87
|
+
rescue => e
|
88
|
+
puts e.message, *e.backtrace
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/ponder/thaum.rb
ADDED
@@ -0,0 +1,233 @@
|
|
1
|
+
require 'ponder/callback'
|
2
|
+
require 'ponder/connection'
|
3
|
+
require 'ponder/irc'
|
4
|
+
require 'ponder/async_irc'
|
5
|
+
require 'ponder/filter'
|
6
|
+
require 'ostruct'
|
7
|
+
|
8
|
+
module Ponder
|
9
|
+
class Thaum
|
10
|
+
include IRC
|
11
|
+
include AsyncIRC
|
12
|
+
|
13
|
+
if RUBY_VERSION >= '1.9'
|
14
|
+
require 'ponder/delegate'
|
15
|
+
include Delegate
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :config
|
19
|
+
attr_accessor :connected, :traffic_logger, :error_logger, :console_logger, :empty_logger
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@config = OpenStruct.new(:server => 'localhost',
|
23
|
+
:port => 6667,
|
24
|
+
:nick => 'Ponder',
|
25
|
+
:username => 'Ponder',
|
26
|
+
:real_name => 'Ponder',
|
27
|
+
:verbose => true,
|
28
|
+
:logging => false,
|
29
|
+
:reconnect => true,
|
30
|
+
:reconnect_interval => 30
|
31
|
+
)
|
32
|
+
|
33
|
+
@empty_logger = Logger::BlindIo.new
|
34
|
+
@traffic_logger = @empty_logger
|
35
|
+
@error_logger = @empty_logger
|
36
|
+
@console_logger = Logger::Twoflogger.new($stdout)
|
37
|
+
|
38
|
+
@observer_queues = {}
|
39
|
+
|
40
|
+
@connected = false
|
41
|
+
@reloading = false
|
42
|
+
|
43
|
+
# user callbacks
|
44
|
+
@callbacks = Hash.new { |hash, key| hash[key] = [] }
|
45
|
+
|
46
|
+
# standard callbacks for PING, VERSION, TIME and Nickname is already in use
|
47
|
+
on :query, /^\001PING \d+\001$/ do |env|
|
48
|
+
time = env[:message].scan(/\d+/)[0]
|
49
|
+
notice env[:nick], "\001PING #{time}\001"
|
50
|
+
end
|
51
|
+
|
52
|
+
on :query, /^\001VERSION\001$/ do |env|
|
53
|
+
notice env[:nick], "\001VERSION Ponder #{Ponder::VERSION} (http://github.com/tbuehlmann/ponder)\001"
|
54
|
+
end
|
55
|
+
|
56
|
+
on :query, /^\001TIME\001$/ do |env|
|
57
|
+
notice env[:nick], "\001TIME #{Time.now.strftime('%a %b %d %H:%M:%S %Y')}\001"
|
58
|
+
end
|
59
|
+
|
60
|
+
# before and after filter
|
61
|
+
@before_filters = Hash.new { |hash, key| hash[key] = [] }
|
62
|
+
@after_filters = Hash.new { |hash, key| hash[key] = [] }
|
63
|
+
end
|
64
|
+
|
65
|
+
def configure(&block)
|
66
|
+
unless @reloading
|
67
|
+
block.call(@config)
|
68
|
+
|
69
|
+
# logger changes (if differing from initialize)
|
70
|
+
if @config.logging
|
71
|
+
@traffic_logger = @config.traffic_logger ? @config.traffic_logger : Logger::Twoflogger.new(Ponder.root.join('logs', 'traffic.log'))
|
72
|
+
@error_logger = @config.error_logger ? @config.error_logger : Logger::Twoflogger.new(Ponder.root.join('logs', 'error.log'))
|
73
|
+
end
|
74
|
+
|
75
|
+
unless @config.verbose
|
76
|
+
@console_logger = @empty_logger
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def on(event_types = [:channel], match = //, &block)
|
82
|
+
if event_types.is_a?(Array)
|
83
|
+
callbacks = event_types.map { |event_type| Callback.new(event_type, match, block) }
|
84
|
+
else
|
85
|
+
callbacks = [Callback.new(event_types, match, block)]
|
86
|
+
event_types = [event_types]
|
87
|
+
end
|
88
|
+
|
89
|
+
callbacks.each_with_index do |callback, index|
|
90
|
+
@callbacks[event_types[index]] << callback
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def connect
|
95
|
+
unless @reloading
|
96
|
+
@traffic_logger.start_logging
|
97
|
+
@error_logger.start_logging
|
98
|
+
@console_logger.start_logging
|
99
|
+
|
100
|
+
@traffic_logger.info '-- Starting Ponder'
|
101
|
+
@console_logger.info '-- Starting Ponder'
|
102
|
+
|
103
|
+
EventMachine::run do
|
104
|
+
@connection = EventMachine::connect(@config.server, @config.port, Connection, self)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def reload!
|
110
|
+
@reloading = true
|
111
|
+
@callbacks.clear
|
112
|
+
load $0
|
113
|
+
@reloading = false
|
114
|
+
end
|
115
|
+
|
116
|
+
def reloading?
|
117
|
+
@reloading
|
118
|
+
end
|
119
|
+
|
120
|
+
# parsing incoming traffic
|
121
|
+
def parse(message)
|
122
|
+
message.chomp!
|
123
|
+
@traffic_logger.info "<< #{message}"
|
124
|
+
@console_logger.info "<< #{message}"
|
125
|
+
|
126
|
+
case message
|
127
|
+
when /^PING \S+$/
|
128
|
+
raw message.sub(/PING/, 'PONG')
|
129
|
+
|
130
|
+
when /^:\S+ (\d\d\d) /
|
131
|
+
number = $1.to_i
|
132
|
+
parse_event(number, :type => number, :params => $')
|
133
|
+
|
134
|
+
when /^:(\S+)!(\S+)@(\S+) PRIVMSG #(\S+) :/
|
135
|
+
parse_event(:channel, :type => :channel, :nick => $1, :user => $2, :host => $3, :channel => "##{$4}", :message => $')
|
136
|
+
|
137
|
+
when /^:(\S+)!(\S+)@(\S+) PRIVMSG \S+ :/
|
138
|
+
parse_event(:query, :type => :query, :nick => $1, :user => $2, :host => $3, :message => $')
|
139
|
+
|
140
|
+
when /^:(\S+)!(\S+)@(\S+) JOIN :*(\S+)$/
|
141
|
+
parse_event(:join, :type => :join, :nick => $1, :user => $2, :host => $3, :channel => $4)
|
142
|
+
|
143
|
+
when /^:(\S+)!(\S+)@(\S+) PART (\S+)/
|
144
|
+
parse_event(:part, :type => :part, :nick => $1, :user => $2, :host => $3, :channel => $4, :message => $'.sub(/ :/, ''))
|
145
|
+
|
146
|
+
when /^:(\S+)!(\S+)@(\S+) QUIT/
|
147
|
+
parse_event(:quit, :type => :quit, :nick => $1, :user => $2, :host => $3, :message => $'.sub(/ :/, ''))
|
148
|
+
|
149
|
+
when /^:(\S+)!(\S+)@(\S+) NICK :/
|
150
|
+
parse_event(:nickchange, :type => :nickchange, :nick => $1, :user => $2, :host => $3, :new_nick => $')
|
151
|
+
|
152
|
+
when /^:(\S+)!(\S+)@(\S+) KICK (\S+) (\S+) :/
|
153
|
+
parse_event(:kick, :type => :kick, :nick => $1, :user => $2, :host => $3, :channel => $4, :victim => $5, :reason => $')
|
154
|
+
|
155
|
+
when /^:(\S+)!(\S+)@(\S+) TOPIC (\S+) :/
|
156
|
+
parse_event(:topic, :type => :topic, :nick => $1, :user => $2, :host => $3, :channel => $4, :topic => $')
|
157
|
+
end
|
158
|
+
|
159
|
+
@observer_queues.each do |queue, regexps|
|
160
|
+
regexps.each do |regexp|
|
161
|
+
if message =~ regexp
|
162
|
+
queue << message
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# process callbacks with its begin; rescue; end
|
169
|
+
def process_callbacks(event_type, event_data)
|
170
|
+
@callbacks[event_type].each do |callback|
|
171
|
+
EM.defer(
|
172
|
+
Proc.new do
|
173
|
+
begin
|
174
|
+
stop_running = false
|
175
|
+
|
176
|
+
# before filters (specific filters first, then :all)
|
177
|
+
(@before_filters[event_type] + @before_filters[:all]).each do |filter|
|
178
|
+
if filter.call(event_type, event_data) == false
|
179
|
+
stop_running = true
|
180
|
+
break
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
unless stop_running
|
185
|
+
# handling
|
186
|
+
callback.call(event_type, event_data)
|
187
|
+
|
188
|
+
# after filters (specific filters first, then :all)
|
189
|
+
(@after_filters[event_type] + @after_filters[:all]).each do |filter|
|
190
|
+
filter.call(event_type, event_data)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
rescue => e
|
194
|
+
@error_logger.error(e.message, *e.backtrace)
|
195
|
+
@console_logger.error(e.message, *e.backtrace)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def before_filter(event_types = :all, match = //, &block)
|
203
|
+
filter(@before_filters, event_types, match, block)
|
204
|
+
end
|
205
|
+
|
206
|
+
def after_filter(event_types = :all, match = //, &block)
|
207
|
+
filter(@after_filters, event_types, match, block)
|
208
|
+
end
|
209
|
+
|
210
|
+
private
|
211
|
+
|
212
|
+
# parses incoming traffic (types)
|
213
|
+
def parse_event(event_type, event_data = {})
|
214
|
+
if ((event_type == 376) || (event_type == 422)) && !@connected
|
215
|
+
@connected = true
|
216
|
+
process_callbacks(:connect, event_data)
|
217
|
+
end
|
218
|
+
|
219
|
+
process_callbacks(event_type, event_data)
|
220
|
+
end
|
221
|
+
|
222
|
+
def filter(filter_type, event_types = :all, match = //, &block)
|
223
|
+
if event_types.is_a?(Array)
|
224
|
+
event_types.each do |event_type|
|
225
|
+
filter_type[event_type] << Filter.new(event_type, match, block)
|
226
|
+
end
|
227
|
+
else
|
228
|
+
filter_type[event_types] << Filter.new(event_types, match, block)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
data/ponder.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'ponder'
|
3
|
+
s.version = '0.0.2'
|
4
|
+
s.summary = 'IRC bot framework'
|
5
|
+
s.description = 'Ponder (Stibbons) is a Domain Specific Language for writing IRC Bots using the EventMachine library.'
|
6
|
+
|
7
|
+
s.author = 'Tobias Bühlmann'
|
8
|
+
s.email = 'tobias.buehlmann@gmx.de'
|
9
|
+
s.homepage = 'http://github.com/tbuehlmann/ponder'
|
10
|
+
|
11
|
+
s.required_ruby_version = '>= 1.8.6'
|
12
|
+
s.add_dependency('eventmachine', '>= 0.12.10')
|
13
|
+
s.files = ['examples/echo.rb',
|
14
|
+
'examples/github_blog.rb',
|
15
|
+
'examples/redis_last_seen.rb',
|
16
|
+
'lib/ponder/async_irc.rb',
|
17
|
+
'lib/ponder/callback.rb',
|
18
|
+
'lib/ponder/connection.rb',
|
19
|
+
'lib/ponder/delegate.rb',
|
20
|
+
'lib/ponder/filter.rb',
|
21
|
+
'lib/ponder/formatting.rb',
|
22
|
+
'lib/ponder/irc.rb',
|
23
|
+
'lib/ponder/logger',
|
24
|
+
'lib/ponder/thaum.rb',
|
25
|
+
'lib/ponder/version.rb',
|
26
|
+
'lib/ponder/logger/blind_io.rb',
|
27
|
+
'lib/ponder/logger/twoflogger.rb',
|
28
|
+
'lib/ponder/logger/twoflogger18.rb',
|
29
|
+
'lib/ponder.rb',
|
30
|
+
'test/test_async_irc.rb',
|
31
|
+
'test/test_callback.rb',
|
32
|
+
'test/test_helper.rb',
|
33
|
+
'test/test_irc.rb',
|
34
|
+
'LICENSE',
|
35
|
+
'ponder.gemspec',
|
36
|
+
'README.md']
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
$LOAD_PATH.unshift Pathname.new(__FILE__).dirname.expand_path
|
3
|
+
require 'test_helper'
|
4
|
+
require 'ponder/async_irc'
|
5
|
+
|
6
|
+
module Ponder
|
7
|
+
module AsyncIRC
|
8
|
+
def raw(*args)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
include Ponder::AsyncIRC
|
14
|
+
|
15
|
+
class TestIRC < Test::Unit::TestCase
|
16
|
+
def setup
|
17
|
+
@observer_queues = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_get_topic_no_topic_set
|
21
|
+
topic = Thread.new do
|
22
|
+
assert_equal({:raw_numeric => 331, :message => 'No topic is set'}, get_topic('#mended_drum'))
|
23
|
+
end
|
24
|
+
|
25
|
+
event_loop = Thread.new do
|
26
|
+
loop do
|
27
|
+
message = ':server 331 Ponder #mended_drum :No topic is set.'
|
28
|
+
@observer_queues.each do |queue, regexps|
|
29
|
+
regexps.each do |regexp|
|
30
|
+
if message =~ regexp
|
31
|
+
queue << message
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
sleep 0.1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
topic.join
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_get_topic
|
43
|
+
topic = Thread.new do
|
44
|
+
assert_equal({:raw_numeric => 332, :message => 'No dwarfs in here!'}, get_topic('#mended_drum'))
|
45
|
+
end
|
46
|
+
|
47
|
+
event_loop = Thread.new do
|
48
|
+
loop do
|
49
|
+
message = ':server 332 Ponder #mended_drum :No dwarfs in here!'
|
50
|
+
@observer_queues.each do |queue, regexps|
|
51
|
+
regexps.each do |regexp|
|
52
|
+
if message =~ regexp
|
53
|
+
queue << message
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
sleep 0.1
|
59
|
+
end
|
60
|
+
end
|
61
|
+
topic.join
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_get_topic_no_such_channel
|
65
|
+
topic = Thread.new do
|
66
|
+
assert_equal({:raw_numeric => 403, :message => 'No such channel'}, get_topic('#mended_drum'))
|
67
|
+
end
|
68
|
+
|
69
|
+
event_loop = Thread.new do
|
70
|
+
loop do
|
71
|
+
message = ':server 403 Ponder #mended_drum :No such channel'
|
72
|
+
@observer_queues.each do |queue, regexps|
|
73
|
+
regexps.each do |regexp|
|
74
|
+
if message =~ regexp
|
75
|
+
queue << message
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
sleep 0.1
|
81
|
+
end
|
82
|
+
end
|
83
|
+
topic.join
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_get_topic_you_are_not_on_that_channel
|
87
|
+
topic = Thread.new do
|
88
|
+
assert_equal({:raw_numeric => 442, :message => "You're not on that channel"}, get_topic('#mended_drum'))
|
89
|
+
end
|
90
|
+
|
91
|
+
event_loop = Thread.new do
|
92
|
+
loop do
|
93
|
+
message = ":server 442 Ponder #mended_drum :You're not on that channel"
|
94
|
+
@observer_queues.each do |queue, regexps|
|
95
|
+
regexps.each do |regexp|
|
96
|
+
if message =~ regexp
|
97
|
+
queue << message
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
sleep 0.1
|
103
|
+
end
|
104
|
+
end
|
105
|
+
topic.join
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|