blur 1.7.3 → 1.8.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a2633b6b5cc955fba7d379cdfd86f6fbfb3b954f
4
- data.tar.gz: 86c7b5cedbf4ee9f97efd65580b0b00f8bb59f39
3
+ metadata.gz: 0fd33b08ea566e3cf592a64b5ecaa68e28bf3715
4
+ data.tar.gz: 0780c217f54c21a06ba911d178e0323f822ef310
5
5
  SHA512:
6
- metadata.gz: bd6a9bd936e44ed4b834b83d1a45d1e1c1c7a330491a2302ac9930f6142ceff32117b29fbe8a71267a7c79305b284d3997c3995da85b945c420e6347dfe33823
7
- data.tar.gz: 4c0164dc24f398b4fa0fef4fee3351e1a81158da73cfa478a7b91772b0579a5f1f301c45e9e695c88236d4772a7a808028933b785f6c18ac3185f081d063c53a
6
+ metadata.gz: a7a072a647cf7e8804991dde2b0c82173cfc0931188a46298c5e8b4441ebad34da61cdd160d987123c921da3aaf5c62790cdbac8dd464e9c93a86bc1e5055945
7
+ data.tar.gz: 741af149e93c26ade44f8b177d1466c5cc6960187a2c22d971343d7d69d7dc922e013e4a050d66a71cac5745b5b385e9664089af5f9bd63f7ea1dce20fa6fe96
@@ -0,0 +1,5 @@
1
+ --output-dir documentation/
2
+ --readme README.md
3
+ --title Blur
4
+ --protected
5
+ library/**/*.rb
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2014, Mikkel Kroman
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any
4
+ purpose with or without fee is hereby granted, provided that the above
5
+ copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,35 @@
1
+ Blur
2
+ ====
3
+ Blur is an event-driven IRC-framework written in and for Ruby.
4
+
5
+ There are a bunch of other well-written, well-running IRC libraries made for
6
+ Ruby, but for me, they don't quite cut it as **the** library I wanted to use for
7
+ my IRC services. That's how Blur came to be.
8
+
9
+ Blur scales. A lot.
10
+
11
+ When I stresstested the library on my network, I ended up throttling my VDSL
12
+ connection before Blur even broke a sweat - albeit I only have 20/2.
13
+
14
+ I managed to connect with 5000 clones before it couldn't resolve the hostname
15
+ anymore, while this is an excellent feature, I would not suggest doing it.
16
+
17
+ [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/mkroman/blur/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
18
+ [![Build Status](https://travis-ci.org/mkroman/blur.svg?branch=isupport)](https://travis-ci.org/mkroman/blur)
19
+ [![Dependency Status](https://gemnasium.com/mkroman/blur.svg)](https://gemnasium.com/mkroman/blur)
20
+
21
+ Features
22
+ --------
23
+ * SSL/TLS encryption
24
+ * Connect to multiple networks
25
+ * FiSH (channel-wide) encryptions
26
+ * Non-blocking connections (no threading)
27
+ * Extensible with scripts, (re)loadable during runtime
28
+ * Modular, makes it a piece of cake to extend its IRC-capability
29
+
30
+ Future Plans
31
+ ------------
32
+ * DCC File-transfers
33
+ * DH1080 Key-Exchange
34
+ * ISupport implementation
35
+ * Better event-handling in scripts
@@ -9,27 +9,28 @@ require 'openssl'
9
9
  require 'eventmachine'
10
10
 
11
11
  # Require all library files.
12
+ require 'blur/version'
12
13
  require 'blur/client'
14
+ require 'blur/evaluable'
15
+ require 'blur/script/dsl'
16
+ require 'blur/extension'
13
17
  require 'blur/script'
14
18
  require 'blur/network'
15
19
  require 'blur/encryption'
16
20
  require 'blur/enhancements'
17
21
  require 'blur/script/cache'
18
22
  require 'blur/network/user'
19
- require 'blur/script/fantasy'
20
23
  require 'blur/network/channel'
21
24
  require 'blur/network/command'
25
+ require 'blur/network/isupport'
22
26
  require 'blur/network/connection'
23
- require 'blur/script/messageparsing'
27
+ require 'blur/script/commands'
24
28
 
25
29
  # Blur is a very modular IRC-framework for ruby.
26
30
  #
27
31
  # It allows the developer to extend it in multiple ways.
28
32
  # It can be by handlers, scripts, communications, and what have you.
29
33
  module Blur
30
- # The major and minor version-values of Blur.
31
- Version = "1.7.3"
32
-
33
34
  # Instantiates a client with given options and then makes the client instance
34
35
  # evaluate the given block to form a DSL.
35
36
  #
@@ -33,8 +33,6 @@ module Blur
33
33
 
34
34
  load_scripts
35
35
  trap 2, &method(:quit)
36
-
37
- EventMachine.threadpool_size = 1
38
36
  end
39
37
 
40
38
  # Connect to each network available that is not already connected, then
@@ -43,12 +41,12 @@ module Blur
43
41
  networks = @networks.select {|network| not network.connected? }
44
42
 
45
43
  EventMachine.run do
44
+ EventMachine.error_handler{|e| p e }
45
+
46
46
  networks.each do |network|
47
47
  network.delegate = self
48
48
  network.connect
49
49
  end
50
-
51
- EventMachine.error_handler{|e| p e }
52
50
  end
53
51
  end
54
52
 
@@ -69,6 +67,10 @@ module Blur
69
67
 
70
68
  # Searches for scripts in working_directory/scripts and then loads them.
71
69
  def load_scripts
70
+ # Load script extensions.
71
+ Script.load_extensions!
72
+
73
+ # Load the scripts.
72
74
  script_path = File.dirname $0
73
75
 
74
76
  Dir.glob("#{script_path}/scripts/*.rb").each do |path|
@@ -83,6 +85,9 @@ module Blur
83
85
  #
84
86
  # @see Script#unload!
85
87
  def unload_scripts
88
+ # Unload script extensions.
89
+ Script.unload_extensions!
90
+
86
91
  @scripts.each do |script|
87
92
  script.unload!
88
93
  end.clear
@@ -98,13 +103,13 @@ module Blur
98
103
  #
99
104
  # @param [optional, Symbol] signal The signal received by the system, if any.
100
105
  def quit signal = :SIGINT
106
+ unload_scripts
107
+
101
108
  @networks.each do |network|
102
109
  network.transmit :QUIT, "Got SIGINT?"
103
110
  network.disconnect
104
111
  end
105
112
 
106
- unload_scripts
107
-
108
113
  EventMachine.stop
109
114
  end
110
115
 
@@ -119,16 +124,21 @@ module Blur
119
124
  def emit name, *args
120
125
  EM.defer do
121
126
  @callbacks[name].each do |callback|
122
- callback.call *args
127
+ begin
128
+ callback.call *args
129
+ rescue Exception => e
130
+ log.error "Callback `#{name}' threw an exception - #{exception.message ^ :bold} on line #{exception.line.to_s ^ :bold}"
131
+ puts exception.backtrace.join "\n"
132
+ end
123
133
  end if @callbacks[name]
124
134
 
125
135
  scripts = @scripts.select{|script| script.__emissions.include? name }
126
-
127
136
  scripts.each do |script|
128
137
  begin
129
138
  script.__send__ name, *args
130
139
  rescue Exception => exception
131
140
  log.error "#{File.basename(script.__path) << " - " << exception.message ^ :bold} on line #{exception.line.to_s ^ :bold}"
141
+ puts exception.backtrace.join "\n"
132
142
  end
133
143
  end
134
144
  end
@@ -11,7 +11,9 @@ class Exception
11
11
  #
12
12
  # @return Fixnum the line of the script the exception was raised on.
13
13
  def line
14
- backtrace[0].match(Pattern)[1].to_i + 1
14
+ if result = backtrace[0].match(Pattern)
15
+ result[1].to_i + 1
16
+ end
15
17
  end
16
18
  end
17
19
 
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+
3
+ module Blur
4
+ module Evaluable
5
+ # Evaluate the contents of the input file in the context of +self+.
6
+ def evaluate_source_file path
7
+ instance_eval File.read(path), File.basename(path), 0
8
+ @__evaluated = true
9
+ rescue Exception => exception
10
+ puts "#{exception.message ^ :bold} on line #{exception.line.to_s ^ :bold}"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,42 @@
1
+ # encoding: utf-8
2
+
3
+ module Blur
4
+ # +Extension+ provides a sort of modules for scripts, for common functionality.
5
+ #
6
+ # Think of it as a kind of scripts for scripts.
7
+ class Extension
8
+ include Evaluable
9
+ include Script::DSL
10
+ include Logging
11
+
12
+ # @return the path in which the script remains.
13
+ attr_accessor :__path
14
+ # Can be used inside the script to act with the client itself.
15
+ # @return [Network::Client] the client delegate.
16
+ attr_accessor :__client
17
+
18
+ # Instantiates a new extension context and evaluates the +path+ extension
19
+ # file.
20
+ def initialize path
21
+ @__path = path
22
+
23
+ if evaluate_source_file path
24
+ log.info "Loaded extension #{@__path}"
25
+ end
26
+ end
27
+
28
+ # Purely for DSL purposes.
29
+ #
30
+ # @example
31
+ # Extension :http do
32
+ # …
33
+ # end
34
+ def Extension name, &block
35
+ @__name = name
36
+
37
+ instance_eval &block
38
+
39
+ true
40
+ end
41
+ end
42
+ end
@@ -47,24 +47,24 @@ module Blur
47
47
  # Called when the namelist of a channel was received.
48
48
  def got_name_reply network, command
49
49
  name = command[2]
50
- users = command[3].split.map &Network::User.method(:new)
50
+ users = command[3].split.map do |nick|
51
+ # Slice the nick if the first character is a user mode prefix.
52
+ if network.user_prefixes.include? nick.chr
53
+ nick.slice! 0
54
+ end
55
+
56
+ Network::User.new nick
57
+ end
51
58
 
52
- if channel = network.channel_by_name(name)
59
+ if channel = find_or_create_channel(name, network)
53
60
  users.each do |user|
54
61
  user.channel = channel
55
62
  user.network = network
56
63
 
57
64
  channel.users << user
58
65
  end
59
- else
60
- channel = Network::Channel.new name, network, users
61
66
 
62
- if network.fish? and network.options[:fish].key? name
63
- keyphrase = network.options[:fish][name]
64
- channel.encryption = Encryption::FiSH.new keyphrase
65
- end
66
-
67
- network.channels << channel
67
+ emit :channel_who_reply, channel
68
68
  end
69
69
  end
70
70
 
@@ -75,21 +75,10 @@ module Blur
75
75
  def got_channel_topic network, command
76
76
  me, name, topic = command.params
77
77
 
78
- if channel = network.channel_by_name(name)
78
+ if channel = find_or_create_channel(name, network)
79
79
  emit :topic_change, channel, topic
80
- channel.topic = topic
81
- else
82
- channel = Network::Channel.new name, network, []
83
80
 
84
- if network.fish? and network.options[:fish].key? name
85
- keyphrase = network.options[:fish][name]
86
- channel.encryption = Encryption::FiSH.new keyphrase
87
- end
88
-
89
- emit :topic_change, channel, topic
90
81
  channel.topic = topic
91
-
92
- network.channels << channel
93
82
  end
94
83
  end
95
84
 
@@ -101,15 +90,14 @@ module Blur
101
90
  # Called when a user changed nickname.
102
91
  #
103
92
  # == Callbacks:
104
- # Emits :user_rename with the parameters +channel+, +user+ and +nickname+
105
- # where +nickname+ is the new name.
93
+ # Emits :user_rename with the parameters +channel+, +user+, +old_nick and +new_nick+
106
94
  def got_nick network, command
107
95
  nick = command.sender.nickname
108
96
 
109
97
  if channels = network.channels_with_user(nick)
110
98
  channels.each do |channel|
111
99
  if user = channel.user_by_nick(nick)
112
- emit :user_rename, channel, user, command[0]
100
+ emit :user_rename, channel, user, user.nick, command[0]
113
101
  user.nick = command[0]
114
102
  end
115
103
  end
@@ -268,20 +256,47 @@ module Blur
268
256
  end
269
257
 
270
258
  if user = channel.user_by_nick(nick)
271
- emit :user_mode, user, modes
272
259
  user.merge_modes modes
260
+ emit :user_mode, user, modes
273
261
  end
274
262
  else
275
- emit :channel_mode, channel, modes
276
263
  channel.merge_modes modes
264
+ emit :channel_mode, channel, modes
277
265
  end
278
266
  end
279
267
  end
280
268
 
269
+ # Called when the network announces its ISUPPORT parameters.
270
+ def got_005 network, command
271
+ params = command.params[1..-2]
272
+
273
+ network.isupport.parse *params
274
+ end
275
+
281
276
  alias_method :got_353, :got_name_reply
282
277
  alias_method :got_422, :got_end_of_motd
283
278
  alias_method :got_376, :got_end_of_motd
284
279
  alias_method :got_332, :got_channel_topic
280
+
281
+ private
282
+
283
+ def find_or_create_channel name, network, users = []
284
+ channel = network.channel_by_name name
285
+
286
+ if channel.nil?
287
+ channel = Network::Channel.new name, network, users
288
+ network.channels << channel
289
+
290
+ if network.fish? and network.options[:fish].key? name
291
+ keyphrase = network.options[:fish][name]
292
+ channel.encryption = Encryption::FiSH.new keyphrase
293
+ end
294
+
295
+ emit :channel_created, channel
296
+ end
297
+
298
+ channel
299
+ end
285
300
  end
286
301
  end
287
302
  end
@@ -21,6 +21,8 @@ module Blur
21
21
  attr_accessor :delegate
22
22
  # @return [Network::Connection] the connection instance.
23
23
  attr_accessor :connection
24
+ # @return [Network::ISupport] the network isupport specs.
25
+ attr_accessor :isupport
24
26
 
25
27
  # Check whether or not connection is established.
26
28
  def connected?; @connection and @connection.established? end
@@ -45,17 +47,31 @@ module Blur
45
47
 
46
48
  # Instantiates the network.
47
49
  #
48
- # @param [Hash] options the network options.
49
- # @option options [String] :hostname the remote hostname.
50
- # @option options [String] :nickname the nickname to represent.
51
- # @option options [optional, String] :username the username to represent.
52
- # @option options [optional, String] :realname the “real name” to represent.
53
- # @option options [optional, String] :password the password for the network.
54
- # @option options [optional, Fixnum] :port the remote port.
55
- # @option options [optional, Boolean] :secure use a secure connection.
50
+ # @param [Hash] options The network options.
51
+ # @option options [String] :hostname The hostname or IP-address we want to
52
+ # connect to.
53
+ # @option options [String] :nickname The nickname to use.
54
+ # @option options [optional, String] :username (Copies :nickname)
55
+ # The username to use. This is also known as the ident.
56
+ # @option options [optional, String] :realname (Copies :username)
57
+ # The “real name” that we want to use. This is usually what shows up
58
+ # as "Name" when you whois a user.
59
+ # @option options [optional, String] :password The password for the network.
60
+ # This is sometimes needed for private networks.
61
+ # @option options [optional, Fixnum] :port (6697 if ssl, otherwise 6667)
62
+ # The remote port we want to connect to.
63
+ # @option options [optional, Boolean] :secure Set whether this is a secure
64
+ # (SSL-encrypted) connection.
65
+ # @option options [optional, String] :ssl_cert_file Local path of a
66
+ # readable file that contains a X509 CA certificate to validate against.
67
+ # @option options [optional, String] :ssl_fingerprint Validate that the
68
+ # remote certificate matches the specified fingerprint.
69
+ # @option options [optional, Boolean] :ssl_no_verify Disable verification
70
+ # alltogether.
56
71
  def initialize options
57
72
  @options = options
58
73
  @channels = []
74
+ @isupport = ISupport.new self
59
75
 
60
76
  unless options[:nickname]
61
77
  raise ArgumentError, "nickname is missing from the networks option block"
@@ -100,6 +116,27 @@ module Blur
100
116
  @channels.select { |channel| channel.user_by_nick nick }
101
117
  end
102
118
 
119
+ # Returns a list of user prefixes that a nick might contain.
120
+ #
121
+ # @return [Array<String>] a list of user prefixes.
122
+ def user_prefixes
123
+ isupport["PREFIX"].values
124
+ end
125
+
126
+ # Returns a list of user modes that also gives a users nick a prefix.
127
+ #
128
+ # @return [Array<String>] a list of user modes.
129
+ def user_prefix_modes
130
+ isupport["PREFIX"].keys
131
+ end
132
+
133
+ # Returns a list of channel flags (channel mode D).
134
+ #
135
+ # @return [Array<String>] a list of channel flags.
136
+ def channel_flags
137
+ isupport["CHANMODES"]["D"]
138
+ end
139
+
103
140
  # Attempt to establish a connection and send initial data.
104
141
  #
105
142
  # @see Connection
@@ -137,6 +174,7 @@ module Blur
137
174
 
138
175
  @connection.send_data "#{command}\r\n"
139
176
  end
177
+
140
178
 
141
179
  # Convert it to a debug-friendly format.
142
180
  def to_s
@@ -11,6 +11,8 @@ module Blur
11
11
  # @see EventMachine::Protocols::LineAndTextProtocol
12
12
  # @see EventMachine::Connection
13
13
  class Connection < EM::Protocols::LineAndTextProtocol
14
+ SSLValidationError = Class.new StandardError
15
+
14
16
  # Check whether or not connection is established.
15
17
  def established?; @connected == true end
16
18
 
@@ -26,13 +28,18 @@ module Blur
26
28
  # Called when a new connection is being set up, all we're going to use
27
29
  # it for is to enable SSL/TLS on our connection.
28
30
  def post_init
29
- start_tls if @network.secure?
31
+ if @network.secure?
32
+ verify_peer = (@network.options[:ssl_no_verify] ? false : true)
33
+
34
+ start_tls verify_peer: verify_peer
35
+ end
30
36
  end
31
37
 
32
38
  # Called when a line was received, the connection sends it to the network
33
39
  # delegate which then sends it to the client.
34
40
  def receive_line line
35
41
  command = Command.parse line
42
+
36
43
  @network.got_command command
37
44
  end
38
45
 
@@ -44,9 +51,56 @@ module Blur
44
51
  connected!
45
52
  end
46
53
 
54
+ # Validates that the peer certificate has the correct fingerprint as
55
+ # specified in the :fingerprint :ssl option.
56
+ #
57
+ # @note This doesn't support intermediate certificate authorities!
58
+ # @raise [SSLValidationError] Raised if the specified fingerprint doesn't
59
+ # match the certificates.
60
+ def ssl_verify_peer peer_cert
61
+ ssl_cert_file = @network.options[:ssl_cert_file]
62
+ peer_certificate = OpenSSL::X509::Certificate.new peer_cert
63
+
64
+ if ssl_cert_file
65
+ unless File.readable? ssl_cert_file
66
+ raise SSLValidationError, "Could not read the CA certificate file."
67
+
68
+ return false
69
+ end
70
+ end
71
+
72
+ if fingerprint_verification?
73
+ fingerprint = @network.options[:ssl_fingerprint].to_s
74
+ peer_fingerprint = cert_sha1_fingerprint peer_certificate
75
+
76
+ if fingerprint != peer_fingerprint
77
+ raise SSLValidationError,
78
+ "Expected fingerprint '#{fingerprint}', but got '#{peer_fingerprint}'"
79
+
80
+ return false
81
+ end
82
+ end
83
+
84
+ if certificate_verification?
85
+ ca_certificate = OpenSSL::X509::Certificate.new File.read ssl_cert_file
86
+ valid_signature = peer_certificate.verify ca_certificate.public_key
87
+
88
+ if not valid_signature
89
+ raise SSLValidationError, "Certificate verify failed"
90
+
91
+ return false
92
+ end
93
+ end
94
+
95
+ true
96
+ end
97
+
47
98
  # Called once the connection is finally established.
48
99
  def connection_completed
49
- @network.connected! unless @network.secure?
100
+ # We aren't completely connected yet if the connection is encrypted.
101
+ unless @network.secure?
102
+ connected!
103
+ end
50
104
  end
51
105
 
52
106
  # Called just as the connection is being terminated, either by remote or
@@ -65,6 +119,33 @@ module Blur
65
119
 
66
120
  @network.connected!
67
121
  end
122
+
123
+ # Returns true if we're expected to verify the certificate fingerprint.
124
+ def fingerprint_verification?
125
+ not @network.options[:ssl_fingerprint].nil?
126
+ end
127
+
128
+ # Returns true if we should verify the peer certificate.
129
+ def certificate_verification?
130
+ not @network.options[:ssl_cert_file].nil?
131
+ end
132
+
133
+ # Get the hexadecimal representation of the certificates public key.
134
+ def cert_sha1_fingerprint certificate
135
+ fingerprint = OpenSSL::Digest::SHA1.hexdigest certificate.to_der
136
+
137
+ # Format it the same way OpenSSL does.
138
+ fingerprint = fingerprint.chars.each_slice(2).map(&:join).join ':'
139
+ fingerprint.upcase
140
+ end
141
+
142
+ def ssl_fingerprint_error! peer_fingerprint
143
+ fingerprint = @network.options[:ssl_fingerprint]
144
+
145
+ raise SSLValidationError,
146
+ "Expected fingerprint '#{fingerprint}' but got '#{peer_fingerprint}'"
147
+ end
148
+
68
149
  end
69
150
  end
70
151
  end
@@ -0,0 +1,133 @@
1
+ # encoding: utf-8
2
+
3
+ module Blur
4
+ class Network
5
+ # ISupport class that enables servers to announce what they support.
6
+ #
7
+ # @see https://tools.ietf.org/html/draft-brocklesby-irc-isupport-03
8
+ class ISupport < Hash
9
+ # Return the network reference.
10
+ attr_accessor :network
11
+
12
+ # ISUPPORT parameters which should always be casted to numeric values.
13
+ NumericParams = %w[CHANNELLEN MODES NICKLEN KICKLEN TOPICLEN AWAYLEN
14
+ MAXCHANNELS MAXBANS MAXPARA MAXTARGETS].freeze
15
+
16
+ # Our parsers for parameters that require special treatment.
17
+ Parsers = {
18
+ # CHANLIMIT=pfx:num[,pfx:num,...]
19
+ #
20
+ # This parameter specifies the maximum number of channels that a client
21
+ # may join. The value is a series of "pfx:num" pairs, where 'pfx'
22
+ # refers to one or more channel prefix characters (as specified in
23
+ # CHANTYPES), and 'num' indicates how many of these types of channel
24
+ # the client may join in total. If there is no limit to the number of
25
+ # certain channel type(s) a client may join, the limit should be
26
+ # specified as the empty string, for example "#:".
27
+ %w[CHANLIMIT] => -> (value) do
28
+ Hash.new.tap do |result|
29
+ params = value.split ?,
30
+ mappings = params.map{|param| param.split ?: }
31
+
32
+ mappings.each do |prefixes, limit|
33
+ prefixes.each_char do |prefix|
34
+ result[prefix] = limit ? limit.to_i : Float::INFINITY
35
+ end
36
+ end
37
+ end
38
+ end,
39
+
40
+ # PREFIX=[(modes)prefixes]
41
+ #
42
+ #
43
+ # The PREFIX parameter specifies a list of channel status flags (the
44
+ # "modes" section) that clients may have on channels, followed by a
45
+ # mapping to the equivalent channel status flags ("prefixes"), which
46
+ # are used in NAMES and WHO replies. There is a one to one mapping
47
+ # between each mode and prefix.
48
+ #
49
+ # The order of the modes is from that which gives most privileges on
50
+ # the channel, to that which gives the least.
51
+ %w[PREFIX] => -> (value) do
52
+ Hash.new.tap do |result|
53
+ if value =~ /^\((.+)\)(.*)/
54
+ modes, prefix = $~[1..2]
55
+
56
+ modes.chars.each_with_index do |char, index|
57
+ result[char] = prefix[index]
58
+ end
59
+ end
60
+ end
61
+ end,
62
+
63
+ # CHANMODES=A,B,C,D
64
+ #
65
+ # The CHANMODES token specifies the modes that may be set on a channel.
66
+ # These modes are split into four categories, as follows:
67
+ #
68
+ # o Type A: Modes that add or remove an address to or from a list.
69
+ # These modes always take a parameter when sent by the server to a
70
+ # client; when sent by a client, they may be specified without a
71
+ # parameter, which requests the server to display the current
72
+ # contents of the corresponding list on the channel to the client.
73
+ # o Type B: Modes that change a setting on the channel. These modes
74
+ # always take a parameter.
75
+ # o Type C: Modes that change a setting on the channel. These modes
76
+ # take a parameter only when set; the parameter is absent when the
77
+ # mode is removed both in the client's and server's MODE command.
78
+ # o Type D: Modes that change a setting on the channel. These modes
79
+ # never take a parameter.
80
+ %w[CHANMODES] => -> (value) do
81
+ Hash.new.tap do |r|
82
+ r["A"], r["B"], r["C"], r["D"] = value.split(?,).map &:chars
83
+ end
84
+ end,
85
+
86
+ # Cast known params that are numeric, to a numeric value.
87
+ NumericParams => -> (value) do
88
+ value.to_i
89
+ end
90
+ }
91
+
92
+ # Initialize a new ISupport with a network reference.
93
+ #
94
+ # @param network [Network] The parent network.
95
+ def initialize network
96
+ @network = network
97
+
98
+ # Set default ISUPPORT values.
99
+ #
100
+ # @see
101
+ # https://tools.ietf.org/html/draft-brocklesby-irc-isupport-03#appendix-A
102
+ self["MODES"] = 3
103
+ self["PREFIX"] = { "o" => "@", "v" => "+" }
104
+ self["KICKLEN"] = 200
105
+ self["NICKLEN"] = 9
106
+ self["MAXLIST"] = { "#" => Float::INFINITY, "&" => Float::INFINITY }
107
+ self['TOPICLEN'] = 200
108
+ self["CHANMODES"] = {}
109
+ self["CHANTYPES"] = %w{# &}
110
+ self["CHANLIMIT"] = { "#" => Float::INFINITY, "&" => Float::INFINITY }
111
+ self["CHANNELLEN"] = 200
112
+ self["CASEMAPPING"] = "rfc1459"
113
+ end
114
+
115
+ # Parse a list of parameters to see what the server supports.
116
+ #
117
+ # @param parameters [Array] The list of parameters.
118
+ def parse *params
119
+ params.each do |parameter|
120
+ name, value = parameter.split ?=
121
+
122
+ if value
123
+ _, parser = Parsers.find{|key, value| key.include? name }
124
+
125
+ self[name] = parser.nil? ? value : parser.(value)
126
+ else
127
+ self[name] = true
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -8,19 +8,17 @@ module Blur
8
8
  #
9
9
  # @todo add examples in the documentation
10
10
  # @see Script#Script
11
- class Script < Module
11
+ class Script
12
12
  include Logging
13
+ include Evaluable
14
+ include DSL
13
15
 
16
+ ExtensionNotFoundError = Class.new StandardError
14
17
  Emissions = [:connection_ready, :topic_change, :user_rename, :message,
15
18
  :private_message, :user_entered, :user_left, :user_quit,
16
- :user_kicked, :topic, :user_mode, :channel_mode]
19
+ :user_kicked, :topic, :user_mode, :channel_mode,
20
+ :channel_created, :channel_who_reply]
17
21
 
18
- # @return the name of the script.
19
- attr_accessor :__name
20
- # @return the author of the script.
21
- attr_accessor :__author
22
- # @return the version of the script.
23
- attr_accessor :__version
24
22
  # @return the path in which the script remains.
25
23
  attr_accessor :__path
26
24
  # Can be used inside the script to act with the client itself.
@@ -28,6 +26,27 @@ module Blur
28
26
  attr_accessor :__client
29
27
  # @return [Array] a list of handled emissions.
30
28
  attr_accessor :__emissions
29
+
30
+ # A list of extensions.
31
+ @@__extensions = []
32
+
33
+ # Find and evaluate script extensions.
34
+ def self.load_extensions!
35
+ root_path = File.dirname $0
36
+
37
+ Dir.glob("#{root_path}/extensions/*.rb").each do |path|
38
+ extension = Extension.new path
39
+ extension.__client = self
40
+ extension.extension_loaded if extension.respond_to? :extension_loaded
41
+
42
+ @@__extensions << extension
43
+ end
44
+ end
45
+
46
+ # "Unload" all script extensions.
47
+ def self.unload_extensions!
48
+ @@__extensions.clear
49
+ end
31
50
 
32
51
  # Check to see if the script has been evaluated.
33
52
  def evaluated?; @__evaluated end
@@ -38,7 +57,7 @@ module Blur
38
57
  @__evaluated = false
39
58
  @__emissions = []
40
59
 
41
- if evaluate and @__evaluated
60
+ if evaluate_source_file path
42
61
  cache.load if Cache.exists? @__name
43
62
 
44
63
  Emissions.each do |emission|
@@ -58,15 +77,40 @@ module Blur
58
77
  # # …
59
78
  # end
60
79
  # end
61
- def Script name, version = [1,0], author = nil, &block
62
- @__name = name
63
- @__author = author
64
- @__version = version
80
+ def Script name, options = {}, &block
81
+ @__name = name
82
+
83
+ extensions = options[:using] || options[:uses]
84
+
85
+ # Automatically used extensions.
86
+ if extensions
87
+ extensions.each {|extension_name| using extension_name }
88
+ end
89
+
90
+ # Automatically included modules.
91
+ if options[:includes]
92
+ options[:includes].each{|module_name| self.extend module_name }
93
+ end
65
94
 
66
95
  instance_eval &block
67
96
 
68
97
  true
69
98
  end
99
+
100
+ # Add script extension and define a method with the same name as
101
+ # the extension.
102
+ def using *extension_names
103
+ extension_names.each do |extension_name|
104
+ if extension = @@__extensions.find{|ext| ext.__name.to_s == extension_name.to_s }
105
+ extension.extension_used self if extension.respond_to? :extension_used
106
+ self.metaclass.send :define_method, :"#{extension_name}" do
107
+ return extension
108
+ end
109
+ else
110
+ raise ExtensionNotFoundError, "Extension not found: #{extension_name}"
111
+ end
112
+ end
113
+ end
70
114
 
71
115
  # Unload the script and save the cache, if present.
72
116
  def unload!
@@ -92,15 +136,5 @@ module Blur
92
136
  def inspect
93
137
  File.basename @__path
94
138
  end
95
-
96
- private
97
-
98
- # Attempt to evaluate the contents of the script.
99
- def evaluate
100
- instance_eval File.read(@__path), File.basename(@__path), 0
101
- @__evaluated = true
102
- rescue Exception => exception
103
- log.error "#{exception.message ^ :bold} on line #{exception.line.to_s ^ :bold}"
104
- end
105
139
  end
106
140
  end
@@ -1,7 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Blur
4
- class Script < Module
4
+ class Script
5
5
  # The +Cache+ class enables data storing inside Blur and it scripts.
6
6
  #
7
7
  # What it does is simply store a hash, and act like it is that hash.
@@ -0,0 +1,77 @@
1
+ # encoding: utf-8
2
+
3
+ module Blur
4
+ class Script
5
+ # The +Commands+ module is a module that gives the ability to turn a
6
+ # script into a DSL-like framework.
7
+ module Commands
8
+ class Command
9
+ DefaultOptions = { prefix: '.', hostmask: nil }
10
+
11
+ def initialize triggers, options = {}, &block
12
+ @triggers = Array === triggers ? triggers : [triggers]
13
+ @options = DefaultOptions.merge options
14
+ @block = block
15
+ end
16
+
17
+ # Called by the Commands module.
18
+ #
19
+ # Calls the command block if the trigger matches the criteria.
20
+ def received_message user, channel, message
21
+ prefix = @options[:prefix]
22
+
23
+ # Return if the prefix don't match.
24
+ return unless message.start_with? prefix
25
+
26
+ # Return if the hostmask don't match.
27
+ # FIXME: Maybe use globbing instead of regular expressions?
28
+ unless @options[:hostmask].nil?
29
+ hostmask = "#{user.nick}!#{user.name}@#{user.host}"
30
+
31
+ return unless hostmask =~ @options[:hostmask]
32
+ end
33
+
34
+ command, args = split_message message
35
+
36
+ # Strip the prefix and compare the trigger name.
37
+ if self.matches_trigger? command
38
+ @block.call user, channel, args
39
+ end
40
+ end
41
+
42
+ protected
43
+
44
+ def split_message message
45
+ prefix = @options[:prefix]
46
+ command, args = message[prefix.length..-1].split $;, 2
47
+
48
+ return command, args
49
+ end
50
+
51
+ def matches_trigger? command
52
+ return @triggers.find{|trigger| trigger.to_s == command }
53
+ end
54
+ end
55
+
56
+ # Extend +base+ with self.
57
+ def self.extended base
58
+ base.instance_variable_set :@__commands, []
59
+ end
60
+
61
+ # Add a new command handler and trigger.
62
+ def command name, *args, &block
63
+ @__commands << Command.new(name, *args, &block)
64
+ end
65
+
66
+ # Handle all calls to the scripts +message+ method, check to see if
67
+ # the message containts a valid command, serialize it and pass it to
68
+ # the script as command_name with the parameters +user+, +channel+
69
+ # and +message+.
70
+ def message user, channel, line
71
+ @__commands.each do |command|
72
+ command.received_message user, channel, line
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -1,72 +1,52 @@
1
- # encoding: utf-8
2
-
3
- module Blur
4
- class Script < Module
5
- # The +DSL+ module is a module that gives the ability to turn a
6
- # script into a DSL-like framework.
7
- #
8
- # What it does is automatically test to see if a message starts with a
9
- # trigger, and then, if so, it sends the command-part of the message to
10
- # the script object itself.
11
- #
12
- # This way, the plugin-writer doesn't need to have repetetive code like
13
- # that in every script.
14
- #
15
- # @example
16
- # Script :example do
17
- # include Fantasy
18
- #
19
- # command :test do |user, channel, message|
20
- # channel.say "I hear you."
21
- # end
22
- # end
23
- #
24
- # # And if a user were to send the message ".test my method", it would
25
- # # trigger the test block with the following arguments
26
- # #
27
- # # user => #<Blur::Network::User … >
28
- # # channel => #<Blur::Network::Channel … >
29
- # # message => ".test my method"
30
- module DSL
31
- # The prefix that turns it into a possible command.
32
- MessageTrigger = "."
33
-
34
- # Extend +klass+ with self.
35
- def self.included klass
36
- klass.extend self
37
- end
38
-
39
- # Called when a script has been loaded, for use in modules extending
40
- # the script.
41
- def module_init
42
- @__dsl_triggers = {}
43
- end
44
-
45
- def command name, *args, &block
46
- (@__dsl_triggers ||= {})[name] = block
47
- end
48
-
49
- # Handle all calls to the scripts +message+ method, check to see if
50
- # the message containts a valid command, serialize it and pass it to
51
- # the script as command_name with the parameters +user+, +channel+
52
- # and +message+.
53
- def message user, channel, line
54
- return unless line.start_with? MessageTrigger
55
-
56
- command, args = line.split $;, 2
57
- trigger = strip command
58
-
59
- if handler = @__dsl_triggers[trigger]
60
- handler.(user, channel, args)
61
- end
62
- end
63
-
64
- protected
65
-
66
- # Strip all non-word characters from the input command.
67
- def strip name
68
- name.gsub /\W/, '' if name
69
- end
70
- end
71
- end
72
- end
1
+ # encoding: utf-8
2
+
3
+ module Blur
4
+ class Script
5
+ module DSL
6
+ def self.included base
7
+ base.send :attr_accessor, :__name
8
+ base.send :attr_accessor, :__author
9
+ base.send :attr_accessor, :__version
10
+ base.send :attr_accessor, :__description
11
+ end
12
+
13
+ # Set the author.
14
+ #
15
+ # @example
16
+ # Author "John Doe <john.doe@gmail.com>"
17
+ def Author *authors
18
+ @__author = authors.join ', '
19
+ end
20
+
21
+ # Set the description.
22
+ #
23
+ # @example
24
+ # Description "This is an example script."
25
+ def Description description
26
+ @__description = description
27
+ end
28
+
29
+ # Set the version.
30
+ #
31
+ # @example
32
+ # Version "1.0"
33
+ def Version version
34
+ @__version = version
35
+ end
36
+
37
+ # @return the name of the script.
38
+ def name; @__name end
39
+
40
+ # @return the name of the author(s).
41
+ def author; @__author end
42
+
43
+ # @return the script version.
44
+ def version; @__version end
45
+
46
+ # @return the description.
47
+ def description; @__description end
48
+
49
+ alias_method :Authors, :Author
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+
3
+ module Blur
4
+ # The current version of Blur.
5
+ Version = "1.8.5"
6
+
7
+ # Get the current version.
8
+ #
9
+ # @return [String] The current version of Blur.
10
+ def self.version
11
+ Version
12
+ end
13
+ end
metadata CHANGED
@@ -1,41 +1,41 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blur
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.3
4
+ version: 1.8.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikkel Kroman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-08-02 00:00:00.000000000 Z
11
+ date: 2016-01-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: majic
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '>='
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
19
  version: '0.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - '>='
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: eventmachine
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - '>'
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0.12'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - '>'
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.12'
41
41
  description:
@@ -44,26 +44,32 @@ executables: []
44
44
  extensions: []
45
45
  extra_rdoc_files: []
46
46
  files:
47
+ - ".yardopts"
48
+ - LICENSE
49
+ - README.md
50
+ - library/blur.rb
47
51
  - library/blur/client.rb
52
+ - library/blur/encryption.rb
48
53
  - library/blur/encryption/base64.rb
49
54
  - library/blur/encryption/fish.rb
50
- - library/blur/encryption.rb
51
55
  - library/blur/enhancements.rb
56
+ - library/blur/evaluable.rb
57
+ - library/blur/extension.rb
52
58
  - library/blur/handling.rb
59
+ - library/blur/network.rb
53
60
  - library/blur/network/channel.rb
54
61
  - library/blur/network/command.rb
55
62
  - library/blur/network/connection.rb
63
+ - library/blur/network/isupport.rb
56
64
  - library/blur/network/user.rb
57
- - library/blur/network.rb
65
+ - library/blur/script.rb
58
66
  - library/blur/script/cache.rb
67
+ - library/blur/script/commands.rb
59
68
  - library/blur/script/dsl.rb
60
- - library/blur/script/fantasy.rb
61
- - library/blur/script/messageparsing.rb
62
- - library/blur/script.rb
63
- - library/blur.rb
69
+ - library/blur/version.rb
64
70
  homepage: https://github.com/mkroman/blur
65
71
  licenses:
66
- - Internet Systems Consortium (ISC)
72
+ - MIT
67
73
  metadata: {}
68
74
  post_install_message:
69
75
  rdoc_options: []
@@ -71,19 +77,18 @@ require_paths:
71
77
  - library
72
78
  required_ruby_version: !ruby/object:Gem::Requirement
73
79
  requirements:
74
- - - '>='
80
+ - - ">="
75
81
  - !ruby/object:Gem::Version
76
82
  version: 1.9.1
77
83
  required_rubygems_version: !ruby/object:Gem::Requirement
78
84
  requirements:
79
- - - '>='
85
+ - - ">="
80
86
  - !ruby/object:Gem::Version
81
87
  version: '0'
82
88
  requirements: []
83
89
  rubyforge_project:
84
- rubygems_version: 2.0.0
90
+ rubygems_version: 2.2.0
85
91
  signing_key:
86
92
  specification_version: 4
87
93
  summary: An event-driven IRC-framework for Ruby.
88
94
  test_files: []
89
- has_rdoc:
@@ -1,72 +0,0 @@
1
- # encoding: utf-8
2
-
3
- module Blur
4
- class Script < Module
5
- # The +Fantasy+ module is a module that gives the ability to turn a
6
- # script into a DSL-like framework.
7
- #
8
- # What it does is automatically test to see if a message starts with a
9
- # trigger, and then, if so, it sends the command-part of the message to
10
- # the script object itself.
11
- #
12
- # This way, the plugin-writer doesn't need to have repetetive code like
13
- # that in every script.
14
- #
15
- # @example
16
- # Script :example do
17
- # include Fantasy
18
- #
19
- # def command_test user, channel, message
20
- # channel.say "I hear you."
21
- # end
22
- # end
23
- #
24
- # # And if a user were to send the message ".test my method", it would
25
- # # trigger the #command_test method with the following arguments
26
- # #
27
- # # user => #<Blur::Network::User … >
28
- # # channel => #<Blur::Network::Channel … >
29
- # # message => ".test my method"
30
- module Fantasy
31
- # The prefix that turns it into a possible command.
32
- MessageTrigger = "."
33
-
34
- # Extend +klass+ with self.
35
- def self.included klass
36
- klass.extend self
37
- end
38
-
39
- # Called when a script has been loaded, for use in modules extending
40
- # the script.
41
- def module_init
42
- @__fantasy_triggers = []
43
-
44
- public_methods.grep(/^command_/).each do |trigger|
45
- @__fantasy_triggers << trigger.to_s[8..-1]
46
- end
47
- end
48
-
49
- # Handle all calls to the scripts +message+ method, check to see if
50
- # the message containts a valid command, serialize it and pass it to
51
- # the script as command_name with the parameters +user+, +channel+
52
- # and +message+.
53
- def message user, channel, line
54
- return unless line.start_with? MessageTrigger
55
-
56
- command, args = line.split $;, 2
57
- trigger = serialize command
58
-
59
- if @__fantasy_triggers.include? trigger
60
- __send__ :"command_#{trigger}", user, channel, args
61
- end
62
- end
63
-
64
- protected
65
-
66
- # Strip all non-word characters from the input command.
67
- def serialize name
68
- name.gsub /\W/, '' if name
69
- end
70
- end
71
- end
72
- end
@@ -1,61 +0,0 @@
1
- # encoding: utf-8
2
-
3
- module Blur
4
- class Script < Module
5
- # The +MessageParsing+ module is a module that gives the ability to turn a
6
- # script into a DSL-like framework.
7
- #
8
- # What it does is automatically test to see if a message starts with a
9
- # trigger, and then, if so, it sends the command-part of the message to
10
- # the script object itself.
11
- #
12
- # This way, the plugin-writer doesn't need to have repetetive code like
13
- # that in every script.
14
- #
15
- # @example
16
- # Script :example do
17
- # extend MessageParsing
18
- #
19
- # def command_test user, channel, message
20
- # channel.say "I hear you."
21
- # end
22
- # end
23
- #
24
- # # And if a user were to send the message ".test my method", it would
25
- # # trigger the #command_test method with the following arguments
26
- # #
27
- # # user => #<Blur::Network::User … >
28
- # # channel => #<Blur::Network::Channel … >
29
- # # message => ".test my method"
30
- module MessageParsing
31
- # The prefix that turns it into a possible command.
32
- MessageTrigger = "."
33
-
34
- def self.extended klass
35
- warn "#{File.realpath __FILE__}: MessageParsing will be deprecated in the future, use Fantasy."
36
- end
37
-
38
- # Handle all calls to the scripts +message+ method, check to see if
39
- # the message containts a valid command, serialize it and pass it to
40
- # the script as command_name with the parameters +user+, +channel+
41
- # and +message+.
42
- def message user, channel, line
43
- return unless line.start_with? MessageTrigger
44
-
45
- command, args = line.split $;, 2
46
- name = :"command_#{serialize command}"
47
-
48
- if respond_to? name
49
- __send__ name, user, channel, args
50
- end
51
- end
52
-
53
- protected
54
-
55
- # Strip all non-word characters from the input command.
56
- def serialize name
57
- name.gsub /\W/, '' if name
58
- end
59
- end
60
- end
61
- end