blur 1.5.3 → 1.6

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