rumpy 0.9.17 → 0.9.19

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README.rdoc +79 -50
  2. data/lib/rumpy.rb +118 -106
  3. metadata +4 -4
data/README.rdoc CHANGED
@@ -1,61 +1,90 @@
1
- == Welcome to Rumpy
1
+ = Welcome to Rumpy
2
2
 
3
3
  Rumpy is some kind of framework to make up your own jabber bot quickly.
4
4
  It uses {ActiveRecord}[https://github.com/rails/rails/tree/master/activerecord] and {XMPP4R}[http://home.gna.org/xmpp4r/].
5
5
 
6
- Our goal is 'DO NOT REINVENT THE WHEEL'.
6
+ Our goal is <b>'DO NOT REINVENT THE WHEEL'</b>.
7
7
 
8
- == Features
8
+ = Features
9
9
 
10
- * Forget about xmpp-related things. Just set your login & password.
10
+ * Forget about xmpp-related things. Just set your login and password.
11
11
  * Forget about database-related things. Just set your database preferences.
12
12
  * Write logic using ActiveRecord and callback functions.
13
13
 
14
- == Getting started
15
-
16
- * Rumpy uses 3 configs:
17
- +database.yml+ :: Your bot's database preferences.
18
- +lang.yml+ :: Your bot's responces. Append to existing keys more answers and use them like @lang['someanswer]. **There must be at least 3 keys : hello /used when somebody add bot/, stranger /when somebody trying talk to bot without authorization/, authorized /when bot get authorized/.
19
- +xmpp.yml+ :: Your bot's account settings.
20
- Look at Examples section to see this configs.
21
- * Implement your ActiveRecord models in one directory. You have to implement at least one model, for users.
22
- * Prepare your database.
23
- * Start writing your class:
24
- * Mix in Rumpy::Bot module
25
-
26
- include Rumpy::Bot
27
- * Define some instance variables:
28
- @models_path :: Path to directory, containing all ruby files with models.
29
- @config_path :: Path to directory, containing all ruby config files
30
- @main_model :: Symbol, that stands for main model. If your main model is, e.g. +User+, set @main_model to +:user+
31
- @pid_file :: Optional variable, sets location of the file to which pid of detached process will be saved. If not set, it equals +NameOfYourBotClass.downcase+ + '.pid'.
32
- @log_file :: Optional variable, sets location of the file for errors. Default is +STDERR+.
33
- @log_level :: Optional variable, sets the logging severity threshold. Possible values are Logger::DEBUG < Logger::INFO < Logger::WARN < Logger::ERROR < Logger::FATAL < Logger::UNKNOWN. Default is Logger::INFO.
34
- @log_shift_age :: Optional variable, sets number of old log files to keep, or frequency of rotation (daily, weekly or monthly).
35
- @log_shift_size :: Optional variable, sets maximum logfile size.
36
- @log_progname :: Optional variable, sets logging program name.
37
- @log_formatter :: Optional variable, sets logging formatter. @log_formatter#call is invoked with 4 arguments; +severity+, +time+, +progname+ and +msg+ for each log.
38
- @bot_name :: Optional name of the bot. Default is name of bot's class.
39
- @bot_version :: Optional version of the bot. Default is 1.0.0.
40
- * Write 3 methods:
41
- backend_func() -> [[receiver, message]*] :: This optional method is running all the time in the loop. Returns array of pairs [receiver, message].
42
- parser_func(msg) -> +pars_result+ :: This method parses any incoming message and returs results.
43
- do_func(usermodel, pars_results) -> +msg+ :: This method uses results from +parser_func+, doing some stuff with model of user, from whom message was received. Returns message to be send to this user
44
- * Run bot:
45
- You can run your bot without detaching:
46
- Rumpy.run YourBotClassName
47
- Or with detaching:
48
- #To start your bot:
49
- Rumpy.start YourBotClassName
50
- #To stop it:
51
- Rumpy.stop YourBotClassName
52
-
53
- == Example
54
-
55
- Look at {CuteBot}[https://github.com/MPogoda/CuteBot], {yatodo}[https://github.com/MPogoda/yatodo], {Noty}[https://github.com/Ximik/Noty].
56
-
57
- Feel free to contact us about any questions related to Rumpy.
58
-
59
- == License
14
+ = Getting started
15
+
16
+ == Configs
17
+
18
+ Rumpy uses 3 configs:
19
+ +database.yml+ :: Your bot's database preferences.
20
+ +lang.yml+ :: Your bot's responces. Append to existing keys more answers and use them like <tt>@lang['someanswer']</tt>. There *MUST* be at least 3 keys: +hello+ (<em>used when somebody adds bot</em>), +stranger+ (<em>used when somebody trying to speak with bot without authorization</em>) and +authorized+ (<em>used when bot gets authorization</em>).
21
+ +xmpp.yml+ :: Your bot's jabber account settings.
22
+ Look at Examples section to see this configs.
23
+
24
+ == ActiveRecord models
25
+
26
+ Implement your *ActiveRecord* models.
27
+ You have to implement at least one model, for users.
28
+
29
+ == Prepare your database
30
+
31
+ == Your bot's class
32
+
33
+ === Rumpy::Bot module
34
+
35
+ You have to mix in your bot's class the <tt>Rumpy::Bot</tt> module:
36
+
37
+ include Rumpy::Bot
38
+
39
+ === Instance variables
40
+
41
+ +Rumpy+ uses next instance variables:
42
+ <tt>@models_files</tt> :: Array of your models files.
43
+ <tt>@config_path</tt> :: Path to directory, containing all ruby config files. Default is +config+.
44
+ <tt>@main_model</tt> :: Symbol, that stands for main model. For example, if your main model is +User+, you have to set <tt>@main_model</tt> to <tt>:user</tt>. Default is <tt>:user</tt>.
45
+ <tt>@pid_file</tt> :: Location of the file to which pid of detached process will be saved. Default is <tt>NameOfYourBotClass.downcase + '.pid'</tt>.
46
+ <tt>@log_file</tt> :: Location of the logfile. Default is +STDERR+.
47
+ <tt>@log_level</tt> :: Logging severity threshold. Possible values are the same the logger from standard library has. Default is <tt>Logger::INFO</tt>.
48
+ <tt>@log_shift_age</tt> :: Number of old log files to keep, or frequency of rotation (_daily_, _weekly_ or _monthly_). Default is +0+.
49
+ <tt>@log_shift_size</tt> :: Maximum logfile size. Default is +1048576+.
50
+ <tt>@logger</tt> :: If you need more accuracy in configuring logger, simply create one. It have to be compatible with standard library's +logger+.
51
+ <tt>@bot_name</tt> :: Name of the bot. Default is name of bot's class.
52
+ <tt>@bot_version</tt> :: Optional version of the bot. Default is <tt>1.0.0</tt>.
53
+
54
+ === Instance methods
55
+
56
+ +Rumpy+ needs only three methods:
57
+ <tt>backend_func() -> [[receiver, message]*]</tt> :: This *optional* method is running all the time in the loop. Returns array of pairs <tt>[receiver, message]</tt>.
58
+ <tt>parser_func(msg) -> pars_result</tt> :: This method parses any incoming message and returs results of parsing.
59
+ <tt>do_func(usermodel, pars_results) -> msg</tt> :: This method uses results from +parser_func+, doing some stuff with model of user, from whom the message was received. Returns the answer to this user.
60
+
61
+ _Hint_: empty answer will not be sent.
62
+
63
+ == Run bot
64
+
65
+ You can run your bot without detaching:
66
+
67
+ Rumpy.run YourBotClassName.new
68
+
69
+ Or with detaching:
70
+
71
+ bot = YourBotClassName.new
72
+ #To start your bot:
73
+ Rumpy.start bot
74
+ #To stop it:
75
+ Rumpy.stop bot
76
+
77
+ = Examples
78
+
79
+ Look at
80
+ * {CuteBot}[https://github.com/MPogoda/CuteBot]
81
+ * {yatodo}[https://github.com/MPogoda/yatodo]
82
+ * {Noty}[https://github.com/Ximik/Noty]
83
+
84
+ = Contacts
85
+
86
+ Feel free to contact us about any questions related to +Rumpy+.
87
+
88
+ = License
60
89
 
61
90
  Rumpy is released under the MIT license.
data/lib/rumpy.rb CHANGED
@@ -7,14 +7,13 @@ require 'logger'
7
7
 
8
8
  module Rumpy
9
9
 
10
- # Create new instance of `botclass`, start it in new process,
10
+ # Start bot in new process,
11
11
  # detach this process and save the pid of process in pid_file
12
- def self.start(botclass)
13
- bot = botclass.new
12
+ def self.start(bot)
14
13
  pf = pid_file bot
15
14
  return false if File.exist? pf
16
15
 
17
- bot.log_file = "#{botclass.to_s.downcase}.log"
16
+ bot.log_file = "#{bot.class.to_s.downcase}.log"
18
17
 
19
18
  pid = fork do
20
19
  bot.start
@@ -24,12 +23,12 @@ module Rumpy
24
23
  file.puts pid
25
24
  end
26
25
  true
27
- end # def self.start(botclass)
26
+ end # def self.start(bot)
28
27
 
29
28
  # Determine the name of pid_file, read pid from this file
30
29
  # and try to kill process with this pid
31
- def self.stop(botclass)
32
- pf = pid_file botclass.new
30
+ def self.stop(bot)
31
+ pf = pid_file bot
33
32
  return false unless File.exist? pf
34
33
  begin
35
34
  File.open(pf) do |file|
@@ -39,11 +38,11 @@ module Rumpy
39
38
  File.unlink pf
40
39
  end
41
40
  true
42
- end # def self.stop(botclass)
41
+ end # def self.stop(bot)
43
42
 
44
- # Create new instance of `botclass` and start it without detaching
45
- def self.run(botclass)
46
- botclass.new.start
43
+ # Start bot without detaching
44
+ def self.run(bot)
45
+ bot.start
47
46
  end
48
47
 
49
48
  # Determine the name of file where thid pid will stored to
@@ -69,17 +68,20 @@ module Rumpy
69
68
  logger_init
70
69
 
71
70
  init
71
+
72
72
  connect
73
- prepare_users
74
73
 
74
+ set_iq_callback
75
75
  set_subscription_callback
76
76
  set_message_callback
77
- set_iq_callback
78
77
 
79
78
  start_backend_thread
79
+ start_output_queue_thread
80
+
81
+ prepare_users
80
82
 
81
83
  @logger.info 'Bot is going ONLINE'
82
- send_msg Jabber::Presence.new(nil, @status, @priority)
84
+ @output_queue.enq Jabber::Presence.new(nil, @status, @priority)
83
85
 
84
86
  add_signal_trap
85
87
 
@@ -92,18 +94,21 @@ module Rumpy
92
94
  private
93
95
 
94
96
  def logger_init
95
- @log_file ||= STDERR
96
- @log_level ||= Logger::INFO
97
- @logger = Logger.new @log_file, @log_shift_age, @log_shift_size
98
- @logger.level = @log_level
99
- @logger.progname = @log_progname
100
- @logger.formatter = @log_formatter
101
- @logger.datetime_format = "%Y-%m-%d %H:%M:%S"
97
+ if @logger.nil? then
98
+ @log_file ||= STDERR
99
+ @log_level ||= Logger::INFO
100
+ @logger = Logger.new @log_file, @log_shift_age, @log_shift_size
101
+ @logger.level = @log_level
102
+ @logger.datetime_format = "%Y-%m-%d %H:%M:%S"
103
+ end
102
104
 
103
105
  @logger.info 'starting bot'
104
106
  end
105
107
 
106
108
  def init
109
+ @config_path ||= 'config'
110
+ @main_model ||= :user
111
+
107
112
  @logger.debug 'initializing some variables'
108
113
 
109
114
  xmppconfig = YAML::load_file @config_path + '/xmpp.yml'
@@ -119,13 +124,13 @@ module Rumpy
119
124
  @client = Jabber::Client.new @jid
120
125
  Jabber::Version::SimpleResponder.new(@client, @bot_name || self.class.to_s, @bot_version || '1.0.0', RUBY_PLATFORM)
121
126
 
122
- if @models_path then
127
+ if @models_files then
123
128
  dbconfig = YAML::load_file @config_path + '/database.yml'
124
129
  @logger.info 'loaded database.yml'
125
130
  @logger.debug "database.yml: #{dbconfig.inspect}"
126
131
  ActiveRecord::Base.establish_connection dbconfig
127
132
  @logger.info 'database connection established'
128
- Dir[@models_path].each do |file|
133
+ @models_files.each do |file|
129
134
  self.class.require file
130
135
  @logger.info "added models file '#{file}'"
131
136
  end
@@ -133,13 +138,12 @@ module Rumpy
133
138
 
134
139
  @main_model = Object.const_get @main_model.to_s.capitalize
135
140
  @logger.info "main model set to #{@main_model}"
136
- def @main_model.find_by_jid(jid)
137
- super jid.strip.to_s
138
- end
139
141
 
140
142
  @queues = Hash.new do |h, k|
141
143
  h[k] = Queue.new
142
144
  end
145
+
146
+ @output_queue = Queue.new
143
147
  end # def init
144
148
 
145
149
  def connect
@@ -153,35 +157,33 @@ module Rumpy
153
157
  @logger.info 'xmpp connection established'
154
158
  end
155
159
 
156
- def prepare_users
157
- @logger.debug 'clear wrong users'
158
-
159
- @roster.items.each do |jid, item|
160
- user = @main_model.find_by_jid jid
161
- if user.nil? or item.subscription != :both then
162
- @logger.info "deleting from roster user with jid #{jid}"
163
- item.remove
164
- end
165
- end
166
- @main_model.find_each do |user|
167
- items = @roster.find user.jid
168
- if items.empty? then
169
- @logger.info "deleting from database user with jid #{user.jid}"
170
- user.destroy
171
- else
172
- start_user_thread user
160
+ def set_iq_callback
161
+ @client.add_iq_callback do |iq|
162
+ @logger.debug "got iq #{iq}"
163
+ if iq.type == :get then # hack for pidgin (STOP USING IT)
164
+ response = iq.answer true
165
+ if iq.elements['time'] == "<time xmlns='urn:xmpp:time'/>" then
166
+ @logger.debug 'this is time request, okay'
167
+ response.set_type :result
168
+ tm = Time.now
169
+ response.elements['time'].add REXML::Element.new('tzo')
170
+ response.elements['time/tzo'].text = tm.xmlschema[-6..-1]
171
+ response.elements['time'].add REXML::Element.new('utc')
172
+ response.elements['time/utc'].text = tm.utc.xmlschema
173
+ else
174
+ response.set_type :error
175
+ end # if iq.elements['time']
176
+ @output_queue.enq response
173
177
  end
174
178
  end
175
-
176
- @main_model.connection_pool.release_connection
177
- end # def prepare_users
179
+ end # def set_iq_callback
178
180
 
179
181
  def set_subscription_callback
180
182
  @roster.add_subscription_request_callback do |item, presence|
181
183
  jid = presence.from
182
184
  @roster.accept_subscription jid
183
- send_msg presence.answer.set_type :subscribe
184
- send_msg Jabber::Message.new(jid, @lang['hello']).set_type :chat
185
+ @output_queue.enq presence.answer.set_type :subscribe
186
+ @output_queue.enq Jabber::Message.new(jid, @lang['hello']).set_type :chat
185
187
 
186
188
  @logger.info "#{jid} just subscribed"
187
189
  end
@@ -199,10 +201,10 @@ module Rumpy
199
201
  start_user_thread user
200
202
 
201
203
  @logger.info "added new user: #{user.jid}"
202
- send_msg Jabber::Message.new(item.jid, @lang['authorized']).set_type :chat
204
+ @output_queue.enq Jabber::Message.new(item.jid, @lang['authorized']).set_type :chat
203
205
  end
204
206
  rescue ActiveRecord::StatementInvalid
205
- statement_invalid
207
+ statement_invalid_error
206
208
  retry
207
209
  rescue ActiveRecord::ConnectionTimeoutError
208
210
  connection_timeout_error
@@ -220,35 +222,14 @@ module Rumpy
220
222
  @logger.debug "got normal message #{msg}"
221
223
 
222
224
  @queues[msg.from.strip.to_s].enq msg
223
- else # if @roster[msg.from] and @roster[msg.from].subscription == :both
225
+ else
224
226
  @logger.debug "user not in roster: #{msg.from}"
225
227
 
226
- send_msg msg.answer.set_body @lang['stranger']
227
- end # if @roster[msg.from] and @roster[msg.from].subscription == :both
228
- end # if msg.type != :error and msg.body and msg.from
229
- end # @client.add_message_callback
230
- end # def set_message_callback
231
-
232
- def set_iq_callback
233
- @client.add_iq_callback do |iq|
234
- @logger.debug "got iq #{iq}"
235
- if iq.type == :get then # hack for pidgin (STOP USING IT)
236
- response = iq.answer true
237
- if iq.elements['time'] == "<time xmlns='urn:xmpp:time'/>" then
238
- @logger.debug 'this is time request, okay'
239
- response.set_type :result
240
- tm = Time.now
241
- response.elements['time'].add REXML::Element.new('tzo')
242
- response.elements['time/tzo'].text = tm.xmlschema[-6..-1]
243
- response.elements['time'].add REXML::Element.new('utc')
244
- response.elements['time/utc'].text = tm.utc.xmlschema
245
- else
246
- response.set_type :error
247
- end # if iq.elements['time']
248
- send_msg response
228
+ @output_queue.enq msg.answer.set_body @lang['stranger']
229
+ end
249
230
  end
250
231
  end
251
- end # def set_iq_callback
232
+ end # def set_message_callback
252
233
 
253
234
  def start_backend_thread
254
235
  Thread.new do
@@ -256,11 +237,11 @@ module Rumpy
256
237
  loop do
257
238
  backend_func().each do |result|
258
239
  message = Jabber::Message.new(*result).set_type :chat
259
- send_msg message if message.body and message.to
240
+ @output_queue.enq message if message.body and message.to
260
241
  end
261
242
  end
262
243
  rescue ActiveRecord::StatementInvalid
263
- statement_invalid
244
+ statement_invalid_error
264
245
  retry
265
246
  rescue ActiveRecord::ConnectionTimeoutError
266
247
  connection_timeout_error
@@ -271,17 +252,34 @@ module Rumpy
271
252
  end if self.respond_to? :backend_func
272
253
  end # def start_backend_thread
273
254
 
255
+ def start_output_queue_thread
256
+ Thread.new do
257
+ @logger.info "Output queue initialized"
258
+ until (msg = @output_queue.deq) == :halt do
259
+ if msg.nil? then
260
+ @logger.debug "got nil message. wtf?"
261
+ else
262
+ @logger.debug "sending message #{msg}"
263
+ @client.send msg
264
+ end
265
+ end
266
+ @logger.info "Output queue destroyed"
267
+ end
268
+ end # def start_output_queue_thread
269
+
274
270
  def add_signal_trap
275
271
  Signal.trap :TERM do |signo| # soft stop
276
272
  @logger.info 'Bot is unavailable'
277
- send_msg Jabber::Presence.new.set_type :unavailable
273
+ @output_queue.enq Jabber::Presence.new.set_type :unavailable
278
274
 
279
275
  @queues.each do |user, queue|
280
276
  queue.enq :halt
281
277
  end
282
- until @queues.empty?
283
- sleep 1
284
- end
278
+ sleep 1 until @queues.empty?
279
+
280
+ @output_queue.enq :halt
281
+ sleep 1 until @output_queue.empty?
282
+
285
283
  @client.close
286
284
 
287
285
  @logger.info 'terminating'
@@ -290,28 +288,41 @@ module Rumpy
290
288
  end
291
289
  end
292
290
 
291
+ def prepare_users
292
+ @logger.debug 'clear wrong users'
293
+
294
+ @roster.items.each do |jid, item|
295
+ user = @main_model.find_by_jid jid.strip.to_s
296
+ if user.nil? or item.subscription != :both then
297
+ @logger.info "deleting from roster user with jid #{jid}"
298
+ item.remove
299
+ end
300
+ end
301
+ @main_model.find_each do |user|
302
+ items = @roster.find user.jid
303
+ if items.empty? then
304
+ @logger.info "deleting from database user with jid #{user.jid}"
305
+ user.destroy
306
+ else
307
+ start_user_thread user
308
+ end
309
+ end
310
+
311
+ @main_model.connection_pool.release_connection
312
+ end # def prepare_users
313
+
293
314
  def start_user_thread(user)
294
315
  Thread.new(user) do |user|
316
+ @logger.debug "thread for user #{user.jid} started"
295
317
 
296
- loop do
297
- msg = @queues[user.jid].deq
298
-
318
+ until (msg = @queues[user.jid].deq).kind_of? Symbol do
299
319
  begin
300
- if msg.kind_of? Symbol then # :unsubscribe or :halt
301
- if msg == :unsubscribe
302
- @logger.info "removing user #{user.jid}"
303
- user.destroy
304
- end
305
- @queues.delete user.jid
306
- break
307
- end
308
-
309
320
  pars_results = parser_func msg.body
310
321
  @logger.debug "parsed message: #{pars_results.inspect}"
311
- message = do_func user, pars_results
312
- send_msg msg.answer.set_body message unless message.empty?
322
+ answer = do_func user, pars_results
323
+ @output_queue.enq msg.answer.set_body answer unless answer.nil? or answer.empty?
313
324
  rescue ActiveRecord::StatementInvalid
314
- statement_invalid
325
+ statement_invalid_error
315
326
  retry
316
327
  rescue ActiveRecord::ConnectionTimeoutError
317
328
  connection_timeout_error
@@ -321,18 +332,19 @@ module Rumpy
321
332
  end # begin
322
333
 
323
334
  @main_model.connection_pool.release_connection
324
- end # loop do
325
- Thread.current.join
326
- end # Thread.new do
327
- end # def start_user_thread(queue)
335
+ end # until (msg = @queues[user.jid].deq).kind_of? Symbol do
328
336
 
329
- def send_msg(msg)
330
- return if msg.nil?
331
- @logger.debug "sending message: #{msg}"
332
- @client.send msg
333
- end
337
+ if msg == :unsubscribe
338
+ @logger.info "removing user #{user.jid}"
339
+ user.destroy
340
+ end
341
+
342
+ @queues.delete user.jid
343
+
344
+ end # Thread.new do
345
+ end # def start_user_thread(user)
334
346
 
335
- def statement_invalid
347
+ def statement_invalid_error
336
348
  @logger.warn 'Statement Invalid catched'
337
349
  @logger.info 'Reconnecting to database'
338
350
  @main_model.connection.reconnect!
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rumpy
3
3
  version: !ruby/object:Gem::Version
4
- hash: 25
4
+ hash: 29
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 9
9
- - 17
10
- version: 0.9.17
9
+ - 19
10
+ version: 0.9.19
11
11
  platform: ruby
12
12
  authors:
13
13
  - Tsokurov A.G.
@@ -16,7 +16,7 @@ autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
18
 
19
- date: 2011-08-18 00:00:00 +03:00
19
+ date: 2011-08-20 00:00:00 +03:00
20
20
  default_executable:
21
21
  dependencies:
22
22
  - !ruby/object:Gem::Dependency