joggle 0.1.0
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/COPYING +20 -0
- data/README +212 -0
- data/Rakefile +120 -0
- data/TODO +36 -0
- data/bin/joggle +6 -0
- data/lib/joggle/Session.vim +1223 -0
- data/lib/joggle/cli/option-parser.rb +139 -0
- data/lib/joggle/cli/runner.rb +47 -0
- data/lib/joggle/commands.rb +163 -0
- data/lib/joggle/config-parser.rb +37 -0
- data/lib/joggle/engine.rb +276 -0
- data/lib/joggle/jabber/client.rb +82 -0
- data/lib/joggle/pablotron/cache.rb +131 -0
- data/lib/joggle/pablotron/observable.rb +104 -0
- data/lib/joggle/runner/pstore.rb +252 -0
- data/lib/joggle/store/pstore/all.rb +26 -0
- data/lib/joggle/store/pstore/cache.rb +65 -0
- data/lib/joggle/store/pstore/message.rb +54 -0
- data/lib/joggle/store/pstore/user.rb +96 -0
- data/lib/joggle/twitter/engine.rb +186 -0
- data/lib/joggle/twitter/fetcher.rb +123 -0
- data/lib/joggle/version.rb +6 -0
- data/setup.rb +1596 -0
- data/test/test_cli.rb +10 -0
- data/test/test_runner.rb +10 -0
- metadata +131 -0
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'joggle/version'
|
3
|
+
require 'joggle/config-parser'
|
4
|
+
require 'joggle/cli/option-parser'
|
5
|
+
|
6
|
+
module Joggle
|
7
|
+
module CLI
|
8
|
+
#
|
9
|
+
# Option parser for Joggle command-line interface.
|
10
|
+
#
|
11
|
+
class OptionParser
|
12
|
+
#
|
13
|
+
# Default configuration.
|
14
|
+
#
|
15
|
+
DEFAULTS = {
|
16
|
+
# pull default jabber username and password from environment
|
17
|
+
'jabber.user' => ENV['JOGGLE_USERNAME'],
|
18
|
+
'jabber.pass' => ENV['JOGGLE_PASSWORD'],
|
19
|
+
}
|
20
|
+
|
21
|
+
#
|
22
|
+
# Create and run a new command-line option parser.
|
23
|
+
#
|
24
|
+
def self.run(app, args)
|
25
|
+
new(app).run(args)
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Create new command-line option parser.
|
30
|
+
#
|
31
|
+
def initialize(app)
|
32
|
+
@app = app
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Run command-line option parser.
|
37
|
+
#
|
38
|
+
def run(args)
|
39
|
+
ret = DEFAULTS.merge({})
|
40
|
+
|
41
|
+
# create option parser
|
42
|
+
o = ::OptionParser.new do |o|
|
43
|
+
o.banner = "Usage: #@app [options]"
|
44
|
+
o.separator " "
|
45
|
+
|
46
|
+
# add command-line options
|
47
|
+
o.separator "Options:"
|
48
|
+
|
49
|
+
o.on('-A', '--allow USER', 'Allow Jabber subscription from USER.') do |v|
|
50
|
+
add_allowed(ret, v)
|
51
|
+
end
|
52
|
+
|
53
|
+
o.on('-c', '--config FILE', 'Use configuration file FILE.') do |v|
|
54
|
+
Joggle::ConfigParser.run(v) do |key, val|
|
55
|
+
if key == 'engine.allow'
|
56
|
+
add_allowed(ret, val)
|
57
|
+
elsif key == 'engine.update.range'
|
58
|
+
add_update_range(ret, val)
|
59
|
+
else
|
60
|
+
ret[key] = val
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
o.on('-D', '--daemon', 'Run as daemon (in background).') do |v|
|
66
|
+
ret['cli.daemon'] = true
|
67
|
+
end
|
68
|
+
|
69
|
+
o.on('--foreground', 'Run in foreground (the default).') do |v|
|
70
|
+
ret['cli.daemon'] = false
|
71
|
+
end
|
72
|
+
|
73
|
+
o.on('-L', '--log-level LEVEL', 'Set log level to LEVEL.') do |v|
|
74
|
+
ret['runner.log.level'] = v
|
75
|
+
end
|
76
|
+
|
77
|
+
o.on('-l', '--log FILE', 'Log to FILE.') do |v|
|
78
|
+
ret['runner.log.path'] = v
|
79
|
+
end
|
80
|
+
|
81
|
+
o.on('-p', '--password PASS', 'Jabber password (INSECURE!).') do |v|
|
82
|
+
ret['jabber.pass'] = v
|
83
|
+
end
|
84
|
+
|
85
|
+
o.on('-s', '--store FILE', 'Use FILE as backing store.') do |v|
|
86
|
+
ret['runner.store.path'] = v
|
87
|
+
end
|
88
|
+
|
89
|
+
o.on('-u', '--username USER', 'Jabber username.') do |v|
|
90
|
+
ret['jabber.user'] = v
|
91
|
+
end
|
92
|
+
|
93
|
+
o.separator ' '
|
94
|
+
|
95
|
+
o.on_tail('-v', '--version', 'Print version string.') do
|
96
|
+
puts "Joggle %s, by %s" % [
|
97
|
+
Joggle::VERSION,
|
98
|
+
'Paul Duncan <pabs@pablotron.org>',
|
99
|
+
]
|
100
|
+
exit
|
101
|
+
end
|
102
|
+
|
103
|
+
o.on_tail('-h', '--help', 'Print help information.') do
|
104
|
+
puts o
|
105
|
+
exit
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# parse arguments
|
110
|
+
o.parse(args)
|
111
|
+
|
112
|
+
# return results
|
113
|
+
ret
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
#
|
119
|
+
# Add an allowed user.
|
120
|
+
#
|
121
|
+
def add_allowed(ret, val)
|
122
|
+
return unless val && val =~ /\S/
|
123
|
+
|
124
|
+
ret['engine.allow'] ||= []
|
125
|
+
ret['engine.allow'].concat(val.strip.downcase.split(/\s*,\s*/))
|
126
|
+
end
|
127
|
+
|
128
|
+
def add_update_range(ret, val)
|
129
|
+
return unless val && val =~ /\S/
|
130
|
+
|
131
|
+
if md = val.match(/(\d+)\s*-\s*(\d+)\s+(\d+)/)
|
132
|
+
key, mins = "#{md[1]}-#{md[2]}", md[3].to_i
|
133
|
+
ret['engine.update.range'] ||= {}
|
134
|
+
ret['engine.update.range'][key] = mins
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'joggle/runner/pstore'
|
2
|
+
require 'joggle/cli/option-parser'
|
3
|
+
|
4
|
+
module Joggle
|
5
|
+
module CLI
|
6
|
+
#
|
7
|
+
# Basic command-line interface for Joggle.
|
8
|
+
#
|
9
|
+
class Runner
|
10
|
+
#
|
11
|
+
# Create and run a CLI object.
|
12
|
+
#
|
13
|
+
def self.run(app, args)
|
14
|
+
new(app, args).run
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Create CLI object.
|
19
|
+
#
|
20
|
+
def initialize(app, args)
|
21
|
+
@opt = OptionParser.run(app, args)
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# Run command-line interface.
|
26
|
+
#
|
27
|
+
def run
|
28
|
+
if @opt['cli.daemon']
|
29
|
+
pid = Process.fork {
|
30
|
+
Joggle::Runner::PStore.run(@opt)
|
31
|
+
exit 0;
|
32
|
+
}
|
33
|
+
|
34
|
+
# detach from background process
|
35
|
+
Process.detach(pid)
|
36
|
+
|
37
|
+
# print process id and exit
|
38
|
+
$stderr.puts "Detached from pid #{pid}"
|
39
|
+
else
|
40
|
+
Joggle::Runner::PStore.run(@opt)
|
41
|
+
end
|
42
|
+
|
43
|
+
exit 0;
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module Joggle
|
2
|
+
#
|
3
|
+
# Mixin to handle commands.
|
4
|
+
#
|
5
|
+
module Commands
|
6
|
+
#
|
7
|
+
# Handle .register command.
|
8
|
+
#
|
9
|
+
def do_register(who, arg)
|
10
|
+
# see if user is registered
|
11
|
+
if user = @tweeter.registered?(who)
|
12
|
+
# user is registered, return error
|
13
|
+
msg = "Already registered as #{user}"
|
14
|
+
else
|
15
|
+
# user isn't registered, so add them
|
16
|
+
|
17
|
+
# get twitter username and password from argument
|
18
|
+
user, pass = arg.split(/\s+/, 2)
|
19
|
+
|
20
|
+
# register user
|
21
|
+
begin
|
22
|
+
@tweeter.register(who, user, pass)
|
23
|
+
msg = "Registered as #{user}"
|
24
|
+
rescue Exception => err
|
25
|
+
msg = "Couldn't register: #{err}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# reply to request
|
30
|
+
reply(who, msg)
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# Handle .unregister command.
|
35
|
+
#
|
36
|
+
def do_unregister(who, arg)
|
37
|
+
# see if user is registered
|
38
|
+
if @tweeter.registered?(who)
|
39
|
+
# user is registerd, so unregister them
|
40
|
+
|
41
|
+
begin
|
42
|
+
# unregister user
|
43
|
+
@tweeter.unregister(who)
|
44
|
+
|
45
|
+
# send success
|
46
|
+
msg = "Unregistered."
|
47
|
+
rescue Exception => err
|
48
|
+
msg = "Couldn't unregister: #{err}"
|
49
|
+
end
|
50
|
+
else
|
51
|
+
# user isn't registered, send error
|
52
|
+
msg = "Not registered."
|
53
|
+
end
|
54
|
+
|
55
|
+
# reply to request
|
56
|
+
reply(who, msg)
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Handle .list command.
|
61
|
+
#
|
62
|
+
def do_list(who, arg)
|
63
|
+
# see if user is registered
|
64
|
+
if @tweeter.registered?(who)
|
65
|
+
# user is registerd, so unregister them
|
66
|
+
|
67
|
+
begin
|
68
|
+
msgs = []
|
69
|
+
|
70
|
+
# build list
|
71
|
+
@tweeter.list(who) do |id, time, from, msg|
|
72
|
+
msgs << make_response(id, time, from, msg)
|
73
|
+
end
|
74
|
+
|
75
|
+
# build response
|
76
|
+
if msgs.size > 0
|
77
|
+
msg = msgs.join("\n")
|
78
|
+
else
|
79
|
+
msg = 'No tweets.'
|
80
|
+
end
|
81
|
+
rescue Exception => err
|
82
|
+
msg = "Couldn't list tweets: #{err}"
|
83
|
+
end
|
84
|
+
else
|
85
|
+
# user isn't registered, send error
|
86
|
+
msg = "Not registered."
|
87
|
+
end
|
88
|
+
|
89
|
+
# reply to request
|
90
|
+
reply(who, msg)
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# String constant for .help command.
|
95
|
+
#
|
96
|
+
HELP = [
|
97
|
+
"Joggle Help:",
|
98
|
+
"Available commands:",
|
99
|
+
" .help - Display this help screen.",
|
100
|
+
" .register <user> <pass> - Register Twitter username and password.",
|
101
|
+
" .unregister - Forget Twitter username and password.",
|
102
|
+
# TODO: " .force <msg> - Force tweet.",
|
103
|
+
# TODO: " .list - List recent tweets.",
|
104
|
+
"Any other message with two words or more is sent as a tweet. See the Joggle home page at http://pablotron.org/software/joggle/ for additional information",
|
105
|
+
].join("\n")
|
106
|
+
|
107
|
+
#
|
108
|
+
# Handle .help command.
|
109
|
+
#
|
110
|
+
def do_help(who, arg)
|
111
|
+
reply(who, HELP)
|
112
|
+
end
|
113
|
+
|
114
|
+
EGGS = [
|
115
|
+
# movie quotes
|
116
|
+
"This is what happens, Larry!",
|
117
|
+
"Got a package, people!",
|
118
|
+
"Billy, do you like wrestling?",
|
119
|
+
"NO AND DEN!",
|
120
|
+
"I'm the dude playing the dude disguised as another dude.",
|
121
|
+
"Hey, want to hear the most annoying sound in the world?",
|
122
|
+
"Bueller?",
|
123
|
+
"Inconcievable!",
|
124
|
+
|
125
|
+
# non-movie quotes
|
126
|
+
"You are in a maze of twisty compiler features, all different.",
|
127
|
+
"You can tune a filesystem, but you can't tuna fish.",
|
128
|
+
"Never attribute to malice that which can adequately explained by stupidity.",
|
129
|
+
"The first thing to do when you find yourself in a hole is to stop digging.",
|
130
|
+
"I once knew a man who had a dog with only three legs, and yet that man could play the banjo like anything.",
|
131
|
+
"The needs of the many outweigh the needs of the guy who can't run fast.",
|
132
|
+
"It may look like I'm doing nothing, but I'm actively waiting for my problems to go away.",
|
133
|
+
"Nobody ever went broke underestimating the intelligence of the American people.",
|
134
|
+
"There are only two things wrong with C++: The initial concept and the implementation.",
|
135
|
+
"There are only two kinds of people, those who finish what they start, and so on.",
|
136
|
+
|
137
|
+
# exclamations
|
138
|
+
"I'LL SAY!!!",
|
139
|
+
"AND HOW!!!",
|
140
|
+
"Cha-ching!",
|
141
|
+
"Crikey!",
|
142
|
+
"Nope.",
|
143
|
+
"Sweet!",
|
144
|
+
"Blimey!",
|
145
|
+
|
146
|
+
# emoticons
|
147
|
+
":-D",
|
148
|
+
"^_^",
|
149
|
+
":(",
|
150
|
+
":-)",
|
151
|
+
":')-|-<",
|
152
|
+
|
153
|
+
# misc
|
154
|
+
"http://pablotron.org/offended",
|
155
|
+
"<INSERT WITTY REMARK HERE>",
|
156
|
+
"0xBEEFCAFE",
|
157
|
+
]
|
158
|
+
|
159
|
+
def do_easteregg(who, arg)
|
160
|
+
reply(who, EGGS[rand(EGGS.size)])
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Joggle
|
2
|
+
#
|
3
|
+
# Simple configuration file parser.
|
4
|
+
#
|
5
|
+
class ConfigParser
|
6
|
+
#
|
7
|
+
# Parse configuration file and pass each directive to the
|
8
|
+
# specified block.
|
9
|
+
#
|
10
|
+
def self.run(path, &block)
|
11
|
+
new(path).run(&block)
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# Create a new config file parser.
|
16
|
+
#
|
17
|
+
def initialize(path)
|
18
|
+
@path = path
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Parse configuration file and pass each directive to the specified
|
23
|
+
# block.
|
24
|
+
#
|
25
|
+
def run(&block)
|
26
|
+
File.readlines(@path).each do |line|
|
27
|
+
next if line =~ /\s*#/ || line !~ /\S/
|
28
|
+
line = line.strip
|
29
|
+
key, val = line.split(/\s+/, 2)
|
30
|
+
|
31
|
+
if key && key.size > 0
|
32
|
+
block.call(key, val)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,276 @@
|
|
1
|
+
require 'joggle/pablotron/observable'
|
2
|
+
require 'joggle/commands'
|
3
|
+
|
4
|
+
module Joggle
|
5
|
+
#
|
6
|
+
# Joggle engine object. This is where the magic happens.
|
7
|
+
#
|
8
|
+
class Engine
|
9
|
+
include Joggle::Pablotron::Observable
|
10
|
+
include Commands
|
11
|
+
|
12
|
+
DEFAULTS = {
|
13
|
+
# output time format
|
14
|
+
'engine.time_format' => '%H:%M',
|
15
|
+
|
16
|
+
# enable sanity checks
|
17
|
+
'engine.message_sanity_checks' => true,
|
18
|
+
|
19
|
+
# update every 60 minutes by default
|
20
|
+
'engine.update.default' => 60,
|
21
|
+
|
22
|
+
# time ranges for updates
|
23
|
+
'engine.update.range' => {
|
24
|
+
# from 8am until 3pm, update every 10 minutes
|
25
|
+
'8-15' => 10,
|
26
|
+
|
27
|
+
# from 3pm until 10pm, update every 5 minutes
|
28
|
+
'15-22' => 5,
|
29
|
+
|
30
|
+
# from 10pm until midnight, update every 10 minutes
|
31
|
+
'22-24' => 10,
|
32
|
+
|
33
|
+
# from midnight until 2am, update every 20 minutes
|
34
|
+
'0-2' => 20,
|
35
|
+
},
|
36
|
+
}
|
37
|
+
|
38
|
+
#
|
39
|
+
# Create a new Joggle engine object.
|
40
|
+
#
|
41
|
+
def initialize(client, tweeter, opt = nil)
|
42
|
+
@opt = DEFAULTS.merge(opt || {})
|
43
|
+
|
44
|
+
@client = client
|
45
|
+
@client.on(self)
|
46
|
+
|
47
|
+
@tweeter = tweeter
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Run forever.
|
52
|
+
#
|
53
|
+
def run
|
54
|
+
loop {
|
55
|
+
# check for updates (if we need to)
|
56
|
+
if need_update?
|
57
|
+
update
|
58
|
+
end
|
59
|
+
|
60
|
+
# fire idle method
|
61
|
+
fire('engine_idle')
|
62
|
+
|
63
|
+
# sleep for thirty seconds
|
64
|
+
sleep 30
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Reply to a user with the given message.
|
70
|
+
#
|
71
|
+
def reply(who, msg)
|
72
|
+
begin
|
73
|
+
if fire('engine_before_reply', who, msg)
|
74
|
+
@client.deliver(who, msg)
|
75
|
+
fire('engine_reply', who, msg)
|
76
|
+
else
|
77
|
+
fire('engine_reply_stopped', who, msg, err)
|
78
|
+
end
|
79
|
+
rescue Exception => err
|
80
|
+
fire('engine_reply_error', who, msg, err)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
####################
|
85
|
+
# client listeners #
|
86
|
+
####################
|
87
|
+
|
88
|
+
COMMAND_REGEX = /^\s*\.(\w+)\s*(\S.*|)\s*$/
|
89
|
+
|
90
|
+
#
|
91
|
+
# Jabber message listener callback.
|
92
|
+
#
|
93
|
+
def on_jabber_client_message(client, msg)
|
94
|
+
# get the message source
|
95
|
+
who = msg.from.to_s
|
96
|
+
|
97
|
+
# only listen to allowed users
|
98
|
+
if allowed?(who)
|
99
|
+
if md = msg.body.match(COMMAND_REGEX)
|
100
|
+
cmd = md[1].downcase
|
101
|
+
handle_command(who, cmd, md[2])
|
102
|
+
else
|
103
|
+
handle_message(who, msg.body)
|
104
|
+
end
|
105
|
+
else
|
106
|
+
fire('engine_ignored_message', who, msg)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
#
|
111
|
+
# Jabber subscription listener callback.
|
112
|
+
#
|
113
|
+
def on_before_jabber_client_accept_subscription(client, who)
|
114
|
+
unless allowed?(who)
|
115
|
+
fire('engine_ignored_subscription', who)
|
116
|
+
raise Joggle::Pablotron::Observable::StopEvent, "denied subscription: #{who}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def handle_command(who, cmd, arg)
|
123
|
+
# build method name
|
124
|
+
meth = "do_#{cmd}"
|
125
|
+
|
126
|
+
if respond_to?(meth)
|
127
|
+
# return if @tweeter.ignored?(who)
|
128
|
+
fire('engine_command', who, cmd, arg)
|
129
|
+
send("do_#{cmd}", who, arg)
|
130
|
+
else
|
131
|
+
# unknown commands
|
132
|
+
# FIXME: is this the correct behavior?
|
133
|
+
reply(who, "Unknown command: #{cmd}")
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def handle_message(who, msg)
|
138
|
+
# remove extraneous whitespace
|
139
|
+
msg, out_msg = msg.strip, nil
|
140
|
+
|
141
|
+
# notify listeners
|
142
|
+
fire('engine_message', who, msg)
|
143
|
+
|
144
|
+
# make sure message isn't too long
|
145
|
+
if msg.length < 140
|
146
|
+
# make sure message is sane
|
147
|
+
if sane_message?(msg)
|
148
|
+
begin
|
149
|
+
row = @tweeter.tweet(who, msg)
|
150
|
+
out = "Done (id: #{row['id']})"
|
151
|
+
rescue Exception => err
|
152
|
+
out = "Error: #{err.backtrace.first}: #{err.message}"
|
153
|
+
end
|
154
|
+
else
|
155
|
+
out = "Error: Message is too short (try adding more words)"
|
156
|
+
end
|
157
|
+
else
|
158
|
+
out = 'Message length is greater than 140 characters'
|
159
|
+
end
|
160
|
+
|
161
|
+
# send reply
|
162
|
+
reply(who, out)
|
163
|
+
end
|
164
|
+
|
165
|
+
def allowed?(who)
|
166
|
+
# get list of allowed users
|
167
|
+
a = @opt['engine.allow']
|
168
|
+
|
169
|
+
# default to true
|
170
|
+
ret = true
|
171
|
+
|
172
|
+
if a && a.size > 0
|
173
|
+
ret = a.any? { |str| who.match(/^#{str}/i) }
|
174
|
+
end
|
175
|
+
|
176
|
+
# return result
|
177
|
+
ret
|
178
|
+
end
|
179
|
+
|
180
|
+
#
|
181
|
+
# Get the update interval for the given time
|
182
|
+
#
|
183
|
+
def get_update_interval(time = Time.now)
|
184
|
+
hour = time.hour
|
185
|
+
default, ranges = %w{default range}.map { |k| @opt["engine.update.#{k}"] }
|
186
|
+
|
187
|
+
if ranges
|
188
|
+
ranges.each do |key, val|
|
189
|
+
# get start/end hour
|
190
|
+
hours = key.split(/\s*-\s*/).map { |s| s.to_i }
|
191
|
+
|
192
|
+
# if this interval matches the given time, return it
|
193
|
+
if hour >= hours.first && hour <= hours.last
|
194
|
+
return val.to_i
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# return the default interval
|
200
|
+
default.to_i
|
201
|
+
end
|
202
|
+
|
203
|
+
#
|
204
|
+
# Time of the next update
|
205
|
+
#
|
206
|
+
def next_update(time = Time.now)
|
207
|
+
# get the update interval for the given time
|
208
|
+
m = get_update_interval(time)
|
209
|
+
m = 5 if m < 5
|
210
|
+
|
211
|
+
(@last_update || 0) + (m * 60)
|
212
|
+
end
|
213
|
+
|
214
|
+
#
|
215
|
+
# Do we need an update?
|
216
|
+
#
|
217
|
+
def need_update?
|
218
|
+
return true unless @last_update
|
219
|
+
|
220
|
+
# get the current timestamp
|
221
|
+
now = Time.now
|
222
|
+
|
223
|
+
# return true if the last update was more than m minutes ago
|
224
|
+
next_update(now) < now.to_i
|
225
|
+
end
|
226
|
+
|
227
|
+
def update
|
228
|
+
begin
|
229
|
+
if fire('before_engine_update')
|
230
|
+
# save last update time
|
231
|
+
@last_update = Time.now.to_i
|
232
|
+
|
233
|
+
# send updates
|
234
|
+
@tweeter.update do |who, id, time, from, msg|
|
235
|
+
reply(who, make_response(id, time, from, msg))
|
236
|
+
end
|
237
|
+
|
238
|
+
# notify listeners
|
239
|
+
fire('engine_update')
|
240
|
+
else
|
241
|
+
fire('engine_update_stopped')
|
242
|
+
end
|
243
|
+
rescue Exception => err
|
244
|
+
fire('engine_update_error', err)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
#
|
249
|
+
# Constraints to prevent garbage tweets.
|
250
|
+
#
|
251
|
+
MESSAGE_SANITY_CHECKS = [
|
252
|
+
# contains at least three consecutive word characters
|
253
|
+
proc { |m| m.match(/\w{3}/) },
|
254
|
+
|
255
|
+
# contains at least three, at least two of which are longer than
|
256
|
+
# three characters
|
257
|
+
proc { |m|
|
258
|
+
words = m.split(/\s+/)
|
259
|
+
(words.size > 2) && (words.select { |w| w && w.size > 2 }.size > 1)
|
260
|
+
},
|
261
|
+
]
|
262
|
+
|
263
|
+
def sane_message?(msg)
|
264
|
+
# are sanity checks enabled?
|
265
|
+
return true unless @opt['engine.message_sanity_checks']
|
266
|
+
|
267
|
+
# pass message through all sanity checks
|
268
|
+
MESSAGE_SANITY_CHECKS.all? { |p| p.call(msg) }
|
269
|
+
end
|
270
|
+
|
271
|
+
def make_response(id, time, from, msg)
|
272
|
+
stamp = time.strftime(@opt['engine.time_format'])
|
273
|
+
"%s: %s (%s, #%d)" % [from, msg, stamp, id]
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|