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.
- data/README.rdoc +79 -50
- data/lib/rumpy.rb +118 -106
- metadata +4 -4
data/README.rdoc
CHANGED
@@ -1,61 +1,90 @@
|
|
1
|
-
|
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
|
-
|
8
|
+
= Features
|
9
9
|
|
10
|
-
* Forget about xmpp-related things. Just set your login
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
#
|
10
|
+
# Start bot in new process,
|
11
11
|
# detach this process and save the pid of process in pid_file
|
12
|
-
def self.start(
|
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 = "#{
|
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(
|
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(
|
32
|
-
pf = pid_file
|
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(
|
41
|
+
end # def self.stop(bot)
|
43
42
|
|
44
|
-
#
|
45
|
-
def self.run(
|
46
|
-
|
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
|
-
|
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
|
-
@
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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 @
|
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
|
-
|
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
|
157
|
-
@
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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
|
-
|
184
|
-
|
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
|
-
|
204
|
+
@output_queue.enq Jabber::Message.new(item.jid, @lang['authorized']).set_type :chat
|
203
205
|
end
|
204
206
|
rescue ActiveRecord::StatementInvalid
|
205
|
-
|
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
|
225
|
+
else
|
224
226
|
@logger.debug "user not in roster: #{msg.from}"
|
225
227
|
|
226
|
-
|
227
|
-
end
|
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
|
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
|
-
|
240
|
+
@output_queue.enq message if message.body and message.to
|
260
241
|
end
|
261
242
|
end
|
262
243
|
rescue ActiveRecord::StatementInvalid
|
263
|
-
|
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
|
-
|
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
|
-
|
284
|
-
|
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
|
-
|
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
|
-
|
312
|
-
|
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
|
-
|
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 #
|
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
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
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
|
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:
|
4
|
+
hash: 29
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 9
|
9
|
-
-
|
10
|
-
version: 0.9.
|
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-
|
19
|
+
date: 2011-08-20 00:00:00 +03:00
|
20
20
|
default_executable:
|
21
21
|
dependencies:
|
22
22
|
- !ruby/object:Gem::Dependency
|