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
@@ -46,10 +46,6 @@ class String
46
46
  strip_user_prefixes.valid_nickname?
47
47
  end
48
48
 
49
- def same_nick?(other)
50
- strip_user_prefixes.downcase == other.to_str.strip_user_prefixes.downcase
51
- end
52
-
53
49
  # removes indicators from nicknames and channelnames
54
50
  def strip_user_prefixes
55
51
  prefixes = Butler::IRC::User::PREFIXES.dup
@@ -126,7 +122,7 @@ class String
126
122
 
127
123
  Note: not every font/size combination displays bold/italic text.
128
124
  =end
129
- def mirc_formatted # FIXME - copied code, ugly & slow
125
+ def mirc_formatted
130
126
  self.gsub( /!\[(.*?)\]/ ) do |match|
131
127
  codes = $1.downcase
132
128
  repl = ""
@@ -42,8 +42,9 @@ class Butler
42
42
  @name = File.basename(base).freeze
43
43
  @commands = []
44
44
  @listener = []
45
+ @schedules = []
45
46
  @mapping_type = Hash.new { |h,k| MappingTypes[k] }
46
- @config = ConfigProxy.new(@butler.config, "plugin/#{name}")
47
+ @config = ConfigProxy.new(@butler.config, "plugins/#{base}")
47
48
 
48
49
  if File.directory?(path) then
49
50
  raise "Not supported yet"
@@ -84,18 +85,20 @@ class Butler
84
85
 
85
86
  # a class instance variable for the plugin
86
87
  # defines attr_readers for it
87
- def plugin_attribute(*names)
88
+ def plugin_attribute(name, initial_value=nil)
88
89
  (class <<self; self; end).instance_eval {
89
- attr_reader(*names)
90
+ attr_reader(name)
90
91
  }
92
+ instance_variable_set(:"@#{name}", initial_value)
91
93
  end
92
94
 
93
95
  # a class instance variable for the plugin,
94
96
  # defines attr_accessors for it
95
- def plugin_accessor(*names)
97
+ def plugin_accessor(name, initial_value=nil)
96
98
  (class <<self; self; end).instance_eval {
97
- attr_accessor(*names)
99
+ attr_accessor(name)
98
100
  }
101
+ instance_variable_set(:"@#{name}", initial_value)
99
102
  end
100
103
 
101
104
  def create_templates(tmpl, name) # :nodoc:
@@ -149,15 +152,21 @@ class Butler
149
152
  end
150
153
 
151
154
  def every(*args, &block)
152
- @butler.scheduler.every(*args, &block)
155
+ scheduler = @butler.scheduler.every(*args, &block)
156
+ @schedules << scheduler
157
+ scheduler
153
158
  end
154
159
 
155
160
  def at(*args, &block)
156
- @butler.scheduler.at(*args, &block)
161
+ scheduler = @butler.scheduler.at(*args, &block)
162
+ @schedules << scheduler
163
+ scheduler
157
164
  end
158
165
 
159
166
  def timed(*args, &block)
160
- @butler.scheduler.timed(*args, &block)
167
+ scheduler = @butler.scheduler.timed(*args, &block)
168
+ @schedules << scheduler
169
+ scheduler
161
170
  end
162
171
 
163
172
  def subscribe(*args, &block)
@@ -179,6 +188,7 @@ class Butler
179
188
  info("Unloading plugin '#{@base}' (#{self})")
180
189
  @commands.each { |command| @butler.delete_command(command) }
181
190
  @listener.each { |listener| listener.unsubscribe }
191
+ @schedules.each { |scheduler| scheduler.finished }
182
192
  end
183
193
 
184
194
  end
@@ -243,24 +253,47 @@ class Butler
243
253
  # can see the rest of the text.
244
254
  def answer(text, vars={})
245
255
  text = localize(text, vars) if text.kind_of?(Symbol)
246
- @message.from.session["more"] = More.new(
247
- @message,
248
- nil,
249
- text.mirc_formatted
250
- )
251
- @message.answer("#{@message.from.to_s+': ' if @message.public?}#{@message.from.session['more'].show}")
256
+ if @message.remote? then
257
+ @message.answer(text)
258
+ else
259
+ @message.from.session["more"] = More.new(
260
+ @message,
261
+ nil,
262
+ text.mirc_formatted
263
+ )
264
+ @message.answer("#{@message.from.to_s+': ' if @message.public?}#{@message.from.session['more'].show}")
265
+ end
252
266
  end
253
267
 
254
268
  # Same as answer, but 'more' gets a lead prefixed (see Butler::Plugin::More for more info)
255
- def answer_with_lead(lead, string, vars={})
269
+ def answer_with_lead(lead, text, vars={})
270
+ lead = localize(lead, vars) if lead.kind_of?(Symbol)
256
271
  text = localize(text, vars) if text.kind_of?(Symbol)
257
- lead = localize(lead, vars) if text.kind_of?(Symbol)
258
- @message.from.session["more"] = More.new(
259
- @message,
260
- lead.mirc_formatted,
261
- text.mirc_formatted
262
- )
263
- @message.answer("#{@message.from.to_s+': ' if @message.public?}#{@message.from.session['more'].show}")
272
+ if @message.remote? then
273
+ @message.answer(lead+text) # FIXME, what does More use to concat lead & text?
274
+ else
275
+ @message.from.session["more"] = More.new(
276
+ @message,
277
+ lead.mirc_formatted,
278
+ text.mirc_formatted
279
+ )
280
+ @message.answer("#{@message.from.to_s+': ' if @message.public?}#{@message.from.session['more'].show}")
281
+ end
282
+ end
283
+
284
+ # Same as answer, but will not prefix the nick in public messages
285
+ def say(text, vars={})
286
+ text = localize(text, vars) if text.kind_of?(Symbol)
287
+ if @message.remote? then
288
+ @message.answer(text)
289
+ else
290
+ @message.from.session["more"] = More.new(
291
+ @message,
292
+ nil,
293
+ text.mirc_formatted
294
+ )
295
+ @message.answer(@message.from.session['more'].show)
296
+ end
264
297
  end
265
298
 
266
299
 
@@ -300,4 +333,4 @@ class Butler
300
333
  @butler.notice(string.mirc_formatted, *args)
301
334
  end
302
335
  end
303
- end
336
+ end
@@ -17,6 +17,7 @@ class Butler
17
17
  @config = config
18
18
  @base = base
19
19
  @mutex = Mutex.new
20
+ FileUtils.mkdir_p("#{config.base}/#{File.dirname(base)}")
20
21
  end
21
22
 
22
23
  # Synchronize access to this piece of config
@@ -9,7 +9,7 @@
9
9
  class Butler
10
10
  class Plugin
11
11
  class More
12
- MessageLength = 300
12
+ MessageLength = 500
13
13
  String = {
14
14
  "en" => "![c(white),(black)]more...![o]".mirc_formatted,
15
15
  "de" => "![c(white),(black)]mehr...![o]".mirc_formatted,
@@ -54,7 +54,7 @@ class Butler
54
54
 
55
55
  # show adds lead and tail depending on requirement (no tail for last message, no lead if empty)
56
56
  def show
57
- "#{lead+': ' if lead}#{@pieces[@index]}#{', '+tail if succ?}"
57
+ "#{lead+': ' if lead}#{@pieces[@index]}#{' '+tail if succ?}"
58
58
  end
59
59
 
60
60
  def tail
@@ -102,6 +102,8 @@ class Butler
102
102
  end
103
103
  begin
104
104
  unload(base) if loaded?(base) # a plugin may not be loaded twice
105
+ rescue Interrupt
106
+ raise
105
107
  rescue Exception => e
106
108
  exception(e)
107
109
  end
@@ -114,6 +116,8 @@ class Butler
114
116
  plugin.load_plugin(@butler, base, path)
115
117
  plugin.class_eval(File.read(codefile), codefile)
116
118
  plugin.on_load
119
+ rescue Interrupt
120
+ raise
117
121
  rescue Exception => e
118
122
  e.extend Exception::Detailed
119
123
  e.prepend "Loading plugin #{base} failed."
@@ -126,8 +130,10 @@ class Butler
126
130
 
127
131
  def unload(base)
128
132
  begin
129
- @plugins[base].on_unload
130
133
  @plugins[base].unload_plugin
134
+ @plugins[base].on_unload
135
+ rescue Interrupt
136
+ raise
131
137
  rescue Exception => e
132
138
  e.extend Exception::Detailed
133
139
  e.prepend "Exception raised while unloading plugin #{base}."
@@ -0,0 +1,113 @@
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ require 'timeout'
10
+
11
+
12
+
13
+ class Butler
14
+ module Remote
15
+ class Connection
16
+ def self.open_thread(server, socket)
17
+ connection = new(server, socket)
18
+ Thread.new {
19
+ connection.start
20
+ connection.read_loop
21
+ }
22
+ connection
23
+ end
24
+
25
+ def self.open(server, socket)
26
+ connection = new(server, socket)
27
+ connection.start
28
+ connection
29
+ end
30
+
31
+ include Log::Comfort
32
+
33
+ attr_reader :user
34
+
35
+ def initialize(server, socket)
36
+ @server = server
37
+ @socket = socket
38
+ @user = nil
39
+ @logger = nil
40
+ end
41
+
42
+ def start
43
+ puts("Please identify yourself")
44
+ tries = 3
45
+ access = nil
46
+ username = nil
47
+ timeout(180) {
48
+ tries.times { |i|
49
+ @socket.print("Username: ")
50
+ username = @socket.gets.chomp
51
+ @socket.print("Password: ")
52
+ password = @socket.gets.chomp
53
+ access = @server.login(username, password)
54
+ break if access
55
+ sleep(i)
56
+ puts("Failed to authenticate, please try again.")
57
+ }
58
+ }
59
+ return quit("Failed to authenticate") unless access
60
+ puts "You're logged in, welcome #{username}."
61
+ puts "To terminate the session, enter an empty line."
62
+ puts ""
63
+ @user = User.new(username, access)
64
+ rescue Timeout::Error
65
+ quit("Login timed out.")
66
+ rescue Exception => e
67
+ exception(e)
68
+ quit(nil)
69
+ end
70
+
71
+ def read_loop
72
+ while line = @socket.gets
73
+ line.chomp!
74
+ p line
75
+ break if line.empty?
76
+ begin
77
+ @server.dispatch(Message.new(@server, self, @user, line))
78
+
79
+ # on these errors we got to get out of the loop
80
+ rescue Errno::EPIPE, SocketError
81
+ raise
82
+ # all others should not put the read-thread in any danger
83
+ rescue Exception => e
84
+ exception(e)
85
+ end
86
+ end
87
+ rescue Exception => e
88
+ exception(e)
89
+ ensure
90
+ @server.disconnected(self)
91
+ close
92
+ end
93
+
94
+ def puts(*text)
95
+ @socket.puts(*text)
96
+ end
97
+
98
+ def quit(reason=nil)
99
+ puts reason if reason
100
+ close
101
+ @server.disconnected(self)
102
+ end
103
+
104
+ def closed?
105
+ @socket.closed?
106
+ end
107
+
108
+ def close
109
+ @socket.close unless @socket.closed?
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,157 @@
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ class Butler
10
+ module IRC
11
+ class Message
12
+ def remote?
13
+ false
14
+ end
15
+ end
16
+ end
17
+
18
+ module Remote
19
+ # Butler::Remote::Message emulates a Butler::IRC::Message
20
+ class Message
21
+ Command = "privmsg".freeze
22
+
23
+ attr_reader :text
24
+ attr_reader :from
25
+
26
+ # the language of this message, as determined by butler
27
+ attr_accessor :language
28
+
29
+ # the invocation-sequence (e.g. "butler, "), nil if none was used (e.g. in
30
+ # private messages)
31
+ attr_accessor :invocation
32
+
33
+ def initialize(server, client, from, text)
34
+ @server = server
35
+ @client = client
36
+ @from = from
37
+ @text = text
38
+ @language = nil
39
+ @invocation = nil
40
+ @arguments = nil
41
+ @post_arguments = nil
42
+ end
43
+
44
+ # FIXME complete
45
+ def initialize_copy(original) #:nodoc:
46
+ super
47
+ @server = original.instance_variable_get(:@server)
48
+ @client = original.instance_variable_get(:@client)
49
+ @from = original.instance_variable_get(:@from)
50
+ @text = original.text.dup
51
+ end
52
+
53
+ # parses a given string into argument-tokens. a token is either a word or a quoted string. escapes are respected.
54
+ # e.g. 'Hallo "this is token2" "and this \"token\" is token3"'
55
+ # would be parsed into: ["hallo", "this is token2", "and this \"token\" is token3"]
56
+ def arguments
57
+ return [] unless text # only messages with text can be tokenized to parameters
58
+ @arguments ||= begin # don't do double-work
59
+ text[@invocation.length..-1].arguments
60
+ rescue String::SingleQuoteException => ex
61
+ ex.pre+[ex.post.join(" ")]
62
+ end
63
+ end
64
+
65
+ # FIXME due to it's current working, post_arguments will strip formatting
66
+ # Returns the rest of message text after argument
67
+ # == Synopsis
68
+ # "foo bar baz".post_arguments[0] # => "foo bar baz"
69
+ # "foo bar baz".post_arguments[1] # => "bar baz"
70
+ # "foo bar baz".post_arguments[2] # => "baz"
71
+ def post_arguments
72
+ return [] unless text # only messages with text can be tokenized to parameters
73
+ @post_arguments ||= text[@invocation.length..-1].post_arguments # don't do double-work
74
+ end
75
+
76
+ alias raw text
77
+ alias params text
78
+
79
+ def symbol
80
+ :PRIVMSG
81
+ end
82
+
83
+ def prefix
84
+ nil
85
+ end
86
+
87
+ def command
88
+ Command
89
+ end
90
+
91
+ def realm
92
+ :private
93
+ end
94
+
95
+ def remote?
96
+ true
97
+ end
98
+
99
+ def private?
100
+ true
101
+ end
102
+
103
+ def public?
104
+ false
105
+ end
106
+
107
+ def answer(text)
108
+ @client.puts text.mirc_stripped
109
+ end
110
+
111
+ def identified?
112
+ true
113
+ end
114
+
115
+ def for
116
+ @server.myself
117
+ end
118
+
119
+ def channel
120
+ nil
121
+ end
122
+
123
+ def text
124
+ @text
125
+ end
126
+
127
+ # access to raw parsed data
128
+ def [](index)
129
+ nil
130
+ end
131
+
132
+ def []=(index, value)
133
+ raise NoMethodError, "Can't write to #{index}" # unless respond_to?("#{index}=")
134
+ end
135
+
136
+ def has_key?(key)
137
+ false #@fields.has_key?(key.to_sym)
138
+ end
139
+
140
+ # change charset encoding
141
+ def transcode!(from, to)
142
+ end
143
+
144
+ def ===(other)
145
+ case other
146
+ when Symbol: :PRIVMSG == other
147
+ when Regexp: @text =~ other
148
+ end
149
+ end
150
+
151
+ # return a hash of the fields
152
+ def to_hash
153
+ raise NoMethodError
154
+ end
155
+ end
156
+ end
157
+ end