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,96 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module Joggle
|
4
|
+
module Store
|
5
|
+
module PStore
|
6
|
+
#
|
7
|
+
# Mixin that adds user store methods for PStore backend.
|
8
|
+
#
|
9
|
+
# Note: You're probably looking for Joggle::Store::PStore::All
|
10
|
+
#
|
11
|
+
module User
|
12
|
+
#
|
13
|
+
# Add user to store.
|
14
|
+
#
|
15
|
+
def add_user(key, row)
|
16
|
+
key = user_store_key(key)
|
17
|
+
|
18
|
+
@store.transaction do |s|
|
19
|
+
s[key] = row
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Update user's store entry.
|
25
|
+
#
|
26
|
+
def update_user(key, row)
|
27
|
+
key = user_store_key(key)
|
28
|
+
|
29
|
+
@store.transaction do |s|
|
30
|
+
row.each { |k, v| s[key][k] = v }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# Get information about given user.
|
36
|
+
#
|
37
|
+
def get_user(key)
|
38
|
+
key = user_store_key(key)
|
39
|
+
|
40
|
+
@store.transaction(true) do |s|
|
41
|
+
s[key]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# Is the given user ignored?
|
47
|
+
#
|
48
|
+
def ignored?(key)
|
49
|
+
get_user(key)['ignored']
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Does the given user exist?
|
54
|
+
#
|
55
|
+
def has_user?(key)
|
56
|
+
key = user_store_key(key)
|
57
|
+
|
58
|
+
@store.transaction(true) do |s|
|
59
|
+
s.root?(key)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
#
|
64
|
+
# Iterate over all users in store.
|
65
|
+
#
|
66
|
+
def each_user(&block)
|
67
|
+
users = @store.transaction(true) do |s|
|
68
|
+
s.roots.inject([]) do |r, key|
|
69
|
+
(key =~ /^user-/) ? r << s[key] : r
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
users.each(&block)
|
74
|
+
end
|
75
|
+
|
76
|
+
#
|
77
|
+
# Delete given user from store.
|
78
|
+
#
|
79
|
+
def delete_user(key)
|
80
|
+
key = user_store_key(key)
|
81
|
+
|
82
|
+
@store.transaction do |s|
|
83
|
+
s.delete(key)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
# Map user key to PStore root key.
|
89
|
+
#
|
90
|
+
def user_store_key(key)
|
91
|
+
'user-' << Digest::MD5.hexdigest(key.gsub(/\/.*$/, ''))
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'time'
|
3
|
+
require 'joggle/pablotron/observable'
|
4
|
+
|
5
|
+
module Joggle
|
6
|
+
module Twitter
|
7
|
+
#
|
8
|
+
# Twitter engine object.
|
9
|
+
#
|
10
|
+
class Engine
|
11
|
+
include Joggle::Pablotron::Observable
|
12
|
+
|
13
|
+
DEFAULTS = {
|
14
|
+
# update interval, in minutes
|
15
|
+
'twitter.engine.update_interval' => 5,
|
16
|
+
}
|
17
|
+
|
18
|
+
#
|
19
|
+
# Create new twitter engine.
|
20
|
+
#
|
21
|
+
def initialize(user_store, fetcher, opt = nil)
|
22
|
+
@opt = DEFAULTS.merge(opt || {})
|
23
|
+
@store = user_store
|
24
|
+
@fetcher = fetcher
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# Is the given Jabber user ignored?
|
29
|
+
#
|
30
|
+
def ignored?(who)
|
31
|
+
if rec = @store.get_user(who)
|
32
|
+
rec['ignored']
|
33
|
+
else
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Is the given Jabber registered?
|
40
|
+
#
|
41
|
+
def registered?(who)
|
42
|
+
@store.has_user?(who)
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# Bind the given Jabber user to the given Twitter username and
|
47
|
+
# password.
|
48
|
+
#
|
49
|
+
def register(who, user, pass)
|
50
|
+
store = @store
|
51
|
+
|
52
|
+
stoppable_action('twitter_engine_register_user', who, user, pass) do
|
53
|
+
store.add_user(who, {
|
54
|
+
'who' => who.gsub(/\/.*$/, ''),
|
55
|
+
'user' => user,
|
56
|
+
'pass' => pass,
|
57
|
+
'updated_at' => 0,
|
58
|
+
'last_id' => 0,
|
59
|
+
'ignored' => false,
|
60
|
+
'sleep_bgn' => 0, # sleep start time, in hours
|
61
|
+
'sleep_end' => 0, # sleep end time, in hours
|
62
|
+
})
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# Forget registration for the given Jabber user.
|
68
|
+
#
|
69
|
+
def unregister(who)
|
70
|
+
store = @store
|
71
|
+
|
72
|
+
stoppable_action('twitter_engine_unregister_user', who) do
|
73
|
+
store.delete_user(who)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# Send a tweet as the given Jabber user.
|
79
|
+
#
|
80
|
+
def tweet(who, msg)
|
81
|
+
ret, store, fetcher = nil, @store, @fetcher
|
82
|
+
|
83
|
+
stoppable_action('twitter_engine_tweet', who, msg) do
|
84
|
+
if user = store.get_user(who)
|
85
|
+
ret = fetcher.tweet(user, msg)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# return result
|
90
|
+
ret
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# List recent tweets for the given user.
|
95
|
+
#
|
96
|
+
def list(who, &block)
|
97
|
+
store, fetcher = @store, @fetcher
|
98
|
+
|
99
|
+
stoppable_action('twitter_engine_list', who) do
|
100
|
+
if user = store.get_user(who)
|
101
|
+
fetcher.get(user, true) do |id, who, time, msg|
|
102
|
+
block.call(id, who, time, msg)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
#
|
109
|
+
# Update all users.
|
110
|
+
#
|
111
|
+
def update(&block)
|
112
|
+
store, updates = @store, []
|
113
|
+
|
114
|
+
# make list of updates
|
115
|
+
@store.each_user do |user|
|
116
|
+
updates << user if needs_update?(user)
|
117
|
+
end
|
118
|
+
|
119
|
+
# iterate over updates and do each one
|
120
|
+
updates.each do |user|
|
121
|
+
stoppable_action('twitter_engine_update', user) do
|
122
|
+
update_user(user) do |id, time, src, msg|
|
123
|
+
block.call(user['who'], id, time, src, msg)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
#
|
132
|
+
# Update specific Jabber user.
|
133
|
+
#
|
134
|
+
def update_user(user, &block)
|
135
|
+
last_id = nil
|
136
|
+
|
137
|
+
@fetcher.get(user) do |id, who, time, msg|
|
138
|
+
last_id = id
|
139
|
+
block.call(id, who, time, msg)
|
140
|
+
end
|
141
|
+
|
142
|
+
@store.update_user(user['who'], {
|
143
|
+
'last_id' => last_id,
|
144
|
+
'updated_at' => Time.now.to_i,
|
145
|
+
})
|
146
|
+
end
|
147
|
+
|
148
|
+
#
|
149
|
+
# Does the given Jabber user need an update?
|
150
|
+
#
|
151
|
+
def needs_update?(user)
|
152
|
+
now, since = Time.now, Time.now - @opt['twitter.engine.update_interval']
|
153
|
+
|
154
|
+
# check update interval
|
155
|
+
if user['updated_at'].to_i < since.to_i
|
156
|
+
# check sleep interval
|
157
|
+
!is_sleeping?(user, now)
|
158
|
+
else
|
159
|
+
# updated within update_interval
|
160
|
+
false
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
#
|
165
|
+
# Is the given Jabber user asleep?
|
166
|
+
#
|
167
|
+
def is_sleeping?(user, time = Time.now)
|
168
|
+
# build sleep range
|
169
|
+
sleep = %w{bgn end}.map { |s| user["sleep_#{s}"].to_i }
|
170
|
+
|
171
|
+
# compare user sleep time
|
172
|
+
time.hour >= sleep.first && time.hour <= sleep.last
|
173
|
+
end
|
174
|
+
|
175
|
+
#
|
176
|
+
# Fire a stoppable event.
|
177
|
+
#
|
178
|
+
def stoppable_action(key, *args, &block)
|
179
|
+
if fire("before_#{key}", *args)
|
180
|
+
block.call
|
181
|
+
fire(key, *args)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/http'
|
3
|
+
require 'openssl'
|
4
|
+
require 'json'
|
5
|
+
require 'joggle/pablotron/cache'
|
6
|
+
|
7
|
+
module Joggle
|
8
|
+
module Twitter
|
9
|
+
#
|
10
|
+
# Handler for Twitter HTTP requests.
|
11
|
+
#
|
12
|
+
class Fetcher
|
13
|
+
DEFAULTS = {
|
14
|
+
'twitter.fetcher.url.timeline' => 'https://twitter.com/statuses/friends_timeline.json',
|
15
|
+
'twitter.fetcher.url.tweet' => 'https://twitter.com/statuses/update.json',
|
16
|
+
}
|
17
|
+
|
18
|
+
#
|
19
|
+
# Create a new Twitter::Fetcher object.
|
20
|
+
#
|
21
|
+
def initialize(message_store, cache, opt = {})
|
22
|
+
@opt = DEFAULTS.merge(opt || {})
|
23
|
+
@store = message_store
|
24
|
+
@cache = cache
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# Get Twitter updates for given user and pass them to the
|
29
|
+
# specified block.
|
30
|
+
#
|
31
|
+
def get(user, show_all = false, &block)
|
32
|
+
url, opt = url_for(user, 'timeline'), opt_for(user)
|
33
|
+
|
34
|
+
if data = @cache.get(url, opt)
|
35
|
+
JSON.parse(data).reverse.each do |row|
|
36
|
+
cached = @store.has_message?(row['id'])
|
37
|
+
|
38
|
+
if show_all || !cached
|
39
|
+
# cache message
|
40
|
+
@store.add_message(row['id'], row)
|
41
|
+
|
42
|
+
# send to parent
|
43
|
+
block.call(
|
44
|
+
row['id'],
|
45
|
+
Time.parse(row['created_at']),
|
46
|
+
row['user']['screen_name'],
|
47
|
+
row['text']
|
48
|
+
)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Send a tweet. Use Joggle::Engine#tweet instead.
|
56
|
+
#
|
57
|
+
def tweet(user, msg)
|
58
|
+
# build URI and headers (opt)
|
59
|
+
url, opt = url_for(user, 'tweet'), opt_for(user)
|
60
|
+
uri = URI.parse(url)
|
61
|
+
ret = nil
|
62
|
+
|
63
|
+
# build post data
|
64
|
+
data = {
|
65
|
+
'status' => msg,
|
66
|
+
}.map { |a|
|
67
|
+
a.map { |v| CGI.escape(v) }.join('=')
|
68
|
+
}.join('&')
|
69
|
+
|
70
|
+
# FIXME: add user-agent to headers
|
71
|
+
req = Net::HTTP.new(uri.host, uri.port)
|
72
|
+
req.use_ssl = (uri.scheme == 'https')
|
73
|
+
|
74
|
+
# start http request
|
75
|
+
req.start do |http|
|
76
|
+
# post request
|
77
|
+
r = http.post(uri.path, data, opt)
|
78
|
+
|
79
|
+
# check response
|
80
|
+
case r
|
81
|
+
when Net::HTTPSuccess
|
82
|
+
ret = JSON.parse(r.body)
|
83
|
+
|
84
|
+
# File.open('/tmp/foo.log', 'a') do |fh|
|
85
|
+
# fh.puts "r.body = #{r.body}"
|
86
|
+
# end
|
87
|
+
|
88
|
+
# check result
|
89
|
+
if ret && ret.key?('id')
|
90
|
+
@store.add_message(ret['id'], ret)
|
91
|
+
else
|
92
|
+
throw "got weird response from twitter"
|
93
|
+
end
|
94
|
+
else
|
95
|
+
throw r
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# return result
|
100
|
+
ret
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def url_for(user, key)
|
106
|
+
args = nil
|
107
|
+
|
108
|
+
if false && key == 'timeline'
|
109
|
+
if user['last_id'] && user['last_id'] > 0
|
110
|
+
args = { 'since_id' => user['last_id'] }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
Joggle::Pablotron::Cache.urlify(@opt['twitter.fetcher.url.' + key], args)
|
115
|
+
end
|
116
|
+
|
117
|
+
def opt_for(user)
|
118
|
+
str = ["#{user['user']}:#{user['pass']}"].pack('m').strip
|
119
|
+
{ 'Authorization' => 'Basic ' + str }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|