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
data/lib/ponder.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'rubygems'
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift Pathname.new(__FILE__).dirname.expand_path
|
5
|
+
|
6
|
+
module Ponder
|
7
|
+
def self.root
|
8
|
+
Pathname.new($0).dirname.expand_path
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'ponder/version'
|
12
|
+
require 'ponder/thaum'
|
13
|
+
require 'ponder/formatting'
|
14
|
+
require 'ponder/logger/blind_io'
|
15
|
+
|
16
|
+
if RUBY_VERSION < '1.9'
|
17
|
+
require 'ponder/logger/twoflogger18'
|
18
|
+
else
|
19
|
+
require 'ponder/logger/twoflogger'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
module Ponder
|
5
|
+
module AsyncIRC
|
6
|
+
def get_topic(channel)
|
7
|
+
queue = Queue.new
|
8
|
+
@observer_queues[queue] = [/:\S+ (331|332|403|442) \S+ #{Regexp.escape(channel)} :/i]
|
9
|
+
raw "TOPIC #{channel}"
|
10
|
+
|
11
|
+
topic = begin
|
12
|
+
Timeout::timeout(30) do
|
13
|
+
response = queue.pop
|
14
|
+
raw_numeric = response.scan(/^:\S+ (\d{3})/)[0][0]
|
15
|
+
|
16
|
+
case raw_numeric
|
17
|
+
when '331'
|
18
|
+
{:raw_numeric => 331, :message => 'No topic is set'}
|
19
|
+
when '332'
|
20
|
+
{:raw_numeric => 332, :message => response.scan(/ :(.*)/)[0][0]}
|
21
|
+
when '403'
|
22
|
+
{:raw_numeric => 403, :message => 'No such channel'}
|
23
|
+
when '442'
|
24
|
+
{:raw_numeric => 442, :message => "You're not on that channel"}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
rescue Timeout::Error
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
@observer_queues.delete queue
|
32
|
+
return topic
|
33
|
+
end
|
34
|
+
|
35
|
+
def channel_info(channel)
|
36
|
+
queue = Queue.new
|
37
|
+
@observer_queues[queue] = [/:\S+ (324|329|403) \S+ #{Regexp.escape(channel)}/i]
|
38
|
+
raw "MODE #{channel}"
|
39
|
+
information = {}
|
40
|
+
running = true
|
41
|
+
|
42
|
+
begin
|
43
|
+
Timeout::timeout(30) do
|
44
|
+
while running
|
45
|
+
response = queue.pop
|
46
|
+
raw_numeric = response.scan(/^:\S+ (\d{3})/)[0][0]
|
47
|
+
|
48
|
+
case raw_numeric
|
49
|
+
when '324'
|
50
|
+
information[:modes] = response.scan(/^:\S+ 324 \S+ \S+ \+(\w*)/)[0][0].split('')
|
51
|
+
limit = response.scan(/^:\S+ 324 \S+ \S+ \+\w* (\w*)/)[0]
|
52
|
+
information[:channel_limit] = limit[0].to_i if limit
|
53
|
+
when '329'
|
54
|
+
information[:created_at] = Time.at(response.scan(/^:\S+ 329 \S+ \S+ (\d+)/)[0][0].to_i)
|
55
|
+
running = false
|
56
|
+
when '403'
|
57
|
+
information = false
|
58
|
+
running = false
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
rescue Timeout::Error
|
63
|
+
information = false
|
64
|
+
end
|
65
|
+
|
66
|
+
@observer_queues.delete queue
|
67
|
+
return information
|
68
|
+
end
|
69
|
+
|
70
|
+
def whois(nick)
|
71
|
+
queue = Queue.new
|
72
|
+
@observer_queues[queue] = [/^:\S+ (307|311|312|318|319|401) \S+ #{Regexp.escape(nick)}/i]
|
73
|
+
raw "WHOIS #{nick}"
|
74
|
+
whois = {}
|
75
|
+
running = true
|
76
|
+
|
77
|
+
while running
|
78
|
+
begin
|
79
|
+
Timeout::timeout(30) do
|
80
|
+
response = queue.pop
|
81
|
+
raw_numeric = response.scan(/^:\S+ (\d{3})/)[0][0]
|
82
|
+
|
83
|
+
case raw_numeric
|
84
|
+
when '307'
|
85
|
+
whois[:registered] = true
|
86
|
+
when '311'
|
87
|
+
response = response.scan(/^:\S+ 311 \S+ (\S+) (\S+) (\S+) \* :(.*)$/)[0]
|
88
|
+
whois[:nick] = response[0]
|
89
|
+
whois[:username] = response[1]
|
90
|
+
whois[:host] = response[2]
|
91
|
+
whois[:real_name] = response[3]
|
92
|
+
when '312'
|
93
|
+
response = response.scan(/^:\S+ 312 \S+ \S+ (\S+) :(.*)/)[0]
|
94
|
+
whois[:server] = {:address => response[0], :name => response[1]}
|
95
|
+
when '318'
|
96
|
+
running = false
|
97
|
+
when '319'
|
98
|
+
channels_with_mode = response.scan(/^:\S+ 319 \S+ \S+ :(.*)/)[0][0].split(' ')
|
99
|
+
whois[:channels] = {}
|
100
|
+
channels_with_mode.each do |c|
|
101
|
+
whois[:channels][c.scan(/(.)?(#\S+)/)[0][1]] = c.scan(/(.)?(#\S+)/)[0][0]
|
102
|
+
end
|
103
|
+
when '401'
|
104
|
+
whois = false
|
105
|
+
running = false
|
106
|
+
end
|
107
|
+
end
|
108
|
+
rescue Timeout::Error
|
109
|
+
nil
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
@observer_queues.delete queue
|
114
|
+
return whois
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Ponder
|
2
|
+
class Callback
|
3
|
+
LISTENED_TYPES = [:connect, :channel, :query, :join, :part, :quit, :nickchange, :kick, :topic, :disconnect] # + 3-digit numbers
|
4
|
+
|
5
|
+
def initialize(event_type = :channel, match = //, proc = Proc.new {})
|
6
|
+
unless self.class::LISTENED_TYPES.include?(event_type) || event_type.is_a?(Integer)
|
7
|
+
raise TypeError, "#{event_type} is an unsupported event-type"
|
8
|
+
end
|
9
|
+
|
10
|
+
self.match = match
|
11
|
+
self.proc = proc
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(event_type, event_data = {})
|
15
|
+
if (event_type == :channel) || (event_type == :query)
|
16
|
+
@proc.call(event_data) if event_data[:message] =~ @match
|
17
|
+
elsif event_type == :topic
|
18
|
+
@proc.call(event_data) if event_data[:topic] =~ @match
|
19
|
+
else
|
20
|
+
@proc.call(event_data)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def match=(match)
|
27
|
+
if match.is_a?(Regexp)
|
28
|
+
@match = match
|
29
|
+
else
|
30
|
+
raise TypeError, "#{match} must be a Regexp"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def proc=(proc)
|
35
|
+
if proc.is_a?(Proc)
|
36
|
+
@proc = proc
|
37
|
+
else
|
38
|
+
raise TypeError, "#{proc} must be a Proc"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
module Ponder
|
4
|
+
class Connection < EventMachine::Connection
|
5
|
+
include EventMachine::Protocols::LineText2
|
6
|
+
|
7
|
+
def initialize(thaum)
|
8
|
+
@thaum = thaum
|
9
|
+
end
|
10
|
+
|
11
|
+
def connection_completed
|
12
|
+
@thaum.register
|
13
|
+
end
|
14
|
+
|
15
|
+
def unbind
|
16
|
+
@thaum.connected = false
|
17
|
+
@thaum.process_callbacks :disconnect, {}
|
18
|
+
@thaum.traffic_logger.info '-- Ponder disconnected'
|
19
|
+
@thaum.console_logger.info '-- Ponder disconnected'
|
20
|
+
|
21
|
+
if @thaum.config.reconnect
|
22
|
+
@thaum.traffic_logger.info "-- Reconnecting in #{@thaum.config.reconnect_interval} seconds"
|
23
|
+
@thaum.console_logger.info "-- Reconnecting in #{@thaum.config.reconnect_interval} seconds"
|
24
|
+
|
25
|
+
EventMachine::add_timer(@thaum.config.reconnect_interval) do
|
26
|
+
reconnect @thaum.config.server, @thaum.config.port
|
27
|
+
end
|
28
|
+
else
|
29
|
+
EventMachine::stop_event_loop
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def receive_line(line)
|
34
|
+
@thaum.parse line
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Ponder
|
2
|
+
module Delegate
|
3
|
+
def delegate
|
4
|
+
thaum = self
|
5
|
+
|
6
|
+
(IRC.instance_methods + [:configure, :on, :connect, :reload!, :reloading?]).each do |method|
|
7
|
+
Object.send(:define_method, method) { |*args, &block| thaum.send(method, *args, &block) }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Ponder
|
2
|
+
module Formatting
|
3
|
+
PLAIN = 15.chr
|
4
|
+
BOLD = 2.chr
|
5
|
+
ITALIC = 22.chr
|
6
|
+
UNDERLINE = 31.chr
|
7
|
+
COLOR_CODE = 3.chr
|
8
|
+
UNCOLOR_CODE = COLOR_CODE
|
9
|
+
|
10
|
+
#mIRC color codes from http://www.mirc.com/help/colors.html
|
11
|
+
COLORS = {:white => '00',
|
12
|
+
:black => '01',
|
13
|
+
:blue => '02',
|
14
|
+
:green => '03',
|
15
|
+
:red => '04',
|
16
|
+
:brown => '05',
|
17
|
+
:purple => '06',
|
18
|
+
:orange => '07',
|
19
|
+
:yellow => '08',
|
20
|
+
:lime => '09',
|
21
|
+
:teal => '10',
|
22
|
+
:cyan => '11',
|
23
|
+
:royal => '12',
|
24
|
+
:pink => '13',
|
25
|
+
:gray => '14',
|
26
|
+
:silver => '15'
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
data/lib/ponder/irc.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
module Ponder
|
2
|
+
module IRC
|
3
|
+
# raw IRC messages
|
4
|
+
def raw(message)
|
5
|
+
@connection.send_data "#{message}\r\n"
|
6
|
+
@traffic_logger.info ">> #{message}"
|
7
|
+
@console_logger.info ">> #{message}"
|
8
|
+
end
|
9
|
+
|
10
|
+
# send a message
|
11
|
+
def message(recipient, message)
|
12
|
+
raw "PRIVMSG #{recipient} :#{message}"
|
13
|
+
end
|
14
|
+
|
15
|
+
# register when connected
|
16
|
+
def register
|
17
|
+
raw "NICK #{@config.nick}"
|
18
|
+
raw "USER #{@config.username} * * :#{@config.real_name}"
|
19
|
+
raw "PASS #{@config.password}" if @config.password
|
20
|
+
end
|
21
|
+
|
22
|
+
# send a notice
|
23
|
+
def notice(recipient, message)
|
24
|
+
raw "NOTICE #{recipient} :#{message}"
|
25
|
+
end
|
26
|
+
|
27
|
+
# set a mode
|
28
|
+
def mode(recipient, option)
|
29
|
+
raw "MODE #{recipient} #{option}"
|
30
|
+
end
|
31
|
+
|
32
|
+
# kick a user
|
33
|
+
def kick(channel, user, reason = nil)
|
34
|
+
if reason
|
35
|
+
raw "KICK #{channel} #{user} :#{reason}"
|
36
|
+
else
|
37
|
+
raw "KICK #{channel} #{user}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# perform an action
|
42
|
+
def action(recipient, message)
|
43
|
+
raw "PRIVMSG #{recipient} :\001ACTION #{message}\001"
|
44
|
+
end
|
45
|
+
|
46
|
+
# set a topic
|
47
|
+
def topic(channel, topic)
|
48
|
+
raw "TOPIC #{channel} :#{topic}"
|
49
|
+
end
|
50
|
+
|
51
|
+
# joining a channel
|
52
|
+
def join(channel, password = nil)
|
53
|
+
if password
|
54
|
+
raw "JOIN #{channel} #{password}"
|
55
|
+
else
|
56
|
+
raw "JOIN #{channel}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# parting a channel
|
61
|
+
def part(channel, message = nil)
|
62
|
+
if message
|
63
|
+
raw "PART #{channel} :#{message}"
|
64
|
+
else
|
65
|
+
raw "PART #{channel}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# quitting
|
70
|
+
def quit(message = nil)
|
71
|
+
if message
|
72
|
+
raw "QUIT :#{message}"
|
73
|
+
else
|
74
|
+
raw 'QUIT'
|
75
|
+
end
|
76
|
+
|
77
|
+
@config.reconnect = false # so Ponder does not reconnect after the socket has been closed
|
78
|
+
end
|
79
|
+
|
80
|
+
# rename
|
81
|
+
def rename(nick)
|
82
|
+
raw "NICK :#{nick}"
|
83
|
+
end
|
84
|
+
|
85
|
+
# set an away status
|
86
|
+
def away(message = nil)
|
87
|
+
if message
|
88
|
+
raw "AWAY :#{message}"
|
89
|
+
else
|
90
|
+
raw "AWAY"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# cancel an away status
|
95
|
+
def back
|
96
|
+
away
|
97
|
+
end
|
98
|
+
|
99
|
+
# invite an user to a channel
|
100
|
+
def invite(nick, channel)
|
101
|
+
raw "INVITE #{nick} #{channel}"
|
102
|
+
end
|
103
|
+
|
104
|
+
# ban an user
|
105
|
+
def ban(channel, address)
|
106
|
+
mode channel, "+b #{address}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Ponder
|
2
|
+
module Logger
|
3
|
+
class BlindIo
|
4
|
+
def initialize
|
5
|
+
[:debug, :info, :warn, :error, :fatal, :unknown, :start_logging, :stop_logging].each do |method_name|
|
6
|
+
self.class.send(:define_method, method_name, Proc.new { |*args| nil })
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -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.key(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
|