cinch 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/README.md +25 -44
  2. data/examples/basic/autovoice.rb +1 -1
  3. data/examples/basic/join_part.rb +0 -4
  4. data/examples/plugins/autovoice.rb +2 -5
  5. data/examples/plugins/google.rb +1 -2
  6. data/examples/plugins/hooks.rb +36 -0
  7. data/examples/plugins/lambdas.rb +35 -0
  8. data/examples/plugins/last_nick.rb +24 -0
  9. data/examples/plugins/multiple_matches.rb +1 -10
  10. data/examples/plugins/own_events.rb +37 -0
  11. data/examples/plugins/timer.rb +22 -0
  12. data/examples/plugins/url_shorten.rb +1 -1
  13. data/lib/cinch.rb +50 -1
  14. data/lib/cinch/ban.rb +5 -2
  15. data/lib/cinch/bot.rb +360 -193
  16. data/lib/cinch/cache_manager.rb +15 -0
  17. data/lib/cinch/callback.rb +6 -0
  18. data/lib/cinch/channel.rb +150 -96
  19. data/lib/cinch/channel_manager.rb +26 -0
  20. data/lib/cinch/constants.rb +6 -4
  21. data/lib/cinch/exceptions.rb +9 -0
  22. data/lib/cinch/irc.rb +197 -82
  23. data/lib/cinch/logger/formatted_logger.rb +8 -8
  24. data/lib/cinch/logger/zcbot_logger.rb +37 -0
  25. data/lib/cinch/mask.rb +17 -3
  26. data/lib/cinch/message.rb +14 -7
  27. data/lib/cinch/message_queue.rb +8 -4
  28. data/lib/cinch/mode_parser.rb +56 -0
  29. data/lib/cinch/pattern.rb +45 -0
  30. data/lib/cinch/plugin.rb +129 -34
  31. data/lib/cinch/rubyext/string.rb +4 -4
  32. data/lib/cinch/syncable.rb +8 -0
  33. data/lib/cinch/user.rb +68 -13
  34. data/lib/cinch/user_manager.rb +60 -0
  35. metadata +17 -35
  36. data/Rakefile +0 -66
  37. data/lib/cinch/PLANNED +0 -4
  38. data/spec/bot_spec.rb +0 -5
  39. data/spec/channel_spec.rb +0 -5
  40. data/spec/cinch_spec.rb +0 -5
  41. data/spec/irc_spec.rb +0 -5
  42. data/spec/message_spec.rb +0 -5
  43. data/spec/plugin_spec.rb +0 -5
  44. data/spec/spec.opts +0 -2
  45. data/spec/spec_helper.rb +0 -8
  46. data/spec/user_spec.rb +0 -5
@@ -27,15 +27,14 @@ module Cinch
27
27
  # (see Logger::Logger#log)
28
28
  def log(messages, kind = :generic)
29
29
  @mutex.synchronize do
30
- messages = [messages].flatten.map {|s| s.chomp}
31
- # message = message.to_s.chomp # don't want to tinker with the original string
32
-
33
- messages.each do |message|
30
+ messages = [messages].flatten.map {|s| s.to_s.chomp}
31
+ messages.each do |msg|
32
+ message = Time.now.strftime("[%Y/%m/%d %H:%M:%S.%L] ")
34
33
  if kind == :debug
35
34
  prefix = colorize("!! ", :yellow)
36
- message = prefix + message
35
+ message << prefix + msg
37
36
  else
38
- pre, msg = message.split(" :", 2)
37
+ pre, msg = msg.split(" :", 2)
39
38
  pre_parts = pre.split(" ")
40
39
 
41
40
  if kind == :incoming
@@ -53,10 +52,10 @@ module Cinch
53
52
  pre_parts[0] = colorize(pre_parts[0], :bold)
54
53
  end
55
54
 
56
- message = prefix + pre_parts.join(" ")
55
+ message << prefix + pre_parts.join(" ")
57
56
  message << colorize(" :#{msg}", :yellow) if msg
58
57
  end
59
- @output.puts message.encode
58
+ @output.puts message.encode("locale", {:invalid => :replace, :undef => :replace})
60
59
  end
61
60
  end
62
61
  end
@@ -66,6 +65,7 @@ module Cinch
66
65
  # @param [Array<Symbol>] codes array of colors to apply
67
66
  # @return [String] colorized string
68
67
  def colorize(text, *codes)
68
+ return text unless @output.tty?
69
69
  COLORS.values_at(*codes).join + text + COLORS[:reset]
70
70
  end
71
71
 
@@ -0,0 +1,37 @@
1
+ require "cinch/logger/logger"
2
+ module Cinch
3
+ module Logger
4
+ # This logger logs all incoming messages in the format of zcbot.
5
+ # All other debug output (outgoing messages, exceptions, ...) will
6
+ # silently be dropped. The sole purpose of this logger is to
7
+ # produce logs parseable by pisg (with the zcbot formatter) to
8
+ # create channel statistics..
9
+ class ZcbotLogger < Cinch::Logger::Logger
10
+ # @param [IO] output An IO to log to.
11
+ def initialize(output = STDERR)
12
+ @output = output
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ # (see Logger::Logger#debug)
17
+ def debug(messages)
18
+ end
19
+
20
+ # (see Logger::Logger#log)
21
+ def log(messages, kind = :generic)
22
+ return if kind != :incoming
23
+
24
+ @mutex.synchronize do
25
+ messages = [messages].flatten.map {|s| s.to_s.chomp}
26
+ messages.each do |msg|
27
+ @output.puts Time.now.strftime("%m/%d/%Y %H:%M:%S ") + msg.encode("locale", {:invalid => :replace, :undef => :replace})
28
+ end
29
+ end
30
+ end
31
+
32
+ # (see Logger::Logger#log_exception)
33
+ def log_exception(e)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -11,13 +11,27 @@ module Cinch
11
11
  def initialize(mask)
12
12
  @mask = mask
13
13
  @nick, @user, @host = mask.match(/(.+)!(.+)@(.+)/)[1..-1]
14
- @regexp = Regexp.new(Regexp.escape(mask).gsub("\\*", ".*"))
14
+ @regexp = Regexp.new("^" + Regexp.escape(mask).gsub("\\*", ".*") + "$")
15
15
  end
16
16
 
17
+ # @return [Boolean]
18
+ def ==(other)
19
+ other.respond_to?(:mask) && other.mask == @mask
20
+ end
21
+
22
+ # @return [Boolean]
23
+ def eql?(other)
24
+ other.is_a?(self.class) && self == other
25
+ end
26
+
27
+ def hash
28
+ @mask.hash
29
+ end
30
+
31
+ # @param [User] user
17
32
  # @return [Boolean]
18
33
  def match(user)
19
- mask = "%s!%s@%s" % [nick, user, host]
20
- return mask =~ @regexp
34
+ return user.mask =~ @regexp
21
35
 
22
36
  # TODO support CIDR (freenode)
23
37
  end
@@ -10,7 +10,8 @@ module Cinch
10
10
  # @return [Array<String>]
11
11
  attr_accessor :params
12
12
  attr_reader :events
13
-
13
+ # @return [Bot]
14
+ attr_reader :bot
14
15
  def initialize(msg, bot)
15
16
  @raw = msg
16
17
  @bot = bot
@@ -50,7 +51,7 @@ module Cinch
50
51
  host = @prefix[/@(\S+)$/, 1]
51
52
 
52
53
  return nil if nick.nil?
53
- @user ||= User.find_ensured(user, nick, host, @bot)
54
+ @user ||= @bot.user_manager.find_ensured(user, nick, host)
54
55
  end
55
56
 
56
57
  # @return [String, nil]
@@ -91,14 +92,14 @@ module Cinch
91
92
  @channel ||= begin
92
93
  case command
93
94
  when "INVITE", RPL_CHANNELMODEIS.to_s, RPL_BANLIST.to_s
94
- Channel.find_ensured(params[1], @bot)
95
+ @bot.channel_manager.find_ensured(params[1])
95
96
  when RPL_NAMEREPLY.to_s
96
- Channel.find_ensured(params[2], @bot)
97
+ @bot.channel_manager.find_ensured(params[2])
97
98
  else
98
99
  if params.first.start_with?("#")
99
- Channel.find_ensured(params.first, @bot)
100
+ @bot.channel_manager.find_ensured(params.first)
100
101
  elsif numeric_reply? and params[1].start_with?("#")
101
- Channel.find_ensured(params[1], @bot)
102
+ @bot.channel_manager.find_ensured(params[1])
102
103
  end
103
104
  end
104
105
  end
@@ -121,6 +122,7 @@ module Cinch
121
122
  $1
122
123
  end
123
124
 
125
+ # @return [Array<String>, nil]
124
126
  def ctcp_args
125
127
  return unless ctcp?
126
128
  ctcp_message.split(" ")[1..-1]
@@ -172,7 +174,12 @@ module Cinch
172
174
  # @return [void]
173
175
  def ctcp_reply(answer)
174
176
  return unless ctcp?
175
- @bot.raw "NOTICE #{user.nick} :\001#{ctcp_command} #{answer}\001"
177
+ user.notice "\001#{ctcp_command} #{answer}\001"
178
+ end
179
+
180
+ # @return [String]
181
+ def to_s
182
+ "#<Cinch::Message @raw=#{raw.chomp.inspect} @params=#{@params.inspect} channel=#{channel.inspect} user=#{user.inspect}>"
176
183
  end
177
184
 
178
185
  private
@@ -51,11 +51,15 @@ module Cinch
51
51
 
52
52
  message = @queue.pop.to_s.chomp
53
53
 
54
- @log << Time.now
55
- @bot.logger.log(message, :outgoing) if @bot.config.verbose
54
+ begin
55
+ @socket.writeline Cinch.encode_outgoing(message, @bot.config.encoding) + "\r\n"
56
+ @log << Time.now
57
+ @bot.logger.log(message, :outgoing) if @bot.config.verbose
56
58
 
57
- @time_since_last_send = Time.now
58
- @socket.print message.encode(@bot.config.encoding, {:invalid => :replace, :undef => :replace}) + "\r\n"
59
+ @time_since_last_send = Time.now
60
+ rescue IOError
61
+ @bot.debug "Could not send message (connectivity problems): #{message}"
62
+ end
59
63
  end
60
64
  end
61
65
  end
@@ -0,0 +1,56 @@
1
+ module Cinch
2
+ # @api private
3
+ module ModeParser
4
+ def self.parse_modes(modes, params, param_modes = {})
5
+ if modes.size == 0
6
+ raise InvalidModeString, 'Empty mode string'
7
+ end
8
+
9
+ if modes[0] !~ /[+-]/
10
+ raise InvalidModeString, "Malformed modes string: %s" % modes
11
+ end
12
+
13
+ changes = []
14
+
15
+ direction = nil
16
+ count = -1
17
+
18
+ modes.each_char do |ch|
19
+ if ch =~ /[+-]/
20
+ if count == 0
21
+ raise InvalidModeString, 'Empty mode sequence: %s' % modes
22
+ end
23
+
24
+ direction = case ch
25
+ when "+"
26
+ :add
27
+ when "-"
28
+ :remove
29
+ end
30
+ count = 0
31
+ else
32
+ param = nil
33
+ if param_modes[direction].include?(ch)
34
+ if params.size > 0
35
+ param = params.shift
36
+ else
37
+ raise InvalidModeString, 'Not enough parameters: %s' % ch.inspect
38
+ end
39
+ end
40
+ changes << [direction, ch, param]
41
+ count += 1
42
+ end
43
+ end
44
+
45
+ if params.size > 0
46
+ raise InvalidModeString, 'Too many parameters: %s %s' % [modes, params].inspect
47
+ end
48
+
49
+ if count == 0
50
+ raise InvalidModeString, 'Empty mode sequence: %r' % modes
51
+ end
52
+
53
+ return changes
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,45 @@
1
+ # -*- coding: utf-8 -*-
2
+ module Cinch
3
+ # @api private
4
+ class Pattern
5
+ # @param [String, Regexp, NilClass, Proc, #to_s] obj The object to
6
+ # convert to a regexp
7
+ # @return [Regexp, nil]
8
+ def self.obj_to_r(obj)
9
+ case obj
10
+ when Regexp, NilClass
11
+ return obj
12
+ else
13
+ return Regexp.new(Regexp.escape(obj.to_s))
14
+ end
15
+ end
16
+
17
+ def self.resolve_proc(obj, msg = nil)
18
+ if obj.is_a?(Proc)
19
+ return resolve_proc(obj.call(msg), msg)
20
+ else
21
+ return obj
22
+ end
23
+ end
24
+
25
+ attr_reader :prefix
26
+ attr_reader :suffix
27
+ attr_reader :pattern
28
+ def initialize(prefix, pattern, suffix)
29
+ @prefix, @pattern, @suffix = prefix, pattern, suffix
30
+ end
31
+
32
+ def to_r(msg = nil)
33
+ prefix = Pattern.obj_to_r(Pattern.resolve_proc(@prefix, msg))
34
+ suffix = Pattern.obj_to_r(Pattern.resolve_proc(@suffix, msg))
35
+ pattern = Pattern.resolve_proc(@pattern, msg)
36
+
37
+ case pattern
38
+ when Regexp, NilClass
39
+ /#{prefix}#{pattern}#{suffix}/
40
+ else
41
+ /^#{prefix}#{Pattern.obj_to_r(pattern)}#{suffix}$/
42
+ end
43
+ end
44
+ end
45
+ end
@@ -3,8 +3,14 @@ module Cinch
3
3
  include Helpers
4
4
 
5
5
  module ClassMethods
6
- Pattern = Struct.new(:pattern, :use_prefix, :method)
6
+ # @api private
7
+ Match = Struct.new(:pattern, :use_prefix, :use_suffix, :method)
8
+ # @api private
7
9
  Listener = Struct.new(:event, :method)
10
+ # @api private
11
+ Timer = Struct.new(:interval, :method, :threaded, :registered)
12
+ # @api private
13
+ Hook = Struct.new(:type, :for, :method)
8
14
 
9
15
  # Set a match pattern.
10
16
  #
@@ -15,9 +21,9 @@ module Cinch
15
21
  # pattern.
16
22
  # @return [void]
17
23
  def match(pattern, options = {})
18
- options = {:use_prefix => true, :method => :execute}.merge(options)
19
- @__cinch_patterns ||= []
20
- @__cinch_patterns << Pattern.new(pattern, options[:use_prefix], options[:method])
24
+ options = {:use_prefix => true, :use_suffix => true, :method => :execute}.merge(options)
25
+ @__cinch_matches ||= []
26
+ @__cinch_matches << Match.new(pattern, options[:use_prefix], options[:use_suffix], options[:method])
21
27
  end
22
28
 
23
29
  # Events to listen to.
@@ -53,8 +59,8 @@ module Cinch
53
59
  (@__cinch_ctcps ||= []) << command.to_s.upcase
54
60
  end
55
61
 
56
- # Define a help message which will be returned on "<prefix>help
57
- # <pluginname>".
62
+ # Define a help message which will be returned on "&lt;prefix&gt;help
63
+ # &lt;pluginname&gt;".
58
64
  #
59
65
  # @param [String] message
60
66
  # @return [void]
@@ -66,10 +72,22 @@ module Cinch
66
72
  #
67
73
  # @param [String] prefix
68
74
  # @return [void]
69
- def prefix(prefix)
70
- @__cinch_prefix = prefix
75
+ def prefix(prefix = nil, &block)
76
+ raise ArgumentError if prefix.nil? && block.nil?
77
+ @__cinch_prefix = prefix || block
71
78
  end
72
79
 
80
+ # Set the plugin suffix.
81
+ #
82
+ # @param [String] suffix
83
+ # @return [void]
84
+ def suffix(suffix = nil, &block)
85
+ raise ArgumentError if suffix.nil? && block.nil?
86
+ @__cinch_suffix = suffix || block
87
+ end
88
+
89
+
90
+
73
91
  # Set which kind of messages to react on (i.e. call {#execute})
74
92
  #
75
93
  # @param [Symbol<:message, :channel, :private>] target React to all,
@@ -87,12 +105,63 @@ module Cinch
87
105
  @__cinch_name = name
88
106
  end
89
107
 
108
+ # @example
109
+ # timer 5, method: :some_method
110
+ # def some_method
111
+ # Channel("#cinch-bots").send(Time.now.to_s)
112
+ # end
113
+ # @param [Number] interval Interval in seconds
114
+ # @option options [Symbol] :method (:timer) Method to call
115
+ # @option options [Boolean] :threaded (true) Call method in a thread?
116
+ # @return [void]
117
+ def timer(interval, options = {})
118
+ options = {:method => :timer, :threaded => true}.merge(options)
119
+ @__cinch_timers ||= []
120
+ @__cinch_timers << Timer.new(interval, options[:method], options[:threaded], false)
121
+ end
122
+
123
+ # Defines a hook which will be run before or after a handler is
124
+ # executed, depending on the value of `type`.
125
+ #
126
+ # @param [Symbol<:pre, :post>] type Run the hook before or after
127
+ # a handler?
128
+ # @option options [Array<:match, :listen_to, :ctcp>] :for ([:match, :listen_to, :ctcp])
129
+ # Which kinds of events to run the hook for.
130
+ # @option options [Symbol] :method (true) The method to execute.
131
+ # @return [void]
132
+ def hook(type, options = {})
133
+ options = {:for => [:match, :listen_to, :ctcp], :method => :hook}.merge(options)
134
+ __hooks(type) << Hook.new(type, options[:for], options[:method])
135
+ end
136
+
90
137
  # @return [String]
91
138
  # @api private
92
139
  def __plugin_name
93
140
  @__cinch_name || self.name.split("::").last.downcase
94
141
  end
95
142
 
143
+ # @return [Hash]
144
+ # @api private
145
+ def __hooks(type = nil, events = nil)
146
+ @__cinch_hooks ||= Hash.new{|h,k| h[k] = []}
147
+
148
+ if type.nil?
149
+ hooks = @__cinch_hooks
150
+ else
151
+ hooks = @__cinch_hooks[type]
152
+ end
153
+
154
+ if events.nil?
155
+ return hooks
156
+ else
157
+ events = [*events]
158
+ if hooks.is_a?(Hash)
159
+ hooks = hooks.map { |k, v| v }
160
+ end
161
+ return hooks.select { |hook| (events & hook.for).size > 0 }
162
+ end
163
+ end
164
+
96
165
  # @return [void]
97
166
  # @api private
98
167
  def __register_with_bot(bot, instance)
@@ -100,36 +169,30 @@ module Cinch
100
169
 
101
170
  (@__cinch_listeners || []).each do |listener|
102
171
  bot.debug "[plugin] #{plugin_name}: Registering listener for type `#{listener.event}`"
103
- bot.on(listener.event, [], instance) do |message, plugin|
104
- plugin.__send__(listener.method, message) if plugin.respond_to?(listener.method)
172
+ bot.on(listener.event, [], instance) do |message, plugin, *args|
173
+ if plugin.respond_to?(listener.method)
174
+ plugin.class.__hooks(:pre, :listen_to).each {|hook| plugin.__send__(hook.method, message)}
175
+ plugin.__send__(listener.method, message, *args)
176
+ plugin.class.__hooks(:post, :listen_to).each {|hook| plugin.__send__(hook.method, message)}
177
+ end
105
178
  end
106
179
  end
107
180
 
108
- if (@__cinch_patterns ||= []).empty?
109
- @__cinch_patterns << Pattern.new(plugin_name, true, nil)
181
+ if (@__cinch_matches ||= []).empty?
182
+ @__cinch_matches << Match.new(plugin_name, true, true, :execute)
110
183
  end
111
184
 
112
185
  prefix = @__cinch_prefix || bot.config.plugins.prefix
113
- if prefix.is_a?(String)
114
- prefix = Regexp.escape(prefix)
115
- end
116
- @__cinch_patterns.each do |pattern|
117
- pattern_to_register = nil
118
-
119
- if pattern.use_prefix && prefix
120
- case pattern.pattern
121
- when Regexp
122
- pattern_to_register = /^#{prefix}#{pattern.pattern}/
123
- when String
124
- pattern_to_register = prefix + pattern.pattern
125
- end
126
- else
127
- pattern_to_register = pattern.pattern
128
- end
186
+ suffix = @__cinch_suffix || bot.config.plugins.suffix
187
+
188
+ @__cinch_matches.each do |pattern|
189
+ _prefix = pattern.use_prefix ? prefix : nil
190
+ _suffix = pattern.use_suffix ? suffix : nil
129
191
 
192
+ pattern_to_register = Pattern.new(_prefix, pattern.pattern, _suffix)
130
193
  react_on = @__cinch_react_on || :message
131
194
 
132
- bot.debug "[plugin] #{plugin_name}: Registering executor with pattern `#{pattern_to_register}`, reacting on `#{react_on}`"
195
+ bot.debug "[plugin] #{plugin_name}: Registering executor with pattern `#{pattern_to_register.inspect}`, reacting on `#{react_on}`"
133
196
 
134
197
  bot.on(react_on, pattern_to_register, instance, pattern) do |message, plugin, pattern, *args|
135
198
  if plugin.respond_to?(pattern.method)
@@ -140,7 +203,9 @@ module Cinch
140
203
  elsif arity == 0
141
204
  args = []
142
205
  end
206
+ plugin.class.__hooks(:pre, :match).each {|hook| plugin.__send__(hook.method, message)}
143
207
  method.call(message, *args)
208
+ plugin.class.__hooks(:post, :match).each {|hook| plugin.__send__(hook.method, message)}
144
209
  end
145
210
  end
146
211
  end
@@ -148,13 +213,46 @@ module Cinch
148
213
  (@__cinch_ctcps || []).each do |ctcp|
149
214
  bot.debug "[plugin] #{plugin_name}: Registering CTCP `#{ctcp}`"
150
215
  bot.on(:ctcp, ctcp, instance, ctcp) do |message, plugin, ctcp, *args|
216
+ plugin.class.__hooks(:pre, :ctcp).each {|hook| plugin.__send__(hook.method, message)}
151
217
  plugin.__send__("ctcp_#{ctcp.downcase}", message, *args)
218
+ plugin.class.__hooks(:post, :ctcp).each {|hook| plugin.__send__(hook.method, message)}
219
+ end
220
+ end
221
+
222
+ (@__cinch_timers || []).each do |timer|
223
+ bot.debug "[plugin] #{__plugin_name}: Registering timer with interval `#{timer.interval}` for method `#{timer.method}`"
224
+ bot.on :connect do
225
+ next if timer.registered
226
+ timer.registered = true
227
+ Thread.new do
228
+ bot.debug "registering timer..."
229
+ loop do
230
+ sleep timer.interval
231
+ if instance.respond_to?(timer.method)
232
+ l = lambda {
233
+ begin
234
+ instance.__send__(timer.method)
235
+ rescue => e
236
+ bot.logger.log_exception(e)
237
+ end
238
+ }
239
+
240
+ if timer.threaded
241
+ Thread.new do
242
+ l.call
243
+ end
244
+ else
245
+ l.call
246
+ end
247
+ end
248
+ end
249
+ end
152
250
  end
153
251
  end
154
252
 
155
253
  if @__cinch_help_message
156
254
  bot.debug "[plugin] #{plugin_name}: Registering help message"
157
- bot.on(:message, /#{prefix}help #{Regexp.escape(plugin_name)}/, @__cinch_help_message) do |message, help_message|
255
+ bot.on(:message, "#{prefix}help #{plugin_name}", @__cinch_help_message) do |message, help_message|
158
256
  message.reply(help_message)
159
257
  end
160
258
  end
@@ -169,10 +267,7 @@ module Cinch
169
267
  self.class.__register_with_bot(bot, self)
170
268
  end
171
269
 
172
- # @param (see Bot#synchronize)
173
- # @yield
174
- # @return (see Bot#synchronize)
175
- # @see Bot#synchronize
270
+ # (see Bot#synchronize)
176
271
  def synchronize(*args, &block)
177
272
  @bot.synchronize(*args, &block)
178
273
  end