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