zmb 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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