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.
@@ -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
@@ -0,0 +1,6 @@
1
+ module Joggle
2
+ #
3
+ # Joggle release version.
4
+ #
5
+ VERSION = '0.1.0'
6
+ end