weeter 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/Gemfile +4 -0
- data/LICENSE +9 -0
- data/README.md +98 -0
- data/Rakefile +2 -0
- data/bin/weeter +6 -0
- data/bin/weeter_control +7 -0
- data/lib/weeter/.DS_Store +0 -0
- data/lib/weeter/cli.rb +24 -0
- data/lib/weeter/configuration/client_app_config.rb +12 -0
- data/lib/weeter/configuration/twitter_config.rb +21 -0
- data/lib/weeter/configuration.rb +22 -0
- data/lib/weeter/plugins/lib/oauth_http.rb +40 -0
- data/lib/weeter/plugins/lib/redis.rb +17 -0
- data/lib/weeter/plugins/notification/http.rb +26 -0
- data/lib/weeter/plugins/notification/resque.rb +39 -0
- data/lib/weeter/plugins/notification_plugin.rb +26 -0
- data/lib/weeter/plugins/subscription/http.rb +45 -0
- data/lib/weeter/plugins/subscription/redis.rb +46 -0
- data/lib/weeter/plugins/subscription_plugin.rb +26 -0
- data/lib/weeter/plugins.rb +4 -0
- data/lib/weeter/runner.rb +40 -0
- data/lib/weeter/tasks.rb +16 -0
- data/lib/weeter/twitter/tweet_consumer.rb +67 -0
- data/lib/weeter/twitter/tweet_item.rb +35 -0
- data/lib/weeter/twitter.rb +2 -0
- data/lib/weeter/version.rb +3 -0
- data/lib/weeter.rb +29 -0
- data/log/.gitignore +0 -0
- data/spec/.DS_Store +0 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/weeter/configuration/client_app_config_spec.rb +18 -0
- data/spec/weeter/configuration/twitter_config_spec.rb +39 -0
- data/spec/weeter/configuration_spec.rb +15 -0
- data/spec/weeter/plugins/notification_plugin_spec.rb +22 -0
- data/spec/weeter/plugins/subscription/update_server_spec.rb +30 -0
- data/spec/weeter/plugins/subscription_plugin_spec.rb +23 -0
- data/spec/weeter/runner_spec.rb +7 -0
- data/spec/weeter/twitter/tweet_consumer_spec.rb +64 -0
- data/spec/weeter/twitter/tweet_item_spec.rb +66 -0
- data/weeter.conf.example +51 -0
- data/weeter.gemspec +34 -0
- metadata +228 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
Portions copyright (c) 2010-2011 Weplay, Inc.
|
3
|
+
Portions copyright (c) 2011 Yapp, Inc.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
6
|
+
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
8
|
+
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
Weeter is a tireless worker who accepts a set of Twitter users to follow and terms to track, subscribes using Twitter's streaming API, and notifies your app with each new tweet (or tweet deletion).
|
2
|
+
|
3
|
+
Supported strategies for tweet notification include HTTP (issue a POST to your app) and Resque (queue a job). Weeter is extensible with other notification strategies.
|
4
|
+
|
5
|
+
Status
|
6
|
+
======
|
7
|
+
Alpha. A previous version of this code has been in production for some time. It has been substantially refactored and will be battle-tested soon.
|
8
|
+
|
9
|
+
Getting set up
|
10
|
+
==============
|
11
|
+
|
12
|
+
$ bundle install
|
13
|
+
|
14
|
+
Make a copy of the weeter.conf.example file named weeter.conf. Twitter configuration, client app configuration and weeter configuration are defined in separate
|
15
|
+
blocks. To configure how you connect to Twitter (basic auth or oauth), modify the twitter section of the configuration.
|
16
|
+
|
17
|
+
To configure how weeter connects to your client app, modify the client app configuration section:
|
18
|
+
|
19
|
+
Notifications
|
20
|
+
-------------
|
21
|
+
|
22
|
+
* *notification_plugin*: A symbol matching the underscorized name of the NotificationPlugin subclass to use. Current options are :http and :resque
|
23
|
+
|
24
|
+
For option :http, also provide the following:
|
25
|
+
|
26
|
+
* *oauth*: See the conf file for an example
|
27
|
+
|
28
|
+
* *publish_url*: The URL to which new tweets should be posted. Request will be sent with POST method. Example body:
|
29
|
+
`id=1111&twitter_user_id=19466709&text=Wassup`
|
30
|
+
* *delete_url*: The URL to which data about deleted tweets should be posted. Request will be sent with DELETE method. Example body:
|
31
|
+
`id=1111&twitter_user_id=19466709`
|
32
|
+
|
33
|
+
For option :resque, provide the following:
|
34
|
+
|
35
|
+
* *queue*: Name of the queue to add the job to
|
36
|
+
|
37
|
+
* *redis_uri*: Redis connection string
|
38
|
+
|
39
|
+
Subscriptions
|
40
|
+
-------------
|
41
|
+
|
42
|
+
* *subscription_plugin*: A symbol matching the underscorized name of the SubscrptionsPlugin subclass to use. Current options are :http and :redis
|
43
|
+
|
44
|
+
For option :http, also provide the following:
|
45
|
+
|
46
|
+
* *oauth*: See the conf file for an example
|
47
|
+
|
48
|
+
* *subscriptions_url*: The URL at which to find JSON describing the Twitter users to follow (maximum 5,000 at the default API access level) and the terms to track (maximum 400 at the default API access level). Example content:
|
49
|
+
`{"follow":"19466709", "759251"},{"track":"#lolcats","#bieber"}`
|
50
|
+
|
51
|
+
* *subscription_updates_port*: The port Weeter should listen on for HTTP connections. If you have changes to your subscriptions data, POST the full JSON to the weeter's root URL. This will trigger weeter to reconnect to Twitter with the updated filters in place.
|
52
|
+
|
53
|
+
For option :redis, also provide the following:
|
54
|
+
|
55
|
+
* *subscriptions_key*: The Redis key at which the Weeter can find JSON describing the Twitter users to follow (maximum 5,000 at the default API access level) and the terms to track (maximum 400 at the default API access level). Example content:
|
56
|
+
`{"follow":"19466709", "759251"},{"track":"#lolcats","#bieber"}`
|
57
|
+
|
58
|
+
* *subscriptions_changed_channel*: The Redis publish/subscribe channel to subscribe to in order to be notified that the subscriptions have changed. When your app has an updated set of subscriptions, it should update the _subscriptions_key_ and publish a "CHANGED" message to this channel. Weeter will then retrieve an updated set of subscriptions from Redis and reconnect to twitter.
|
59
|
+
|
60
|
+
* *redis_uri*: Redis connection string
|
61
|
+
|
62
|
+
Running weeter
|
63
|
+
==============
|
64
|
+
|
65
|
+
Weeter can be run using the weeter executable installed by the gem.
|
66
|
+
|
67
|
+
Running Weeter as a daemon
|
68
|
+
--------------------------
|
69
|
+
The gem also installs a weeter_control executable that can be used to start Weeter as a daemon
|
70
|
+
|
71
|
+
$ bin/weeter_control start
|
72
|
+
|
73
|
+
This starts Weeter as a daemon. For other commands and options, run:
|
74
|
+
|
75
|
+
$ bin/weeter_control --help
|
76
|
+
|
77
|
+
|
78
|
+
|
79
|
+
Running specs
|
80
|
+
=============
|
81
|
+
|
82
|
+
$ bundle exec rspec spec/
|
83
|
+
|
84
|
+
TODO
|
85
|
+
====
|
86
|
+
- Better error reporting
|
87
|
+
- Make tweet filtering strategy (re-tweets, replies, etc.) more configurable
|
88
|
+
- Add specs for plugins
|
89
|
+
- Extract plugins into separate gems
|
90
|
+
- integration tests
|
91
|
+
|
92
|
+
Credits
|
93
|
+
======
|
94
|
+
Thanks to Weplay for initial development and open sourcing Weeter. In particular, credit goes to Noah Davis and Joey Aghion. Further development by Luke Melia at Yapp.
|
95
|
+
|
96
|
+
License
|
97
|
+
=======
|
98
|
+
Weeter is available under the terms of the MIT License http://www.opensource.org/licenses/mit-license.php
|
data/Rakefile
ADDED
data/bin/weeter
ADDED
data/bin/weeter_control
ADDED
Binary file
|
data/lib/weeter/cli.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Weeter
|
4
|
+
class Cli
|
5
|
+
|
6
|
+
def initialize(args)
|
7
|
+
@configuration_file = File.join(File.dirname(__FILE__), '..', '..', 'weeter.conf')
|
8
|
+
args.options do |opts|
|
9
|
+
opts.banner = "Usage: #{$0} [options]"
|
10
|
+
opts.on("-c", "--configuration=filename", String,
|
11
|
+
"Specifies an executable ruby file containing weeter configuration",
|
12
|
+
"Default: weeter.conf") do |val|
|
13
|
+
@configuration_file = val
|
14
|
+
end
|
15
|
+
end.parse!
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
load @configuration_file
|
20
|
+
Weeter::Runner.new(Configuration.instance).start
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "singleton"
|
2
|
+
|
3
|
+
module Weeter
|
4
|
+
class Configuration
|
5
|
+
class TwitterConfig
|
6
|
+
include Singleton
|
7
|
+
attr_accessor :basic_auth, :oauth
|
8
|
+
|
9
|
+
def auth_options
|
10
|
+
if oauth
|
11
|
+
{:oauth => oauth}
|
12
|
+
else
|
13
|
+
username = basic_auth[:username]
|
14
|
+
password = basic_auth[:password]
|
15
|
+
{:auth => "#{username}:#{password}"}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "singleton"
|
2
|
+
require "weeter/configuration/client_app_config"
|
3
|
+
require "weeter/configuration/twitter_config"
|
4
|
+
|
5
|
+
module Weeter
|
6
|
+
|
7
|
+
class Configuration
|
8
|
+
include Singleton
|
9
|
+
attr_accessor :log_path
|
10
|
+
|
11
|
+
def twitter
|
12
|
+
yield Configuration::TwitterConfig.instance if block_given?
|
13
|
+
Configuration::TwitterConfig.instance
|
14
|
+
end
|
15
|
+
|
16
|
+
def client_app
|
17
|
+
@client_app_config ||= Configuration::ClientAppConfig.new
|
18
|
+
yield @client_app_config if block_given?
|
19
|
+
@client_app_config
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'em-http'
|
2
|
+
require 'simple_oauth'
|
3
|
+
|
4
|
+
module Weeter
|
5
|
+
module Plugins
|
6
|
+
module Net
|
7
|
+
class OauthHttp
|
8
|
+
def self.get(config, url, params = {})
|
9
|
+
request(config, :get, url, params)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.put(config, url, params = {})
|
13
|
+
request(config, :put, url, params)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.post(config, url, params = {})
|
17
|
+
request(config, :post, url, params)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.delete(config, url, params = {})
|
21
|
+
request(config, :delete, url, params)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.request(config, method, url, params = {})
|
25
|
+
if method == :post
|
26
|
+
request_options = {:body => params}
|
27
|
+
else
|
28
|
+
request_options = {:query => params}
|
29
|
+
end
|
30
|
+
request_options.merge!(:head => {"Authorization" => oauth_header(config, url, params, method.to_s.upcase)}) if config.oauth
|
31
|
+
EM::HttpRequest.new(url).send(method, request_options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.oauth_header(config, uri, params, http_method)
|
35
|
+
::SimpleOauth::Header.new(http_method, uri, params, config.oauth).to_s
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'em-hiredis'
|
2
|
+
|
3
|
+
module Weeter
|
4
|
+
module Plugins
|
5
|
+
module Net
|
6
|
+
module Redis
|
7
|
+
def create_redis_client
|
8
|
+
@redis ||= begin
|
9
|
+
redis = EM::Hiredis.connect(@config.redis_uri)
|
10
|
+
redis.callback { Weeter.logger.info "Connected to Redis" }
|
11
|
+
redis
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Weeter
|
2
|
+
module Plugins
|
3
|
+
module Notification
|
4
|
+
class Http
|
5
|
+
def initialize(client_app_config)
|
6
|
+
@config = client_app_config
|
7
|
+
end
|
8
|
+
|
9
|
+
def publish_tweet(tweet_item)
|
10
|
+
id = tweet_item['id_str']
|
11
|
+
text = tweet_item['text']
|
12
|
+
user_id = tweet_item['user']['id_str']
|
13
|
+
Weeter.logger.info("Publishing tweet #{id} from user #{user_id}: #{text}")
|
14
|
+
Weeter::Plugins::Net::OauthHttp.post(@config, @config.publish_url, {:id => id, :text => text, :twitter_user_id => user_id})
|
15
|
+
end
|
16
|
+
|
17
|
+
def delete_tweet(tweet_item)
|
18
|
+
id = tweet_item['delete']['status']['id'].to_s
|
19
|
+
user_id = tweet_item['delete']['status']['user_id'].to_s
|
20
|
+
Weeter.logger.info("Deleting tweet #{id} for user #{user_id}")
|
21
|
+
Weeter::Plugins::Net::OauthHttp.delete(@config, @config.delete_url, {:id => id, :twitter_user_id => user_id})
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Weeter
|
2
|
+
module Plugins
|
3
|
+
module Notification
|
4
|
+
class Resque
|
5
|
+
include Weeter::Plugins::Net::Redis
|
6
|
+
|
7
|
+
def initialize(client_app_config)
|
8
|
+
@config = client_app_config
|
9
|
+
end
|
10
|
+
|
11
|
+
def publish_tweet(tweet_item)
|
12
|
+
resque_job = %Q|{"class":"WeeterPublishTweetJob","args":[#{tweet_item.to_json}]}|
|
13
|
+
Weeter.logger.info("Publishing tweet #{tweet_item['id']} from user #{tweet_item['user']['id_str']}: #{tweet_item['text']}")
|
14
|
+
enqueue(resque_job)
|
15
|
+
end
|
16
|
+
|
17
|
+
def delete_tweet(tweet_item)
|
18
|
+
resque_job = %Q|{"class":"WeeterDeleteTweetJob","args":[#{tweet_item.to_json}]}|
|
19
|
+
Weeter.logger.info("Deleting tweet #{tweet_item['id']} for user #{tweet_item['user']['id_str']}")
|
20
|
+
enqueue(resque_job)
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def redis
|
26
|
+
@redis ||= create_redis_client
|
27
|
+
end
|
28
|
+
|
29
|
+
def enqueue(job)
|
30
|
+
redis.rpush(queue_key, job)
|
31
|
+
end
|
32
|
+
|
33
|
+
def queue_key
|
34
|
+
"resque:queue:#{@config.queue}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'weeter/plugins/notification/http'
|
2
|
+
require 'weeter/plugins/notification/resque'
|
3
|
+
|
4
|
+
require 'active_support/core_ext/string/inflections'
|
5
|
+
require 'active_support/core_ext/module/delegation'
|
6
|
+
|
7
|
+
module Weeter
|
8
|
+
module Plugins
|
9
|
+
class NotificationPlugin
|
10
|
+
delegate :publish_tweet, :to => :configured_plugin
|
11
|
+
delegate :delete_tweet, :to => :configured_plugin
|
12
|
+
|
13
|
+
def initialize(client_app_config)
|
14
|
+
@config = client_app_config
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
def configured_plugin
|
19
|
+
@configured_plugin ||= begin
|
20
|
+
Weeter.logger.info("Using #{@config.notification_plugin} notification plugin")
|
21
|
+
Notification.const_get(@config.notification_plugin.to_s.camelize).new(@config)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'evma_httpserver'
|
2
|
+
require 'multi_json'
|
3
|
+
|
4
|
+
module Weeter
|
5
|
+
module Plugins
|
6
|
+
module Subscription
|
7
|
+
class Http
|
8
|
+
def initialize(client_app_config)
|
9
|
+
@config = client_app_config
|
10
|
+
end
|
11
|
+
|
12
|
+
def get_initial_filters(&block)
|
13
|
+
http = Weeter::Plugins::Net::OauthHttp.get(@config, @config.subscriptions_url)
|
14
|
+
http.callback {
|
15
|
+
filter_params = {}
|
16
|
+
if http.response_header.status == 200
|
17
|
+
yield MultiJson.decode(http.response)
|
18
|
+
else
|
19
|
+
Weeter.logger.error "Initial filters request failed with response code #{http.response_header.status}."
|
20
|
+
yield
|
21
|
+
end
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def listen_for_filter_update(tweet_consumer)
|
26
|
+
EM.start_server('localhost', @config.subscription_updates_port, UpdateServer) do |conn|
|
27
|
+
conn.tweet_consumer = tweet_consumer
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class UpdateServer < EM::Connection
|
32
|
+
include EM::HttpServer
|
33
|
+
attr_accessor :tweet_consumer
|
34
|
+
|
35
|
+
def process_http_request
|
36
|
+
Weeter.logger.info("Reconnecting Twitter stream")
|
37
|
+
filter_params = MultiJson.decode(@http_post_content)
|
38
|
+
tweet_consumer.reconnect(filter_params)
|
39
|
+
EM::DelegatedHttpResponse.new(self).send_response
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'multi_json'
|
2
|
+
|
3
|
+
module Weeter
|
4
|
+
module Plugins
|
5
|
+
module Subscription
|
6
|
+
class Redis
|
7
|
+
include Weeter::Plugins::Net::Redis
|
8
|
+
|
9
|
+
def initialize(client_app_config)
|
10
|
+
@config = client_app_config
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_initial_filters(&block)
|
14
|
+
redis.get(@config.subscriptions_key) do |value|
|
15
|
+
if value.nil?
|
16
|
+
raise "Expected to find subscription data at redis key #{@config.subscriptions_key}"
|
17
|
+
end
|
18
|
+
yield MultiJson.decode(value)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def listen_for_filter_update(tweet_consumer)
|
23
|
+
pub_sub_redis.subscribe(@config.subscriptions_changed_channel)
|
24
|
+
pub_sub_redis.on(:message) do |channel, message|
|
25
|
+
Weeter.logger.info [:message, channel, message]
|
26
|
+
Weeter.logger.info("Reconnecting Twitter stream")
|
27
|
+
get_initial_filters do |filter_params|
|
28
|
+
tweet_consumer.reconnect(filter_params)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def redis
|
36
|
+
@redis ||= create_redis_client
|
37
|
+
end
|
38
|
+
|
39
|
+
def pub_sub_redis
|
40
|
+
@pub_sub_redis ||= create_redis_client
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'weeter/plugins/subscription/http'
|
2
|
+
require 'weeter/plugins/subscription/redis'
|
3
|
+
|
4
|
+
require 'active_support/core_ext/string/inflections'
|
5
|
+
require 'active_support/core_ext/module/delegation'
|
6
|
+
|
7
|
+
module Weeter
|
8
|
+
module Plugins
|
9
|
+
class SubscriptionPlugin
|
10
|
+
delegate :get_initial_filters, :to => :configured_plugin
|
11
|
+
delegate :listen_for_filter_update, :to => :configured_plugin
|
12
|
+
|
13
|
+
def initialize(client_app_config)
|
14
|
+
@config = client_app_config
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
def configured_plugin
|
19
|
+
@configured_plugin ||= begin
|
20
|
+
Weeter.logger.info("Using #{@config.subscription_plugin} subscription plugin")
|
21
|
+
Subscription.const_get(@config.subscription_plugin.to_s.camelize).new(@config)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'em-http'
|
2
|
+
|
3
|
+
module Weeter
|
4
|
+
class Runner
|
5
|
+
|
6
|
+
def initialize(config)
|
7
|
+
@config = config
|
8
|
+
Weeter.logger.info("Starting weeter with configuration: #{@config.inspect}")
|
9
|
+
end
|
10
|
+
|
11
|
+
def start
|
12
|
+
EM.run {
|
13
|
+
subscription_plugin.get_initial_filters do |filter_params|
|
14
|
+
Weeter.logger.info("Connecting to twitter with initial filters")
|
15
|
+
tweet_consumer.connect(filter_params)
|
16
|
+
subscription_plugin.listen_for_filter_update(tweet_consumer)
|
17
|
+
|
18
|
+
trap('TERM') do
|
19
|
+
Weeter.logger.info("Stopping weeter")
|
20
|
+
EM.stop if EM.reactor_running?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def notification_plugin
|
29
|
+
@notification_plugin ||= Weeter::Plugins::NotificationPlugin.new(@config.client_app)
|
30
|
+
end
|
31
|
+
|
32
|
+
def subscription_plugin
|
33
|
+
@subscription_plugin ||= Weeter::Plugins::SubscriptionPlugin.new(@config.client_app)
|
34
|
+
end
|
35
|
+
|
36
|
+
def tweet_consumer
|
37
|
+
@tweet_consumer ||= Weeter::Twitter::TweetConsumer.new(@config.twitter, notification_plugin)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/weeter/tasks.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# require 'weeter/tasks'
|
2
|
+
# will give you the weeter tasks
|
3
|
+
|
4
|
+
namespace :weeter do
|
5
|
+
task :setup do
|
6
|
+
require 'weeter'
|
7
|
+
# extend this task to set the config path
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Start Weeter"
|
11
|
+
task :start => :setup do
|
12
|
+
configuration_file = ENV['WEETER_CONFIG_PATH']
|
13
|
+
load configuration_file
|
14
|
+
Weeter::Runner.new(Weeter::Configuration.instance).start
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'twitter/json_stream'
|
2
|
+
require 'multi_json'
|
3
|
+
|
4
|
+
module Weeter
|
5
|
+
module Twitter
|
6
|
+
class TweetConsumer
|
7
|
+
|
8
|
+
def initialize(twitter_config, notifier)
|
9
|
+
@config = twitter_config
|
10
|
+
@notifier = notifier
|
11
|
+
end
|
12
|
+
|
13
|
+
def connect(filter_params)
|
14
|
+
filter_params = clean_filter_params(filter_params)
|
15
|
+
connect_options = {:ssl => true, :params => filter_params, :method => 'POST'}.merge(@config.auth_options)
|
16
|
+
@stream = ::Twitter::JSONStream.connect(connect_options)
|
17
|
+
|
18
|
+
@stream.each_item do |item|
|
19
|
+
begin
|
20
|
+
tweet_item = TweetItem.new(MultiJson.decode(item))
|
21
|
+
|
22
|
+
if tweet_item.deletion?
|
23
|
+
@notifier.delete_tweet(tweet_item)
|
24
|
+
elsif tweet_item.publishable?
|
25
|
+
@notifier.publish_tweet(tweet_item)
|
26
|
+
else
|
27
|
+
ignore_tweet(tweet_item)
|
28
|
+
end
|
29
|
+
rescue => ex
|
30
|
+
Weeter.logger.error("Twitter stream tweet exception: #{ex.class.name}: #{ex.message}")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
@stream.on_error do |msg|
|
35
|
+
Weeter.logger.error("Twitter stream error: #{msg}. Connect options were #{connect_options.inspect}")
|
36
|
+
end
|
37
|
+
|
38
|
+
@stream.on_max_reconnects do |timeout, retries|
|
39
|
+
Weeter.logger.error("Twitter stream max-reconnects reached: timeout=#{timeout}, retries=#{retries}")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def reconnect(filter_params)
|
44
|
+
@stream.stop
|
45
|
+
connect(filter_params)
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
def clean_filter_params(p)
|
51
|
+
return {} if p.nil?
|
52
|
+
cleaned_params = {}
|
53
|
+
cleaned_params['follow'] = p['follow'] if (p['follow'] || []).any?
|
54
|
+
cleaned_params['track'] = p['track'] if (p['track'] || []).any?
|
55
|
+
cleaned_params
|
56
|
+
end
|
57
|
+
|
58
|
+
def ignore_tweet(tweet_item)
|
59
|
+
id = tweet_item['id_str']
|
60
|
+
text = tweet_item['text']
|
61
|
+
user_id = tweet_item['user']['id_str']
|
62
|
+
Weeter.logger.info("Ignoring tweet #{id} from user #{user_id}: #{text}")
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'multi_json'
|
2
|
+
|
3
|
+
module Weeter
|
4
|
+
|
5
|
+
class TweetItem
|
6
|
+
def initialize(tweet_hash)
|
7
|
+
@tweet_hash = tweet_hash
|
8
|
+
end
|
9
|
+
|
10
|
+
def deletion?
|
11
|
+
!@tweet_hash['delete'].nil?
|
12
|
+
end
|
13
|
+
|
14
|
+
def retweeted?
|
15
|
+
!@tweet_hash['retweeted_status'].nil? || @tweet_hash['text'] =~ /^RT @/i
|
16
|
+
end
|
17
|
+
|
18
|
+
def reply?
|
19
|
+
!@tweet_hash['in_reply_to_user_id_str'].nil? || @tweet_hash['text'] =~ /^@/
|
20
|
+
end
|
21
|
+
|
22
|
+
def publishable?
|
23
|
+
!retweeted? && !reply?
|
24
|
+
end
|
25
|
+
|
26
|
+
def [](val)
|
27
|
+
@tweet_hash[val]
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_json
|
31
|
+
MultiJson.encode(@tweet_hash)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|