rumpy 0.9.17 → 0.9.19

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.
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