zmb 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 kylef
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,39 @@
1
+ ## zmb messenger bot
2
+
3
+ zmb is a complete messenger bot supporting irc, and command line interface.
4
+
5
+ ### Install
6
+ gem install zmb
7
+
8
+ ### Uninstall
9
+ gem uninstall zmb
10
+ rm -rf ~/.zmb # If you used the default settings location
11
+
12
+ ### Creating a bot
13
+
14
+ This command will use the default settings location of ~/.zmb, you can pass `-s <PATH>` to change this.
15
+
16
+ zmb --create
17
+
18
+ ### Launching the bot
19
+ zmb --daemon
20
+
21
+ ### Using the bot in command shell mode
22
+
23
+ You can run zmb in a shell mode to test plugins without even connecting to any irc servers. It will create a shell where you can enter commands.
24
+
25
+ zmb --shell
26
+
27
+ ### Included plugins
28
+
29
+ - IRC
30
+ - Quote
31
+ - Relay - Relay between servers and/or channels
32
+ - Users - User management
33
+ - Bank - Points system
34
+
35
+ ### Support
36
+
37
+ You can find support at #zmb @ efnet.
38
+
39
+ For complete documentation please visit [Documentation](http://kylefuller.co.uk/projects/zmb/)
data/Rakefile ADDED
@@ -0,0 +1,54 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "zmb"
8
+ gem.summary = %Q{ZMB, messenger bot}
9
+ gem.description = %Q{ZMB, messenger bot}
10
+ gem.email = "inbox@kylefuller.co.uk"
11
+ gem.homepage = "http://github.com/kylef/zmb"
12
+ gem.authors = ["kylef"]
13
+ gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
14
+ gem.add_dependency "json", ">= 1.0.0"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
20
+ end
21
+
22
+ require 'rake/testtask'
23
+ Rake::TestTask.new(:test) do |test|
24
+ test.libs << 'lib' << 'test'
25
+ test.pattern = 'test/**/test_*.rb'
26
+ test.verbose = true
27
+ end
28
+
29
+ begin
30
+ require 'rcov/rcovtask'
31
+ Rcov::RcovTask.new do |test|
32
+ test.libs << 'test'
33
+ test.pattern = 'test/**/test_*.rb'
34
+ test.verbose = true
35
+ end
36
+ rescue LoadError
37
+ task :rcov do
38
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
39
+ end
40
+ end
41
+
42
+ task :test => :check_dependencies
43
+
44
+ task :default => :test
45
+
46
+ require 'rake/rdoctask'
47
+ Rake::RDocTask.new do |rdoc|
48
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
49
+
50
+ rdoc.rdoc_dir = 'rdoc'
51
+ rdoc.title = "zmb #{version}"
52
+ rdoc.rdoc_files.include('README*')
53
+ rdoc.rdoc_files.include('lib/**/*.rb')
54
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
data/bin/zmb ADDED
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/ruby
2
+
3
+ $:.unshift File.dirname(__FILE__) + "/../lib"
4
+
5
+ require 'zmb'
6
+ require 'optparse'
7
+
8
+ class AdminUser
9
+ attr_accessor :username, :userhosts
10
+
11
+ def initialize
12
+ @username = 'admin'
13
+ @userhosts = []
14
+ end
15
+
16
+ def admin?
17
+ true
18
+ end
19
+
20
+ def permission?(perm)
21
+ true
22
+ end
23
+
24
+ def authenticated?
25
+ true
26
+ end
27
+ end
28
+
29
+ class Event
30
+ attr_accessor :message
31
+
32
+ def initialize(message)
33
+ @message = message
34
+ end
35
+
36
+ def message?
37
+ true
38
+ end
39
+
40
+ def private?
41
+ true
42
+ end
43
+
44
+ def user
45
+ AdminUser.new
46
+ end
47
+
48
+ def reply(msg)
49
+ puts "> #{msg}"
50
+ end
51
+ end
52
+
53
+ def ask(question)
54
+ puts "#{question} (yes/no)"
55
+ answer = gets.chomp
56
+ answer == 'yes' or answer == 'y'
57
+ end
58
+
59
+ def get_value(question)
60
+ puts question
61
+ answer = gets.chomp
62
+
63
+ return nil if answer == ''
64
+ answer
65
+ end
66
+
67
+ def wizard(zmb, plugin)
68
+ STDOUT.flush
69
+
70
+ if ask("Would you like to add the #{plugin.name} plugin? #{plugin.description}") then
71
+ if plugin.multi_instances? then
72
+ instance = get_value("What would you like to name this instance of #{plugin.name}?")
73
+ else
74
+ instance = plugin.name
75
+ end
76
+
77
+ if not instance then
78
+ puts "Must supply instance name, if this plugin should only be loaded once such as commands or users then you can call it that."
79
+ return wizard zmb, plugin
80
+ end
81
+
82
+ zmb.setup(plugin.name, instance)
83
+ obj = zmb.plugin_manager.plugin plugin.name
84
+ if obj.respond_to?('wizard') then
85
+ settings = zmb.settings.setting(instance)
86
+ settings['plugin'] = plugin.name
87
+
88
+ obj.wizard.each do |key, value|
89
+ if value.has_key?('help') then
90
+ set = get_value("#{value['help']} (default=#{value['default']})")
91
+ settings[key] = set if set
92
+ end
93
+ end
94
+
95
+ zmb.settings.save(instance, settings)
96
+ end
97
+ zmb.load instance
98
+ end
99
+ end
100
+
101
+ options = {}
102
+
103
+ optparse = OptionParser.new do |opts|
104
+ opts.banner = "Usage: zmb [options]"
105
+
106
+ options[:settings] = nil
107
+ opts.on('-s', '--settings SETTING', 'Use a settings folder') do |settings|
108
+ options[:settings] = settings
109
+ end
110
+
111
+ options[:daemon] = false
112
+ opts.on('-d', '--daemon', 'Run ZMB') do
113
+ options[:daemon] = true
114
+ end
115
+
116
+ options[:create] = false
117
+ opts.on('-c', '--create', 'Create a new ZMB settings file') do
118
+ options[:create] = true
119
+ end
120
+
121
+ options[:shell] = false
122
+ opts.on('-b', '--shell', 'Create a commands shell') do
123
+ options[:shell] = true
124
+ end
125
+
126
+ options[:command] = false
127
+ opts.on('-l', '--line LINE', 'Execute a command') do |line|
128
+ options[:command] = line
129
+ end
130
+ end
131
+
132
+ optparse.parse!
133
+
134
+ if not options[:settings] then
135
+ options[:settings] = File.expand_path('~/.zmb')
136
+ puts "No settings file specified, will use #{options[:settings]}"
137
+ end
138
+
139
+ zmb = Zmb.new(options[:settings])
140
+
141
+ if options[:create] then
142
+ STDOUT.flush
143
+
144
+ zmb.save
145
+
146
+ while ask('Would you like to add additional plugin sources?')
147
+ source = get_value('Which path?')
148
+ if source and File.exists?(source) then
149
+ zmb.plugin_manager.add_plugin_source source
150
+ puts 'Source added'
151
+ zmb.save
152
+ else
153
+ puts 'Invalid source'
154
+ end
155
+ end
156
+
157
+ zmb.plugin_manager.plugins.reject{ |plugin| zmb.instances.has_key? plugin.name }.each{ |plugin| wizard(zmb, plugin) }
158
+
159
+ if zmb.instances.has_key?('users') and ask('Would you like to add a admin user?') then
160
+ username = get_value('Username:')
161
+ password = get_value('Password:')
162
+ userhost = get_value('Userhost: (Leave blank for none)')
163
+ zmb.instances['users'].create_user(username, password, userhost).permit('admin')
164
+ end
165
+
166
+ zmb.save
167
+ end
168
+
169
+ if options[:command] then
170
+ zmb.event(nil, Event.new(options[:command]))
171
+ zmb.save
172
+ end
173
+
174
+ if options[:shell] then
175
+ STDOUT.flush
176
+
177
+ begin
178
+ while 1
179
+ zmb.event(nil, Event.new(gets.chomp))
180
+ end
181
+ rescue Interrupt
182
+ zmb.save
183
+ end
184
+ end
185
+
186
+ if options[:daemon] then
187
+ zmb.run
188
+ end
data/lib/zmb.rb ADDED
@@ -0,0 +1,296 @@
1
+ require 'socket'
2
+
3
+ begin
4
+ require 'json'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ gem 'json'
8
+ end
9
+
10
+ require 'zmb/plugin'
11
+ require 'zmb/settings'
12
+ require 'zmb/event'
13
+ require 'zmb/commands'
14
+ require 'zmb/timer'
15
+
16
+ class Zmb
17
+ attr_accessor :instances, :plugin_manager, :settings
18
+
19
+ def initialize(config_dir)
20
+ @plugin_manager = PluginManager.new
21
+ @settings = Settings.new(config_dir)
22
+
23
+ @instances = {'core/zmb' => self}
24
+ @sockets = Hash.new
25
+
26
+ @minimum_timeout = 0.5 # Half a second
27
+ @maximum_timeout = 60.0 # Sixty seconds
28
+ @timers = Array.new
29
+ timer_add(Timer.new(self, :save, 120.0, true)) # Save every 2 minutes
30
+
31
+ @settings.get('core/zmb', 'plugin_sources', []).each{|source| @plugin_manager.add_plugin_source source}
32
+
33
+ if @plugin_manager.plugin_sources.empty? then
34
+ @plugin_manager.add_plugin_source File.join(File.expand_path(File.dirname(File.dirname(__FILE__))), 'plugins')
35
+ end
36
+
37
+ @settings.get('core/zmb', 'plugin_instances', []).each{|instance| load instance}
38
+
39
+ @running = false
40
+ end
41
+
42
+ def running?
43
+ @running
44
+ end
45
+
46
+ def to_json(*a)
47
+ {
48
+ 'plugin_sources' => @plugin_manager.plugin_sources,
49
+ 'plugin_instances' => @instances.keys,
50
+ }.to_json(*a)
51
+ end
52
+
53
+ def save
54
+ @instances.each{ |k,v| @settings.save(k, v) }
55
+ end
56
+
57
+ def load(key)
58
+ return true if @instances.has_key?(key)
59
+
60
+ if p = @settings.get(key, 'plugin') then
61
+ object = @plugin_manager.plugin(p)
62
+ return false if not object
63
+ @instances[key] = object.new(self, @settings.setting(key))
64
+ post! :plugin_loaded, key, @instances[key]
65
+ true
66
+ else
67
+ false
68
+ end
69
+ end
70
+
71
+ def unload(key, tell=true)
72
+ return false if not @instances.has_key?(key)
73
+ instance = @instances.delete(key)
74
+ @settings.save key, instance
75
+ socket_delete instance
76
+ timer_delete instance
77
+ instance.unloaded if instance.respond_to?('unloaded') and tell
78
+ post! :plugin_unloaded, key, instance
79
+ end
80
+
81
+ def run
82
+ post! :running, self
83
+
84
+ @running = true
85
+ begin
86
+ while @running
87
+ socket_run(timeout)
88
+ timer_run
89
+ end
90
+ rescue Interrupt
91
+ save
92
+ end
93
+ end
94
+
95
+ def timeout
96
+ if timer_timeout > @maximum_timeout
97
+ if @sockets.count < 1 then
98
+ 5
99
+ else
100
+ @maximum_timeout
101
+ end
102
+ elsif timer_timeout > @minimum_timeout
103
+ timer_timeout
104
+ else
105
+ @minimum_timeout
106
+ end
107
+ end
108
+
109
+ def socket_add(delegate, socket)
110
+ @sockets[socket] = delegate
111
+ end
112
+
113
+ def socket_delete(item)
114
+ if @sockets.has_value?(item) then
115
+ @sockets.select{ |sock, delegate| delegate == item }.each{ |sock, delegate| @sockets.delete(sock) }
116
+ end
117
+
118
+ if @sockets.has_key?(item) then
119
+ @sockets.delete(item)
120
+ end
121
+ end
122
+
123
+ def socket_run(timeout)
124
+ result = select(@sockets.keys, nil, nil, timeout)
125
+
126
+ if result != nil then
127
+ result[0].select{|sock| @sockets.has_key?(sock)}.each do |sock|
128
+ if sock.eof? then
129
+ @sockets[sock].disconnected(self, sock) if @sockets[sock].respond_to?('disconnected')
130
+ socket_delete sock
131
+ else
132
+ @sockets[sock].received(self, sock, sock.gets()) if @sockets[sock].respond_to?('received')
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ def timer_add(timer)
139
+ @timers << timer
140
+ end
141
+
142
+ def timer_delete(search)
143
+ @timers.each{ |timer| @timers.delete(timer) if timer.delegate == search }
144
+ @timers.delete(search)
145
+ end
146
+
147
+ def timer_timeout # When will the next timer run?
148
+ @timers.map{|timer| timer.timeout}.sort.fetch(0, @maximum_timeout)
149
+ end
150
+
151
+ def timer_run
152
+ @timers.select{|timer| timer.timeout <= 0.0 and timer.respond_to?("fire") }.each{|timer| timer.fire(self)}
153
+ end
154
+
155
+ def post(signal, *args)
156
+ results = Array.new
157
+
158
+ @instances.select{|name, instance| instance.respond_to?(signal)}.each do |name, instance|
159
+ results << instance.send(signal, *args) rescue nil
160
+ end
161
+
162
+ results
163
+ end
164
+
165
+ def post!(signal, *args) # This will exclude the plugin manager
166
+ @instances.select{|name, instance| instance.respond_to?(signal) and instance != self}.each do |name, instance|
167
+ instance.send(signal, *args) rescue nil
168
+ end
169
+ end
170
+
171
+ def setup(plugin, instance)
172
+ object = @plugin_manager.plugin plugin
173
+ return false if not object
174
+
175
+ settings = Hash.new
176
+ settings['plugin'] = plugin
177
+
178
+ if object.respond_to? 'wizard' then
179
+ d = object.wizard
180
+ d.each{ |k,v| settings[k] = v['default'] if v.has_key?('default') and v['default'] }
181
+ end
182
+
183
+ @settings.save instance, settings
184
+
185
+ true
186
+ end
187
+
188
+ def event(sender, e)
189
+ post! :pre_event, sender, e
190
+ post! :event, sender, e
191
+ end
192
+
193
+ def commands
194
+ {
195
+ 'reload' => PermCommand.new('admin', self, :reload_command),
196
+ 'unload' => PermCommand.new('admin', self, :unload_command),
197
+ 'load' => PermCommand.new('admin', self, :load_command),
198
+ 'save' => PermCommand.new('admin', self, :save_command, 0),
199
+ 'loaded' => PermCommand.new('admin', self, :loaded_command, 0),
200
+ 'setup' => PermCommand.new('admin', self, :setup_command, 2),
201
+ 'set' => PermCommand.new('admin', self, :set_command, 3),
202
+ 'get' => PermCommand.new('admin', self, :get_command, 2),
203
+ 'clone' => PermCommand.new('admin', self, :clone_command, 2),
204
+ 'reset' => PermCommand.new('admin', self, :reset_command),
205
+ 'addsource' => PermCommand.new('admin', self, :addsource_command),
206
+ }
207
+ end
208
+
209
+ def reload_command(e, instance)
210
+ if @instances.has_key?(instance) then
211
+ sockets = Array.new
212
+ @sockets.each{ |sock,delegate| sockets << sock if delegate == @instances[instance] }
213
+ unload(instance, false)
214
+ reloaded = @plugin_manager.reload_plugin(@settings.get(instance, 'plugin'))
215
+ load(instance)
216
+
217
+ sockets.each{ |socket| @sockets[socket] = @instances[instance] }
218
+ @instances[instance].socket = sockets[0] if sockets.size == 1 and @instances[instance].respond_to?('socket=')
219
+
220
+ reloaded ? "#{instance} reloaded" : "#{instance} refreshed"
221
+ else
222
+ "No such instance #{instance}"
223
+ end
224
+ end
225
+
226
+ def unload_command(e, instance)
227
+ if @instances.has_key?(instance) then
228
+ unload(instance)
229
+ "#{instance} unloaded"
230
+ else
231
+ "No such instance #{instance}"
232
+ end
233
+ end
234
+
235
+ def load_command(e, instance)
236
+ if not @instances.has_key?(instance) then
237
+ load(instance) ? "#{instance} loaded" : "#{instance} did not load correctly"
238
+ else
239
+ "Instance already #{instance}"
240
+ end
241
+ end
242
+
243
+ def save_command(e)
244
+ save
245
+ 'settings saved'
246
+ end
247
+
248
+ def loaded_command(e)
249
+ @instances.keys.join(', ')
250
+ end
251
+
252
+ def setup_command(e, plugin, instance)
253
+ if setup(plugin, instance) then
254
+ object = @plugin_manager.plugin plugin
255
+ result = ["Instance saved, please use the set command to override the default configuration for this instance."]
256
+ result += d.map{ |k,v| "#{k} - #{v['help']} (default=#{v['default']})" } if object.respond_to? 'wizard'
257
+ result.join("\n")
258
+ else
259
+ "plugin not found"
260
+ end
261
+ end
262
+
263
+ def set_command(e, instance, key, value)
264
+ settings = @settings.setting(instance)
265
+ settings[key] = value
266
+ @settings.save(instance, settings)
267
+ "#{key} set to #{value} for #{instance}"
268
+ end
269
+
270
+ def get_command(e, instance, key)
271
+ if value = @settings.get(instance, key) then
272
+ "#{key} is #{value} for #{instance}"
273
+ else
274
+ "#{instance} or #{instance}/#{key} not found."
275
+ end
276
+ end
277
+
278
+ def clone_command(e, instance, new_instance)
279
+ if (settings = @settings.setting(instance)) != {} then
280
+ @settings.save(new_instance, settings)
281
+ "The settings for #{instance} were copied to #{new_instance}"
282
+ else
283
+ "No settings for #{instance}"
284
+ end
285
+ end
286
+
287
+ def reset_command(e, instance)
288
+ @settings.save(instance, {})
289
+ "Settings for #{instance} have been deleted."
290
+ end
291
+
292
+ def addsource_command(e, source)
293
+ @plugin_manager.add_plugin_source source
294
+ "#{source} added to plugin manager"
295
+ end
296
+ end