blur 1.5.3 → 1.6

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.
@@ -5,15 +5,37 @@ require 'majic'
5
5
  require 'socket'
6
6
  require 'ostruct'
7
7
  require 'openssl'
8
+ require 'eventmachine'
8
9
 
9
- Dir.glob("#{File.dirname __FILE__}/blur/**/*.rb").each &method(:require)
10
+ # Require all library files.
11
+ require 'blur/client'
12
+ require 'blur/script'
13
+ require 'blur/network'
14
+ require 'blur/encryption'
15
+ require 'blur/enhancements'
16
+ require 'blur/script/cache'
17
+ require 'blur/network/user'
18
+ require 'blur/network/channel'
19
+ require 'blur/network/command'
20
+ require 'blur/network/connection'
21
+ require 'blur/script/messageparsing'
10
22
 
23
+ # Blur is a very modular IRC-framework for ruby.
24
+ #
25
+ # It allows the developer to extend it in multiple ways.
26
+ # It can be by handlers, scripts, communications, and what have you.
11
27
  module Blur
12
- class << Version = [1,5,3]
13
- def to_s; join '.' end
14
- end
28
+ # The major and minor version-values of Blur.
29
+ Version = "1.6"
15
30
 
16
- def self.connect options, &block
31
+ # Instantiates a client with given options and then makes the client instance
32
+ # evaluate the given block to form a DSL.
33
+ #
34
+ # @note The idea is that this should never stop or return anything.
35
+ # @param [Hash] options the options for the client.
36
+ # @option options [Array] networks list of hashes that contain network
37
+ # options.
38
+ def self.connect options = {}, &block
17
39
  Client.new(options).tap do |client|
18
40
  client.instance_eval &block
19
41
  end.connect
@@ -3,35 +3,59 @@
3
3
  require 'blur/handling'
4
4
 
5
5
  module Blur
6
+ # The +Client+ class is the controller of the low-level access.
7
+ #
8
+ # It stores networks, scripts and callbacks, and is also encharge of
9
+ # distributing the incoming commands to the right networks and scripts.
6
10
  class Client
7
11
  include Handling
8
12
 
9
- attr_accessor :options, :scripts, :networks
13
+ # @return [Array] the options that is passed upon initialization.
14
+ attr_accessor :options
15
+ # @return [Array] a list of scripts that is loaded during runtime.
16
+ attr_accessor :scripts
17
+ # @return [Array] a list of instantiated networks.
18
+ attr_accessor :networks
10
19
 
20
+ # Instantiates the client, stores the options, instantiates the networks
21
+ # and then loads available scripts.
22
+ #
23
+ # @param [Hash] options the options for the client.
24
+ # @option options [Array] networks list of hashes that contain network
25
+ # options.
11
26
  def initialize options
12
27
  @options = options
13
28
  @scripts = []
14
29
  @networks = []
15
30
  @callbacks = {}
16
- @connected = true
17
31
 
18
- @networks = @options[:networks].map { |options| Network.new options }
32
+ @networks = @options[:networks].map {|options| Network.new options }
19
33
 
20
34
  load_scripts
21
35
  trap 2, &method(:quit)
22
36
  end
23
37
 
38
+ # Connect to each network available that is not already connected, then
39
+ # proceed to start the run-loop.
24
40
  def connect
25
- networks = @networks.select { |network| not network.connected? }
41
+ networks = @networks.select {|network| not network.connected? }
26
42
 
27
- networks.each do |network|
28
- network.delegate = self
29
- network.connect
43
+ EventMachine.run do
44
+ networks.each do |network|
45
+ network.delegate = self
46
+ network.connect
47
+ end
48
+
49
+ EventMachine.error_handler{|e| p e }
30
50
  end
31
-
32
- run_loop
33
51
  end
34
52
 
53
+ # Is called when a command have been received and parsed, this distributes
54
+ # the command to the loader, which then further distributes it to events
55
+ # and scripts.
56
+ #
57
+ # @param [Network] network the network that received the command.
58
+ # @param [Network::Command] command the received command.
35
59
  def got_command network, command
36
60
  puts "<- #{network.inspect ^ :bold} | #{command}"
37
61
  name = :"got_#{command.name.downcase}"
@@ -41,6 +65,7 @@ module Blur
41
65
  end
42
66
  end
43
67
 
68
+ # Searches for scripts in working_directory/scripts and then loads them.
44
69
  def load_scripts
45
70
  script_path = File.dirname $0
46
71
 
@@ -52,50 +77,38 @@ module Blur
52
77
  end
53
78
  end
54
79
 
80
+ # Unload all scripts gracefully that have been loaded into the client.
81
+ #
82
+ # @see Script#unload!
55
83
  def unload_scripts
56
84
  @scripts.each do |script|
57
85
  script.unload!
58
86
  end.clear
59
87
  end
60
88
 
61
- def quit signal
89
+ # Try to gracefully disconnect from each network, unload all scripts and
90
+ # exit properly.
91
+ #
92
+ # @param [optional, Symbol] signal The signal received by the system, if any.
93
+ def quit signal = :SIGINT
62
94
  @networks.each do |network|
63
95
  network.transmit :QUIT, "Got SIGINT?"
64
- network.transcieve
65
96
  network.disconnect
66
97
  end
67
98
 
68
- @connected = false
69
99
  unload_scripts
70
100
 
71
- exit 0
72
- end
73
-
74
- private
75
-
76
- def run_loop
77
- puts "Starting run loop ..."
78
-
79
- while @connected
80
- @networks.select(&:connected?).each do |network|
81
- begin
82
- network.transcieve
83
- sleep 0.05
84
- rescue StandardError => exception
85
- if network.connected?
86
- network.disconnect
87
- end
88
-
89
- emit :connection_terminated, network
90
-
91
- puts "#{"Network error" ^ :red} (#{exception.class.name}): #{exception.message}"
92
- end
93
- end
94
- end
95
-
96
- puts "Ended run loop ..."
101
+ EventMachine.stop
97
102
  end
98
103
 
104
+ private
105
+ # Finds all callbacks with name `name` and then calls them.
106
+ # It also sends `name` to {Script} if the script responds to `name`, to all
107
+ # available scripts.
108
+ #
109
+ # @param [Symbol] name the corresponding event-handlers name.
110
+ # @param [...] args Arguments that is passed to the event-handler.
111
+ # @private
99
112
  def emit name, *args
100
113
  @callbacks[name].each do |callback|
101
114
  callback.call *args
@@ -110,6 +123,11 @@ module Blur
110
123
  end
111
124
  end
112
125
 
126
+ # Stores the block as an event-handler with name `name`.
127
+ #
128
+ # @param [Symbol] name the corresponding event-handlers name.
129
+ # @param [Block] block the event-handlers block that serves as a trigger.
130
+ # @private
113
131
  def catch name, &block
114
132
  (@callbacks[name] ||= []) << block
115
133
  end
@@ -0,0 +1,17 @@
1
+ # encoding: utf-8
2
+
3
+ module Blur
4
+ # The +Encryption+ module extends the communication-functionality of
5
+ # the client. It is intended to enable helpers but also core functionality
6
+ # that encrypts the incoming and outgoing data of Blur.
7
+ #
8
+ # Encryption modules are not loaded into the VM until it's required.
9
+ module Encryption
10
+ # Indicates that user-input (a channel user message, e.g.) is in invalid
11
+ # format to a certain encryption algorithm.
12
+ class BadInputError < StandardError; end
13
+
14
+ autoload :FiSH, "blur/encryption/fish"
15
+ autoload :Base64, "blur/encryption/base64"
16
+ end
17
+ end
@@ -0,0 +1,71 @@
1
+ # encoding: utf-8
2
+
3
+ module Blur
4
+ module Encryption
5
+ # The +Encryption::Base64+ module differs from the original Base64
6
+ # implementation. I'm not sure how exactly, perhaps the charset?
7
+ #
8
+ # I originally found the Ruby implementation of FiSH on a website where
9
+ # it was graciously submitted by an anonymous user, since then I've
10
+ # implemented it in a weechat script, and now I've refactored it for use in
11
+ # Blur.
12
+ #
13
+ # @see http://maero.dk/pub/sources/weechat/ruby/autoload/fish.rb
14
+ module Base64
15
+ # The difference I suspect between the original Base64 implementation
16
+ # and the one used in FiSH.
17
+ Charset = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
18
+
19
+ # Decode a base64-encoded string.
20
+ #
21
+ # @return [String] Base64-decoded string.
22
+ def self.decode string
23
+ unless string.length % 12 == 0
24
+ raise BadInputError, "input has to be a multiple of 12 characters."
25
+ end
26
+
27
+ String.new.tap do |buffer|
28
+ j = -1
29
+
30
+ while j < string.length - 1
31
+ right, left = 0, 0
32
+
33
+ 6.times{|i| right |= Charset.index(string[j += 1]) << (i * 6) }
34
+ 6.times{|i| left |= Charset.index(string[j += 1]) << (i * 6) }
35
+
36
+ 4.times do |i|
37
+ buffer << ((left & (0xFF << ((3 - i) * 8))) >> ((3 - i) * 8)).chr
38
+ end
39
+
40
+ 4.times do |i|
41
+ buffer << ((right & (0xFF << ((3 - i) * 8))) >> ((3 - i) * 8)).chr
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ # Encode a string-cipher.
48
+ #
49
+ # @return [String] Base64-encoded string.
50
+ def self.encode string
51
+ unless string.length % 8 == 0
52
+ raise BadInputError, "input has to be a multiple of 8 characters."
53
+ end
54
+
55
+ left = 0
56
+ right = 0
57
+ decimals = [24, 16, 8, 0]
58
+
59
+ String.new.tap do |buffer|
60
+ string.each_block do |block|
61
+ 4.times{|i| left += (block[i].ord << decimals[i]) }
62
+ 4.times{|i| right += (block[i+4].ord << decimals[i]) }
63
+
64
+ 6.times{|i| buffer << Charset[right & 0x3F].chr; right = right >> 6 }
65
+ 6.times{|i| buffer << Charset[left & 0x3F].chr; left = left >> 6 }
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,80 @@
1
+ # encoding: utf-8
2
+
3
+ require 'crypt/blowfish'
4
+
5
+ module Blur
6
+ module Encryption
7
+ # The +FiSH+ algorithm is a combination of Base64 encoding
8
+ # and the blowfish encryption.
9
+ #
10
+ # Shared text messages are prepended by "++OK+", an older implementation
11
+ # prepends it with "+mcps+" - Blur drops support for that implementation.
12
+ #
13
+ # There's multiple client-implementations available on the official FiSH
14
+ # homepage.
15
+ #
16
+ # == DH1080 Key exchange
17
+ # The newer FiSH implementation introduces a 1080bit Diffie-Hellman
18
+ # key-exchange mechanism.
19
+ #
20
+ # Blur does currently not support key exchanges.
21
+ class FiSH
22
+ # The standard FiSH block-size.
23
+ BlockSize = 8
24
+
25
+ # @return [String] the blowfish salt-key.
26
+ attr_accessor :keyphrase
27
+
28
+ # Change the keyphrase and instantiate a new blowfish object.
29
+ def keyphrase= keyphrase
30
+ @keyphrase = keyphrase
31
+ @blowfish = Crypt::Blowfish.new @keyphrase
32
+ end
33
+
34
+ # Instantiate a new fish-encryption object.
35
+ def initialize keyphrase
36
+ @keyphrase = keyphrase
37
+ @blowfish = Crypt::Blowfish.new keyphrase
38
+ end
39
+
40
+ # Encrypt an input string using the keyphrase stored in the +@blowfish+
41
+ # object.
42
+ #
43
+ # @return [String] the encrypted string.
44
+ def encrypt string
45
+ String.new.tap do |buffer|
46
+ nullpad(string).each_block do |block|
47
+ chunk = @blowfish.encrypt_block block
48
+ buffer.concat Base64.encode chunk
49
+ end
50
+ end
51
+ end
52
+
53
+ # Decrypt an input string using the keyphrase stored in the +@blowfish+
54
+ # object.
55
+ #
56
+ # @return [String] the decrypted string.
57
+ def decrypt string
58
+ unless string.length % 12 == 0
59
+ raise BadInputError, "input has to be a multiple of 12 characters."
60
+ end
61
+
62
+ String.new.tap do |buffer|
63
+ string.each_block 12 do |block|
64
+ chunk = @blowfish.decrypt_block Base64.decode block
65
+ buffer.concat chunk
66
+ end
67
+ end.rstrip
68
+ end
69
+
70
+ private
71
+ # Fill up the last block with null-bytes until it's a multiple of 8.
72
+ #
73
+ # @return [String] the nullpadded string.
74
+ def nullpad string
75
+ length = string.length + BlockSize - string.length % BlockSize
76
+ string.ljust length, ?\0
77
+ end
78
+ end
79
+ end
80
+ end
@@ -1,17 +1,39 @@
1
1
  # encoding: utf-8
2
2
 
3
+ # Reopens the scope of the standard Exception-class to extend it with helpful
4
+ # methods.
3
5
  class Exception
6
+ # The pattern to match against the backtrace log.
4
7
  Pattern = /^.*?:(\d+):/
5
8
 
9
+ # Retrieve the line on which the exception was raised from when raised inside
10
+ # a script.
11
+ #
12
+ # @return Fixnum the line of the script the exception was raised on.
6
13
  def line
7
14
  backtrace[0].match(Pattern)[1].to_i + 1
8
15
  end
9
16
  end
10
17
 
18
+ # Reopens the scope of the standard String-class to extend it with helpful
19
+ # methods.
11
20
  class String
21
+ # Checks if the string contains nothing but a numeric value.
22
+ #
23
+ # @return true if it is a numeric value.
12
24
  def numeric?
13
25
  self =~ /^\d+$/
14
26
  end
15
27
 
28
+ # Split a string up in n chunks and then iterate through them, exactly like
29
+ # Enumerable#each_slice.
30
+ #
31
+ # @return [Enumerator] list of slices.
32
+ # @yieldreturn [Array] list of elements in each slice consecutively.
33
+ def each_slice size = 8
34
+ self.chars.each_slice(size).each{|slice| yield slice.join }
35
+ end
36
+
16
37
  alias_method :starts_with?, :start_with?
38
+ alias_method :each_block, :each_slice
17
39
  end
@@ -2,11 +2,41 @@
2
2
 
3
3
  module Blur
4
4
  class Client
5
+ # The +Handling+ module is the very core of the IRC-part in Blur.
6
+ #
7
+ # When the client receives a parsed command instance, it immediately starts
8
+ # looking for a got_(the command name) method inside the client, which
9
+ # is implemented in this module.
10
+ #
11
+ # == Implementing a handler
12
+ # Implementing a handler is very, very easy.
13
+ #
14
+ # All you need to do is define a method named got_(command you want to
15
+ # implement) that accepts 2 parameters, +network+ and +command+.
16
+ #
17
+ # You can then do whatever you need to do with the command instance,
18
+ # you can access the parameters of it through {Network::Command#[]}.
19
+ #
20
+ # Don't forget that this module is inside the clients scope, so you can
21
+ # access all instance-variables and methods.
22
+ #
23
+ # @example
24
+ # # RPL_WHOISUSER
25
+ # # <nick> <user> <host> * :<real name>
26
+ # def got_whois_user network, command
27
+ # puts "nick: #{command[0]} user: #{command[1]} host: #{command[2]} …"
28
+ # end
29
+ #
30
+ # @see http://www.irchelp.org/irchelp/rfc/chapter6.html
5
31
  module Handling
6
-
7
32
  protected
8
33
 
9
- # End of MOTD
34
+ # Called when the MOTD was received, which also means it is ready.
35
+ #
36
+ # == Callbacks:
37
+ # Emits +:connection_ready+ with the parameter +network+.
38
+ #
39
+ # Automatically joins the channels specified in +:channels+.
10
40
  def got_end_of_motd network, command
11
41
  emit :connection_ready, network
12
42
 
@@ -15,8 +45,8 @@ module Blur
15
45
  end
16
46
  end
17
47
 
18
- # The /NAMES list
19
- def got_353 network, command
48
+ # Called when the namelist of a channel was received.
49
+ def got_name_reply network, command
20
50
  name = command[2]
21
51
  users = command[3].split.map &Network::User.method(:new)
22
52
 
@@ -28,30 +58,46 @@ module Blur
28
58
  channel.users << user
29
59
  end
30
60
  else
31
- network.channels.<< Network::Channel.new name, network, users
61
+ channel = Network::Channel.new name, network, users
62
+
63
+ if network.fish? and network.options[:fish].key? name
64
+ keyphrase = network.options[:fish][name]
65
+ channel.encryption = Encryption::FiSH.new keyphrase
66
+ end
67
+
68
+ network.channels << channel
32
69
  end
33
70
  end
34
71
 
35
- # A channels topic
36
- def got_332 network, command
72
+ # Called when a channel topic was changed.
73
+ #
74
+ # == Callbacks:
75
+ # Emits :topic_change with the parameters +channel+ and +topic+.
76
+ def got_channel_topic network, command
37
77
  me, name, topic = command.params
38
78
 
39
79
  if channel = network.channel_by_name(name)
80
+ emit :topic_change, channel, topic
40
81
  channel.topic = topic
41
82
  else
42
83
  channel = Network::Channel.new name, network, []
84
+ emit :topic_change, channel, topic
43
85
  channel.topic = topic
44
86
 
45
87
  network.channels << channel
46
88
  end
47
89
  end
48
90
 
49
- # Are we still breathing?
91
+ # Called when the server needs to verify that we're alive.
50
92
  def got_ping network, command
51
93
  network.transmit :PONG, command[0]
52
94
  end
53
95
 
54
- # Someone changed their nickname
96
+ # Called when a user changed nickname.
97
+ #
98
+ # == Callbacks:
99
+ # Emits :user_rename with the parameters +channel+, +user+ and +nickname+
100
+ # where +nickname+ is the new name.
55
101
  def got_nick network, command
56
102
  nick = command.sender.nickname
57
103
 
@@ -65,7 +111,15 @@ module Blur
65
111
  end
66
112
  end
67
113
 
68
- # Someone send a message
114
+ # Called when a message was received (both channel and private messages).
115
+ #
116
+ # == Callbacks:
117
+ # === When it's a channel message:
118
+ # Emits +:message+ with the parameters +user+, +channel+ and +message+.
119
+ # === When it's a private message:
120
+ # Emits +:private_message+ with the parameters +user+ and +message+.
121
+ #
122
+ # @note Messages are contained as strings.
69
123
  def got_privmsg network, command
70
124
  return if command.sender.is_a? String # Ignore all server privmsgs
71
125
  name, message = command.params
@@ -74,6 +128,18 @@ module Blur
74
128
  if user = channel.user_by_nick(command.sender.nickname)
75
129
  user.name = command.sender.username
76
130
  user.host = command.sender.hostname
131
+
132
+ begin
133
+ if message[0..3] == "+OK " and channel.encrypted?
134
+ message = channel.encryption.decrypt message[4..-1]
135
+ end
136
+ rescue Encryption::BadInputError
137
+ # …
138
+ rescue => exception
139
+ puts "-!- There was a problem with the FiSH encryption, disabling"
140
+
141
+ channel.encryption = nil
142
+ end
77
143
 
78
144
  emit :message, user, channel, message
79
145
  else
@@ -89,7 +155,10 @@ module Blur
89
155
  end
90
156
  end
91
157
 
92
- # Someone joined a channel
158
+ # Called when a user joined a channel.
159
+ #
160
+ # == Callbacks:
161
+ # Emits +:user_entered+ with the parameters +channel+ and +user+.
93
162
  def got_join network, command
94
163
  name = command[0]
95
164
  user = Network::User.new command.sender.nickname
@@ -106,7 +175,10 @@ module Blur
106
175
  end
107
176
  end
108
177
 
109
- # Someone left a channel
178
+ # Called when a user left a channel.
179
+ #
180
+ # == Callbacks:
181
+ # Emits +:user_left+ with the parameters +channel+ and +user+.
110
182
  def got_part network, command
111
183
  name = command[0]
112
184
 
@@ -119,7 +191,10 @@ module Blur
119
191
  end
120
192
  end
121
193
 
122
- # Someone quit irc
194
+ # Called when a user disconnected from a network.
195
+ #
196
+ # == Callbacks:
197
+ # Emits +:user_quit+ with the parameters +channel+ and +user+.
123
198
  def got_quit network, command
124
199
  nick = command.sender.nickname
125
200
 
@@ -134,7 +209,13 @@ module Blur
134
209
  end
135
210
  end
136
211
 
137
- # Someone got kicked
212
+ # Called when a user was kicked from a channel.
213
+ #
214
+ # == Callbacks:
215
+ # Emits +:user_kicked+ with the parameters +kicker+, +channel+, +kickee+
216
+ # and +reason+.
217
+ #
218
+ # +kicker+ is the user that kicked +kickee+.
138
219
  def got_kick network, command
139
220
  name, target, reason = command.params
140
221
 
@@ -149,6 +230,10 @@ module Blur
149
230
  end
150
231
  end
151
232
 
233
+ # Called when a topic was changed for a channel.
234
+ #
235
+ # == Callbacks:
236
+ # Emits :topic with the parameters +user+, +channel+ and +topic+.
152
237
  def got_topic network, command
153
238
  name, topic = command.params
154
239
 
@@ -161,6 +246,13 @@ module Blur
161
246
  end
162
247
  end
163
248
 
249
+ # Called when a channel or a users flags was altered.
250
+ #
251
+ # == Callbacks:
252
+ # === When it's channel modes:
253
+ # Emits +:channel_mode+ with the parameters +channel+ and +modes+.
254
+ # === When it's user modes:
255
+ # Emits +:user_mode+ with the parameters +user+ and +modes+.
164
256
  def got_mode network, command
165
257
  name, modes, limit, nick, mask = command.params
166
258
 
@@ -181,8 +273,10 @@ module Blur
181
273
  end
182
274
  end
183
275
 
276
+ alias_method :got_353, :got_name_reply
184
277
  alias_method :got_422, :got_end_of_motd
185
278
  alias_method :got_376, :got_end_of_motd
279
+ alias_method :got_332, :got_channel_topic
186
280
  end
187
281
  end
188
282
  end