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,82 @@
1
+ require 'joggle/pablotron/observable'
2
+ require 'xmpp4r'
3
+ require 'xmpp4r/roster'
4
+
5
+ module Joggle
6
+ module Jabber
7
+ #
8
+ # Simple XMPP client.
9
+ #
10
+ class Client
11
+ include Joggle::Pablotron::Observable
12
+
13
+ DEFAULTS = {
14
+ 'jabber.client.debug' => false,
15
+ }
16
+
17
+ #
18
+ # Create a new Jabber::Client object.
19
+ #
20
+ # Example:
21
+ #
22
+ # # create new client object
23
+ # client = Client.new('foo@example.com', 'mysekretpassword')
24
+ #
25
+ def initialize(user, pass, opt = {})
26
+ # parse options
27
+ @opt = DEFAULTS.merge(opt || {})
28
+
29
+ # enable debugging to stdout
30
+ if @opt['jabber.client.debug']
31
+ ::Jabber.debug = true
32
+ end
33
+
34
+ # FIXME: this belongs elsewhere
35
+ Thread.abort_on_exception = false
36
+
37
+ # create new jid and client
38
+ jid = ::Jabber::JID.new(user)
39
+ available = ::Jabber::Presence.new.set_type(:available)
40
+ @client = ::Jabber::Client.new(jid)
41
+ @client.connect
42
+
43
+ @client.auth(pass)
44
+ @client.send(available)
45
+
46
+ roster = ::Jabber::Roster::Helper.new(@client)
47
+
48
+ @client.add_message_callback do |msg|
49
+ next unless msg.type == :chat
50
+ fire('jabber_client_message', msg)
51
+ end
52
+
53
+ @client.add_presence_callback do |old_p, new_p|
54
+ fire('jabber_client_presence', old_p, new_p)
55
+ end
56
+
57
+ roster.add_subscription_request_callback do |item, presence|
58
+ from = presence.from
59
+
60
+ if fire('before_jabber_client_accept_subscription', from)
61
+ roster.accept_subscription(from)
62
+ fire('jabber_client_accept_subscription', from)
63
+ end
64
+ end
65
+ end
66
+
67
+ #
68
+ # Deliver jabber message to user.
69
+ #
70
+ # Example:
71
+ #
72
+ # # send message
73
+ # client.deliver('foo@example.com', 'hey there!')
74
+ #
75
+ def deliver(who, body, type = :chat)
76
+ msg = ::Jabber::Message.new(who, body)
77
+ msg.type = type
78
+ @client.send(msg)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,131 @@
1
+ require 'pp' # debug
2
+ require 'cgi'
3
+ require 'open-uri'
4
+ require 'joggle/pablotron/observable'
5
+
6
+ module Joggle
7
+ module Pablotron
8
+ class Cache
9
+ include Joggle::Pablotron::Observable
10
+
11
+ DEFAULTS = {
12
+ 'user-agent' => "Pablotron-Cacher/0.0.0",
13
+ }
14
+
15
+ def initialize(cache_store, extra_headers = nil)
16
+ @extras = extra_headers
17
+ @store = cache_store
18
+ end
19
+
20
+ def self.urlify(base, args = nil, hash = nil)
21
+ ret = base
22
+
23
+ if args && args.size > 0
24
+ ret = base + '?' + args.map { |k, v|
25
+ [k, v].map { |s| CGI.escape(s.to_s) }.join('=')
26
+ }.join('&')
27
+ end
28
+
29
+ if hash
30
+ ret << '#' + hash
31
+ end
32
+
33
+ # return result
34
+ ret
35
+ end
36
+
37
+ # delimiter for appending header string to url
38
+ # (chosen because it won't appear in a valid url)
39
+ HEADER_DELIM = '://?&#'
40
+
41
+ def url_key(url, headers = nil)
42
+ ret = url
43
+
44
+ # append header fragment
45
+ if headers && headers.size > 0
46
+ # sort, concatenate, and append headers to url
47
+ ret += HEADER_DELIM + headers.keys.map { |key|
48
+ key.downcase
49
+ }.sort.map { |k|
50
+ "#{k}:#{headers[k]}"
51
+ }.join(',')
52
+ end
53
+
54
+ # return results
55
+ ret
56
+ end
57
+
58
+ def get(url, headers = nil)
59
+ ret = nil
60
+
61
+ # create store key
62
+ key = url_key(url, headers)
63
+
64
+ # build headers
65
+ opt = expand_headers(headers)
66
+
67
+ # if we have an existing cache entry for this url,
68
+ # then use the last-modified and etag headers
69
+ if entry = @store.get_cached(key)
70
+ opt.update({
71
+ 'if-modified-since' => entry['last'].to_s,
72
+ 'if-none-match' => entry['etag'],
73
+ })
74
+ end
75
+
76
+ # fetch url and handle result
77
+ begin
78
+ open(url, opt) do |fh|
79
+ ret = fh.read
80
+
81
+ # update store
82
+ @store.add_cached(key, {
83
+ 'last' => fh.last_modified.to_s || fh.meta['date'],
84
+ 'etag' => fh.meta['etag'],
85
+ 'data' => ret.to_s.dup,
86
+ })
87
+
88
+ fire('cache_updated', url, ret)
89
+ end
90
+ rescue OpenURI::HTTPError => err
91
+ case err.io.status.first
92
+ when /^304/
93
+ # not modified
94
+ ret = entry['data']
95
+ fire('cache_not_modified', url)
96
+ else
97
+ # unknown status code
98
+ fire('cache_http_error', url, err.io.status.first, err.io.status.last)
99
+ end
100
+ end
101
+
102
+ # return result
103
+ ret
104
+ end
105
+
106
+ def has?(url, headers = nil)
107
+ key = url_key(url, headers)
108
+ @store.has_cached?(key)
109
+ end
110
+
111
+ def delete(url, headers = nil)
112
+ key = url_key(url, headers)
113
+ @store.delete_cached(key)
114
+ end
115
+
116
+ private
117
+
118
+ def expand_headers(headers = nil)
119
+ [DEFAULTS, @extras, headers].inject({}) do |ret, hash|
120
+ if hash && hash.size > 0
121
+ hash.keys.each do |key|
122
+ ret[key.downcase] = hash[key]
123
+ end
124
+ end
125
+
126
+ ret
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,104 @@
1
+ module Joggle
2
+ module Pablotron
3
+ module Observable
4
+ class StopEvent < Exception; end
5
+
6
+ def on(ev, &block)
7
+ d = lazy_observable_init
8
+
9
+ # get next id and create new handler
10
+ ret = (d[:next_id] += 1)
11
+ h = { :id => ret }
12
+
13
+ # set handler type
14
+ if ev.kind_of?(String) && block
15
+ h[:fn] = block
16
+
17
+ # add handler to handler list and id to id lut
18
+ d[:handlers][ev] << h
19
+ d[:id_lut][ret] = ev
20
+
21
+ elsif !ev.kind_of?(String) && !block
22
+ h[:obj] = ev
23
+ d[:object_handlers] << h
24
+ else
25
+ raise "missing listener block"
26
+ end
27
+
28
+ # return id
29
+ ret
30
+ end
31
+
32
+ def fire(ev, *args)
33
+ d = lazy_observable_init
34
+ ret = true
35
+
36
+ # get handlers for this event
37
+ handlers = d[:handlers][ev]
38
+
39
+ begin
40
+ if handlers.size > 0
41
+ handlers.each do |handler|
42
+ if fn = handler[:fn]
43
+ # run handler
44
+ fn.call(self, ev, *args)
45
+ else
46
+ # FIXME: do nothing, is this an error?
47
+ end
48
+ end
49
+ end
50
+
51
+ # get object handlers
52
+ handlers = d[:object_handlers]
53
+
54
+ if handlers.size > 0
55
+ handlers.each do |handler|
56
+ if o = handler[:obj]
57
+ # build method symbol
58
+ meth = "on_#{ev}".intern
59
+
60
+ # check for method
61
+ if o.respond_to?(meth)
62
+ o.send(meth, self, *args)
63
+ end
64
+ else
65
+ # FIXME: do nothing, is this an error?
66
+ end
67
+ end
68
+ end
69
+ rescue StopEvent => err
70
+ fire(ev + '_stopped', err, *args)
71
+ ret = false
72
+ end
73
+
74
+ # return result
75
+ ret
76
+ end
77
+
78
+ def un(id)
79
+ d = lazy_observable_init
80
+
81
+ # look in event handlers
82
+ if ev = d[:id_lut][id]
83
+ d[:handlers][ev].reject! { |fn| fn[:id] == id }
84
+ end
85
+
86
+ # check object handlers
87
+ d[:object_handlers].reject! { |o| o[:id] == id }
88
+
89
+ nil
90
+ end
91
+
92
+ private
93
+
94
+ def lazy_observable_init
95
+ @__observable_data ||= {
96
+ :next_id => 0,
97
+ :handlers => Hash.new { |h, k| h[k] = [] },
98
+ :object_handlers => [],
99
+ :id_lut => {},
100
+ }
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,252 @@
1
+ begin
2
+ require 'rubygems'
3
+ rescue LoadError
4
+ # ignore missing rubygems
5
+ end
6
+
7
+ require 'pstore'
8
+ require 'logger'
9
+ require 'fileutils'
10
+ require 'joggle/pablotron/cache'
11
+ require 'joggle/version'
12
+ require 'joggle/store/pstore/all'
13
+ require 'joggle/jabber/client'
14
+ require 'joggle/twitter/fetcher'
15
+ require 'joggle/twitter/engine'
16
+ require 'joggle/engine'
17
+
18
+ module Joggle
19
+ module Runner
20
+ #
21
+ # Basic PStore-backed runner. Creates all necessary objects from
22
+ # given config and binds them together.
23
+ #
24
+ class PStore
25
+ PATHS = {
26
+ 'store' => ENV['JOGGLE_STORE_PATH'] || '~/.joggle/joggle.pstore',
27
+ 'log' => ENV['JOGGLE_LOG_PATH'] || '~/.joggle/joggle.log',
28
+ }
29
+
30
+ DEFAULTS = {
31
+ # store configuration
32
+ 'runner.store.path' => File.expand_path(PATHS['store']),
33
+
34
+ # log configuration
35
+ 'runner.log.path' => File.expand_path(PATHS['log']),
36
+ # FIXME: change to INFO
37
+ 'runner.log.level' => 'INFO',
38
+ 'runner.log.format' => '%Y-%m-%dT%H:%M:%S',
39
+
40
+ # cache configuration
41
+ 'runner.cache.headers' => {
42
+ 'user-agent' => "Joggle/#{Joggle::VERSION}",
43
+ },
44
+ }
45
+
46
+ attr_reader :log, :store, :cache, :fetcher, :tweeter, :client, :engine
47
+
48
+ #
49
+ # Create and run PStore runner object.
50
+ #
51
+ def self.run(opt = nil)
52
+ new(opt).run
53
+ end
54
+
55
+ PATH_KEYS = %w{store log}
56
+
57
+ #
58
+ # Create new PStore runner object from the given options.
59
+ #
60
+ def initialize(opt = nil)
61
+ @opt = DEFAULTS.merge(opt || {})
62
+
63
+ # make sure paths exist
64
+ PATH_KEYS.each do |key|
65
+ FileUtils.mkdir_p(File.dirname(@opt["runner.#{key}.path"]), {
66
+ # restict access to owner
67
+ :mode => 0700
68
+ })
69
+ end
70
+
71
+ # create logger
72
+ @log = Logger.new(@opt['runner.log.path'])
73
+ @log.level = Logger.const_get(@opt['runner.log.level'].upcase)
74
+ @log.datetime_format = @opt['runner.log.format']
75
+ @log.info('Log started.')
76
+
77
+ # create backing store
78
+ path = @opt['runner.store.path']
79
+ @log.debug("Creating backing store \"#{path}\".")
80
+ pstore = ::PStore.new(path)
81
+ @store = Store::PStore::All.new(pstore)
82
+ end
83
+
84
+ #
85
+ # Run this runner.
86
+ #
87
+ def run
88
+ # create cache
89
+ @log.debug('Creating cache.')
90
+ @cache = Joggle::Pablotron::Cache.new(@store, @opt['runner.cache.headers'])
91
+
92
+ # create fetcher
93
+ @log.debug('Creating twitter fetcher.')
94
+ @fetcher = Twitter::Fetcher.new(@store, @cache, @opt)
95
+
96
+ # create twitter engine
97
+ @log.debug('Creating twitter engine.')
98
+ @tweeter = Twitter::Engine.new(@store, @fetcher, @opt)
99
+ @tweeter.on(self)
100
+
101
+ # create jabber client
102
+ @log.debug('Creating jabber client.')
103
+ @client = Jabber::Client.new(@opt['jabber.user'], @opt['jabber.pass'], @opt)
104
+ @client.on(self)
105
+
106
+ # create new joggle engine
107
+ @log.debug('Creating engine.')
108
+ @engine = Engine.new(@client, @tweeter)
109
+ @engine.on(self)
110
+
111
+ @log.debug('Running engine.')
112
+ @engine.run
113
+ end
114
+
115
+ #################
116
+ # log listeners #
117
+ #################
118
+
119
+ #
120
+ # Log twitter_engine_register_user events.
121
+ #
122
+ # Note: This method is a listener for Twitter::Engine objects; you
123
+ # should never call it directly.
124
+ #
125
+ def on_twitter_engine_register_user(e, who, user, pass)
126
+ pre = '<Twitter::Engine>'
127
+ @log.info("#{pre} Registering user: #{who} (xmpp) => #{user} (twitter).")
128
+ end
129
+
130
+ #
131
+ # Log twitter_engine_unregister_user events.
132
+ #
133
+ # Note: This method is a listener for Twitter::Engine objects; you
134
+ # should never call it directly.
135
+ #
136
+ def on_twitter_engine_unregister_user(e, who)
137
+ pre = '<Twitter::Engine>'
138
+ @log.info("#{pre} Unregistering user: #{who} (xmpp).")
139
+ end
140
+
141
+ #
142
+ # Log twitter_engine_tweet events.
143
+ #
144
+ # Note: This method is a listener for Twitter::Engine objects; you
145
+ # should never call it directly.
146
+ #
147
+ def on_twitter_engine_tweet(e, who, msg)
148
+ pre = '<Twitter::Engine>'
149
+ @log.info("#{pre} Tweet: #{who}: #{msg}.")
150
+ end
151
+
152
+ #
153
+ # Log twitter_engine_update events.
154
+ #
155
+ # Note: This method is a listener for Twitter::Engine objects; you
156
+ # should never call it directly.
157
+ #
158
+ def on_twitter_engine_update(e, user)
159
+ pre = '<Twitter::Engine>'
160
+ @log.info("#{pre} Updating: #{user['who']}.")
161
+ end
162
+
163
+ #
164
+ # Log engine_update_error events.
165
+ #
166
+ # Note: This method is a listener for Joggle::Engine objects; you
167
+ # should never call it directly.
168
+ #
169
+ def on_engine_update_error(e, err)
170
+ pre = '<Engine>'
171
+ @log.warn("#{pre} Twitter update failed: #{err}.")
172
+ end
173
+
174
+ #
175
+ # Log engine_reply events.
176
+ #
177
+ # Note: This method is a listener for Joggle::Engine objects; you
178
+ # should never call it directly.
179
+ #
180
+ def on_engine_reply(e, who, msg)
181
+ pre = '<Engine>'
182
+ @log.info("#{pre} Reply: #{who}: #{msg}.")
183
+ end
184
+
185
+ #
186
+ # Log engine_reply_error events.
187
+ #
188
+ # Note: This method is a listener for Joggle::Engine objects; you
189
+ # should never call it directly.
190
+ #
191
+ def on_engine_reply_error(e, who, msg, err)
192
+ pre = '<Engine>'
193
+ @log.warn("#{pre} Reply Error: Couldn't send reply \"#{msg}\" to #{who}: #{err}.")
194
+ end
195
+
196
+ #
197
+ # Log engine_idle events (debugging only).
198
+ #
199
+ # Note: This method is a listener for Joggle::Engine objects; you
200
+ # should never call it directly.
201
+ #
202
+ def on_engine_command(e, who, cmd, arg)
203
+ pre = '<Engine>'
204
+ @log.debug("#{pre} Command: #{who}: cmd = #{cmd}, arg = #{arg}.")
205
+ end
206
+
207
+ #
208
+ # Log engine_command events.
209
+ #
210
+ # Note: This method is a listener for Joggle::Engine objects; you
211
+ # should never call it directly.
212
+ #
213
+ def on_engine_command(e, who, cmd, arg)
214
+ pre = '<Engine>'
215
+ @log.info("#{pre} Command: #{who}: cmd = #{cmd}, arg = #{arg}.")
216
+ end
217
+
218
+ #
219
+ # Log engine_message events.
220
+ #
221
+ # Note: This method is a listener for Joggle::Engine objects; you
222
+ # should never call it directly.
223
+ #
224
+ def on_engine_message(e, who, msg)
225
+ pre = '<Engine>'
226
+ @log.info("#{pre} Message: #{who}: #{msg}.")
227
+ end
228
+
229
+ #
230
+ # Log engine_ignored_message events.
231
+ #
232
+ # Note: This method is a listener for Joggle::Engine objects; you
233
+ # should never call it directly.
234
+ #
235
+ def on_engine_ignored_message(e, who, msg)
236
+ pre = '<Engine>'
237
+ @log.info("#{pre} IGNORED: #{who}: #{msg}.")
238
+ end
239
+
240
+ #
241
+ # Log engine_ignored_subscription events.
242
+ #
243
+ # Note: This method is a listener for Joggle::Engine objects; you
244
+ # should never call it directly.
245
+ #
246
+ def on_engine_ignored_subscription(e, who)
247
+ pre = '<Engine>'
248
+ @log.info("#{pre} IGNORED: #{who} (subscription)")
249
+ end
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,26 @@
1
+ require 'joggle/store/pstore/cache'
2
+ require 'joggle/store/pstore/message'
3
+ require 'joggle/store/pstore/user'
4
+
5
+ module Joggle
6
+ module Store
7
+ module PStore
8
+ #
9
+ # Wrap all store backends into one object.
10
+ #
11
+ class All
12
+ include Cache
13
+ include Message
14
+ include User
15
+
16
+ #
17
+ # Create new Joggle::Store::PStore::All object from given
18
+ # ::PStore object.
19
+ #
20
+ def initialize(store)
21
+ @store = store
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,65 @@
1
+ require 'digest/md5'
2
+
3
+ module Joggle
4
+ module Store
5
+ module PStore
6
+ #
7
+ # Mixin that implements cache store methods for PStore backend.
8
+ #
9
+ # Note: You're probably looking for Joggle::Store::PStore::All
10
+ #
11
+ module Cache
12
+ #
13
+ # Add cache entry.
14
+ #
15
+ def add_cached(key, row)
16
+ key = cache_store_key(key)
17
+
18
+ @store.transaction do |s|
19
+ s[key] = {}.merge(row)
20
+ end
21
+ end
22
+
23
+ #
24
+ # Get cache entry.
25
+ #
26
+ def get_cached(key)
27
+ key = cache_store_key(key)
28
+
29
+ @store.transaction(true) do |s|
30
+ s[key]
31
+ end
32
+ end
33
+
34
+ #
35
+ # Does the given entry exist?
36
+ #
37
+ def has_cached?(key)
38
+ key = cache_store_key(key)
39
+
40
+ @store.transaction(true) do |s|
41
+ s.root?(key)
42
+ end
43
+ end
44
+
45
+ #
46
+ # Delete the given entry.
47
+ #
48
+ def delete_cached(key)
49
+ key = cache_store_key(key)
50
+
51
+ @store.transaction do |s|
52
+ s.delete(key)
53
+ end
54
+ end
55
+
56
+ #
57
+ # Map the given key to a pstore root key.
58
+ #
59
+ def cache_store_key(key)
60
+ 'cache-' << Digest::MD5.hexdigest(key)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,54 @@
1
+ require 'digest/md5'
2
+
3
+ module Joggle
4
+ module Store
5
+ module PStore
6
+ #
7
+ # Mixin that implements message store methods for pstore objects
8
+ #
9
+ # Note: You're probably looking for Joggle::Store::PStore::All
10
+ #
11
+ module Message
12
+ #
13
+ # Add message to store.
14
+ #
15
+ def add_message(key, row)
16
+ key = message_store_key(key)
17
+
18
+ @store.transaction do |s|
19
+ s[key] = row
20
+ end
21
+ end
22
+
23
+ #
24
+ # Does the given message exist in this store?
25
+ #
26
+ def has_message?(key)
27
+ key = message_store_key(key)
28
+
29
+ @store.transaction(true) do |s|
30
+ s.root?(key)
31
+ end
32
+ end
33
+
34
+ #
35
+ # Delete the given message.
36
+ #
37
+ def delete_message(key)
38
+ key = message_store_key(key)
39
+
40
+ @store.transaction do |s|
41
+ s.delete(key)
42
+ end
43
+ end
44
+
45
+ #
46
+ # Map given key to PStore root key.
47
+ #
48
+ def message_store_key(key)
49
+ 'message-' << Digest::MD5.hexdigest(key.to_s)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end