ponder 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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