ponder 0.0.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.
@@ -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.force_encoding('utf-8')
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,7 @@
1
+ require 'ponder/callback'
2
+
3
+ module Ponder
4
+ class Filter < Callback
5
+ LISTENED_TYPES += [:all]
6
+ end
7
+ 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
+
@@ -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
@@ -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
@@ -0,0 +1,242 @@
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
+ :auto_rename => true
32
+ )
33
+
34
+ @empty_logger = Logger::BlindIo.new
35
+ @traffic_logger = @empty_logger
36
+ @error_logger = @empty_logger
37
+ @console_logger = Logger::Twoflogger.new($stdout)
38
+
39
+ @observer_queues = {}
40
+
41
+ @connected = false
42
+ @reloading = false
43
+
44
+ # user callbacks
45
+ @callbacks = Hash.new { |hash, key| hash[key] = [] }
46
+
47
+ # standard callbacks for PING, VERSION, TIME and Nickname is already in use
48
+ on :query, /^\001PING \d+\001$/ do |env|
49
+ time = env[:message].scan(/\d+/)[0]
50
+ notice env[:nick], "\001PING #{time}\001"
51
+ end
52
+
53
+ on :query, /^\001VERSION\001$/ do |env|
54
+ notice env[:nick], "\001VERSION Ponder #{Ponder::VERSION} (http://github.com/tbuehlmann/ponder)\001"
55
+ end
56
+
57
+ on :query, /^\001TIME\001$/ do |env|
58
+ notice env[:nick], "\001TIME #{Time.now.strftime('%a %b %d %H:%M:%S %Y')}\001"
59
+ end
60
+
61
+ on 433 do
62
+ rename "#{@config.nick}#{rand(10)}#{rand(10)}#{rand(10)}"
63
+ end
64
+
65
+ # before and after filter
66
+ @before_filters = Hash.new { |hash, key| hash[key] = [] }
67
+ @after_filters = Hash.new { |hash, key| hash[key] = [] }
68
+ end
69
+
70
+ def configure(&block)
71
+ unless @reloading
72
+ block.call(@config)
73
+
74
+ # logger changes (if differing from initialize)
75
+ if @config.logging
76
+ @traffic_logger = @config.traffic_logger ? @config.traffic_logger : Logger::Twoflogger.new(Ponder.root.join('logs', 'traffic.log'))
77
+ @error_logger = @config.error_logger ? @config.error_logger : Logger::Twoflogger.new(Ponder.root.join('logs', 'error.log'))
78
+ end
79
+
80
+ unless @config.verbose
81
+ @console_logger = @empty_logger
82
+ end
83
+
84
+ # remove auto_rename callback (if differing from initialize)
85
+ unless @config.auto_rename
86
+ @callbacks.delete(433)
87
+ end
88
+ end
89
+ end
90
+
91
+ def on(event_types = [:channel], match = //, &block)
92
+ if event_types.is_a?(Array)
93
+ callbacks = event_types.map { |event_type| Callback.new(event_type, match, block) }
94
+ else
95
+ callbacks = [Callback.new(event_types, match, block)]
96
+ event_types = [event_types]
97
+ end
98
+
99
+ callbacks.each_with_index do |callback, index|
100
+ @callbacks[event_types[index]] << callback
101
+ end
102
+ end
103
+
104
+ def connect
105
+ unless @reloading
106
+ @traffic_logger.start_logging
107
+ @error_logger.start_logging
108
+ @console_logger.start_logging
109
+
110
+ @traffic_logger.info '-- Starting Ponder'
111
+ @console_logger.info '-- Starting Ponder'
112
+
113
+ EventMachine::run do
114
+ @connection = EventMachine::connect(@config.server, @config.port, Connection, self)
115
+ end
116
+ end
117
+ end
118
+
119
+ def reload!
120
+ @reloading = true
121
+ @callbacks.clear
122
+ load $0
123
+ @reloading = false
124
+ end
125
+
126
+ def reloading?
127
+ @reloading
128
+ end
129
+
130
+ # parsing incoming traffic
131
+ def parse(message)
132
+ message.chomp!
133
+ @traffic_logger.info "<< #{message}"
134
+ @console_logger.info "<< #{message}"
135
+
136
+ case message
137
+ when /^PING \S+$/
138
+ raw message.sub(/PING/, 'PONG')
139
+
140
+ when /^:\S+ (\d\d\d) /
141
+ number = $1.to_i
142
+ parse_event(number, :type => number, :params => $')
143
+
144
+ when /^:(\S+)!(\S+)@(\S+) PRIVMSG #(\S+) :/
145
+ parse_event(:channel, :type => :channel, :nick => $1, :user => $2, :host => $3, :channel => "##{$4}", :message => $')
146
+
147
+ when /^:(\S+)!(\S+)@(\S+) PRIVMSG \S+ :/
148
+ parse_event(:query, :type => :query, :nick => $1, :user => $2, :host => $3, :message => $')
149
+
150
+ when /^:(\S+)!(\S+)@(\S+) JOIN :*(\S+)$/
151
+ parse_event(:join, :type => :join, :nick => $1, :user => $2, :host => $3, :channel => $4)
152
+
153
+ when /^:(\S+)!(\S+)@(\S+) PART (\S+)/
154
+ parse_event(:part, :type => :part, :nick => $1, :user => $2, :host => $3, :channel => $4, :message => $'.sub(/ :/, ''))
155
+
156
+ when /^:(\S+)!(\S+)@(\S+) QUIT/
157
+ parse_event(:quit, :type => :quit, :nick => $1, :user => $2, :host => $3, :message => $'.sub(/ :/, ''))
158
+
159
+ when /^:(\S+)!(\S+)@(\S+) NICK :/
160
+ parse_event(:nickchange, :type => :nickchange, :nick => $1, :user => $2, :host => $3, :new_nick => $')
161
+
162
+ when /^:(\S+)!(\S+)@(\S+) KICK (\S+) (\S+) :/
163
+ parse_event(:kick, :type => :kick, :nick => $1, :user => $2, :host => $3, :channel => $4, :victim => $5, :reason => $')
164
+
165
+ when /^:(\S+)!(\S+)@(\S+) TOPIC (\S+) :/
166
+ parse_event(:topic, :type => :topic, :nick => $1, :user => $2, :host => $3, :channel => $4, :topic => $')
167
+ end
168
+
169
+ @observer_queues.each do |queue, regexps|
170
+ regexps.each do |regexp|
171
+ if message =~ regexp
172
+ queue << message
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ # process callbacks with its begin; rescue; end
179
+ def process_callbacks(event_type, event_data)
180
+ @callbacks[event_type].each do |callback|
181
+ EM.defer(
182
+ Proc.new do
183
+ begin
184
+ stop_running = false
185
+
186
+ # before filters (specific filters first, then :all)
187
+ (@before_filters[event_type] + @before_filters[:all]).each do |filter|
188
+ if filter.call(event_type, event_data) == false
189
+ stop_running = true
190
+ break
191
+ end
192
+ end
193
+
194
+ unless stop_running
195
+ # handling
196
+ callback.call(event_type, event_data)
197
+
198
+ # after filters (specific filters first, then :all)
199
+ (@after_filters[event_type] + @after_filters[:all]).each do |filter|
200
+ filter.call(event_type, event_data)
201
+ end
202
+ end
203
+ rescue => e
204
+ @error_logger.error(e.message, *e.backtrace)
205
+ @console_logger.error(e.message, *e.backtrace)
206
+ end
207
+ end
208
+ )
209
+ end
210
+ end
211
+
212
+ def before_filter(event_types = :all, match = //, &block)
213
+ filter(@before_filters, event_types, match, block)
214
+ end
215
+
216
+ def after_filter(event_types = :all, match = //, &block)
217
+ filter(@after_filters, event_types, match, block)
218
+ end
219
+
220
+ private
221
+
222
+ # parses incoming traffic (types)
223
+ def parse_event(event_type, event_data = {})
224
+ if ((event_type == 376) || (event_type == 422)) && !@connected
225
+ @connected = true
226
+ process_callbacks(:connect, event_data)
227
+ end
228
+
229
+ process_callbacks(event_type, event_data)
230
+ end
231
+
232
+ def filter(filter_type, event_types = :all, match = //, block)
233
+ if event_types.is_a?(Array)
234
+ event_types.each do |event_type|
235
+ filter_type[event_type] << Filter.new(event_type, match, block)
236
+ end
237
+ elsif
238
+ filter_type[event_types] << Filter.new(event_types, match, block)
239
+ end
240
+ end
241
+ end
242
+ end