butler 1.8.3 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.txt +293 -37
- data/README.txt +10 -0
- data/Rakefile +24 -13
- data/bin/botcontrol +6 -5
- data/data/butler/dialogs/create.rb +21 -6
- data/data/butler/dialogs/create_config.rb +5 -2
- data/data/butler/dialogs/en/create.yaml +6 -3
- data/data/butler/dialogs/en/create_config.yaml +1 -0
- data/data/butler/dialogs/en/quickcreate.yaml +1 -1
- data/data/butler/dialogs/en/sync.yaml +7 -0
- data/data/butler/dialogs/en/username.yaml +2 -0
- data/data/butler/dialogs/quickcreate.rb +6 -2
- data/data/butler/dialogs/sync.rb +83 -0
- data/data/butler/dialogs/username.rb +1 -0
- data/data/butler/plugins/core/ping.rb +22 -0
- data/data/butler/plugins/core/remote.rb +38 -0
- data/data/butler/plugins/dev/eval.rb +6 -4
- data/data/butler/plugins/dev/onhandlers.rb +93 -0
- data/data/butler/plugins/dev/rawlog.rb +109 -45
- data/data/butler/plugins/games/countdown.rb +23 -14
- data/data/butler/plugins/games/eightball.rb +21 -13
- data/data/butler/plugins/games/roll.rb +12 -12
- data/data/butler/plugins/irc/join.rb +2 -2
- data/data/butler/plugins/irc/notice.rb +10 -10
- data/data/butler/plugins/irc/part.rb +12 -12
- data/data/butler/plugins/irc/privmsg.rb +10 -10
- data/data/butler/plugins/irc/quit.rb +12 -12
- data/data/butler/plugins/operator/devoice.rb +1 -1
- data/data/butler/plugins/public/help.rb +10 -4
- data/data/butler/plugins/{util → service}/calculator.rb +0 -0
- data/data/butler/plugins/service/define.rb +16 -13
- data/data/butler/plugins/service/log.rb +85 -0
- data/data/butler/plugins/service/seen.rb +64 -0
- data/data/butler/plugins/service/svn.rb +6 -5
- data/data/butler/plugins/util/load.rb +3 -1
- data/data/butler/services/org.rubyforge.butler/calculator/1/service.rb +96 -0
- data/data/butler/services/org.rubyforge.butler/log/1/service.rb +148 -68
- data/lib/access/admin.rb +5 -0
- data/lib/blank.rb +32 -0
- data/lib/butler.rb +4 -4
- data/lib/butler/bot.rb +118 -33
- data/lib/butler/control.rb +5 -4
- data/lib/butler/debuglog.rb +12 -4
- data/lib/butler/dialog.rb +1 -1
- data/lib/butler/initialvalues.rb +1 -1
- data/lib/butler/irc/client.rb +31 -12
- data/lib/butler/irc/message.rb +32 -13
- data/lib/butler/irc/parser.rb +67 -30
- data/lib/butler/irc/parser/{commands.rb → command.rb} +0 -38
- data/lib/butler/irc/parser/generic.rb +9 -12
- data/lib/butler/irc/parser/rfc2812.rb +40 -2
- data/lib/butler/irc/socket.rb +66 -41
- data/lib/butler/irc/string.rb +1 -5
- data/lib/butler/plugin.rb +56 -23
- data/lib/butler/plugin/configproxy.rb +1 -0
- data/lib/butler/plugin/more.rb +2 -2
- data/lib/butler/plugins.rb +7 -1
- data/lib/butler/remote/connection.rb +113 -0
- data/lib/butler/remote/message.rb +157 -0
- data/lib/butler/remote/server.rb +85 -0
- data/lib/butler/remote/user.rb +46 -0
- data/lib/butler/service.rb +2 -1
- data/lib/butler/services.rb +2 -2
- data/lib/butler/version.rb +2 -2
- data/lib/configuration.rb +13 -16
- data/lib/ostructfixed.rb +0 -6
- data/lib/ruby/array/random.rb +2 -1
- data/lib/scriptfile.rb +63 -14
- data/lib/timingoutresource.rb +54 -0
- data/test/test_scriptfile.rb +51 -0
- metadata +63 -61
- data/data/butler/dialogs/en/sync_plugins.yaml +0 -3
- data/data/butler/dialogs/sync_plugins.rb +0 -30
- data/data/butler/services/org.rubyforge.butler/calculator/1/calculator.rb +0 -68
- data/lib/log/splitter.rb +0 -30
- data/test/test_access/privilege/banners.statistics.yaml +0 -3
- data/test/test_access/privilege/banners.yaml +0 -3
- data/test/test_access/privilege/news.create.yaml +0 -3
- data/test/test_access/privilege/news.delete.yaml +0 -3
- data/test/test_access/privilege/news.edit.yaml +0 -3
- data/test/test_access/privilege/news.read.yaml +0 -3
- data/test/test_access/privilege/news.yaml +0 -3
- data/test/test_access/privilege/paid_content.yaml +0 -3
- data/test/test_access/privilege/statistics.ftp.yaml +0 -3
- data/test/test_access/privilege/statistics.web.yaml +0 -3
- data/test/test_access/privilege/statistics.yaml +0 -3
- data/test/test_access/role/chiefeditor.yaml +0 -7
- data/test/test_access/role/editor.yaml +0 -9
- data/test/test_access/user/test.yaml +0 -12
data/lib/butler/irc/message.rb
CHANGED
@@ -18,6 +18,8 @@ class Butler
|
|
18
18
|
# those messages easier, e.g. who (as Butler::IRC::User object) sent the
|
19
19
|
# message in which channel (Butler::IRC::Channel object) with what text.
|
20
20
|
# Raw message and raw parsed data are still available though.
|
21
|
+
# Don't create Butler::IRC::Message manually, leave that up to Butler::IRC::Client (which
|
22
|
+
# uses Butler::IRC::Parser to do that)
|
21
23
|
class Message
|
22
24
|
# the command-symbol, see COMMANDS (e.g. :PRIVMSG, :JOIN, ...)
|
23
25
|
attr_reader :symbol
|
@@ -31,8 +33,8 @@ class Butler
|
|
31
33
|
attr_reader :command
|
32
34
|
# the parameter part
|
33
35
|
attr_reader :params
|
34
|
-
|
35
|
-
def initialize(client, symbol, raw, prefix, command, params)
|
36
|
+
|
37
|
+
def initialize(client, symbol, raw, prefix, command, params) # :nodoc:
|
36
38
|
@client = client
|
37
39
|
|
38
40
|
#raw message
|
@@ -70,30 +72,55 @@ class Butler
|
|
70
72
|
@symbol = @fields[:symbol]
|
71
73
|
end
|
72
74
|
|
75
|
+
# Answer a message in kind.
|
76
|
+
# * :NOTICE: If the notice was sent to a channel, it will send a notice to the same channel,
|
77
|
+
# if it was for a user, it will send a notice back to the sender
|
78
|
+
# * :PRIVMSG: If the privmsg was sent to a channel, it will send a privmsg to the same channel,
|
79
|
+
# if it was for a user, it will send a privmsg back to the sender
|
80
|
+
# * All others: It will send a message to the channel the message was sent to.
|
73
81
|
def answer(text)
|
74
82
|
reply = channel || from
|
75
83
|
case @symbol
|
76
84
|
when :PRIVMSG: @client.irc.privmsg(text, reply)
|
77
85
|
when :NOTICE: @client.irc.notice(text, reply)
|
86
|
+
when :JOIN, :PART, :KICK: @client.irc.privmsg(text, reply)
|
87
|
+
when :TOPIC, :NICK: @client.irc.privmsg(text, reply)
|
88
|
+
else
|
89
|
+
raise "Can't answer a #{@symbol} message."
|
78
90
|
end
|
79
91
|
end
|
80
92
|
|
93
|
+
# If the server supports CAPAB IDENTIFY-MSG (informs irc clients whether the user sending a
|
94
|
+
# PRIVMSG/NOTICE is identified by nickserv), this method will tell you its value,
|
95
|
+
# returns true for messages prefixed with +, false for -, nil if not supported/activated.
|
81
96
|
def identified?
|
82
97
|
@fields[:identified]
|
83
98
|
end
|
84
99
|
|
100
|
+
# The Butler::IRC::User the message is from.
|
85
101
|
def from
|
86
102
|
@fields[:from]
|
87
103
|
end
|
88
|
-
|
104
|
+
|
105
|
+
# The Butler::IRC::User or Butler::IRC::Channel this message is for. If it's a User, it is
|
106
|
+
# most likely Butler::IRC::Client#myself.
|
89
107
|
def for
|
90
108
|
@fields[:for]
|
91
109
|
end
|
92
|
-
|
110
|
+
|
111
|
+
# If the message was sent for/in a channel, this will be a Butler::IRC::Channel the message
|
112
|
+
# was sent for.
|
93
113
|
def channel
|
94
114
|
@fields[:channel]
|
95
115
|
end
|
96
|
-
|
116
|
+
|
117
|
+
# Messages with text will use this attribute to store it. Prominent examples are:
|
118
|
+
# * :PRIVMSG - the message
|
119
|
+
# * :NOTICE - the message
|
120
|
+
# * :PART - part-reason
|
121
|
+
# * :QUIT - quit-reason
|
122
|
+
# * :KICK - kick-reason
|
123
|
+
# * :TOPIC - the new topic
|
97
124
|
def text
|
98
125
|
@fields[:text]
|
99
126
|
end
|
@@ -167,14 +194,6 @@ class Butler
|
|
167
194
|
@fields.dup
|
168
195
|
end
|
169
196
|
|
170
|
-
def hash #:nodoc:
|
171
|
-
@raw.hash
|
172
|
-
end
|
173
|
-
|
174
|
-
def eql?(other) #:nodoc:
|
175
|
-
@raw == other.raw
|
176
|
-
end
|
177
|
-
|
178
197
|
def inspect #:nodoc:
|
179
198
|
fields = @fields.dup
|
180
199
|
#[:raw, :prefix, :command, :params, :command_raw]
|
data/lib/butler/irc/parser.rb
CHANGED
@@ -9,9 +9,11 @@
|
|
9
9
|
require 'butler/irc/channellist'
|
10
10
|
require 'butler/irc/hostmask'
|
11
11
|
require 'butler/irc/message'
|
12
|
-
require 'butler/irc/parser/
|
12
|
+
require 'butler/irc/parser/command'
|
13
13
|
require 'butler/irc/string'
|
14
14
|
require 'butler/irc/userlist'
|
15
|
+
require 'log/comfort'
|
16
|
+
require 'ostructfixed'
|
15
17
|
require 'ruby/exception/detailed'
|
16
18
|
require 'ruby/hash/zip'
|
17
19
|
|
@@ -24,28 +26,12 @@ class Butler
|
|
24
26
|
# regarding who myself is (out_of_sight, back_in_sight for users)
|
25
27
|
# allows creation of dialogs from privmsg and notice messages
|
26
28
|
class Parser
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
Letter = /[A-Za-z]/
|
31
|
-
Hex = /[\dA-Fa-f]/
|
32
|
-
ChannelID = /[A-Z\d]{5}/
|
33
|
-
Chanstring = /[^\x00\x07\x10\x0D\x20,:]/
|
34
|
-
Channel = /(?:[#+&]|!#{ChannelID})#{Chanstring}(?::#{Chanstring})?/
|
35
|
-
User = /[^\x00\x10\x0D\x20@]/
|
36
|
-
Nick = /[A-Za-z\[\]\\`_^{|}][A-Za-z\d\[\]\\`_^{|}-]*/
|
37
|
-
Command = /[A-Za-z]+|\d{3}/
|
38
|
-
IP4addr = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/
|
39
|
-
IP6addr = /[\dA-Fa-f](?::[\dA-Fa-f]){7}|0:0:0:0:0:(?:0|[Ff]{4}):#{IP4addr}/
|
40
|
-
Hostaddr = /#{IP4addr}|#{IP6addr}/
|
41
|
-
Shortname = /[A-Za-z0-9][A-Za-z0-9-]*/
|
42
|
-
Hostname = /#{Shortname}(?:\.#{Shortname})*/
|
43
|
-
Host = /#{Hostname}|#{Hostaddr}/
|
44
|
-
Prefix = /#{Hostname}|#{Nick}(?:(?:!#{User})?@#{Host})?/
|
45
|
-
Params = /.*/ # FIXME
|
46
|
-
Message = /(:#{Prefix} )?#{Command}(#{Params})?/
|
47
|
-
end
|
29
|
+
|
30
|
+
# enables chdirs, path to the command-sets
|
31
|
+
BasePath = (File.expand_path(File.dirname(__FILE__))+'/parser').freeze
|
48
32
|
|
33
|
+
include Log::Comfort
|
34
|
+
|
49
35
|
class InvalidMessageFormat < RuntimeError; end
|
50
36
|
class UnknownCommand < RuntimeError; end
|
51
37
|
module ParseError
|
@@ -56,24 +42,75 @@ class Butler
|
|
56
42
|
end
|
57
43
|
end
|
58
44
|
|
59
|
-
RMessage = /\A(?::([^ \0]+) )?([A-Za-z\d]+|\d{3})(?: (.*))?\z/
|
60
|
-
RHostmask = /(#{Expressions::Nick})(?:(?:!(#{Expressions::User}))?@(#{Expressions::Host}))?/
|
61
|
-
|
62
45
|
attr_reader :client
|
63
46
|
attr_reader :users
|
64
47
|
attr_reader :channels
|
65
48
|
attr_reader :commands
|
49
|
+
attr_reader :expression
|
50
|
+
attr_reader :isupport
|
66
51
|
|
67
52
|
attr_accessor :msg_identify
|
68
53
|
|
69
54
|
def initialize(client, users, channels, *command_sets)
|
55
|
+
@logger = nil
|
70
56
|
@client = client
|
71
57
|
@users = users
|
72
58
|
@channels = channels
|
73
|
-
@commands = Commands.new(*command_sets)
|
74
59
|
@msg_identify = false
|
60
|
+
@command_sets = command_sets
|
61
|
+
@isupport = OpenStruct.new(
|
62
|
+
"nicklen" => 8,
|
63
|
+
"channellen" => 50,
|
64
|
+
"prefixes" => "@+"
|
65
|
+
)
|
66
|
+
|
67
|
+
reset
|
68
|
+
end
|
69
|
+
|
70
|
+
def same_nick?(a,b)
|
71
|
+
a.to_str.strip_user_prefixes.downcase == b.to_str.strip_user_prefixes.downcase
|
72
|
+
end
|
73
|
+
|
74
|
+
def reset(isupport=nil)
|
75
|
+
@isupport = OpenStruct.new(isupport) if isupport
|
76
|
+
@expression = OpenStruct.new
|
77
|
+
@commands = Hash.new { |h,k| raise IndexError, "Unknown command #{k}" }
|
78
|
+
load(*@command_sets)
|
79
|
+
@expression.simple_message = /\A(?::([^ \0]+) )?([A-Za-z\d]+|\d{3})(?: (.*))?\z/
|
80
|
+
@expression.simple_hostmask = /(#{expression.nick})(?:(?:!(#{expression.user}))?@(#{expression.host}))?/
|
81
|
+
end
|
82
|
+
|
83
|
+
def load(*files)
|
84
|
+
raise ArgumentError, "Requires at least one argument." if files.empty?
|
85
|
+
files.each { |name|
|
86
|
+
file = name.include?('/') ? name : "#{BasePath}/#{name}.rb"
|
87
|
+
instance_eval(File.read(file), file)
|
88
|
+
info("Loaded command set #{file}")
|
89
|
+
}
|
75
90
|
end
|
76
91
|
|
92
|
+
def add_expression(name, value)
|
93
|
+
@expression[name] = value
|
94
|
+
end
|
95
|
+
|
96
|
+
def alter_expression(name, value)
|
97
|
+
@expression[name] = value
|
98
|
+
end
|
99
|
+
|
100
|
+
def add(raw, *args, &proc)
|
101
|
+
raise IndexError, "Command #{raw} is already registered. Did you want 'alter'?" if @commands.has_key?(raw)
|
102
|
+
@commands[raw.downcase] = Command.new(raw, *args, &proc)
|
103
|
+
end
|
104
|
+
|
105
|
+
def alter(raw, *args, &proc)
|
106
|
+
raise IndexError, "Command #{raw} is not registered. Did you want 'add'?" unless @commands.has_key?(raw)
|
107
|
+
@commands[raw.downcase] = Command.new(raw, *args, &proc)
|
108
|
+
end
|
109
|
+
|
110
|
+
def inspect
|
111
|
+
"#<%s:0x%x>" % [self.class, object_id]
|
112
|
+
end
|
113
|
+
|
77
114
|
# parses an incomming message and returns a Message object from which you
|
78
115
|
# can easily access parsed data.
|
79
116
|
# Expects the newlines to be already chomped off.
|
@@ -81,15 +118,15 @@ class Butler
|
|
81
118
|
prefix, command, params, symbol, from = nil
|
82
119
|
|
83
120
|
# Basic analysis of the message
|
84
|
-
raise InvalidMessageFormat, raw unless matched = raw.match(
|
121
|
+
raise InvalidMessageFormat, raw unless matched = raw.match(@expression.simple_message)
|
85
122
|
prefix, command, params = *matched.captures
|
86
123
|
command.downcase!
|
87
124
|
|
88
125
|
# Parse prefix if possible (<nick>!<user>@<host>)
|
89
|
-
from = @users.create(*matched.captures) if prefix and matched = prefix.match(
|
90
|
-
|
126
|
+
from = @users.create(*matched.captures) if prefix and matched = prefix.match(@expression.simple_hostmask)
|
127
|
+
|
91
128
|
# in depth analyzis of the message
|
92
|
-
parser = @commands[command]
|
129
|
+
parser = @commands[command.downcase]
|
93
130
|
symbol = parser.symbol
|
94
131
|
message = Message.new(@client, symbol, raw, prefix, command, params)
|
95
132
|
message.alter_member(:from, from)
|
@@ -14,44 +14,6 @@ require 'log/comfort'
|
|
14
14
|
class Butler
|
15
15
|
module IRC
|
16
16
|
class Parser
|
17
|
-
class Commands
|
18
|
-
include Log::Comfort
|
19
|
-
|
20
|
-
# enables chdirs
|
21
|
-
BasePath = File.expand_path(File.dirname(__FILE__))
|
22
|
-
|
23
|
-
def initialize(*files)
|
24
|
-
@commands = Hash.new { |h,k| raise IndexError, "Unknown command #{k}" }
|
25
|
-
load(*files)
|
26
|
-
end
|
27
|
-
|
28
|
-
def load(*files)
|
29
|
-
raise ArgumentError, "Requires at least one argument." if files.empty?
|
30
|
-
files.each { |file|
|
31
|
-
file = "#{BasePath}/#{file}.rb" unless file.include?('/')
|
32
|
-
instance_eval(File.read(file), file)
|
33
|
-
}
|
34
|
-
end
|
35
|
-
|
36
|
-
def add(raw, *args, &proc)
|
37
|
-
raise IndexError, "Command #{raw} is already registered. Did you want 'alter'?" if @commands.has_key?(raw)
|
38
|
-
@commands[raw.downcase] = Command.new(raw, *args, &proc)
|
39
|
-
end
|
40
|
-
|
41
|
-
def alter(raw, *args, &proc)
|
42
|
-
raise IndexError, "Command #{raw} is not registered. Did you want 'add'?" unless @commands.has_key?(raw)
|
43
|
-
@commands[raw.downcase] = Command.new(raw, *args, &proc)
|
44
|
-
end
|
45
|
-
|
46
|
-
def [](raw)
|
47
|
-
@commands[raw.downcase]
|
48
|
-
end
|
49
|
-
|
50
|
-
def inspect
|
51
|
-
"#<%s:0x%x>" % [self.class, object_id]
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
17
|
# Provides parsing information about specific commands.
|
56
18
|
class Command
|
57
19
|
attr_reader :raw
|
@@ -5,35 +5,30 @@
|
|
5
5
|
#++
|
6
6
|
|
7
7
|
|
8
|
+
alter_expression :nick, /[A-Za-z\[\]\\`_^{|}][A-Za-z\d\[\]\\`_^{|}-]{0,#{isupport.nicklen-1}}/
|
8
9
|
|
9
10
|
# A list with all common, but non-rfc2812 IRC-Commands and their parsing
|
10
11
|
# instructions.
|
11
12
|
|
12
13
|
#
|
13
14
|
alter("005", :ISUPPORT) { |message, parser|
|
14
|
-
hash = {
|
15
|
-
|
16
|
-
"NICKLEN" => 8,
|
17
|
-
}
|
18
|
-
message.params.sub(/\s+:.*?$/, '').split(/ /).each { |support|
|
15
|
+
hash = {}
|
16
|
+
message.params.sub(/\s+:.*?$/, '').split(/ /)[1..-1].each { |support|
|
19
17
|
name, value = support.split(/=/,2)
|
20
|
-
hash[name] = case
|
21
|
-
when
|
18
|
+
hash[name] = case name
|
19
|
+
when "CAPAB"
|
22
20
|
true
|
23
|
-
when "NICKLEN"
|
21
|
+
when "CHANNELLEN", "NICKLEN", "MAXCHANNELS", "MODES", "TOPICLEN", "USERLEN", "KEYLEN"
|
24
22
|
value.to_i
|
25
23
|
when "PREFIX"
|
26
24
|
modes, prefixes = value[1..-1].split(/\)/, 2)
|
27
25
|
value = {}
|
28
26
|
modes.split(//).zip(prefixes.split(//)) { |k,v| value[k] = v }
|
29
27
|
value
|
30
|
-
when "MAXCHANNELS"
|
31
|
-
value.to_i
|
32
28
|
else value
|
33
29
|
end
|
34
30
|
}
|
35
31
|
message.create_member(:support, hash)
|
36
|
-
p hash
|
37
32
|
}
|
38
33
|
|
39
34
|
# Seen:
|
@@ -84,4 +79,6 @@ add("396", :RPL_HOSTHIDDEN, /^(\S+) (?:(.*?)@)?([:\S]+) :(.*)/, [:nick, :user, :
|
|
84
79
|
parser.users.myself.force_update(nil, message.displayed_host, nil)
|
85
80
|
}
|
86
81
|
|
87
|
-
add("
|
82
|
+
add("410", :ERR_SERVICES_OFFLINE)
|
83
|
+
|
84
|
+
add("505", :ERR_NOPRIVMSG)
|
@@ -10,6 +10,33 @@
|
|
10
10
|
# Currently many of them are still in generic.rb
|
11
11
|
|
12
12
|
|
13
|
+
# --- Regular expressions -------------------------------
|
14
|
+
add_expression :special, /[\[\]\\`_^{|}]/
|
15
|
+
add_expression :letter, /[A-Za-z]/
|
16
|
+
add_expression :hex, /[\dA-Fa-f]/
|
17
|
+
add_expression :channel_id, /[A-Z\d]{5}/
|
18
|
+
add_expression :chanstring, /[^\x00\x07\x10\x0D\x20,:]/
|
19
|
+
add_expression :channel, /(?:[#+&]|!#{expression.channel_id})#{expression.chanstring}(?::#{expression.chanstring})?/
|
20
|
+
add_expression :user, /[^\x00\x10\x0D\x20@]/
|
21
|
+
add_expression :nick, /[A-Za-z\[\]\\`_^{|}][A-Za-z\d\[\]\\`_^{|}-]{0,7}/
|
22
|
+
add_expression :command, /[A-Za-z]+|\d{3}/
|
23
|
+
add_expression :ip4addr, /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/
|
24
|
+
add_expression :ip6addr, /[\dA-Fa-f](?::[\dA-Fa-f]){7}|0:0:0:0:0:(?:0|[Ff]{4}):#{expression.ip4addr}/
|
25
|
+
add_expression :hostaddr, /#{expression.ip4addr}|#{expression.ip6addr}/
|
26
|
+
add_expression :shortname, /[A-Za-z0-9][A-Za-z0-9-]*/
|
27
|
+
add_expression :hostname, /#{expression.shortname}(?:\.#{expression.shortname})*/
|
28
|
+
add_expression :host, /#{expression.hostname}|#{expression.hostaddr}/
|
29
|
+
add_expression :prefix, /(#{expression.hostname})|(#{expression.nick})(?:(?:!(#{expression.user}))?@(#{expression.host}))?/
|
30
|
+
add_expression :params, /.*/ # FIXME
|
31
|
+
add_expression :message, /^
|
32
|
+
# PREFIX
|
33
|
+
(:#{expression.prefix}\x20)?
|
34
|
+
# COMMAND
|
35
|
+
(#{expression.command})
|
36
|
+
# PARAMS
|
37
|
+
(#{expression.params})?
|
38
|
+
$/x
|
39
|
+
|
13
40
|
|
14
41
|
# --- Text based ----------------------------------------
|
15
42
|
add("error", :ERROR) # ERROR :<error-message>
|
@@ -19,6 +46,8 @@ add("join", :JOIN, /^:(.*)/, [:channel]) { |message, parser|
|
|
19
46
|
message.from.add_channel(message.channel, :join)
|
20
47
|
message.channel.add_user(message.from, :join)
|
21
48
|
end
|
49
|
+
message.create_method(:private?) { false }
|
50
|
+
message.create_method(:public?) { true }
|
22
51
|
}
|
23
52
|
add("kick", :KICK, /^(\S*) (\S*) :(.*)/, [:channel, :for, :text]) { |message, parser|
|
24
53
|
parser.leave_channel(message, :kick, :kicked)
|
@@ -28,6 +57,8 @@ add("kill", :KILL, /^(\S*) (\S*) (.*)/, [:channel, :for, :text]) { |message
|
|
28
57
|
message.for.kill
|
29
58
|
parser.channels.delete_user(message.for, :kill)
|
30
59
|
end
|
60
|
+
message.create_method(:private?) { false }
|
61
|
+
message.create_method(:public?) { true }
|
31
62
|
}
|
32
63
|
add("mode", :MODE, /^(\S*) (.*)/, [:for, :arguments]) { |message, parser|
|
33
64
|
modifiers = message[:arguments].split(" ")
|
@@ -61,6 +92,8 @@ add("mode", :MODE, /^(\S*) (.*)/, [:for, :arguments]) { |message, parser|
|
|
61
92
|
add("nick", :NICK, /^:(.*)/, [:nick]) { |message, parser|
|
62
93
|
message.create_member(:old_nick, message.from.nick)
|
63
94
|
message.from.nick = message.nick if message.from
|
95
|
+
message.create_method(:private?) { false }
|
96
|
+
message.create_method(:public?) { true }
|
64
97
|
}
|
65
98
|
add("notice", :NOTICE, /(\S+) :(.*)/, [:for, :text]) { |message, parser|
|
66
99
|
if message.channel then
|
@@ -75,6 +108,8 @@ add("notice", :NOTICE, /(\S+) :(.*)/, [:for, :text]) { |message, parser|
|
|
75
108
|
}
|
76
109
|
add("part", :PART, /^([^\x00\x07\x10\x0D\x20,:]+)(?: :(.*))?/, [:channel, :reason]) { |message, parser|
|
77
110
|
parser.leave_channel(message, :part, :parted)
|
111
|
+
message.create_method(:private?) { false }
|
112
|
+
message.create_method(:public?) { true }
|
78
113
|
}
|
79
114
|
add("ping", :PING, /:(.*)/, [:pong])
|
80
115
|
add("pong", :PONG)
|
@@ -99,7 +134,10 @@ add("quit", :QUIT, /(.*)/, [:text]) { |message, parser|
|
|
99
134
|
parser.users.delete(message.from, :quit)
|
100
135
|
end
|
101
136
|
}
|
102
|
-
add("topic", :TOPIC, /(\S+) :(.*)/, [:channel, :text])
|
137
|
+
add("topic", :TOPIC, /(\S+) :(.*)/, [:channel, :text]) { |message, parser|
|
138
|
+
message.create_method(:private?) { false }
|
139
|
+
message.create_method(:public?) { true }
|
140
|
+
}
|
103
141
|
|
104
142
|
|
105
143
|
|
@@ -212,7 +250,7 @@ add("349", :RPL_ENDOFEXCEPTLIST)
|
|
212
250
|
add("351", :RPL_VERSION)
|
213
251
|
# :irc.server.net 352 YourNickname <channel> <user> <host> <server> <nick> ( "H" / "G" > ["*"] [ ( "@" / "+" ) ] :<hopcount> <real name>
|
214
252
|
add("352", :RPL_WHOREPLY,
|
215
|
-
/(\S+) (\S+) (\S+) (\S+) (\S+) (\S+) ([HG])[*%]{0,2}([
|
253
|
+
/(\S+) (\S+) (\S+) (\S+) (\S+) (\S+) ([HG])[*%]{0,2}([#{Regexp.escape(isupport.prefixes)}]{0,3}) :(\d+) (.*)/,
|
216
254
|
[:for, :channel, :user, :host, :server, :nick, :status, :flags, :hopcount, :real]) { |message, parser|
|
217
255
|
#"for", "channel", "user", "host", "server", "nick", "status", "flags", "hopcount", "real"
|
218
256
|
user = parser.users.create(message[:nick])
|
data/lib/butler/irc/socket.rb
CHANGED
@@ -26,14 +26,14 @@ class Butler
|
|
26
26
|
# parameters expecting a nickname will accept an Butler::IRC::User as well).
|
27
27
|
# It will adhere to its limit-settings, which will prevent from sending too
|
28
28
|
# many messages in a too short time to avoid excess flooding.
|
29
|
-
# Butler::IRC::Socket#
|
30
|
-
# methods build up on it, IRC::Socket should be safe in threaded
|
31
|
-
# Butler::IRC::Socket#read is NOT synchronized, so unless you
|
32
|
-
# a single thread, statistics might get messed up.
|
29
|
+
# Butler::IRC::Socket#write_with_eol is the only synchronized method, since all
|
30
|
+
# other methods build up on it, IRC::Socket should be safe in threaded
|
31
|
+
# environments. Butler::IRC::Socket#read is NOT synchronized, so unless you
|
32
|
+
# read from only a single thread, statistics might get messed up.
|
33
33
|
# Length limits can only be safely guaranteed by specialized write methods,
|
34
|
-
# Butler::IRC::Socket#
|
35
|
-
# If you are looking for queries (commands that get an answer from the
|
36
|
-
# take a look at Butler::IRC::Client.
|
34
|
+
# Butler::IRC::Socket#read from only will just warn and send the overlength
|
35
|
+
# message. If you are looking for queries (commands that get an answer from the
|
36
|
+
# server) take a look at Butler::IRC::Client.
|
37
37
|
#
|
38
38
|
# ==Synopsis
|
39
39
|
# irc = Butler::IRC::Socket.new('irc.freenode.org', :port => 6667, :charset => 'ISO-8859-1')
|
@@ -50,9 +50,6 @@ class Butler
|
|
50
50
|
# Errno::ECONNRESET: connection works, server did not yet accept connection, resets after
|
51
51
|
# Errno::EPIPE: writing to a server-side closed connection, nil on gets, connection was terminated
|
52
52
|
#
|
53
|
-
# ==FIXME
|
54
|
-
# mode commands don't test for length and split up
|
55
|
-
#
|
56
53
|
class Socket
|
57
54
|
VERSION = "1.0.0"
|
58
55
|
|
@@ -73,6 +70,9 @@ class Butler
|
|
73
70
|
# contains limits for the protocol, burst times/counts etc.
|
74
71
|
attr_reader :limit
|
75
72
|
|
73
|
+
# log raw out, will use log_out.puts(raw)
|
74
|
+
attr_accessor :log_out
|
75
|
+
|
76
76
|
OptionsDefault = {
|
77
77
|
:port => 6667,
|
78
78
|
:eol => "\r\n",
|
@@ -93,6 +93,7 @@ class Butler
|
|
93
93
|
@port = options.delete(:port)
|
94
94
|
@eol = options.delete(:eol).dup.freeze
|
95
95
|
@host = options[:host] ? options.delete(:host).dup.freeze : options.delete(:host)
|
96
|
+
@log_out = nil
|
96
97
|
@last_sent = Time.new()
|
97
98
|
@count = Hash.new(0)
|
98
99
|
@limit = OpenStruct.new({
|
@@ -112,11 +113,16 @@ class Butler
|
|
112
113
|
@connected = false
|
113
114
|
raise ArgumentError, "Unknown arguments: #{options.keys.inspect}" unless options.empty?
|
114
115
|
end
|
115
|
-
|
116
|
+
|
117
|
+
def connected?
|
118
|
+
@connected
|
119
|
+
end
|
120
|
+
|
116
121
|
# connects to the server
|
117
122
|
def connect
|
123
|
+
info("Connecting to #{@server} on port #{@port} from #{@host || '<default>'}")
|
118
124
|
@socket = TCPSocket.open(@server, @port) #, @host)
|
119
|
-
info("
|
125
|
+
info("Successfully connected")
|
120
126
|
rescue ArgumentError => error
|
121
127
|
if @host then
|
122
128
|
warn("host-parameter is not supported by your ruby version. Parameter discarted.")
|
@@ -125,6 +131,8 @@ class Butler
|
|
125
131
|
else
|
126
132
|
raise
|
127
133
|
end
|
134
|
+
rescue Interrupt
|
135
|
+
raise
|
128
136
|
rescue Exception
|
129
137
|
error("Connection failed.")
|
130
138
|
raise
|
@@ -138,6 +146,7 @@ class Butler
|
|
138
146
|
if m = @socket.gets(@eol) then
|
139
147
|
m.chomp(@eol)
|
140
148
|
else
|
149
|
+
@connected = false
|
141
150
|
nil
|
142
151
|
end
|
143
152
|
end
|
@@ -147,7 +156,7 @@ class Butler
|
|
147
156
|
# you from several tasks like translating newlines, take care of overlength
|
148
157
|
# messages etc.
|
149
158
|
# FIXME, wrong methodname, write implies nothing is appended
|
150
|
-
def
|
159
|
+
def write_with_eol(data)
|
151
160
|
@mutex.synchronize {
|
152
161
|
warn("Raw too long (#{data.length} instead of #{@limit[:raw_length]})") if (data.length > @limit.raw_length)
|
153
162
|
now = Time.now
|
@@ -185,6 +194,7 @@ class Butler
|
|
185
194
|
@count[:burst] += 1
|
186
195
|
@count[:burst2] += 1
|
187
196
|
@count[:sent] += 1
|
197
|
+
@log_out.puts(data) if @log_out
|
188
198
|
}
|
189
199
|
rescue IOError
|
190
200
|
error("Writing #{data.inspect} failed")
|
@@ -192,23 +202,24 @@ class Butler
|
|
192
202
|
end
|
193
203
|
|
194
204
|
# log into the irc-server (and connect if necessary)
|
195
|
-
def login(nickname, username, realname)
|
205
|
+
def login(nickname, username, realname, serverpass=nil)
|
196
206
|
connect unless @connected
|
197
|
-
|
198
|
-
|
207
|
+
write_with_eol("PASS #{serverpass}") if serverpass
|
208
|
+
write_with_eol("NICK #{nickname}")
|
209
|
+
write_with_eol("USER #{username} 0 * :#{realname}")
|
199
210
|
end
|
200
211
|
|
201
212
|
# identify nickname to nickserv
|
202
213
|
# FIXME, figure out what the server supports, possibly requires it
|
203
214
|
# to be moved to Butler::IRC::Client (to allow ghosting, nickchange, identify)
|
204
215
|
def identify(password)
|
205
|
-
|
216
|
+
write_with_eol("NS :IDENTIFY #{password}")
|
206
217
|
end
|
207
218
|
|
208
219
|
# FIXME, figure out what the server supports, possibly requires it
|
209
220
|
# to be moved to Butler::IRC::Client (to allow ghosting, nickchange, identify)
|
210
221
|
def ghost(nickname, password)
|
211
|
-
|
222
|
+
write_with_eol("NS :GHOST #{nickname} #{password}")
|
212
223
|
end
|
213
224
|
|
214
225
|
def normalize_message(message, limit=:message_length)
|
@@ -225,7 +236,7 @@ class Butler
|
|
225
236
|
def privmsg(message, *recipients)
|
226
237
|
normalize_message(message).each { |message|
|
227
238
|
recipients.each { |recipient|
|
228
|
-
|
239
|
+
write_with_eol("PRIVMSG #{recipient} :#{message}")
|
229
240
|
}
|
230
241
|
}
|
231
242
|
end
|
@@ -234,7 +245,7 @@ class Butler
|
|
234
245
|
def action(message, *recipients)
|
235
246
|
normalize_message(message).each { |message|
|
236
247
|
recipients.each { |recipient|
|
237
|
-
|
248
|
+
write_with_eol("PRIVMSG #{recipient} :"+(1.chr)+"ACTION "+message+(1.chr))
|
238
249
|
}
|
239
250
|
}
|
240
251
|
end
|
@@ -246,7 +257,7 @@ class Butler
|
|
246
257
|
def notice(message, *recipients)
|
247
258
|
normalize_message(message).each { |message|
|
248
259
|
recipients.each { |recipient|
|
249
|
-
|
260
|
+
write_with_eol("NOTICE #{recipient} :#{message}")
|
250
261
|
}
|
251
262
|
}
|
252
263
|
end
|
@@ -254,9 +265,9 @@ class Butler
|
|
254
265
|
# send a pong
|
255
266
|
def pong(*args)
|
256
267
|
if args.empty? then
|
257
|
-
|
268
|
+
write_with_eol("PONG")
|
258
269
|
else
|
259
|
-
|
270
|
+
write_with_eol("PONG #{args.join(' ')}")
|
260
271
|
end
|
261
272
|
end
|
262
273
|
|
@@ -266,9 +277,9 @@ class Butler
|
|
266
277
|
def join(*channels)
|
267
278
|
channels.map { |channel, password|
|
268
279
|
if password then
|
269
|
-
|
280
|
+
write_with_eol("JOIN #{channel} #{password}")
|
270
281
|
else
|
271
|
-
|
282
|
+
write_with_eol("JOIN #{channel}")
|
272
283
|
end
|
273
284
|
channel
|
274
285
|
}
|
@@ -285,80 +296,94 @@ class Butler
|
|
285
296
|
|
286
297
|
# some servers still can't process lists of channels in part
|
287
298
|
channels.each { |channel|
|
288
|
-
|
299
|
+
write_with_eol("PART #{channel} #{reason}")
|
289
300
|
}
|
290
301
|
end
|
291
302
|
|
292
303
|
# set your own nick
|
293
304
|
# does NO verification/validation of any kind
|
294
305
|
def nick(nick)
|
295
|
-
|
306
|
+
write_with_eol("NICK #{nick}")
|
296
307
|
end
|
297
308
|
|
298
309
|
# set your status to away with reason 'reason'
|
299
310
|
def away(reason="")
|
300
311
|
return back if reason.empty?
|
301
|
-
|
312
|
+
write_with_eol("AWAY :#{reason}")
|
302
313
|
end
|
303
314
|
|
304
315
|
# reset your away status to back
|
305
316
|
def back
|
306
|
-
|
317
|
+
write_with_eol("AWAY")
|
307
318
|
end
|
308
319
|
|
309
320
|
# kick user in channel with reason
|
310
321
|
def kick(user, channel, reason)
|
311
|
-
|
322
|
+
write_with_eol("KICK #{channel} #{user} :#{reason}")
|
312
323
|
end
|
313
324
|
|
314
325
|
# send a mode command to a channel
|
315
326
|
def mode(channel, mode)
|
316
|
-
|
327
|
+
write_with_eol("MODE #{channel} #{mode}")
|
317
328
|
end
|
318
329
|
|
330
|
+
# Give Op to user in channel
|
331
|
+
# User can be a nick or IRC::User, either one or an array.
|
332
|
+
def multiple_mode(channel, pre, flag, targets)
|
333
|
+
(0...targets.length).step(10) { |i|
|
334
|
+
slice = targets[i,10]
|
335
|
+
write_with_eol("MODE #{channel} +#{flag*slice.length} #{slice*' '}")
|
336
|
+
}
|
337
|
+
end
|
338
|
+
|
319
339
|
# Give Op to user in channel
|
320
340
|
# User can be a nick or IRC::User, either one or an array.
|
321
341
|
def op(channel, *users)
|
322
|
-
|
342
|
+
multiple_mode(channel, '+', 'o', users)
|
323
343
|
end
|
324
344
|
|
325
345
|
# Take Op from user in channel
|
326
346
|
# User can be a nick or IRC::User, either one or an array.
|
327
347
|
def deop(channel, *users)
|
328
|
-
|
348
|
+
multiple_mode(channel, '-', 'o', users)
|
329
349
|
end
|
330
350
|
|
331
351
|
# Give voice to user in channel
|
332
352
|
# User can be a nick or IRC::User, either one or an array.
|
333
353
|
def voice(channel, *users)
|
334
|
-
|
354
|
+
multiple_mode(channel, '+', 'v', users)
|
335
355
|
end
|
336
356
|
|
337
357
|
# Take voice from user in channel.
|
338
358
|
# User can be a nick or IRC::User, either one or an array.
|
339
359
|
def devoice(channel, *users)
|
340
|
-
|
360
|
+
multiple_mode(channel, '-', 'v', users)
|
341
361
|
end
|
342
362
|
|
343
363
|
# Set ban in channel to mask
|
344
|
-
def ban(
|
345
|
-
|
364
|
+
def ban(channel, *masks)
|
365
|
+
multiple_mode(channel, '+', 'b', masks)
|
346
366
|
end
|
347
367
|
|
368
|
+
# Remove ban in channel to mask
|
369
|
+
def unban(channel, *masks)
|
370
|
+
multiple_mode(channel, '-', 'b', masks)
|
371
|
+
end
|
372
|
+
|
348
373
|
# Send a "who" to channel
|
349
374
|
def who(channel)
|
350
|
-
|
375
|
+
write_with_eol("WHO #{channel}")
|
351
376
|
end
|
352
377
|
|
353
378
|
# Send a "whois" to server
|
354
379
|
def whois(nick)
|
355
|
-
|
380
|
+
write_with_eol("WHOIS #{nick}")
|
356
381
|
end
|
357
382
|
|
358
383
|
# send the quit message to the server
|
359
384
|
# if you set close to true it will also close the socket
|
360
385
|
def quit(reason="leaving", close=false)
|
361
|
-
|
386
|
+
write_with_eol("QUIT :#{reason}")
|
362
387
|
close() if close
|
363
388
|
end
|
364
389
|
|
@@ -381,4 +406,4 @@ class Butler
|
|
381
406
|
end
|
382
407
|
end
|
383
408
|
end
|
384
|
-
end
|
409
|
+
end
|