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.
- data/library/blur.rb +27 -5
- data/library/blur/client.rb +56 -38
- data/library/blur/encryption.rb +17 -0
- data/library/blur/encryption/base64.rb +71 -0
- data/library/blur/encryption/fish.rb +80 -0
- data/library/blur/enhancements.rb +22 -0
- data/library/blur/handling.rb +108 -14
- data/library/blur/network.rb +78 -24
- data/library/blur/network/channel.rb +39 -1
- data/library/blur/network/command.rb +34 -1
- data/library/blur/network/connection.rb +53 -57
- data/library/blur/network/user.rb +44 -10
- data/library/blur/script.rb +35 -1
- data/library/blur/script/cache.rb +26 -1
- data/library/blur/script/messageparsing.rb +32 -1
- metadata +64 -14
data/library/blur/network.rb
CHANGED
@@ -1,83 +1,137 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
module Blur
|
4
|
+
# The +Network+ module is to be percieved as an IRC network.
|
5
|
+
#
|
6
|
+
# Although the connection is a part of the network module, it is mainly used
|
7
|
+
# for network-related structures, such as {User}, {Channel} and {Command}.
|
4
8
|
class Network
|
9
|
+
# +ConnectionError+ should only be triggered from within {Connection}.
|
5
10
|
class ConnectionError < StandardError; end
|
6
11
|
|
7
|
-
|
12
|
+
# @return [Hash] the network options.
|
13
|
+
attr_accessor :options
|
14
|
+
# @return [Array] the list of channels the client is in.
|
15
|
+
attr_accessor :channels
|
16
|
+
# @return [Array] the list of private messages the client remembers.
|
17
|
+
attr_accessor :dialogues
|
18
|
+
# @return [Client] the client delegate.
|
19
|
+
attr_accessor :delegate
|
20
|
+
# @return [Network::Connection] the connection instance.
|
21
|
+
attr_accessor :connection
|
8
22
|
|
9
|
-
|
23
|
+
# Check whether or not connection is established.
|
24
|
+
def connected?; @connection and @connection.established? end
|
10
25
|
|
11
|
-
|
12
|
-
|
13
|
-
|
26
|
+
# Get the remote hostname.
|
27
|
+
#
|
28
|
+
# @return [String] the remote hostname.
|
29
|
+
def host; @options[:hostname] end
|
14
30
|
|
15
|
-
|
16
|
-
|
17
|
-
|
31
|
+
# Get the remote port.
|
32
|
+
# If no port is specified, it returns 6697 if using a secure connection,
|
33
|
+
# returns 6667 otherwise.
|
34
|
+
#
|
35
|
+
# @return [Fixnum] the remote port
|
36
|
+
def port; @options[:port] ||= secure? ? 6697 : 6667 end
|
18
37
|
|
19
|
-
|
20
|
-
|
21
|
-
end
|
38
|
+
# Check to see if it's a secure connection.
|
39
|
+
def secure?; @options[:secure] == true end
|
22
40
|
|
23
|
-
|
41
|
+
# Check to see if FiSH encryption is enabled.
|
42
|
+
def fish?; not @options[:fish].nil? end
|
43
|
+
|
44
|
+
# Instantiates the network.
|
45
|
+
#
|
46
|
+
# @param [Hash] options the network options.
|
47
|
+
# @option options [String] :hostname the remote hostname.
|
48
|
+
# @option options [String] :nickname the nickname to represent.
|
49
|
+
# @option options [optional, String] :username the username to represent.
|
50
|
+
# @option options [optional, String] :realname the “real name” to represent.
|
51
|
+
# @option options [optional, String] :password the password for the network.
|
52
|
+
# @option options [optional, Fixnum] :port the remote port.
|
53
|
+
# @option options [optional, Boolean] :secure use a secure connection.
|
54
|
+
def initialize options
|
24
55
|
@options = options
|
25
56
|
@channels = []
|
26
57
|
|
27
58
|
unless options[:nickname]
|
28
|
-
raise ArgumentError, "nickname is missing from the
|
59
|
+
raise ArgumentError, "nickname is missing from the networks option block"
|
29
60
|
end
|
30
61
|
|
31
62
|
@options[:username] ||= @options[:nickname]
|
32
63
|
@options[:realname] ||= @options[:username]
|
33
64
|
@options[:channels] ||= []
|
34
|
-
|
35
|
-
@connection = Connection.new self, host, port
|
36
65
|
end
|
37
66
|
|
67
|
+
# Send a message to a recipient.
|
68
|
+
#
|
69
|
+
# @param [String, #to_s] recipient the recipient.
|
70
|
+
# @param [String] message the message.
|
38
71
|
def say recipient, message
|
72
|
+
if recipient.is_a? Channel and recipient.encrypted?
|
73
|
+
message = "+OK #{recipient.encryption.encrypt message}"
|
74
|
+
end
|
75
|
+
|
39
76
|
transmit :PRIVMSG, recipient, message
|
40
77
|
end
|
41
78
|
|
79
|
+
# Called when the network connection has enough data to form a command.
|
42
80
|
def got_command command
|
43
81
|
@delegate.got_command self, command
|
44
82
|
end
|
45
83
|
|
84
|
+
# Find a channel by its name.
|
85
|
+
#
|
86
|
+
# @param [String] name the channel name.
|
87
|
+
# @return [Network::Channel] the matching channel, or nil.
|
46
88
|
def channel_by_name name
|
47
89
|
@channels.find { |channel| channel.name == name }
|
48
90
|
end
|
49
91
|
|
92
|
+
# Find all instances of channels in which there is a user with the nick
|
93
|
+
# +nick+.
|
94
|
+
#
|
95
|
+
# @param [String] nick the nickname.
|
96
|
+
# @return [Array] a list of channels in which the user is located, or nil.
|
50
97
|
def channels_with_user nick
|
51
98
|
@channels.select { |channel| channel.user_by_nick nick }
|
52
99
|
end
|
53
100
|
|
101
|
+
# Attempt to establish a connection and send initial data.
|
102
|
+
#
|
103
|
+
# @see Connection
|
54
104
|
def connect
|
55
|
-
@connection.
|
56
|
-
|
57
|
-
|
105
|
+
@connection = EventMachine.connect host, port, Connection, self
|
106
|
+
end
|
107
|
+
|
108
|
+
# Called when the connection was successfully established.
|
109
|
+
def connected!
|
58
110
|
transmit :PASS, @options[:password] if @options[:password]
|
59
111
|
transmit :NICK, @options[:nickname]
|
60
112
|
transmit :USER, @options[:username], :void, :void, @options[:realname]
|
61
113
|
end
|
62
114
|
|
115
|
+
# Terminate the connection and clear all channels and users.
|
63
116
|
def disconnect
|
64
|
-
@connection.
|
117
|
+
@connection.close_connection_after_writing
|
65
118
|
|
66
119
|
@channels.each { |channel| channel.users.clear }
|
67
120
|
@channels.clear
|
68
121
|
end
|
69
122
|
|
123
|
+
# Transmit a command to the server.
|
124
|
+
#
|
125
|
+
# @param [Symbol, String] name the command name.
|
126
|
+
# @param [...] arguments all the prepended parameters.
|
70
127
|
def transmit name, *arguments
|
71
128
|
command = Command.new name, arguments
|
72
129
|
puts "-> #{inspect ^ :bold} | #{command}"
|
73
130
|
|
74
|
-
@connection.
|
75
|
-
end
|
76
|
-
|
77
|
-
def transcieve
|
78
|
-
@connection.transcieve
|
131
|
+
@connection.send_data "#{command}\r\n"
|
79
132
|
end
|
80
133
|
|
134
|
+
# Convert it to a debug-friendly format.
|
81
135
|
def to_s
|
82
136
|
%{#<#{self.class.name} "#{host}":#{port}>}
|
83
137
|
end
|
@@ -2,9 +2,34 @@
|
|
2
2
|
|
3
3
|
module Blur
|
4
4
|
class Network
|
5
|
+
# The +Channel+ class is used for encapsulating a channel and its properties.
|
6
|
+
#
|
7
|
+
# Users inside the channel is stored in the {#channels} attribute.
|
8
|
+
#
|
9
|
+
# Modes can be set for a channel, but Blur is not
|
10
|
+
# {http://www.irc.org/tech_docs/005.html ISupport}-compliant yet.
|
11
|
+
#
|
12
|
+
# @todo make so that channels *and* users belongs to the network, and not
|
13
|
+
# like now where the user belongs to the channel, resulting in multiple
|
14
|
+
# user instances.
|
5
15
|
class Channel
|
6
|
-
|
16
|
+
# @return [String] the channels name.
|
17
|
+
attr_accessor :name
|
18
|
+
# @return [Array] a list of users in the channel.
|
19
|
+
attr_accessor :users
|
20
|
+
# @return [String] the channels topic.
|
21
|
+
attr_accessor :topic
|
22
|
+
# @return [String] all the modes set on the channel.
|
23
|
+
attr_accessor :modes
|
24
|
+
# @return [Network] a reference to the network.
|
25
|
+
attr_accessor :network
|
26
|
+
# @return [Encryption::Fish] the channel encryption, if any.
|
27
|
+
attr_accessor :encryption
|
7
28
|
|
29
|
+
# Check whether or not this is an encrypted channel.
|
30
|
+
def encrypted?; not @encryption.nil? end
|
31
|
+
|
32
|
+
# Instantiate a user with a nickname, a network and a user list.
|
8
33
|
def initialize name, network = nil, users = []
|
9
34
|
@name = name
|
10
35
|
@users = users
|
@@ -14,6 +39,9 @@ module Blur
|
|
14
39
|
users.each { |user| user.channel = self }
|
15
40
|
end
|
16
41
|
|
42
|
+
# Merge the channels mode corresponding to the leading character (+ or -).
|
43
|
+
#
|
44
|
+
# @param [String] modes the modes to merge with.
|
17
45
|
def merge_modes modes
|
18
46
|
addition = true
|
19
47
|
|
@@ -29,22 +57,32 @@ module Blur
|
|
29
57
|
end
|
30
58
|
end
|
31
59
|
|
60
|
+
# Send a message to the channel.
|
61
|
+
#
|
62
|
+
# @param [String] message the message to send.
|
32
63
|
def say message
|
33
64
|
@network.say self, message
|
34
65
|
end
|
35
66
|
|
67
|
+
# Find a user with +nick+ as its nickname.
|
68
|
+
#
|
69
|
+
# @param [String] nick the nickname to find the user of.
|
36
70
|
def user_by_nick nick
|
37
71
|
@users.find { |user| user.nick == nick }
|
38
72
|
end
|
39
73
|
|
74
|
+
# Convert it to a debug-friendly format.
|
40
75
|
def inspect
|
41
76
|
%{#<#{self.class.name} @name=#{@name.inspect} @users=#{@users.inspect}}
|
42
77
|
end
|
43
78
|
|
79
|
+
# Called when YAML attempts to save the object, which happens when a
|
80
|
+
# scripts cache contains this user and the script is unloaded.
|
44
81
|
def to_yaml options = {}
|
45
82
|
@name.to_yaml options
|
46
83
|
end
|
47
84
|
|
85
|
+
# Get the channels name.
|
48
86
|
def to_s
|
49
87
|
@name
|
50
88
|
end
|
@@ -2,11 +2,30 @@
|
|
2
2
|
|
3
3
|
module Blur
|
4
4
|
class Network
|
5
|
+
# The +Command+ class is used for encapsulating the command-lines.
|
6
|
+
#
|
7
|
+
# Blur is using regular-expression for parsing, this is to be replaced
|
8
|
+
# with a more native way of parsing, making it much, much easier on the
|
9
|
+
# processor.
|
5
10
|
class Command
|
6
|
-
|
11
|
+
# @return [Symbol, Fixnum] the command name.
|
12
|
+
# @example
|
13
|
+
# 332 or :quit
|
14
|
+
attr_accessor :name
|
15
|
+
# @return [Array] a list of parameters.
|
16
|
+
attr_accessor :params
|
17
|
+
# @return [String] a hostname or a hostmask.
|
18
|
+
attr_accessor :prefix
|
7
19
|
|
20
|
+
# The arbitrary regex pattern.
|
8
21
|
Pattern = /^(?:[:@]([^\s]+) )?([^\s]+)(?: ((?:[^:\s][^\s]* ?)*))?(?: ?:(.*))?$/
|
9
22
|
|
23
|
+
# Parse a line and encapsulate it as a Command.
|
24
|
+
#
|
25
|
+
# @return [Command] the parsed command.
|
26
|
+
# @example
|
27
|
+
# Command.parse "ChanServ!ChanServ@services.uplink.io MODE #uplink +v mk"
|
28
|
+
# # => #<Blur::Network::Command … >
|
10
29
|
def self.parse data
|
11
30
|
match = data.strip.match Pattern
|
12
31
|
prefix, name, args, extra = match.captures
|
@@ -17,12 +36,23 @@ module Blur
|
|
17
36
|
end
|
18
37
|
end
|
19
38
|
|
39
|
+
# Get a parameter by its +index+.
|
20
40
|
def [] index; @params[index] end
|
21
41
|
|
42
|
+
# Instantiate a command.
|
43
|
+
#
|
44
|
+
# @see Command.parse
|
22
45
|
def initialize name, params = []
|
23
46
|
@name, @params = name, params
|
24
47
|
end
|
25
48
|
|
49
|
+
# Get the sender of the command.
|
50
|
+
#
|
51
|
+
# @note the return value is a string if it's a hostname, and an openstruct
|
52
|
+
# with the attributes #nickname, #username and #hostname if it's a
|
53
|
+
# hostmask.
|
54
|
+
#
|
55
|
+
# @return [String, OpenStruct] the sender.
|
26
56
|
def sender
|
27
57
|
return @sender if @sender
|
28
58
|
|
@@ -33,6 +63,9 @@ module Blur
|
|
33
63
|
end
|
34
64
|
end
|
35
65
|
|
66
|
+
# Convert it to an IRC-compliant line.
|
67
|
+
#
|
68
|
+
# @return [String] the command line.
|
36
69
|
def to_s
|
37
70
|
String.new.tap do |line|
|
38
71
|
line << "#{prefix} " if prefix
|
@@ -1,72 +1,68 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
module Blur
|
4
|
-
class Network
|
5
|
-
class
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
end
|
16
|
-
|
17
|
-
|
18
|
-
|
4
|
+
class Network
|
5
|
+
# The +Connection+ class inherits the LineAndText protocol bundled with
|
6
|
+
# the eventmachine library.
|
7
|
+
#
|
8
|
+
# It merely acts as a receiving handler for all messages eventmachine throws
|
9
|
+
# at it through its lifetime.
|
10
|
+
#
|
11
|
+
# @see EventMachine::Protocols::LineAndTextProtocol
|
12
|
+
# @see EventMachine::Connection
|
13
|
+
class Connection < EM::Protocols::LineAndTextProtocol
|
14
|
+
# Check whether or not connection is established.
|
15
|
+
def established?; @connected == true end
|
16
|
+
|
17
|
+
# EventMachine instantiates this class, and then sends event messages to
|
18
|
+
# that instance.
|
19
|
+
def initialize network
|
20
|
+
@network = network
|
21
|
+
@connected = false
|
22
|
+
|
23
|
+
super
|
19
24
|
end
|
20
25
|
|
21
|
-
|
22
|
-
|
26
|
+
# Called when a new connection is being set up, all we're going to use
|
27
|
+
# it for is to enable SSL/TLS on our connection.
|
28
|
+
def post_init
|
29
|
+
start_tls if @network.secure?
|
30
|
+
end
|
23
31
|
|
24
|
-
|
25
|
-
|
32
|
+
# Called when a line was received, the connection sends it to the network
|
33
|
+
# delegate which then sends it to the client.
|
34
|
+
def receive_line line
|
35
|
+
command = Command.parse line
|
36
|
+
@network.got_command command
|
37
|
+
end
|
26
38
|
|
27
|
-
|
28
|
-
|
29
|
-
|
39
|
+
# Called when the SSL handshake was completed with the remote server,
|
40
|
+
# the reason we tell the network that we're connected here is to ensure
|
41
|
+
# that the SSL/TLS encryption succeeded before we start talking nonsense
|
42
|
+
# to the server.
|
43
|
+
def ssl_handshake_completed
|
44
|
+
connected!
|
30
45
|
end
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
@
|
35
|
-
@queue.clear
|
46
|
+
|
47
|
+
# Called once the connection is finally established.
|
48
|
+
def connection_completed
|
49
|
+
@network.connected! unless @network.secure?
|
36
50
|
end
|
37
|
-
|
38
|
-
|
39
|
-
|
51
|
+
|
52
|
+
# Called just as the connection is being terminated, either by remote or
|
53
|
+
# local.
|
54
|
+
def unbind
|
55
|
+
@connected = false
|
56
|
+
|
57
|
+
super
|
40
58
|
end
|
41
|
-
|
42
|
-
def transcieve
|
43
|
-
readable, writable = IO.select [@socket], [@socket]
|
44
59
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
while index = @buffer.index(?\n)
|
50
|
-
command = Command.parse @buffer.slice! 0..index
|
51
|
-
@delegate.got_command command
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
# If it's ready to write, do that too until the outoing queue is empty.
|
56
|
-
if socket = writable.first
|
57
|
-
while command = @queue.shift
|
58
|
-
socket.write_nonblock "#{command}\n"
|
59
|
-
end
|
60
|
-
end
|
60
|
+
private
|
61
|
+
# Called when connection has been established.
|
62
|
+
def connected!
|
63
|
+
@connected = true
|
61
64
|
|
62
|
-
|
63
|
-
puts "Insecure connection would block"
|
64
|
-
rescue OpenSSL::SSL::SSLError
|
65
|
-
if $!.message == "read would block"
|
66
|
-
puts "Secure connection would block"
|
67
|
-
else
|
68
|
-
raise $!
|
69
|
-
end
|
65
|
+
@network.connected!
|
70
66
|
end
|
71
67
|
end
|
72
68
|
end
|
@@ -2,19 +2,42 @@
|
|
2
2
|
|
3
3
|
module Blur
|
4
4
|
class Network
|
5
|
+
# The +User+ class is used for encapsulating a user and its properties.
|
6
|
+
#
|
7
|
+
# The user owns a reference to its parent channel.
|
8
|
+
#
|
9
|
+
# Modes can be set for a user, but Blur is not
|
10
|
+
# {http://www.irc.org/tech_docs/005.html ISupport}-compliant yet.
|
11
|
+
#
|
12
|
+
# @todo make so that channels *and* users belongs to the network, and not
|
13
|
+
# like now where the user belongs to the channel, resulting in multiple
|
14
|
+
# user instances.
|
5
15
|
class User
|
6
|
-
|
16
|
+
# @return [String] the users nickname.
|
17
|
+
attr_accessor :nick
|
18
|
+
# @return [String] the users username.
|
19
|
+
attr_accessor :name
|
20
|
+
# @return [String] the users hostname.
|
21
|
+
attr_accessor :host
|
22
|
+
# @return [String] all the modes set on the user.
|
23
|
+
attr_accessor :modes
|
24
|
+
# @return [Channel] a reference to the users channel.
|
25
|
+
attr_accessor :channel
|
26
|
+
# @return [Network] a reference to the network.
|
27
|
+
attr_accessor :network
|
7
28
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
29
|
+
# Check to see if the user is an admin (+a)
|
30
|
+
def admin?; @modes.include? "a" end
|
31
|
+
# Check to see if the user has voice (+v)
|
32
|
+
def voice?; @modes.include? "v" end
|
33
|
+
# Check to see if the user is the owner (+q)
|
34
|
+
def owner?; @modes.include? "q" end
|
35
|
+
# Check to see if the user is an operator (+o)
|
36
|
+
def operator?; @modes.include? "o" end
|
37
|
+
# Check to see if the user is an half-operator (+h)
|
38
|
+
def half_operator?; @modes.include? "h" end
|
17
39
|
|
40
|
+
# Instantiate a user with a nickname.
|
18
41
|
def initialize nick
|
19
42
|
@nick = nick
|
20
43
|
@modes = ""
|
@@ -25,6 +48,9 @@ module Blur
|
|
25
48
|
end
|
26
49
|
end
|
27
50
|
|
51
|
+
# Merge the users mode corresponding to the leading character (+ or -).
|
52
|
+
#
|
53
|
+
# @param [String] modes the modes to merge with.
|
28
54
|
def merge_modes modes
|
29
55
|
addition = true
|
30
56
|
|
@@ -40,24 +66,32 @@ module Blur
|
|
40
66
|
end
|
41
67
|
end
|
42
68
|
|
69
|
+
# Send a private message to the user.
|
70
|
+
#
|
71
|
+
# @param [String] message the message to send.
|
43
72
|
def say message
|
44
73
|
@network.say self, message
|
45
74
|
end
|
46
75
|
|
76
|
+
# Convert it to a debug-friendly format.
|
47
77
|
def inspect
|
48
78
|
%{#<#{self.class.name} @nick=#{@nick.inspect} @channel=#{@channel.name.inspect}>}
|
49
79
|
end
|
50
80
|
|
81
|
+
# Called when YAML attempts to save the object, which happens when a
|
82
|
+
# scripts cache contains this user and the script is unloaded.
|
51
83
|
def to_yaml options = {}
|
52
84
|
@nick.to_yaml options
|
53
85
|
end
|
54
86
|
|
87
|
+
# Get the users nickname.
|
55
88
|
def to_s
|
56
89
|
@nick
|
57
90
|
end
|
58
91
|
|
59
92
|
private
|
60
93
|
|
94
|
+
# Translate a nickname-prefix to a mode character.
|
61
95
|
def prefix_to_mode prefix
|
62
96
|
case prefix
|
63
97
|
when '@' then 'o'
|