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