joggle 0.1.0

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