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.
- data/README.md +25 -44
- data/examples/basic/autovoice.rb +1 -1
- data/examples/basic/join_part.rb +0 -4
- data/examples/plugins/autovoice.rb +2 -5
- data/examples/plugins/google.rb +1 -2
- data/examples/plugins/hooks.rb +36 -0
- data/examples/plugins/lambdas.rb +35 -0
- data/examples/plugins/last_nick.rb +24 -0
- data/examples/plugins/multiple_matches.rb +1 -10
- data/examples/plugins/own_events.rb +37 -0
- data/examples/plugins/timer.rb +22 -0
- data/examples/plugins/url_shorten.rb +1 -1
- data/lib/cinch.rb +50 -1
- data/lib/cinch/ban.rb +5 -2
- data/lib/cinch/bot.rb +360 -193
- data/lib/cinch/cache_manager.rb +15 -0
- data/lib/cinch/callback.rb +6 -0
- data/lib/cinch/channel.rb +150 -96
- data/lib/cinch/channel_manager.rb +26 -0
- data/lib/cinch/constants.rb +6 -4
- data/lib/cinch/exceptions.rb +9 -0
- data/lib/cinch/irc.rb +197 -82
- data/lib/cinch/logger/formatted_logger.rb +8 -8
- data/lib/cinch/logger/zcbot_logger.rb +37 -0
- data/lib/cinch/mask.rb +17 -3
- data/lib/cinch/message.rb +14 -7
- data/lib/cinch/message_queue.rb +8 -4
- data/lib/cinch/mode_parser.rb +56 -0
- data/lib/cinch/pattern.rb +45 -0
- data/lib/cinch/plugin.rb +129 -34
- data/lib/cinch/rubyext/string.rb +4 -4
- data/lib/cinch/syncable.rb +8 -0
- data/lib/cinch/user.rb +68 -13
- data/lib/cinch/user_manager.rb +60 -0
- metadata +17 -35
- data/Rakefile +0 -66
- data/lib/cinch/PLANNED +0 -4
- data/spec/bot_spec.rb +0 -5
- data/spec/channel_spec.rb +0 -5
- data/spec/cinch_spec.rb +0 -5
- data/spec/irc_spec.rb +0 -5
- data/spec/message_spec.rb +0 -5
- data/spec/plugin_spec.rb +0 -5
- data/spec/spec.opts +0 -2
- data/spec/spec_helper.rb +0 -8
- data/spec/user_spec.rb +0 -5
    
        data/lib/cinch/bot.rb
    CHANGED
    
    | @@ -25,19 +25,24 @@ require "cinch/ban" | |
| 25 25 | 
             
            require "cinch/mask"
         | 
| 26 26 | 
             
            require "cinch/isupport"
         | 
| 27 27 | 
             
            require "cinch/plugin"
         | 
| 28 | 
            +
            require "cinch/pattern"
         | 
| 29 | 
            +
            require "cinch/mode_parser"
         | 
| 30 | 
            +
            require "cinch/cache_manager"
         | 
| 31 | 
            +
            require "cinch/channel_manager"
         | 
| 32 | 
            +
            require "cinch/user_manager"
         | 
| 28 33 |  | 
| 29 34 | 
             
            module Cinch
         | 
| 30 35 |  | 
| 31 36 | 
             
              class Bot
         | 
| 32 37 | 
             
                # @return [Config]
         | 
| 33 | 
            -
                 | 
| 38 | 
            +
                attr_reader :config
         | 
| 34 39 | 
             
                # @return [IRC]
         | 
| 35 | 
            -
                 | 
| 40 | 
            +
                attr_reader :irc
         | 
| 36 41 | 
             
                # @return [Logger]
         | 
| 37 42 | 
             
                attr_accessor :logger
         | 
| 38 43 | 
             
                # @return [Array<Channel>] All channels the bot currently is in
         | 
| 39 44 | 
             
                attr_reader :channels
         | 
| 40 | 
            -
                # @return [String]
         | 
| 45 | 
            +
                # @return [String] the bot's hostname
         | 
| 41 46 | 
             
                attr_reader :host
         | 
| 42 47 | 
             
                # @return [Mask]
         | 
| 43 48 | 
             
                attr_reader :mask
         | 
| @@ -47,18 +52,34 @@ module Cinch | |
| 47 52 | 
             
                attr_reader :realname
         | 
| 48 53 | 
             
                # @return [Time]
         | 
| 49 54 | 
             
                attr_reader :signed_on_at
         | 
| 55 | 
            +
                # @return [Array<Plugin>] All registered plugins
         | 
| 56 | 
            +
                attr_reader :plugins
         | 
| 57 | 
            +
                # @return [Array<Thread>]
         | 
| 58 | 
            +
                # @api private
         | 
| 59 | 
            +
                attr_reader :handler_threads
         | 
| 60 | 
            +
                # @return [Boolean] whether the bot is in the process of disconnecting
         | 
| 61 | 
            +
                attr_reader :quitting
         | 
| 62 | 
            +
                # @return [UserManager]
         | 
| 63 | 
            +
                attr_reader :user_manager
         | 
| 64 | 
            +
                # @return [ChannelManager]
         | 
| 65 | 
            +
                attr_reader :channel_manager
         | 
| 66 | 
            +
                # @return [Boolean]
         | 
| 67 | 
            +
                # @api private
         | 
| 68 | 
            +
                attr_accessor :last_connection_was_successful
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                # @group Helper methods
         | 
| 50 71 |  | 
| 51 72 | 
             
                # Helper method for turning a String into a {Channel} object.
         | 
| 52 73 | 
             
                #
         | 
| 53 74 | 
             
                # @param [String] channel a channel name
         | 
| 54 75 | 
             
                # @return [Channel] a {Channel} object
         | 
| 55 76 | 
             
                # @example
         | 
| 56 | 
            -
                #   on :message, /^please join (#.+)$/ do |target|
         | 
| 77 | 
            +
                #   on :message, /^please join (#.+)$/ do |m, target|
         | 
| 57 78 | 
             
                #     Channel(target).join
         | 
| 58 79 | 
             
                #   end
         | 
| 59 80 | 
             
                def Channel(channel)
         | 
| 60 81 | 
             
                  return channel if channel.is_a?(Channel)
         | 
| 61 | 
            -
                   | 
| 82 | 
            +
                  @channel_manager.find_ensured(channel)
         | 
| 62 83 | 
             
                end
         | 
| 63 84 |  | 
| 64 85 | 
             
                # Helper method for turning a String into an {User} object.
         | 
| @@ -66,75 +87,21 @@ module Cinch | |
| 66 87 | 
             
                # @param [String] user a user's nickname
         | 
| 67 88 | 
             
                # @return [User] an {User} object
         | 
| 68 89 | 
             
                # @example
         | 
| 69 | 
            -
                #   on :message, /^tell me everything about (.+)$/ do |target|
         | 
| 90 | 
            +
                #   on :message, /^tell me everything about (.+)$/ do |m, target|
         | 
| 70 91 | 
             
                #     user = User(target)
         | 
| 71 | 
            -
                #     reply "%s is named %s and connects from %s" % [user.nick, user.name, user.host]
         | 
| 92 | 
            +
                #     m.reply "%s is named %s and connects from %s" % [user.nick, user.name, user.host]
         | 
| 72 93 | 
             
                #   end
         | 
| 73 94 | 
             
                def User(user)
         | 
| 74 95 | 
             
                  return user if user.is_a?(User)
         | 
| 75 | 
            -
                   | 
| 76 | 
            -
                end
         | 
| 77 | 
            -
             | 
| 78 | 
            -
                # @return [void]
         | 
| 79 | 
            -
                # @see Logger#debug
         | 
| 80 | 
            -
                def debug(msg)
         | 
| 81 | 
            -
                  @logger.debug(msg)
         | 
| 82 | 
            -
                end
         | 
| 83 | 
            -
             | 
| 84 | 
            -
                # @return [Boolean]
         | 
| 85 | 
            -
                def strict?
         | 
| 86 | 
            -
                  @config.strictness == :strict
         | 
| 87 | 
            -
                end
         | 
| 88 | 
            -
             | 
| 89 | 
            -
                # @yield
         | 
| 90 | 
            -
                def initialize(&b)
         | 
| 91 | 
            -
                  @logger = Logger::FormattedLogger.new($stderr)
         | 
| 92 | 
            -
                  @events = {}
         | 
| 93 | 
            -
                  @config = OpenStruct.new({
         | 
| 94 | 
            -
                                             :server => "localhost",
         | 
| 95 | 
            -
                                             :port   => 6667,
         | 
| 96 | 
            -
                                             :ssl    => false,
         | 
| 97 | 
            -
                                             :password => nil,
         | 
| 98 | 
            -
                                             :nick   => "cinch",
         | 
| 99 | 
            -
                                             :realname => "cinch",
         | 
| 100 | 
            -
                                             :verbose => true,
         | 
| 101 | 
            -
                                             :messages_per_second => 0.5,
         | 
| 102 | 
            -
                                             :server_queue_size => 10,
         | 
| 103 | 
            -
                                             :strictness => :forgiving,
         | 
| 104 | 
            -
                                             :message_split_start => '... ',
         | 
| 105 | 
            -
                                             :message_split_end   => ' ...',
         | 
| 106 | 
            -
                                             :max_messages => nil,
         | 
| 107 | 
            -
                                             :plugins => OpenStruct.new({
         | 
| 108 | 
            -
                                                                          :plugins => [],
         | 
| 109 | 
            -
                                                                          :prefix  => "!",
         | 
| 110 | 
            -
                                                                          :options => Hash.new {|h,k| h[k] = {}},
         | 
| 111 | 
            -
                                                                        }),
         | 
| 112 | 
            -
                                             :channels => [],
         | 
| 113 | 
            -
                                             :encoding => Encoding.default_external,
         | 
| 114 | 
            -
                                           })
         | 
| 115 | 
            -
             | 
| 116 | 
            -
                  @semaphores_mutex = Mutex.new
         | 
| 117 | 
            -
                  @semaphores = Hash.new { |h,k| h[k] = Mutex.new }
         | 
| 118 | 
            -
                  @plugins = []
         | 
| 119 | 
            -
                  @callback = Callback.new(self)
         | 
| 120 | 
            -
                  @channels = []
         | 
| 121 | 
            -
             | 
| 122 | 
            -
                  on :connect do
         | 
| 123 | 
            -
                    bot.config.channels.each do |channel|
         | 
| 124 | 
            -
                      bot.join channel
         | 
| 125 | 
            -
                    end
         | 
| 126 | 
            -
                  end
         | 
| 127 | 
            -
             | 
| 128 | 
            -
                  instance_eval(&b) if block_given?
         | 
| 96 | 
            +
                  @user_manager.find_ensured(user)
         | 
| 129 97 | 
             
                end
         | 
| 130 98 |  | 
| 131 | 
            -
                #  | 
| 132 | 
            -
                # nothing else but yielding {Bot#config}, but it makes for a nice DSL.
         | 
| 99 | 
            +
                # Define helper methods in the context of the bot.
         | 
| 133 100 | 
             
                #
         | 
| 134 | 
            -
                # @ | 
| 101 | 
            +
                # @yield Expects a block containing method definitions
         | 
| 135 102 | 
             
                # @return [void]
         | 
| 136 | 
            -
                def  | 
| 137 | 
            -
                   | 
| 103 | 
            +
                def helpers(&b)
         | 
| 104 | 
            +
                  Callback.class_eval(&b)
         | 
| 138 105 | 
             
                end
         | 
| 139 106 |  | 
| 140 107 | 
             
                # Since Cinch uses threads, all handlers can be run
         | 
| @@ -175,56 +142,6 @@ module Cinch | |
| 175 142 | 
             
                  semaphore.synchronize(&block)
         | 
| 176 143 | 
             
                end
         | 
| 177 144 |  | 
| 178 | 
            -
                # Registers a handler.
         | 
| 179 | 
            -
                #
         | 
| 180 | 
            -
                # @param [String, Symbol, Integer] event the event to match. Available
         | 
| 181 | 
            -
                #   events are all IRC commands in lowercase as symbols, all numeric
         | 
| 182 | 
            -
                #   replies, and the following:
         | 
| 183 | 
            -
                #
         | 
| 184 | 
            -
                #     - :channel (a channel message)
         | 
| 185 | 
            -
                #     - :private (a private message)
         | 
| 186 | 
            -
                #     - :message (both channel and private messages)
         | 
| 187 | 
            -
                #     - :error   (handling errors, use a numeric error code as `match`)
         | 
| 188 | 
            -
                #     - :ctcp    (ctcp requests, use a ctcp command as `match`)
         | 
| 189 | 
            -
                #
         | 
| 190 | 
            -
                # @param [Regexp, String, Integer] match every message of the
         | 
| 191 | 
            -
                #   right event will be checked against this argument and the event
         | 
| 192 | 
            -
                #   will only be called if it matches
         | 
| 193 | 
            -
                #
         | 
| 194 | 
            -
                # @yieldparam [String] *args each capture group of the regex will
         | 
| 195 | 
            -
                #   be one argument to the block. It is optional to accept them,
         | 
| 196 | 
            -
                #   though
         | 
| 197 | 
            -
                #
         | 
| 198 | 
            -
                # @return [void]
         | 
| 199 | 
            -
                def on(event, regexps = [], *args, &block)
         | 
| 200 | 
            -
                  regexps = [*regexps]
         | 
| 201 | 
            -
                  regexps = [//] if regexps.empty?
         | 
| 202 | 
            -
             | 
| 203 | 
            -
                  event = event.to_sym
         | 
| 204 | 
            -
             | 
| 205 | 
            -
                  regexps.map! do |regexp|
         | 
| 206 | 
            -
                    case regexp
         | 
| 207 | 
            -
                    when String, Integer
         | 
| 208 | 
            -
                      if event == :ctcp
         | 
| 209 | 
            -
                        /^#{Regexp.escape(regexp.to_s)}(?:$| .+)/
         | 
| 210 | 
            -
                      else
         | 
| 211 | 
            -
                        /^#{Regexp.escape(regexp.to_s)}$/
         | 
| 212 | 
            -
                      end
         | 
| 213 | 
            -
                    else
         | 
| 214 | 
            -
                      regexp
         | 
| 215 | 
            -
                    end
         | 
| 216 | 
            -
                  end
         | 
| 217 | 
            -
                  (@events[event] ||= []) << [regexps, args, block]
         | 
| 218 | 
            -
                end
         | 
| 219 | 
            -
             | 
| 220 | 
            -
                # Define helper methods in the context of the bot.
         | 
| 221 | 
            -
                #
         | 
| 222 | 
            -
                # @yield Expects a block containing method definitions
         | 
| 223 | 
            -
                # @return [void]
         | 
| 224 | 
            -
                def helpers(&b)
         | 
| 225 | 
            -
                  Callback.class_eval(&b)
         | 
| 226 | 
            -
                end
         | 
| 227 | 
            -
             | 
| 228 145 | 
             
                # Stop execution of the current {#on} handler.
         | 
| 229 146 | 
             
                #
         | 
| 230 147 | 
             
                # @return [void]
         | 
| @@ -232,6 +149,9 @@ module Cinch | |
| 232 149 | 
             
                  throw :halt
         | 
| 233 150 | 
             
                end
         | 
| 234 151 |  | 
| 152 | 
            +
                # @endgroup
         | 
| 153 | 
            +
                # @group Sending messages
         | 
| 154 | 
            +
             | 
| 235 155 | 
             
                # Sends a raw message to the server.
         | 
| 236 156 | 
             
                #
         | 
| 237 157 | 
             
                # @param [String] command The message to send.
         | 
| @@ -246,18 +166,20 @@ module Cinch | |
| 246 166 | 
             
                #
         | 
| 247 167 | 
             
                # @param [String] recipient the recipient
         | 
| 248 168 | 
             
                # @param [String] text the message to send
         | 
| 169 | 
            +
                # @param [Boolean] notice Use NOTICE instead of PRIVMSG?
         | 
| 249 170 | 
             
                # @return [void]
         | 
| 250 171 | 
             
                # @see Channel#send
         | 
| 251 172 | 
             
                # @see User#send
         | 
| 252 173 | 
             
                # @see #safe_msg
         | 
| 253 | 
            -
                def msg(recipient, text)
         | 
| 174 | 
            +
                def msg(recipient, text, notice = false)
         | 
| 254 175 | 
             
                  text = text.to_s
         | 
| 255 176 | 
             
                  split_start = @config.message_split_start || ""
         | 
| 256 177 | 
             
                  split_end   = @config.message_split_end   || ""
         | 
| 178 | 
            +
                  command = notice ? "NOTICE" : "PRIVMSG"
         | 
| 257 179 |  | 
| 258 180 | 
             
                  text.split(/\r\n|\r|\n/).each do |line|
         | 
| 259 | 
            -
                     | 
| 260 | 
            -
                    maxlength =  | 
| 181 | 
            +
                    maxlength = 510 - (":" + " #{command} " + " :").size
         | 
| 182 | 
            +
                    maxlength = maxlength - self.mask.to_s.length - recipient.to_s.length
         | 
| 261 183 | 
             
                    maxlength_without_end = maxlength - split_end.bytesize
         | 
| 262 184 |  | 
| 263 185 | 
             
                    if line.bytesize > maxlength
         | 
| @@ -273,16 +195,29 @@ module Cinch | |
| 273 195 | 
             
                      splitted << line
         | 
| 274 196 | 
             
                      splitted[0, (@config.max_messages || splitted.size)].each do |string|
         | 
| 275 197 | 
             
                        string.tr!("\u00A0", " ") # clean string from any non-breaking spaces
         | 
| 276 | 
            -
                        raw(" | 
| 198 | 
            +
                        raw("#{command} #{recipient} :#{string}")
         | 
| 277 199 | 
             
                      end
         | 
| 278 200 | 
             
                    else
         | 
| 279 | 
            -
                      raw(" | 
| 201 | 
            +
                      raw("#{command} #{recipient} :#{line}")
         | 
| 280 202 | 
             
                    end
         | 
| 281 203 | 
             
                  end
         | 
| 282 204 | 
             
                end
         | 
| 283 205 | 
             
                alias_method :privmsg, :msg
         | 
| 284 206 | 
             
                alias_method :send, :msg
         | 
| 285 207 |  | 
| 208 | 
            +
                # Sends a NOTICE to a recipient (a channel or user).
         | 
| 209 | 
            +
                # You should be using {Channel#notice} and {User#notice} instead.
         | 
| 210 | 
            +
                #
         | 
| 211 | 
            +
                # @param [String] recipient the recipient
         | 
| 212 | 
            +
                # @param [String] text the message to send
         | 
| 213 | 
            +
                # @return [void]
         | 
| 214 | 
            +
                # @see Channel#notice
         | 
| 215 | 
            +
                # @see User#notice
         | 
| 216 | 
            +
                # @see #safe_notice
         | 
| 217 | 
            +
                def notice(recipient, text)
         | 
| 218 | 
            +
                  msg(recipient, text, true)
         | 
| 219 | 
            +
                end
         | 
| 220 | 
            +
             | 
| 286 221 | 
             
                # Like {#msg}, but remove any non-printable characters from
         | 
| 287 222 | 
             
                # `text`. The purpose of this method is to send text of untrusted
         | 
| 288 223 | 
             
                # sources, like other users or feeds.
         | 
| @@ -302,6 +237,19 @@ module Cinch | |
| 302 237 | 
             
                alias_method :safe_privmsg, :safe_msg
         | 
| 303 238 | 
             
                alias_method :safe_send, :safe_msg
         | 
| 304 239 |  | 
| 240 | 
            +
                # Like {#safe_msg} but for notices.
         | 
| 241 | 
            +
                #
         | 
| 242 | 
            +
                # @return (see #safe_msg)
         | 
| 243 | 
            +
                # @param (see #safe_msg)
         | 
| 244 | 
            +
                # @see #safe_notice
         | 
| 245 | 
            +
                # @see #notice
         | 
| 246 | 
            +
                # @see User#safe_notice
         | 
| 247 | 
            +
                # @see Channel#safe_notice
         | 
| 248 | 
            +
                # @todo (see #safe_msg)
         | 
| 249 | 
            +
                def safe_notice(recipient, text)
         | 
| 250 | 
            +
                  msg(recipient, Cinch.filter_string(text), true)
         | 
| 251 | 
            +
                end
         | 
| 252 | 
            +
             | 
| 305 253 | 
             
                # Invoke an action (/me) in/to a recipient (a channel or user).
         | 
| 306 254 | 
             
                # You should be using {Channel#action} and {User#action} instead.
         | 
| 307 255 | 
             
                #
         | 
| @@ -332,112 +280,336 @@ module Cinch | |
| 332 280 | 
             
                  action(recipient, Cinch.filter_string(text))
         | 
| 333 281 | 
             
                end
         | 
| 334 282 |  | 
| 335 | 
            -
                #  | 
| 283 | 
            +
                # @endgroup
         | 
| 284 | 
            +
                # @group Events & Plugins
         | 
| 285 | 
            +
             | 
| 286 | 
            +
                # Registers a handler.
         | 
| 287 | 
            +
                #
         | 
| 288 | 
            +
                # @param [String, Symbol, Integer] event the event to match. Available
         | 
| 289 | 
            +
                #   events are all IRC commands in lowercase as symbols, all numeric
         | 
| 290 | 
            +
                #   replies, and the following:
         | 
| 291 | 
            +
                #
         | 
| 292 | 
            +
                #     - :channel (a channel message)
         | 
| 293 | 
            +
                #     - :private (a private message)
         | 
| 294 | 
            +
                #     - :message (both channel and private messages)
         | 
| 295 | 
            +
                #     - :error   (handling errors, use a numeric error code as `match`)
         | 
| 296 | 
            +
                #     - :ctcp    (ctcp requests, use a ctcp command as `match`)
         | 
| 297 | 
            +
                #
         | 
| 298 | 
            +
                # @param [Regexp, String, Integer] match every message of the
         | 
| 299 | 
            +
                #   right event will be checked against this argument and the event
         | 
| 300 | 
            +
                #   will only be called if it matches
         | 
| 301 | 
            +
                #
         | 
| 302 | 
            +
                # @yieldparam [String] *args each capture group of the regex will
         | 
| 303 | 
            +
                #   be one argument to the block. It is optional to accept them,
         | 
| 304 | 
            +
                #   though
         | 
| 336 305 | 
             
                #
         | 
| 337 | 
            -
                # @param [String, Channel] channel either the name of a channel or a {Channel} object
         | 
| 338 | 
            -
                # @param [String] key optionally the key of the channel
         | 
| 339 306 | 
             
                # @return [void]
         | 
| 340 | 
            -
                 | 
| 341 | 
            -
             | 
| 342 | 
            -
                   | 
| 307 | 
            +
                def on(event, regexps = [], *args, &block)
         | 
| 308 | 
            +
                  regexps = [*regexps]
         | 
| 309 | 
            +
                  regexps = [//] if regexps.empty?
         | 
| 310 | 
            +
             | 
| 311 | 
            +
                  event = event.to_sym
         | 
| 312 | 
            +
             | 
| 313 | 
            +
                  regexps.map! do |regexp|
         | 
| 314 | 
            +
                    pattern = case regexp
         | 
| 315 | 
            +
                             when Pattern
         | 
| 316 | 
            +
                               regexp
         | 
| 317 | 
            +
                             when Regexp
         | 
| 318 | 
            +
                               Pattern.new(nil, regexp, nil)
         | 
| 319 | 
            +
                             else
         | 
| 320 | 
            +
                               if event == :ctcp
         | 
| 321 | 
            +
                                 Pattern.new(/^/, /#{Regexp.escape(regexp.to_s)}(?:$| .+)/)
         | 
| 322 | 
            +
                               else
         | 
| 323 | 
            +
                                 Pattern.new(/^/, /#{Regexp.escape(regexp.to_s)}/, /$/)
         | 
| 324 | 
            +
                               end
         | 
| 325 | 
            +
                             end
         | 
| 326 | 
            +
                    debug "[on handler] Registering handler with pattern `#{pattern.inspect}`, reacting on `#{event}`"
         | 
| 327 | 
            +
                    pattern
         | 
| 328 | 
            +
                  end
         | 
| 329 | 
            +
                  (@events[event] ||= []) << [regexps, args, block]
         | 
| 343 330 | 
             
                end
         | 
| 344 331 |  | 
| 345 | 
            -
                #  | 
| 346 | 
            -
                #
         | 
| 347 | 
            -
                #  | 
| 348 | 
            -
                # @param [ | 
| 332 | 
            +
                # @param [Symbol] event The event type
         | 
| 333 | 
            +
                # @param [Message, nil] msg The message which is responsible for
         | 
| 334 | 
            +
                #   and attached to the event, or nil.
         | 
| 335 | 
            +
                # @param [Array] *arguments A list of additional arguments to pass
         | 
| 336 | 
            +
                #   to event handlers
         | 
| 349 337 | 
             
                # @return [void]
         | 
| 350 | 
            -
                 | 
| 351 | 
            -
             | 
| 352 | 
            -
             | 
| 353 | 
            -
             | 
| 338 | 
            +
                def dispatch(event, msg = nil, *arguments)
         | 
| 339 | 
            +
                  if handlers = find(event, msg)
         | 
| 340 | 
            +
                    handlers.each do |handler|
         | 
| 341 | 
            +
                      regexps, args, block = *handler
         | 
| 342 | 
            +
                      # calling Message#match multiple times is not a problem
         | 
| 343 | 
            +
                      # because we cache the result
         | 
| 344 | 
            +
                      if msg
         | 
| 345 | 
            +
                        regexp = regexps.find { |rx|
         | 
| 346 | 
            +
                          msg.match(rx.to_r(msg), event)
         | 
| 347 | 
            +
                        }
         | 
| 348 | 
            +
                        captures = msg.match(regexp.to_r(msg), event).captures
         | 
| 349 | 
            +
                      else
         | 
| 350 | 
            +
                        captures = []
         | 
| 351 | 
            +
                      end
         | 
| 354 352 |  | 
| 355 | 
            -
             | 
| 356 | 
            -
             | 
| 357 | 
            -
             | 
| 358 | 
            -
                  @config.nick
         | 
| 353 | 
            +
                      invoke(block, args, msg, captures, arguments)
         | 
| 354 | 
            +
                    end
         | 
| 355 | 
            +
                  end
         | 
| 359 356 | 
             
                end
         | 
| 360 357 |  | 
| 361 | 
            -
                 | 
| 362 | 
            -
             | 
| 358 | 
            +
                # Register all plugins from `@config.plugins.plugins`.
         | 
| 359 | 
            +
                #
         | 
| 360 | 
            +
                # @return [void]
         | 
| 361 | 
            +
                def register_plugins
         | 
| 362 | 
            +
                  @config.plugins.plugins.each do |plugin|
         | 
| 363 | 
            +
                    register_plugin(plugin)
         | 
| 364 | 
            +
                  end
         | 
| 363 365 | 
             
                end
         | 
| 364 366 |  | 
| 365 | 
            -
                 | 
| 366 | 
            -
             | 
| 367 | 
            +
                # Registers a plugin.
         | 
| 368 | 
            +
                #
         | 
| 369 | 
            +
                # @param [Class<Plugin>] plugin The plugin class to register
         | 
| 370 | 
            +
                # @return [void]
         | 
| 371 | 
            +
                def register_plugin(plugin)
         | 
| 372 | 
            +
                  @plugins << plugin.new(self)
         | 
| 367 373 | 
             
                end
         | 
| 368 374 |  | 
| 369 | 
            -
                 | 
| 370 | 
            -
             | 
| 371 | 
            -
                    User(nick).__send__(attr)
         | 
| 372 | 
            -
                  end
         | 
| 373 | 
            -
                end
         | 
| 375 | 
            +
                # @endgroup
         | 
| 376 | 
            +
                # @group Bot Control
         | 
| 374 377 |  | 
| 375 | 
            -
                #  | 
| 378 | 
            +
                # This method is used to set a bot's options. It indeed does
         | 
| 379 | 
            +
                # nothing else but yielding {Bot#config}, but it makes for a nice DSL.
         | 
| 376 380 | 
             
                #
         | 
| 377 | 
            -
                # @ | 
| 378 | 
            -
                # @ | 
| 379 | 
            -
                def  | 
| 380 | 
            -
                   | 
| 381 | 
            -
                    raise Exceptions::NickTooLong, new_nick
         | 
| 382 | 
            -
                  end
         | 
| 383 | 
            -
                  @config.nick = new_nick
         | 
| 384 | 
            -
                  raw "NICK #{new_nick}"
         | 
| 381 | 
            +
                # @yieldparam [Struct] config the bot's config
         | 
| 382 | 
            +
                # @return [void]
         | 
| 383 | 
            +
                def configure(&block)
         | 
| 384 | 
            +
                  @callback.instance_exec(@config, &block)
         | 
| 385 385 | 
             
                end
         | 
| 386 386 |  | 
| 387 387 | 
             
                # Disconnects from the server.
         | 
| 388 388 | 
             
                #
         | 
| 389 | 
            +
                # @param [String] message The quit message to send while quitting
         | 
| 389 390 | 
             
                # @return [void]
         | 
| 390 391 | 
             
                def quit(message = nil)
         | 
| 392 | 
            +
                  @quitting = true
         | 
| 391 393 | 
             
                  command = message ? "QUIT :#{message}" : "QUIT"
         | 
| 392 394 | 
             
                  raw command
         | 
| 393 395 | 
             
                end
         | 
| 394 396 |  | 
| 397 | 
            +
                # Connects the bot to a server.
         | 
| 398 | 
            +
                #
         | 
| 399 | 
            +
                # @param [Boolean] plugins Automatically register plugins from
         | 
| 400 | 
            +
                #   `@config.plugins.plugins`?
         | 
| 401 | 
            +
                # @return [void]
         | 
| 395 402 | 
             
                # Connects the bot to a server.
         | 
| 396 403 | 
             
                #
         | 
| 397 404 | 
             
                # @param [Boolean] plugins Automatically register plugins from
         | 
| 398 405 | 
             
                #   `@config.plugins.plugins`?
         | 
| 399 406 | 
             
                # @return [void]
         | 
| 400 407 | 
             
                def start(plugins = true)
         | 
| 408 | 
            +
                  @reconnects = 0
         | 
| 401 409 | 
             
                  register_plugins if plugins
         | 
| 402 | 
            -
             | 
| 403 | 
            -
                   | 
| 404 | 
            -
             | 
| 410 | 
            +
             | 
| 411 | 
            +
                  begin
         | 
| 412 | 
            +
                    @user_manager.each do |user|
         | 
| 413 | 
            +
                      user.in_whois = false
         | 
| 414 | 
            +
                      user.unsync_all
         | 
| 415 | 
            +
                    end # reset state of all users
         | 
| 416 | 
            +
             | 
| 417 | 
            +
                    @channel_manager.each do |channel|
         | 
| 418 | 
            +
                      channel.unsync_all
         | 
| 419 | 
            +
                    end # reset state of all channels
         | 
| 420 | 
            +
             | 
| 421 | 
            +
                    @logger.debug "Connecting to #{@config.server}:#{@config.port}"
         | 
| 422 | 
            +
                    @irc = IRC.new(self)
         | 
| 423 | 
            +
                    @irc.connect
         | 
| 424 | 
            +
             | 
| 425 | 
            +
                    if @config.reconnect && !@quitting
         | 
| 426 | 
            +
                      # double the delay for each unsuccesful reconnection attempt
         | 
| 427 | 
            +
                      if @last_connection_was_successful
         | 
| 428 | 
            +
                        @reconnects = 0
         | 
| 429 | 
            +
                        @last_connection_was_successful = false
         | 
| 430 | 
            +
                      else
         | 
| 431 | 
            +
                        @reconnects += 1
         | 
| 432 | 
            +
                      end
         | 
| 433 | 
            +
             | 
| 434 | 
            +
                      # Sleep for a few seconds before reconnecting to prevent being
         | 
| 435 | 
            +
                      # throttled by the IRC server
         | 
| 436 | 
            +
                      wait = 2**@reconnects
         | 
| 437 | 
            +
                      @logger.debug "Waiting #{wait} seconds before reconnecting"
         | 
| 438 | 
            +
                      sleep wait
         | 
| 439 | 
            +
                    end
         | 
| 440 | 
            +
                  end while @config.reconnect and not @quitting
         | 
| 405 441 | 
             
                end
         | 
| 406 442 |  | 
| 407 | 
            -
                #  | 
| 443 | 
            +
                # @endgroup
         | 
| 444 | 
            +
                # @group Channel Control
         | 
| 445 | 
            +
             | 
| 446 | 
            +
                # Join a channel.
         | 
| 408 447 | 
             
                #
         | 
| 448 | 
            +
                # @param [String, Channel] channel either the name of a channel or a {Channel} object
         | 
| 449 | 
            +
                # @param [String] key optionally the key of the channel
         | 
| 409 450 | 
             
                # @return [void]
         | 
| 410 | 
            -
                 | 
| 411 | 
            -
             | 
| 412 | 
            -
             | 
| 413 | 
            -
                  end
         | 
| 451 | 
            +
                # @see Channel#join
         | 
| 452 | 
            +
                def join(channel, key = nil)
         | 
| 453 | 
            +
                  Channel(channel).join(key)
         | 
| 414 454 | 
             
                end
         | 
| 415 455 |  | 
| 416 | 
            -
                #  | 
| 456 | 
            +
                # Part a channel.
         | 
| 417 457 | 
             
                #
         | 
| 418 | 
            -
                # @param [ | 
| 458 | 
            +
                # @param [String, Channel] channel either the name of a channel or a {Channel} object
         | 
| 459 | 
            +
                # @param [String] reason an optional reason/part message
         | 
| 419 460 | 
             
                # @return [void]
         | 
| 420 | 
            -
                 | 
| 421 | 
            -
             | 
| 461 | 
            +
                # @see Channel#part
         | 
| 462 | 
            +
                def part(channel, reason = nil)
         | 
| 463 | 
            +
                  Channel(channel).part(reason)
         | 
| 464 | 
            +
                end
         | 
| 465 | 
            +
             | 
| 466 | 
            +
                # @endgroup
         | 
| 467 | 
            +
             | 
| 468 | 
            +
                # (see Logger::Logger#debug)
         | 
| 469 | 
            +
                # @see Logger::Logger#debug
         | 
| 470 | 
            +
                def debug(msg)
         | 
| 471 | 
            +
                  @logger.debug(msg)
         | 
| 472 | 
            +
                end
         | 
| 473 | 
            +
             | 
| 474 | 
            +
                # @return [Boolean] True if the bot reports ISUPPORT violations as
         | 
| 475 | 
            +
                #   exceptions.
         | 
| 476 | 
            +
                def strict?
         | 
| 477 | 
            +
                  @config.strictness == :strict
         | 
| 478 | 
            +
                end
         | 
| 479 | 
            +
             | 
| 480 | 
            +
                # @yield
         | 
| 481 | 
            +
                def initialize(&b)
         | 
| 482 | 
            +
                  @logger = Logger::FormattedLogger.new($stderr)
         | 
| 483 | 
            +
                  @events = {}
         | 
| 484 | 
            +
                  @config = OpenStruct.new({
         | 
| 485 | 
            +
                                             :server => "localhost",
         | 
| 486 | 
            +
                                             :port   => 6667,
         | 
| 487 | 
            +
                                             :ssl    => OpenStruct.new({
         | 
| 488 | 
            +
                                                                         :use => false,
         | 
| 489 | 
            +
                                                                         :verify => false,
         | 
| 490 | 
            +
                                                                         :client_cert => nil,
         | 
| 491 | 
            +
                                                                         :ca_path => "/etc/ssl/certs",
         | 
| 492 | 
            +
                                                                       }),
         | 
| 493 | 
            +
                                             :password => nil,
         | 
| 494 | 
            +
                                             :nick   => "cinch",
         | 
| 495 | 
            +
                                             :nicks  => nil,
         | 
| 496 | 
            +
                                             :realname => "cinch",
         | 
| 497 | 
            +
                                             :verbose => true,
         | 
| 498 | 
            +
                                             :messages_per_second => 0.5,
         | 
| 499 | 
            +
                                             :server_queue_size => 10,
         | 
| 500 | 
            +
                                             :strictness => :forgiving,
         | 
| 501 | 
            +
                                             :message_split_start => '... ',
         | 
| 502 | 
            +
                                             :message_split_end   => ' ...',
         | 
| 503 | 
            +
                                             :max_messages => nil,
         | 
| 504 | 
            +
                                             :plugins => OpenStruct.new({
         | 
| 505 | 
            +
                                                                          :plugins => [],
         | 
| 506 | 
            +
                                                                          :prefix  => /^!/,
         | 
| 507 | 
            +
                                                                          :suffix  => nil,
         | 
| 508 | 
            +
                                                                          :options => Hash.new {|h,k| h[k] = {}},
         | 
| 509 | 
            +
                                                                        }),
         | 
| 510 | 
            +
                                             :channels => [],
         | 
| 511 | 
            +
                                             :encoding => :irc,
         | 
| 512 | 
            +
                                             :reconnect => true,
         | 
| 513 | 
            +
                                             :local_host => nil,
         | 
| 514 | 
            +
                                             :timeouts => OpenStruct.new({
         | 
| 515 | 
            +
                                                                           :read => 240,
         | 
| 516 | 
            +
                                                                           :connect => 10,
         | 
| 517 | 
            +
                                                                         }),
         | 
| 518 | 
            +
                                             :ping_interval => 120,
         | 
| 519 | 
            +
                                           })
         | 
| 520 | 
            +
             | 
| 521 | 
            +
                  @semaphores_mutex = Mutex.new
         | 
| 522 | 
            +
                  @semaphores = Hash.new { |h,k| h[k] = Mutex.new }
         | 
| 523 | 
            +
                  @plugins = []
         | 
| 524 | 
            +
                  @callback = Callback.new(self)
         | 
| 525 | 
            +
                  @channels = []
         | 
| 526 | 
            +
                  @handler_threads = []
         | 
| 527 | 
            +
                  @quitting = false
         | 
| 528 | 
            +
             | 
| 529 | 
            +
                  @user_manager = UserManager.new(self)
         | 
| 530 | 
            +
                  @channel_manager = ChannelManager.new(self)
         | 
| 531 | 
            +
             | 
| 532 | 
            +
                  on :connect do
         | 
| 533 | 
            +
                    bot.config.channels.each do |channel|
         | 
| 534 | 
            +
                      bot.join channel
         | 
| 535 | 
            +
                    end
         | 
| 536 | 
            +
                  end
         | 
| 537 | 
            +
             | 
| 538 | 
            +
                  instance_eval(&b) if block_given?
         | 
| 422 539 | 
             
                end
         | 
| 423 540 |  | 
| 541 | 
            +
                # The bot's nickname.
         | 
| 542 | 
            +
                # @overload nick=(new_nick)
         | 
| 543 | 
            +
                #   @raise [Exceptions::NickTooLong] Raised if the bot is
         | 
| 544 | 
            +
                #     operating in {#strict? strict mode} and the new nickname is
         | 
| 545 | 
            +
                #     too long
         | 
| 546 | 
            +
                #   @return [String]
         | 
| 547 | 
            +
                # @overload nick
         | 
| 548 | 
            +
                #   @return [String]
         | 
| 549 | 
            +
                # @return [String]
         | 
| 550 | 
            +
                attr_accessor :nick
         | 
| 551 | 
            +
                undef_method "nick"
         | 
| 552 | 
            +
                undef_method "nick="
         | 
| 553 | 
            +
                def nick
         | 
| 554 | 
            +
                  @config.nick
         | 
| 555 | 
            +
                end
         | 
| 556 | 
            +
             | 
| 557 | 
            +
                def nick=(new_nick)
         | 
| 558 | 
            +
                  if new_nick.size > @irc.isupport["NICKLEN"] && strict?
         | 
| 559 | 
            +
                    raise Exceptions::NickTooLong, new_nick
         | 
| 560 | 
            +
                  end
         | 
| 561 | 
            +
                  @config.nick = new_nick
         | 
| 562 | 
            +
                  raw "NICK #{new_nick}"
         | 
| 563 | 
            +
                end
         | 
| 564 | 
            +
             | 
| 565 | 
            +
                # Try to create a free nick, first by cycling through all
         | 
| 566 | 
            +
                # available alternatives and then by appending underscores.
         | 
| 567 | 
            +
                #
         | 
| 568 | 
            +
                # @param [String] base The base nick to start trying from
         | 
| 424 569 | 
             
                # @api private
         | 
| 425 | 
            -
                # @return  | 
| 426 | 
            -
                def  | 
| 427 | 
            -
                   | 
| 428 | 
            -
             | 
| 429 | 
            -
             | 
| 430 | 
            -
             | 
| 431 | 
            -
             | 
| 432 | 
            -
             | 
| 433 | 
            -
             | 
| 434 | 
            -
             | 
| 570 | 
            +
                # @return String
         | 
| 571 | 
            +
                def generate_next_nick(base = nil)
         | 
| 572 | 
            +
                  nicks = @config.nicks || []
         | 
| 573 | 
            +
             | 
| 574 | 
            +
                  if base
         | 
| 575 | 
            +
                    # if `base` is not in our list of nicks to try, assume that it's
         | 
| 576 | 
            +
                    # custom and just append an underscore
         | 
| 577 | 
            +
                    if !nicks.include?(base)
         | 
| 578 | 
            +
                      return base + "_"
         | 
| 579 | 
            +
                    else
         | 
| 580 | 
            +
                      # if we have a base, try the next nick or append an
         | 
| 581 | 
            +
                      # underscore if no more nicks are left
         | 
| 582 | 
            +
                      new_index = nicks.index(base) + 1
         | 
| 583 | 
            +
                      if nicks[new_index]
         | 
| 584 | 
            +
                        return nicks[new_index]
         | 
| 435 585 | 
             
                      else
         | 
| 436 | 
            -
                         | 
| 586 | 
            +
                        return base + "_"
         | 
| 437 587 | 
             
                      end
         | 
| 438 | 
            -
             | 
| 439 | 
            -
                      invoke(block, args, msg, captures)
         | 
| 440 588 | 
             
                    end
         | 
| 589 | 
            +
                  else
         | 
| 590 | 
            +
                    # if we have no base, try the first possible nick
         | 
| 591 | 
            +
                    new_nick = @config.nicks ? @config.nicks.first : @config.nick
         | 
| 592 | 
            +
                  end
         | 
| 593 | 
            +
                end
         | 
| 594 | 
            +
             | 
| 595 | 
            +
                # @return [Boolean] True if the bot is using SSL to connect to the
         | 
| 596 | 
            +
                #   server.
         | 
| 597 | 
            +
                def secure?
         | 
| 598 | 
            +
                  @config[:ssl] == true || (@config[:ssl].is_a?(Hash) && @config[:ssl][:use])
         | 
| 599 | 
            +
                end
         | 
| 600 | 
            +
             | 
| 601 | 
            +
                # This method is only provided in order to give Bot and User a
         | 
| 602 | 
            +
                # common interface.
         | 
| 603 | 
            +
                #
         | 
| 604 | 
            +
                # @return [false] Always returns `false`.
         | 
| 605 | 
            +
                # @see User#unknown? See User#unknown? for the method's real use.
         | 
| 606 | 
            +
                def unknown?
         | 
| 607 | 
            +
                  false
         | 
| 608 | 
            +
                end
         | 
| 609 | 
            +
             | 
| 610 | 
            +
                [:host, :mask, :user, :realname, :signed_on_at, :secure?].each do |attr|
         | 
| 611 | 
            +
                  define_method(attr) do
         | 
| 612 | 
            +
                    User(nick).__send__(attr)
         | 
| 441 613 | 
             
                  end
         | 
| 442 614 | 
             
                end
         | 
| 443 615 |  | 
| @@ -450,28 +622,23 @@ module Cinch | |
| 450 622 |  | 
| 451 623 | 
             
                    events.select { |regexps|
         | 
| 452 624 | 
             
                      regexps.first.any? { |regexp|
         | 
| 453 | 
            -
                        msg.match(regexp, type)
         | 
| 625 | 
            +
                        msg.match(regexp.to_r(msg), type)
         | 
| 454 626 | 
             
                      }
         | 
| 455 627 | 
             
                    }
         | 
| 456 628 | 
             
                  end
         | 
| 457 629 | 
             
                end
         | 
| 458 630 |  | 
| 459 | 
            -
                def invoke(block, args, msg, match)
         | 
| 460 | 
            -
                   | 
| 461 | 
            -
                   | 
| 462 | 
            -
                  #  1  defined number of args, send only those
         | 
| 463 | 
            -
                  bargs = case block.arity <=> 0
         | 
| 464 | 
            -
                          when -1; match
         | 
| 465 | 
            -
                          when 0; []
         | 
| 466 | 
            -
                          when 1; match[0..block.arity-1 - args.size]
         | 
| 467 | 
            -
                          end
         | 
| 468 | 
            -
                  Thread.new do
         | 
| 631 | 
            +
                def invoke(block, args, msg, match, arguments)
         | 
| 632 | 
            +
                  bargs = match + arguments
         | 
| 633 | 
            +
                  @handler_threads << Thread.new do
         | 
| 469 634 | 
             
                    begin
         | 
| 470 635 | 
             
                      catch(:halt) do
         | 
| 471 636 | 
             
                        @callback.instance_exec(msg, *args, *bargs, &block)
         | 
| 472 637 | 
             
                      end
         | 
| 473 638 | 
             
                    rescue => e
         | 
| 474 639 | 
             
                      @logger.log_exception(e)
         | 
| 640 | 
            +
                    ensure
         | 
| 641 | 
            +
                      @handler_threads.delete Thread.current
         | 
| 475 642 | 
             
                    end
         | 
| 476 643 | 
             
                  end
         | 
| 477 644 | 
             
                end
         |