blur 1.7.3 → 1.8.5

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