zmb 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -13,7 +13,7 @@ zmb is a complete messenger bot supporting irc, and command line interface.
13
13
 
14
14
  This command will use the default settings location of ~/.zmb, you can pass `-s <PATH>` to change this.
15
15
 
16
- zmb --create
16
+ zmb --wizard
17
17
 
18
18
  ### Launching the bot
19
19
  zmb --daemon
@@ -27,7 +27,7 @@ You can run zmb in a shell mode to test plugins without even connecting to any i
27
27
  ### Included plugins
28
28
 
29
29
  - IRC - Connect to a IRC server
30
- - Quote - Quotes
30
+ - Quote
31
31
  - Poll - Voting system
32
32
  - Relay - Relay between servers and/or channels
33
33
  - Users - User management
@@ -39,6 +39,10 @@ You can run zmb in a shell mode to test plugins without even connecting to any i
39
39
  - Security - Hashes, rot13, and morse code
40
40
  - Random - Pick a random value from a list, yes or no, coinflip
41
41
  - URL - Dpaste, pastie, bitly, tinyurl, isgd
42
+ - Translate - Translate a message into another language
43
+ - RSS - Subscribe and watch RSS/ATOM feeds
44
+ - Usermodes - Auto opping, voicing users in a channel
45
+ - Weather - Get the weather for a town/city
42
46
  - Bank - Points system
43
47
 
44
48
  #### Other features
@@ -50,5 +54,3 @@ You can run zmb in a shell mode to test plugins without even connecting to any i
50
54
  ### Support
51
55
 
52
56
  You can find support at #zmb @ efnet.
53
-
54
- For complete documentation please visit [Documentation](http://kylefuller.co.uk/projects/zmb/)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.1
1
+ 0.3.2
data/bin/zmb CHANGED
@@ -6,25 +6,11 @@ require 'zmb'
6
6
  require 'optparse'
7
7
 
8
8
  class AdminUser
9
- attr_accessor :username, :userhosts, :permissions
10
-
11
- def initialize
12
- @username = 'admin'
13
- @userhosts = []
14
- @permissions = ['admin']
15
- end
16
-
17
- def admin?
18
- true
19
- end
20
-
21
- def permission?(perm)
22
- true
23
- end
24
-
25
- def authenticated?
26
- true
27
- end
9
+ def username; 'admin'; end
10
+ def userhost; 'admin@zmb'; end
11
+ def admin?; true; end
12
+ def permission?(perm); true; end
13
+ def authenticated?; true; end
28
14
  end
29
15
 
30
16
  class Event
@@ -32,34 +18,63 @@ class Event
32
18
 
33
19
  def initialize(message)
34
20
  @message = message
35
- @user = AdminUser.new
36
- end
37
-
38
- def message?
39
- true
40
- end
41
-
42
- def private?
43
- true
44
- end
45
-
46
- def reply(msg)
47
- puts "> #{msg}"
48
21
  end
49
22
 
50
- def name
51
- "admin"
23
+ def message?; true; end
24
+ def private?; true; end
25
+ def name; "admin"; end
26
+ def user; AdminUser.new; end
27
+ def reply(msg); notice "#{msg}"; end
28
+ end
29
+
30
+ class Tty
31
+ class <<self
32
+ def blue; color 34; end
33
+ def red; color 31; end
34
+ def yellow; color 33 ; end
35
+ def reset; escape 0; end
36
+
37
+ private
38
+ def color n
39
+ escape "0;#{n}"
40
+ end
41
+ def bold n
42
+ escape "1;#{n}"
43
+ end
44
+ def underline n
45
+ escape "4;#{n}"
46
+ end
47
+ def escape n
48
+ "\033[#{n}m" if $stdout.tty?
49
+ end
52
50
  end
53
51
  end
54
52
 
55
- def ask(question)
56
- puts "#{question} (yes/no)"
53
+ def question(message)
54
+ print "#{Tty.blue}[ #{Tty.yellow}?? #{Tty.blue}]#{Tty.reset} #{message}"
55
+ end
56
+
57
+ def warning(message)
58
+ puts "#{Tty.blue}[ #{Tty.yellow}** #{Tty.blue}]#{Tty.reset} #{message}"
59
+ end
60
+
61
+ def error(message)
62
+ puts "#{Tty.blue}[ #{Tty.red}!! #{Tty.blue}]#{Tty.reset} #{message}"
63
+ end
64
+
65
+ def notice(message)
66
+ puts "#{Tty.blue}[#{Tty.reset} ok #{Tty.blue}]#{Tty.reset} #{message}"
67
+ end
68
+
69
+ def question_bool(message)
70
+ question(message + ' (yes/no): ')
57
71
  answer = gets.chomp
58
72
  answer == 'yes' or answer == 'y'
59
73
  end
60
74
 
61
- def get_value(question)
62
- puts question
75
+ def question_string(message='')
76
+ message += ': ' unless message == ''
77
+ question(message)
63
78
  answer = gets.chomp
64
79
 
65
80
  return nil if answer == ''
@@ -69,15 +84,15 @@ end
69
84
  def wizard(zmb, plugin)
70
85
  STDOUT.flush
71
86
 
72
- if ask("Would you like to add the #{plugin.name} plugin? #{plugin.description}") then
87
+ if question_bool("Would you like to add the #{plugin.name} plugin? #{plugin.description}") then
73
88
  if plugin.multi_instances? then
74
- instance = get_value("What would you like to name this instance of #{plugin.name}?")
89
+ instance = question_string("What would you like to name this instance of #{plugin.name}?")
75
90
  else
76
91
  instance = plugin.name
77
92
  end
78
93
 
79
94
  if not instance then
80
- puts "Must supply instance name, if this plugin should only be loaded once such as commands or users then you can call it that."
95
+ notice "Must supply instance name, if this plugin should only be loaded once such as commands or users then you can call it that."
81
96
  return wizard(zmb, plugin)
82
97
  end
83
98
 
@@ -89,7 +104,7 @@ def wizard(zmb, plugin)
89
104
 
90
105
  obj.wizard.each do |key, value|
91
106
  if value.has_key?('help') then
92
- set = get_value("#{value['help']} (default=#{value['default']})")
107
+ set = question_string("#{value['help']} (default=#{value['default']})")
93
108
  settings[key] = set if set
94
109
  end
95
110
  end
@@ -105,6 +120,11 @@ options = {}
105
120
  optparse = OptionParser.new do |opts|
106
121
  opts.banner = "Usage: zmb [options]"
107
122
 
123
+ opts.on('-h', '--help', 'Displays this usage screen') do
124
+ puts optparse
125
+ exit
126
+ end
127
+
108
128
  options[:settings] = nil
109
129
  opts.on('-s', '--settings SETTING', 'Use a settings folder') do |settings|
110
130
  options[:settings] = settings
@@ -115,13 +135,13 @@ optparse = OptionParser.new do |opts|
115
135
  options[:daemon] = true
116
136
  end
117
137
 
118
- options[:create] = false
119
- opts.on('-c', '--create', 'Create a new ZMB settings file') do
120
- options[:create] = true
138
+ options[:wizard] = false
139
+ opts.on('-w', '--wizard', 'Interactively create a new config file') do
140
+ options[:wizard] = true
121
141
  end
122
142
 
123
143
  options[:shell] = false
124
- opts.on('-b', '--shell', 'Create a commands shell') do
144
+ opts.on('-S', '--shell', 'Create a commands shell') do
125
145
  options[:shell] = true
126
146
  end
127
147
 
@@ -130,9 +150,15 @@ optparse = OptionParser.new do |opts|
130
150
  options[:command] = line
131
151
  end
132
152
 
133
- options[:fork] = false
134
- opts.on('-f', '--fork', 'Detach (fork) ZMB (for use with -d daemon)') do
135
- options[:fork] = true
153
+ options[:foreground] = false
154
+ opts.on('-f', '--foreground', 'Don\'t fork into the background') do
155
+ options[:foreground] = true
156
+ end
157
+
158
+ options[:debug] = false
159
+ opts.on('-D', '--daemon', 'Output debugging information (Implies -f)') do
160
+ options[:foreground] = true
161
+ options[:debug] = true
136
162
  end
137
163
  end
138
164
 
@@ -140,33 +166,33 @@ optparse.parse!
140
166
 
141
167
  if not options[:settings] then
142
168
  options[:settings] = File.expand_path('~/.zmb')
143
- puts "No settings file specified, will use #{options[:settings]}"
169
+ notice "No settings file specified, will use #{options[:settings]}"
144
170
  end
145
171
 
146
172
  zmb = Zmb.new(options[:settings])
147
173
 
148
- if options[:create] then
174
+ if options[:wizard] then
149
175
  STDOUT.flush
150
176
 
151
177
  zmb.save
152
178
 
153
- while ask('Would you like to add additional plugin sources?')
154
- source = get_value('Which path?')
179
+ while question_bool('Would you like to add additional plugin sources?')
180
+ source = question_string('Which path?')
155
181
  if source and File.exists?(source) then
156
182
  zmb.plugin_manager.add_plugin_source source
157
- puts 'Source added'
183
+ notice "Source added"
158
184
  zmb.save
159
185
  else
160
- puts 'Invalid source'
186
+ warning "Invalid source directory, does this folder exist?"
161
187
  end
162
188
  end
163
189
 
164
190
  zmb.plugin_manager.plugins.reject{ |plugin| zmb.instances.has_key? plugin.name }.each{ |plugin| wizard(zmb, plugin) }
165
191
 
166
- if zmb.instances.has_key?('users') and ask('Would you like to add a admin user?') then
167
- username = get_value('Username:')
168
- password = get_value('Password:')
169
- userhost = get_value('Userhost: (Leave blank for none)')
192
+ if zmb.instances.has_key?('users') and question_bool('Would you like to add a admin user?') then
193
+ username = question_string('Username:')
194
+ password = question_string('Password:')
195
+ userhost = question_string('Userhost: (Leave blank for none)')
170
196
  zmb.instances['users'].create_user(username, password, userhost).permit('admin')
171
197
  end
172
198
 
@@ -183,18 +209,22 @@ if options[:shell] then
183
209
 
184
210
  begin
185
211
  while 1
186
- zmb.event(nil, Event.new(gets.chomp))
212
+ zmb.event(nil, Event.new(question_string))
187
213
  end
188
214
  rescue Interrupt
189
215
  zmb.save
216
+ puts
217
+ warning "Exiting"
190
218
  end
191
219
  end
192
220
 
221
+ zmb.debug = options[:debug]
222
+
193
223
  if options[:daemon] then
194
- if options[:fork] then
195
- Process.detach(fork { STDOUT.reopen(File.open('/dev/null','w')); zmb.run })
196
- puts "ZMB detached"
197
- else
224
+ if options[:foreground] then
198
225
  zmb.run
226
+ warning "Exiting"
227
+ else
228
+ notice "Forking into the background (pid: #{zmb.run_fork})"
199
229
  end
200
230
  end
data/lib/zmb.rb CHANGED
@@ -14,16 +14,21 @@ require 'zmb/event'
14
14
  require 'zmb/timer'
15
15
 
16
16
  class Zmb
17
- attr_accessor :instances, :plugin_manager, :settings_manager, :plugin_sources
17
+ attr_accessor :instances, :plugin_manager, :settings_manager, :plugin_sources, :debug
18
18
 
19
19
  def plugin
20
20
  'zmb'
21
21
  end
22
22
 
23
+ def instance
24
+ 'zmb'
25
+ end
26
+
23
27
  def initialize(config_dir)
24
- @debug = true
28
+ @debug = false
29
+ @running = false
25
30
 
26
- @plugin_manager = PluginManager.new
31
+ @plugin_manager = PluginManager.new(self)
27
32
  @settings_manager = Settings.new(config_dir)
28
33
 
29
34
  @instances = {'zmb' => self}
@@ -45,8 +50,9 @@ class Zmb
45
50
  @plugin_manager.add_plugin_source plugin_dir
46
51
 
47
52
  @settings_manager.get('zmb', 'plugin_instances', []).each{|instance| load instance}
53
+ @debug = @settings_manager.get('zmb', 'debug', false)
48
54
 
49
- @running = false
55
+ trap("HUP") { @plugin_manager.refresh_plugin_sources; load "commands"; load "users" }
50
56
  end
51
57
 
52
58
  def running?
@@ -57,13 +63,39 @@ class Zmb
57
63
  {
58
64
  'plugin_sources' => @plugin_sources,
59
65
  'plugin_instances' => @instances.keys,
66
+ 'debug' => @debug,
60
67
  }
61
68
  end
62
69
 
63
70
  def save
71
+ debug(self, "Saving settings")
64
72
  @instances.each{ |k,v| @settings_manager.save(k, v) }
65
73
  end
66
74
 
75
+ def debug(sender, message, exception=nil)
76
+ return unless @debug
77
+ line = Array.new
78
+
79
+ if sender then
80
+ if sender.respond_to?('instance') then
81
+ line << "(#{sender.instance})"
82
+ elsif sender == self
83
+ line << "(core)"
84
+ elsif sender == plugin_manager
85
+ line << "(plugins)"
86
+ else
87
+ line << "(#{sender})"
88
+ end
89
+ else
90
+ line << "(unknown)"
91
+ end
92
+
93
+ line << message
94
+ line << exception if exception
95
+
96
+ puts line.join(' ')
97
+ end
98
+
67
99
  def load(key)
68
100
  return true if @instances.has_key?(key)
69
101
 
@@ -91,6 +123,7 @@ class Zmb
91
123
  end
92
124
 
93
125
  def run
126
+ debug(self, 'Start runloop')
94
127
  post! :running, self
95
128
 
96
129
  @running = true
@@ -100,10 +133,30 @@ class Zmb
100
133
  timer_run
101
134
  end
102
135
  rescue Interrupt
136
+ debug(self, 'Runloop interrupted')
103
137
  save
104
138
  end
105
139
  end
106
140
 
141
+ def run_fork
142
+ @running = false
143
+ pid = fork {
144
+ STDIN.reopen(File.open('/dev/null','r'))
145
+ STDOUT.reopen(File.open('/dev/null','w'))
146
+ STDERR.reopen(File.open('/dev/null','w'))
147
+ run
148
+ }
149
+
150
+ Process.detach(pid)
151
+ debug(self, 'zmb Forked')
152
+ pid
153
+ end
154
+
155
+ def fork_command(e=nil)
156
+ run_fork
157
+ "Forked"
158
+ end
159
+
107
160
  def timeout
108
161
  if timer_timeout > @maximum_timeout
109
162
  if @sockets.size < 1 then
@@ -119,6 +172,7 @@ class Zmb
119
172
  end
120
173
 
121
174
  def socket_add(delegate, socket)
175
+ debug(delegate, "Socked added")
122
176
  @sockets[socket] = delegate
123
177
  end
124
178
 
@@ -139,7 +193,9 @@ class Zmb
139
193
  result[0].select{|sock| @sockets.has_key?(sock)}.each do |sock|
140
194
  if sock.eof? then
141
195
  @sockets[sock].disconnected(self, sock) if @sockets[sock].respond_to?('disconnected')
196
+ sock.close
142
197
  socket_delete sock
198
+ debug(@sockets[sock], "Socket EOF")
143
199
  else
144
200
  @sockets[sock].received(self, sock, sock.gets()) if @sockets[sock].respond_to?('received')
145
201
  end
@@ -148,6 +204,7 @@ class Zmb
148
204
  end
149
205
 
150
206
  def timer_add(timer)
207
+ debug(timer.delegate, "Timer added (#{timer.symbol})")
151
208
  @timers << timer
152
209
  end
153
210
 
@@ -161,14 +218,23 @@ class Zmb
161
218
  end
162
219
 
163
220
  def timer_run
164
- @timers.select{|timer| timer.timeout <= 0.0 and timer.respond_to?("fire") }.each{|timer| timer.fire(self)}
221
+ @timers.select{|timer| timer.timeout <= 0.0 and timer.respond_to?("fire") }.each do |timer|
222
+ debug(timer.delegate, "Timer #{timer.symbol} fired")
223
+ timer.fire(self)
224
+ end
165
225
  end
166
226
 
167
227
  def post(signal, *args)
168
228
  results = Array.new
169
229
 
170
230
  @instances.select{|name, instance| instance.respond_to?(signal)}.each do |name, instance|
171
- results << instance.send(signal, *args) rescue nil
231
+ begin
232
+ result = instance.send(signal, *args)
233
+ break if result == :halt
234
+ results << result
235
+ rescue Exception
236
+ debug(instance, "Sending signal `#{signal}` failed", $!)
237
+ end
172
238
  end
173
239
 
174
240
  results
@@ -176,7 +242,11 @@ class Zmb
176
242
 
177
243
  def post!(signal, *args) # This will exclude the plugin manager
178
244
  @instances.select{|name, instance| instance.respond_to?(signal) and instance != self}.each do |name, instance|
179
- instance.send(signal, *args) rescue nil
245
+ begin
246
+ break if instance.send(signal, *args) == :halt
247
+ rescue Exception
248
+ debug(instance, "Sending signal `#{signal}` failed", $!)
249
+ end
180
250
  end
181
251
  end
182
252
 
@@ -198,8 +268,6 @@ class Zmb
198
268
  end
199
269
 
200
270
  def event(sender, e)
201
- puts e.line if @debug and e.respond_to?('line')
202
-
203
271
  Thread.new do
204
272
  post! :pre_event, sender, e
205
273
  post! :event, sender, e
@@ -221,6 +289,14 @@ class Zmb
221
289
  'addsource' => [:addource_command, 1, { :permission => 'admin' }],
222
290
  'refresh' => [:refresh_command, 1, { :permission => 'admin' }],
223
291
  'quit' => [:quit_command, 0, { :permission => 'admin' }],
292
+ 'debug' => [:debug_command, 0, {
293
+ :permission => 'admin',
294
+ :help => 'Toggle debug' }],
295
+ 'stdout' => [:stdout_command, {
296
+ :permission => 'admin',
297
+ :usage => '/dev/null' }],
298
+ 'fork' => [:fork_command, 0, {
299
+ :permission => 'admin' }],
224
300
  }
225
301
  end
226
302
 
@@ -331,4 +407,19 @@ class Zmb
331
407
  @running = false
332
408
  instances.keys.each{ |i| unload(i) }
333
409
  end
410
+
411
+ def debug_command(e)
412
+ @debug = (not @debug)
413
+
414
+ if @debug then
415
+ "Debugging enabled"
416
+ else
417
+ "Debugging disabled"
418
+ end
419
+ end
420
+
421
+ def stdout_command(e, out)
422
+ STDOUT.reopen(File.open(out,'w'))
423
+ "STDOUT set to #{out}"
424
+ end
334
425
  end
@@ -1,7 +1,8 @@
1
1
  class PluginManager
2
2
  attr_accessor :plugin_sources, :plugins
3
3
 
4
- def initialize
4
+ def initialize(delegate=nil)
5
+ @delegate = delegate
5
6
  @plugins = Array.new
6
7
  @plugin_sources = Array.new
7
8
  end
@@ -22,7 +23,7 @@ class PluginManager
22
23
  definition.definitition_file = File.expand_path(file)
23
24
  @plugins << definition
24
25
  rescue Exception
25
- nil
26
+ @delegate.debug(self, "Cannot load source `#{file}`", $!)
26
27
  end
27
28
  end
28
29
 
@@ -35,9 +36,7 @@ class PluginManager
35
36
  ]
36
37
 
37
38
  definition_files.map do |file|
38
- begin
39
- load_plugin_source(file)
40
- end
39
+ load_plugin_source(file)
41
40
  end
42
41
  end
43
42
 
@@ -1,3 +1,5 @@
1
+ require 'fileutils'
2
+
1
3
  begin
2
4
  require 'json'
3
5
  rescue LoadError
@@ -1,7 +1,7 @@
1
1
  class Timer
2
2
  require 'date'
3
3
 
4
- attr_accessor :delegate
4
+ attr_accessor :delegate, :symbol
5
5
 
6
6
  def initialize(delegate, symbol, interval, repeat=false, data=nil) # interval is in seconds (decimals accepted)
7
7
  @delegate = delegate
@@ -21,7 +21,7 @@ class Timer
21
21
  @delegate.send @symbol
22
22
  end
23
23
  rescue Exception
24
-
24
+ sender.debug(@delegate, "Timer #{@symbol} failed", $!)
25
25
  end
26
26
 
27
27
  if not @repeat
@@ -1,3 +1,5 @@
1
+ require 'cgi'
2
+
1
3
  class String
2
4
  def self.random(len=8)
3
5
  characters = ('a'..'z').to_a + ('1'..'9').to_a
@@ -112,10 +114,10 @@ class Hash
112
114
  map { |k, v|
113
115
  if v.instance_of?(Hash)
114
116
  v.map { |sk, sv|
115
- "#{k}[#{sk}]=#{sv}"
117
+ "#{k}[#{sk}]=#{CGI.escape(sv)}"
116
118
  }.join('&')
117
119
  else
118
- "#{k}=#{v}"
120
+ "#{k}=#{CGI.escape(v)}"
119
121
  end
120
122
  }.join('&')
121
123
  end
@@ -49,5 +49,6 @@ end
49
49
 
50
50
  Plugin.define do
51
51
  name 'alias'
52
+ description 'Alias a command as another command'
52
53
  object Alias
53
54
  end
@@ -0,0 +1,38 @@
1
+ class AutoVoice
2
+ def initialize(sender, s)
3
+ @settings = s
4
+ @settings['noperm'] = true unless @settings.has_key?('noperm')
5
+ end
6
+
7
+ def event(sender, e)
8
+ if e.command == 'join' and e.respond_to?('user') then
9
+ if @settings['noperm'] or e.user.permission?('autovoice') then
10
+ e.delegate.write "MODE #{e.channel} +v #{e.name}"
11
+ end
12
+ end
13
+ end
14
+
15
+ def commands
16
+ {
17
+ 'autovoice' => [:autovoice, 0, {
18
+ :permission => 'admin',
19
+ :help => 'Toggle the permission setting for autovoice'
20
+ }]
21
+ }
22
+ end
23
+
24
+ def autovoice(e)
25
+ @settings['noperm'] = (not @settings['noperm'])
26
+ if @settings['noperm'] then
27
+ "No permissions are required to be autovoiced"
28
+ else
29
+ "The permission `autovoice` is required to be autovoiced"
30
+ end
31
+ end
32
+ end
33
+
34
+ Plugin.define do
35
+ name 'autovoice'
36
+ description 'Auto voice everyone joining a channel where zmb has op'
37
+ object AutoVoice
38
+ end
@@ -125,11 +125,14 @@ class Commands
125
125
  elsif c.has_key?(:proc) then
126
126
  c[:proc].call(e, *args)
127
127
  else
128
+ delegate(self, "Bad command definition (#{cmd})")
128
129
  "Bad command definition"
129
130
  end
130
131
  rescue ArgumentError
131
132
  'incorrect arguments'
132
133
  rescue Exception
134
+ @delegate.debug(c.has_key?(:instance) ? c[:instance] : self, "Command #{cmd} failed", $!)
135
+
133
136
  if e.respond_to?('user') and e.user.admin? and e.private? then
134
137
  "#{$!.message}\n#{$!.inspect}\n#{$!.backtrace[0..2].join("\n")}"
135
138
  else
@@ -172,13 +175,21 @@ class Commands
172
175
  'help' => :help,
173
176
  'instance' => [:instance_command, { :help => 'List all commands availible for a instance.'}],
174
177
  'which' => [:which, { :help => 'Find which plugin handles a command' }],
175
- 'cc' => [:control_command, { :permission => 'admin' }],
176
- 'eval' => [:evaluate, { :permission => 'admin' }],
177
- 'ieval' => [:instance_evaluate, 2, { :permission => 'admin' }],
178
- 'count' => lambda { |e, data| "#{data.split_seperators.size}" },
179
- 'grep' => [:grep, 2],
180
- 'not' => [:not_command, 2],
181
- 'tail' => :tail,
178
+ 'cc' => [:control_command, {
179
+ :permission => 'admin',
180
+ :help => 'Set the control character for commands' }],
181
+ 'eval' => [:evaluate, {
182
+ :permission => 'admin',
183
+ :help => 'Evaluate ruby code' }],
184
+ 'ieval' => [:instance_evaluate, 2, {
185
+ :permission => 'admin',
186
+ :help => 'Evaluate ruby on on a instance',
187
+ :usage => 'commands @cc' }],
188
+ 'count' => [lambda { |e, data| "#{data.split_seperators.size}" }, {
189
+ :help => 'Count the amount of items in a list' } ],
190
+ 'grep' => [:grep, 2, { :help => 'print lines matching a pattern' } ],
191
+ 'not' => [:not_command, 2, { :help => 'Opposite to grep' } ],
192
+ 'tail' => [:tail, { :help => 'List the last three items in a list' }],
182
193
  'echo' => [:echo, { :example => 'Hello, {username}' }],
183
194
  'reverse' => lambda { |e, data| data.reverse },
184
195
  'first' => lambda { |e, data| data.split_seperators.first },
@@ -6,9 +6,18 @@ class DNS
6
6
 
7
7
  def commands
8
8
  {
9
- 'dns' => lambda { |e, host| Resolv.new.getaddress(host) },
10
- 'rdns' => :rdns,
11
- 'whois' => [:whois, 1, { :help => 'perform a whois on a domain' }],
9
+ 'dns' => [lambda { |e, host| Resolv.new.getaddress(host) }, {
10
+ :help => 'Lookup the ip address for a domain',
11
+ :usage => 'domain',
12
+ :example => 'apple.com' }],
13
+ 'rdns' => [:rdns, {
14
+ :help => 'Perform a reverse dns on a host',
15
+ :usage => 'ip',
16
+ :example => '17.149.160.49' }],
17
+ 'whois' => [:whois, 1, {
18
+ :help => 'perform a whois on a domain',
19
+ :usage => 'domain',
20
+ :example => 'apple.com' }],
12
21
  }
13
22
  end
14
23
 
@@ -53,5 +53,6 @@ end
53
53
 
54
54
  Plugin.define do
55
55
  name 'file'
56
+ description 'Plugin to read/write to files'
56
57
  object FileIO
57
58
  end
@@ -4,6 +4,7 @@ require 'zmb/timer'
4
4
 
5
5
  class Event
6
6
  attr_accessor :delegate, :command, :args, :name, :userhost, :message, :channel, :line
7
+ attr_accessor :nick
7
8
 
8
9
  def initialize(sender, line)
9
10
  @line = line
@@ -34,7 +35,7 @@ class Event
34
35
  @name, @userhost = @userhost.split('!', 2)
35
36
  when 'kick'
36
37
  @userhost, @channel, @name, @message = args.split(' ', 4)
37
- nick, @userhost = @userhost.split('!', 2)
38
+ @nick, @userhost = @userhost.split('!', 2)
38
39
  @message = @message[1..-1]
39
40
  end
40
41
  end
@@ -206,8 +207,10 @@ class IrcConnection
206
207
 
207
208
  def write(line)
208
209
  begin
210
+ @delegate.debug(self, "> #{line}")
209
211
  @socket.write line + "\r\n" if @socket
210
212
  rescue
213
+ @delegate.debug(self, "Disconnected at write")
211
214
  disconnected(self, nil)
212
215
  end
213
216
  end
@@ -235,6 +238,7 @@ class IrcConnection
235
238
  @buffer = '' if @buffer == nil
236
239
 
237
240
  while line != nil do
241
+ @delegate.debug(self, "< #{line}")
238
242
  e = Event.new(self, line)
239
243
 
240
244
  # Catch some events
@@ -44,5 +44,6 @@ end
44
44
 
45
45
  Plugin.define do
46
46
  name 'linecount'
47
+ description 'Count the amount of lines users type'
47
48
  object LineCount
48
49
  end
@@ -11,15 +11,32 @@ class Log
11
11
  File.join(path, "#{channel}-#{Time.now.strftime('%d%m%Y')}.log")
12
12
  end
13
13
 
14
+ def time
15
+ t = Time.now
16
+ "#{sprintf("%.2i", t.hour)}:#{sprintf("%.2i", t.min)}:#{sprintf("%.2i", t.sec)}"
17
+ end
18
+
19
+ def log(instance, channel, message)
20
+ File.open(log_file(instance, channel), 'a+') { |f| f.write("[#{time}] #{message}" + "\n") }
21
+ end
22
+
14
23
  def event(sender, e)
15
- if e.respond_to?('channel') and e.respond_to?('line') and e.channel then
16
- File.open(log_file(e.delegate.instance, e.channel), 'a+') { |f| f.write(e.line + "\n") }
24
+ if e.respond_to?('channel') and e.channel then
25
+ if e.command == 'join' then
26
+ log(e.delegate.instance, e.channel, "*** Joins: #{e.name} (#{e.userhost})")
27
+ elsif e.command == 'part'
28
+ log(e.delegate.instance, e.channel, "*** Parts: #{e.name} (#{e.userhost})")
29
+ elsif e.command == 'kick'
30
+ log(e.delegate.instance, e.channel, "*** #{e.nick} was kicked by #{e.name} (#{e.message})")
31
+ elsif e.message?
32
+ log(e.delegate.instance, e.channel, "#{e.name}: #{e.message}")
33
+ end
17
34
  end
18
35
  end
19
36
  end
20
37
 
21
38
  Plugin.define do
22
39
  name 'log'
23
- description 'log everything received from irc'
40
+ description 'Log every message to a log file'
24
41
  object Log
25
42
  end
@@ -3,11 +3,14 @@ class Random
3
3
 
4
4
  def commands
5
5
  {
6
- 'random' => :random,
7
- 'yesno' => [:yesno, 0],
8
- 'headstails' => [:coinflip, 0],
9
- 'coinflip' => [:coinflip, 0],
10
- 'dice' => [lambda { |e| "#{rand(6) + 1}" }, 0],
6
+ 'random' => [:random, {
7
+ :help => 'Select a item randomly from a list',
8
+ :usage => 'item 1, item 2, item 3',
9
+ :example => 'egg, tomatoe, sausage' }],
10
+ 'yesno' => [:yesno, 0, { :help => 'yes or no?' }],
11
+ 'headstails' => [:coinflip, 0, { :help => 'Flip a coin' }],
12
+ 'coinflip' => [:coinflip, 0, { :help => 'Flip a coin' }],
13
+ 'dice' => [lambda { |e| "#{rand(6) + 1}" }, 0, { :help => 'Roll a dice' }],
11
14
  }
12
15
  end
13
16
 
@@ -27,5 +30,6 @@ end
27
30
 
28
31
  Plugin.define do
29
32
  name 'random'
33
+ description 'Commands for coinflips, dice, yesno, random'
30
34
  object Random
31
35
  end
@@ -27,16 +27,17 @@ class Feeds
27
27
  rss = RSS::Parser.parse(feed['feed'].get().body, false)
28
28
  rss.items.each do |item|
29
29
  link = item.link.to_s
30
- link = $1 if link =~ /href="([\w:\/\.\?]*)"/
30
+ link = $1 if link =~ /\b(([\w-]+:\/\/?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|\/)))/
31
31
 
32
32
  break if feed['link'] == link
33
33
  @delegate.instances[feed['instance']].message(feed['sender'], "RSS: #{item.title.to_s.gsub(/<\/?[^>]*>/, "")} (#{link})")
34
34
  end
35
35
 
36
36
  link = rss.items[0].link.to_s
37
- link = $1 if link =~ /href="([\w:\/\.\?]*)"/
37
+ link = $1 if link =~ /\b(([\w-]+:\/\/?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|\/)))/
38
38
  feed['link'] = link
39
39
  rescue Exception
40
+ @delegate.debug(self, "Feed #{feed['feed']} failed", $!)
40
41
  @delegate.instances[feed['instance']].message(feed['sender'], "RSS: #{feed['feed']} failed")
41
42
  end
42
43
  end
@@ -112,5 +113,6 @@ end
112
113
 
113
114
  Plugin.define do
114
115
  name 'rss'
116
+ description 'Subscribe and watch RSS/ATOM feeds'
115
117
  object Feeds
116
118
  end
@@ -13,8 +13,8 @@ class Translate
13
13
  def translate(e, to, from, message)
14
14
  request = JSON.parse("http://ajax.googleapis.com/ajax/services/language/translate".get({
15
15
  :v => '1.0',
16
- :key => 'ABQIAAAAhwR5TtcQxY9fSuKy7yrBJhQ-sC4I4KvMQ8RG81t2M9sVc21w2xQUb9Dipx99m8XrHBsa3OctXe2rQw',
17
- :langpair => "#{to}%7C#{from}",
16
+ :key => 'ABQIAAAAtWAKkpCW6vL4tULVhqm_ZxQVlmwHIG1k2CM6GldPK9kOhyhAchSCYvJg4eFXVQuYYE7r4s1oNbga9A',
17
+ :langpair => "#{to}|#{from}",
18
18
  :q => message
19
19
  }).body)
20
20
 
@@ -30,5 +30,6 @@ end
30
30
 
31
31
  Plugin.define do
32
32
  name 'translate'
33
+ description 'Translate a message into another language'
33
34
  object Translate
34
35
  end
@@ -1,6 +1,5 @@
1
1
  require 'uri'
2
2
  require 'net/http'
3
- require 'cgi'
4
3
 
5
4
  class URL
6
5
  def initialize(sender, s) ;end
@@ -9,9 +8,9 @@ class URL
9
8
  {
10
9
  'head' => :head,
11
10
  'url' => [:get, { :permission => 'admin' }],
12
- 'bitly' => :bitly,
13
- 'isgd' => :isgd,
14
- 'tinyurl' => :tinyurl,
11
+ 'bitly' => [:bitly, { :help => 'Shorten a URL' }],
12
+ 'isgd' => [:isgd, { :help => 'Shorten a URL' }],
13
+ 'tinyurl' => [:tinyurl, { :help => 'Shorten a URL' }],
15
14
  'dpaste' => :dpaste,
16
15
  'pastie' => :pastie_command,
17
16
  'ppastie' => [:private_pastie_command, { :help => 'Create a private pastie' }],
@@ -54,7 +53,7 @@ class URL
54
53
 
55
54
  def pastie(data, is_private=false, format='plaintext')
56
55
  resp, body = 'http://pastie.org/pastes'.post({ :paste => {
57
- :body => CGI.escape(data),
56
+ :body => data,
58
57
  :parser => format,
59
58
  :restricted => is_private,
60
59
  :authorization => 'burger'
@@ -27,7 +27,7 @@ class Usermodes
27
27
  end
28
28
 
29
29
  def event(sender, e)
30
- if e.command == 'join' and e.respond_to?('user') then
30
+ if e.command == 'join' and e.respond_to?('user') and not e.user.anonymous? then
31
31
  if @usermodes.has_key?(k = e.delegate.instance + ':' + e.channel) then
32
32
  if @usermodes[k].has_key?(e.user.username) then
33
33
  @usermodes[k][e.user.username].each{ |mode| e.delegate.write "MODE #{e.channel} +#{mode} #{e.name}" }
@@ -238,7 +238,7 @@ class Users
238
238
  def pre_event(sender, e)
239
239
  e.users = self
240
240
  e.user = user(e.userhost, true) if not e.user and e.respond_to?('userhost')
241
- e.user.saw(e)
241
+ e.user.saw(e) unless e.user.anonymous?
242
242
  end
243
243
 
244
244
  def commands
@@ -322,7 +322,11 @@ class Users
322
322
  'networks' => [:networks, 0, { :help => 'List all availible networks' }],
323
323
  'profile' => [:profile, 1, {
324
324
  :permission => 'authenticated' }],
325
- 'message' => [:message, 2, { :permission => 'authenticated' }]
325
+ 'message' => [:message, 2, { :permission => 'authenticated' }],
326
+ 'broadcast' => [:broadcast, {
327
+ :permission => 'admin',
328
+ :help => 'Broadcast a message to every user',
329
+ :usage => 'message' }],
326
330
  }
327
331
  end
328
332
 
@@ -455,7 +459,9 @@ class Users
455
459
  end
456
460
 
457
461
  def seen(e, username)
458
- if user = user!(username) and user.seen then
462
+ if username == e.user.username then
463
+ "Are you looking for yourself?"
464
+ elsif user = user!(username) and user.seen then
459
465
  "#{username} last seen #{user.seen.since_words}"
460
466
  else
461
467
  "#{username} has never been seen"
@@ -558,6 +564,14 @@ class Users
558
564
  "no such user"
559
565
  end
560
566
  end
567
+
568
+ def broadcast(e, message)
569
+ @users.each do |user|
570
+ user.send(message)
571
+ end
572
+
573
+ "Message broadcasted to all #{@users.count} users"
574
+ end
561
575
  end
562
576
 
563
577
  Plugin.define do
@@ -90,5 +90,6 @@ end
90
90
 
91
91
  Plugin.define do
92
92
  name 'vouch'
93
+ description 'Make users require votes from another user to activate their account'
93
94
  object Vouch
94
95
  end
@@ -6,7 +6,10 @@ class Weather
6
6
 
7
7
  def commands
8
8
  {
9
- 'weather' => [:weather, 1],
9
+ 'weather' => [:weather, 1, {
10
+ :help => 'Check the weather',
11
+ :usage => 'location',
12
+ :example => 'Cupertino' }],
10
13
  }
11
14
  end
12
15
 
@@ -21,28 +24,33 @@ class Weather
21
24
  end
22
25
  end
23
26
 
24
- location = location.sub(' ', '+')
25
27
  xml_data = 'http://www.google.com/ig/api'.get({ :weather => location }).body
26
28
  doc = REXML::Document.new(xml_data)
27
29
 
28
- info = doc.root.elements['weather/forecast_information']
29
- city = info.elements['city'].attributes['data']
30
-
31
- current = doc.root.elements['weather/current_conditions']
32
- condition = current.elements['condition'].attributes['data']
33
- temp = current.elements['temp_c'].attributes['data']
34
- humidity = current.elements['humidity'].attributes['data']
35
- wind_condition = current.elements['wind_condition'].attributes['data']
36
-
37
- tomorrow = doc.root.elements['weather/forecast_conditions']
38
- tomorrow_cond = tomorrow.elements['condition'].attributes['data']
39
-
40
- "#{condition} #{temp}c #{humidity} #{wind_condition} for #{city}\n"+
41
- "Forcast for tomorrow #{tomorrow_cond}"
30
+ begin
31
+ info = doc.root.elements['weather/forecast_information']
32
+ city = info.elements['city'].attributes['data']
33
+
34
+ current = doc.root.elements['weather/current_conditions']
35
+ condition = current.elements['condition'].attributes['data']
36
+ temp_c = current.elements['temp_c'].attributes['data']
37
+ temp_f = current.elements['temp_f'].attributes['data']
38
+ humidity = current.elements['humidity'].attributes['data']
39
+ wind_condition = current.elements['wind_condition'].attributes['data']
40
+
41
+ tomorrow = doc.root.elements['weather/forecast_conditions']
42
+ tomorrow_cond = tomorrow.elements['condition'].attributes['data']
43
+
44
+ "#{condition} #{temp_c}C/#{temp_f}F #{humidity} #{wind_condition} for #{city}\n"+
45
+ "Forcast for tomorrow #{tomorrow_cond}"
46
+ rescue NoMethodError
47
+ "Command failed, maybe the location is invalid?"
48
+ end
42
49
  end
43
50
  end
44
51
 
45
52
  Plugin.define do
46
53
  name 'weather'
54
+ description 'Get the weather for a town/city'
47
55
  object Weather
48
56
  end
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{zmb}
8
- s.version = "0.3.1"
8
+ s.version = "0.3.2"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["kylef"]
12
- s.date = %q{2010-07-05}
12
+ s.date = %q{2010-07-18}
13
13
  s.default_executable = %q{zmb}
14
14
  s.description = %q{ZMB, messenger bot}
15
15
  s.email = %q{inbox@kylefuller.co.uk}
@@ -35,6 +35,7 @@ Gem::Specification.new do |s|
35
35
  "plugins/alias.rb",
36
36
  "plugins/announce.rb",
37
37
  "plugins/autorejoin.rb",
38
+ "plugins/autovoice.rb",
38
39
  "plugins/bank.rb",
39
40
  "plugins/commands.rb",
40
41
  "plugins/countdown.rb",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zmb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - kylef
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-07-05 00:00:00 +01:00
12
+ date: 2010-07-18 00:00:00 +01:00
13
13
  default_executable: zmb
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -58,6 +58,7 @@ files:
58
58
  - plugins/alias.rb
59
59
  - plugins/announce.rb
60
60
  - plugins/autorejoin.rb
61
+ - plugins/autovoice.rb
61
62
  - plugins/bank.rb
62
63
  - plugins/commands.rb
63
64
  - plugins/countdown.rb