cinch 1.0.2 → 1.1.0

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.
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