butler 1.8.3 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. data/CHANGELOG.txt +293 -37
  2. data/README.txt +10 -0
  3. data/Rakefile +24 -13
  4. data/bin/botcontrol +6 -5
  5. data/data/butler/dialogs/create.rb +21 -6
  6. data/data/butler/dialogs/create_config.rb +5 -2
  7. data/data/butler/dialogs/en/create.yaml +6 -3
  8. data/data/butler/dialogs/en/create_config.yaml +1 -0
  9. data/data/butler/dialogs/en/quickcreate.yaml +1 -1
  10. data/data/butler/dialogs/en/sync.yaml +7 -0
  11. data/data/butler/dialogs/en/username.yaml +2 -0
  12. data/data/butler/dialogs/quickcreate.rb +6 -2
  13. data/data/butler/dialogs/sync.rb +83 -0
  14. data/data/butler/dialogs/username.rb +1 -0
  15. data/data/butler/plugins/core/ping.rb +22 -0
  16. data/data/butler/plugins/core/remote.rb +38 -0
  17. data/data/butler/plugins/dev/eval.rb +6 -4
  18. data/data/butler/plugins/dev/onhandlers.rb +93 -0
  19. data/data/butler/plugins/dev/rawlog.rb +109 -45
  20. data/data/butler/plugins/games/countdown.rb +23 -14
  21. data/data/butler/plugins/games/eightball.rb +21 -13
  22. data/data/butler/plugins/games/roll.rb +12 -12
  23. data/data/butler/plugins/irc/join.rb +2 -2
  24. data/data/butler/plugins/irc/notice.rb +10 -10
  25. data/data/butler/plugins/irc/part.rb +12 -12
  26. data/data/butler/plugins/irc/privmsg.rb +10 -10
  27. data/data/butler/plugins/irc/quit.rb +12 -12
  28. data/data/butler/plugins/operator/devoice.rb +1 -1
  29. data/data/butler/plugins/public/help.rb +10 -4
  30. data/data/butler/plugins/{util → service}/calculator.rb +0 -0
  31. data/data/butler/plugins/service/define.rb +16 -13
  32. data/data/butler/plugins/service/log.rb +85 -0
  33. data/data/butler/plugins/service/seen.rb +64 -0
  34. data/data/butler/plugins/service/svn.rb +6 -5
  35. data/data/butler/plugins/util/load.rb +3 -1
  36. data/data/butler/services/org.rubyforge.butler/calculator/1/service.rb +96 -0
  37. data/data/butler/services/org.rubyforge.butler/log/1/service.rb +148 -68
  38. data/lib/access/admin.rb +5 -0
  39. data/lib/blank.rb +32 -0
  40. data/lib/butler.rb +4 -4
  41. data/lib/butler/bot.rb +118 -33
  42. data/lib/butler/control.rb +5 -4
  43. data/lib/butler/debuglog.rb +12 -4
  44. data/lib/butler/dialog.rb +1 -1
  45. data/lib/butler/initialvalues.rb +1 -1
  46. data/lib/butler/irc/client.rb +31 -12
  47. data/lib/butler/irc/message.rb +32 -13
  48. data/lib/butler/irc/parser.rb +67 -30
  49. data/lib/butler/irc/parser/{commands.rb → command.rb} +0 -38
  50. data/lib/butler/irc/parser/generic.rb +9 -12
  51. data/lib/butler/irc/parser/rfc2812.rb +40 -2
  52. data/lib/butler/irc/socket.rb +66 -41
  53. data/lib/butler/irc/string.rb +1 -5
  54. data/lib/butler/plugin.rb +56 -23
  55. data/lib/butler/plugin/configproxy.rb +1 -0
  56. data/lib/butler/plugin/more.rb +2 -2
  57. data/lib/butler/plugins.rb +7 -1
  58. data/lib/butler/remote/connection.rb +113 -0
  59. data/lib/butler/remote/message.rb +157 -0
  60. data/lib/butler/remote/server.rb +85 -0
  61. data/lib/butler/remote/user.rb +46 -0
  62. data/lib/butler/service.rb +2 -1
  63. data/lib/butler/services.rb +2 -2
  64. data/lib/butler/version.rb +2 -2
  65. data/lib/configuration.rb +13 -16
  66. data/lib/ostructfixed.rb +0 -6
  67. data/lib/ruby/array/random.rb +2 -1
  68. data/lib/scriptfile.rb +63 -14
  69. data/lib/timingoutresource.rb +54 -0
  70. data/test/test_scriptfile.rb +51 -0
  71. metadata +63 -61
  72. data/data/butler/dialogs/en/sync_plugins.yaml +0 -3
  73. data/data/butler/dialogs/sync_plugins.rb +0 -30
  74. data/data/butler/services/org.rubyforge.butler/calculator/1/calculator.rb +0 -68
  75. data/lib/log/splitter.rb +0 -30
  76. data/test/test_access/privilege/banners.statistics.yaml +0 -3
  77. data/test/test_access/privilege/banners.yaml +0 -3
  78. data/test/test_access/privilege/news.create.yaml +0 -3
  79. data/test/test_access/privilege/news.delete.yaml +0 -3
  80. data/test/test_access/privilege/news.edit.yaml +0 -3
  81. data/test/test_access/privilege/news.read.yaml +0 -3
  82. data/test/test_access/privilege/news.yaml +0 -3
  83. data/test/test_access/privilege/paid_content.yaml +0 -3
  84. data/test/test_access/privilege/statistics.ftp.yaml +0 -3
  85. data/test/test_access/privilege/statistics.web.yaml +0 -3
  86. data/test/test_access/privilege/statistics.yaml +0 -3
  87. data/test/test_access/role/chiefeditor.yaml +0 -7
  88. data/test/test_access/role/editor.yaml +0 -9
  89. data/test/test_access/user/test.yaml +0 -12
@@ -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]
@@ -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/commands'
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
- # RFC 2812, Parser itself uses a simplified matching
28
- module Expressions
29
- Special = /[\[\]\\`_^{|}]/
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(RMessage)
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(RHostmask)
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
- "CHANNELLEN" => 50,
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 value
21
- when nil
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("505", :ERR__NOPRIVMSG)
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}([@+-]{0,3}) :(\d+) (.*)/,
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])
@@ -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#write is the only synchronized method, since all other
30
- # methods build up on it, IRC::Socket should be safe in threaded environments.
31
- # Butler::IRC::Socket#read is NOT synchronized, so unless you read from only
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#write will just warn and send the overlength message.
35
- # If you are looking for queries (commands that get an answer from the server)
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("Connected to #{@server}:#{@port} from #{@host || '<default>'}")
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 write(data)
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
- write("NICK #{nickname}")
198
- write("USER #{username} 0 * :#{realname}")
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
- write("NS :IDENTIFY #{password}")
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
- write("NS :GHOST #{nickname} #{password}")
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
- write("PRIVMSG #{recipient} :#{message}")
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
- write("PRIVMSG #{recipient} :"+(1.chr)+"ACTION "+message+(1.chr))
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
- write("NOTICE #{recipient} :#{message}")
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
- write("PONG")
268
+ write_with_eol("PONG")
258
269
  else
259
- write("PONG #{args.join(' ')}")
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
- write("JOIN #{channel} #{password}")
280
+ write_with_eol("JOIN #{channel} #{password}")
270
281
  else
271
- write("JOIN #{channel}")
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
- write("PART #{channel} #{reason}")
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
- write("NICK #{nick}")
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
- write("AWAY :#{reason}")
312
+ write_with_eol("AWAY :#{reason}")
302
313
  end
303
314
 
304
315
  # reset your away status to back
305
316
  def back
306
- write("AWAY")
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
- write("KICK #{channel} #{user} :#{reason}")
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
- write("MODE #{channel} #{mode}")
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
- write("MODE #{channel} +#{'o'*users.length} #{users*' '}")
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
- write("MODE #{channel} -#{'o'*users.length} #{users*' '}")
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
- write("MODE #{channel} +#{'v'*users.length} #{users*' '}")
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
- write("MODE #{channel} -#{'v'*users.length} #{users*' '}")
360
+ multiple_mode(channel, '-', 'v', users)
341
361
  end
342
362
 
343
363
  # Set ban in channel to mask
344
- def ban(mask, channel)
345
- write("MODE #{channel} +b #{mask}")
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
- write("WHO #{channel}")
375
+ write_with_eol("WHO #{channel}")
351
376
  end
352
377
 
353
378
  # Send a "whois" to server
354
379
  def whois(nick)
355
- write("WHOIS #{nick}")
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
- write("QUIT :#{reason}")
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