retjilp 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,14 +1,51 @@
1
- require_relative 'options'
1
+ require 'json/pure'
2
+ require 'optparse'
3
+
4
+ require_relative 'log'
2
5
  require_relative 'retweeter'
6
+ require_relative 'twitter'
3
7
 
4
8
  module Retjilp
9
+ DATA_DIR = File.expand_path("~/.retjilp")
10
+
5
11
  class Runner
6
12
  def initialize(argv)
7
- @options = Options.new(argv)
13
+ # Parse command-line options
14
+ OptionParser.new do |opts|
15
+ opts.banner = "Usage: retjilp [ --help ] [ --verbose | --debug ]"
16
+ opts.on("--verbose", "Run with verbose output") { Retjilp::log.level = Logger::INFO }
17
+ opts.on("--debug", "Run with debug output") { Retjilp::log.level = Logger::DEBUG }
18
+ opts.on_tail("-h", "--help", "Show this help") { puts opts ; exit }
19
+ begin
20
+ opts.parse!(argv)
21
+ rescue => e
22
+ fatal_error("Invalid option(s): #{e.message}\n" + opts.to_s)
23
+ end
24
+ end
25
+
26
+ # Parse config file
27
+ config_filename = File.join(DATA_DIR, "config")
28
+ begin
29
+ @options = File.open(config_filename) { |f| JSON.load(f) }
30
+ rescue => e
31
+ fatal_error("Error parsing configuration file #{config_filename}: #{e.message}")
32
+ end
33
+
34
+ # Convert keys to symbols
35
+ @options = @options.inject({}){|m,(k,v)| m[k.to_sym] = v; m}
8
36
  end
9
37
 
10
38
  def run
11
- Retweeter.new(@options).run
39
+ consumer_key = (@options[:consumer_key] or fatal_error("Consumer key missing"))
40
+ consumer_secret = (@options[:consumer_secret] or fatal_error("Consumer secret missing"))
41
+ twitter = Twitter.new(@options[:consumer_key], @options[:consumer_secret])
42
+ Retweeter.new(twitter, @options).run
12
43
  end
44
+
45
+ private
46
+ def fatal_error(msg)
47
+ STDERR.puts msg
48
+ exit(-1)
49
+ end
13
50
  end
14
51
  end
@@ -0,0 +1,118 @@
1
+ require_relative 'log'
2
+
3
+ module Retjilp
4
+ class Twitter
5
+ TWITTER_URI = "http://api.twitter.com"
6
+ API_VERSION = "1.1"
7
+ ACCESS_TOKEN_FILENAME = File.join(File.expand_path("~/.retjilp"), "access_token")
8
+
9
+ def initialize(consumer_key, consumer_secret)
10
+ @consumer_key = consumer_key
11
+ @consumer_secret = consumer_secret
12
+ end
13
+
14
+ def user_timeline
15
+ Retjilp::log.info("Fetching own tweets")
16
+ get "/statuses/user_timeline.json?trim_user=true&include_rts=true"
17
+ end
18
+
19
+ def list_statuses(list, options)
20
+ Retjilp::log.info("Fetching list tweets of #{list}")
21
+ since_id = options[:since_id] ? "&since_id=#{options[:since_id]}" : ""
22
+ get "/lists/statuses.json?slug=#{list}&owner_screen_name=#{@user_info['screen_name']}&include_rts=true" + since_id
23
+ end
24
+
25
+ def home_timeline(options)
26
+ since_id = options[:since_id] ? "&since_id=#{options[:since_id]}" : ""
27
+ get "/statuses/home_timeline.json?trim_user=true" + since_id
28
+ end
29
+
30
+ def retweet(id)
31
+ result = @access_token.post("/#{API_VERSION}/statuses/retweet/#{id}.json")
32
+ result.class == Net::HTTPOK or Retjilp::log.error("Error retweeting #{result.body}")
33
+ end
34
+
35
+ def login
36
+ # Request the token if the cached access token does not exist
37
+ @access_token, @user_info = cached_access_token
38
+ unless @access_token
39
+ STDIN.tty? or raise "This script must be run interactively the first time to be able to authenticate."
40
+ Retjilp::log.info("Requesting new access token")
41
+ consumer = OAuth::Consumer.new(
42
+ @consumer_key,
43
+ @consumer_secret,
44
+ :site => TWITTER_URI,
45
+ :scheme => :header,
46
+ :request_token_path => "/oauth/request_token",
47
+ :authorize_path => "/oauth/authorize",
48
+ :@access_token_path => "/oauth/@access_token",
49
+ :http_method => :post)
50
+ request_token = consumer.get_request_token(:oauth_callback => "oob")
51
+
52
+ puts "Please open #{request_token.authorize_url} in your browser, authorize Retjilp, and enter the PIN code below:"
53
+ verifier = STDIN.gets.chomp
54
+
55
+ begin
56
+ @access_token = request_token.get_access_token(:oauth_verifier => verifier)
57
+ rescue OAuth::Unauthorized
58
+ raise "Invalid PIN verification!"
59
+ end
60
+ @user_info = verify_token(@access_token) or raise "Access token not authorized!"
61
+ cached_access_token = @access_token
62
+ end
63
+ Retjilp::log.info("Logged in as #{@user_info["screen_name"]}")
64
+ end
65
+
66
+ protected
67
+ def cached_access_token
68
+ access_token = nil
69
+ user_info = nil
70
+ if File.exist?(ACCESS_TOKEN_FILENAME)
71
+ Retjilp::log.info("Loading cached access token from #{ACCESS_TOKEN_FILENAME}")
72
+ File.open(ACCESS_TOKEN_FILENAME) do |f|
73
+ begin
74
+ access_token_data = JSON.load(f)
75
+ consumer = OAuth::Consumer.new(@consumer_key, @consumer_secret, { :site => TWITTER_URI })
76
+ access_token = OAuth::AccessToken.new(consumer, access_token_data["token"], access_token_data["secret"])
77
+ unless user_info = verify_token(access_token)
78
+ Retjilp::log.warn("Cached token not authorized")
79
+ access_token = nil
80
+ end
81
+ rescue JSON::ParserError
82
+ Retjilp::log.warn("Cached token does not parse")
83
+ end
84
+ end
85
+ end
86
+ [access_token, user_info]
87
+ end
88
+
89
+ def cached_access_token=(access_token)
90
+ Retjilp::log.info("Caching token in #{ACCESS_TOKEN_FILENAME}")
91
+ File.open(ACCESS_TOKEN_FILENAME, 'w+') do |f|
92
+ access_token_data = {
93
+ "token" => access_token.token,
94
+ "secret" => access_token.secret
95
+ }
96
+ JSON.dump(access_token_data, f)
97
+ end
98
+ end
99
+
100
+ private
101
+ # Helper method to verify the validity of an access token.
102
+ # Returns the user info if the token verified correctly.
103
+ def verify_token(token)
104
+ response = token.get("/#{API_VERSION}/account/verify_credentials.json")
105
+ response.class == Net::HTTPOK ? JSON.parse(response.body) : nil
106
+ end
107
+
108
+ private
109
+ def get(url)
110
+ full_url = "/#{API_VERSION}#{url}"
111
+ Retjilp::log.debug("-> " + full_url)
112
+ result = JSON.parse(@access_token.get("/#{API_VERSION}/#{url}").body)
113
+ Retjilp::log.debug("<- " + JSON.pretty_generate(result))
114
+ raise "Error fetching result: #{result.to_s}" if result.include? "errors"
115
+ result
116
+ end
117
+ end
118
+ end
@@ -5,18 +5,24 @@ Gem::Specification.new do |s|
5
5
  s.summary = 'Automatically retweet tweets'
6
6
  s.description = 'Retjilp logs into your account, scans all the tweets from your following list or another defined list for a set of matching words, and retweets the ones that match (using the native retweet API).'
7
7
  s.requirements = ['']
8
- s.version = '0.2'
8
+ s.version = '0.3'
9
9
  s.author = 'Remko Tronçon'
10
10
  s.email = 'remko@el-tramo.be'
11
11
  s.homepage = 'http://el-tramo.be/blog/retjilp'
12
12
  s.platform = Gem::Platform::RUBY
13
13
  s.required_ruby_version = '>=1.8'
14
- s.files = Dir['**/**']
14
+ s.files = Dir['{bin,lib,doc}/**/*'] + Dir['[A-Z]*'] + ['retjilp.gemspec']
15
15
  s.executables = 'retjilp'
16
16
  s.require_paths = ['lib']
17
17
  s.has_rdoc = false
18
18
  s.license = 'BSD'
19
+ s.test_files = Dir.glob("test/**/*.rb")
19
20
 
20
21
  s.add_runtime_dependency('oauth')
21
22
  s.add_runtime_dependency('json_pure')
23
+
24
+ s.add_development_dependency('rake')
25
+ s.add_development_dependency('rspec-core')
26
+ s.add_development_dependency('rspec-mocks')
27
+ s.add_development_dependency('rspec-expectations')
22
28
  end
@@ -0,0 +1,82 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
2
+
3
+ require 'retjilp/retweeter'
4
+ require 'retjilp/twitter'
5
+
6
+ module Retjilp
7
+ describe Retweeter do
8
+ let(:twitter) { double(Twitter) }
9
+
10
+ before (:each) do
11
+ twitter.stub(:login)
12
+ end
13
+
14
+ context 'match keyword' do
15
+ let(:retweeter) { Retweeter.new(twitter, :match => ['MatchingKeyword']) }
16
+
17
+ context 'empty user timeline' do
18
+ before(:each) do
19
+ twitter.stub(:user_timeline) {[]}
20
+ end
21
+
22
+ it 'retweets new matching items' do
23
+ twitter.stub(:home_timeline) {[{'text' => 'MatchingKeyword', 'id' => 'id1'}]}
24
+ twitter.should_receive(:retweet).with('id1')
25
+ retweeter.run
26
+ end
27
+
28
+ it 'does not retweet non-matching items' do
29
+ twitter.stub(:home_timeline) {[{'text' => 'OtherKeyword', 'id' => 'id1'}]}
30
+ twitter.should_not_receive(:retweet)
31
+ retweeter.run
32
+ end
33
+
34
+ it 'retweets retweets with their id' do
35
+ twitter.stub(:home_timeline) {[{'text' => 'MatchingKeyword', 'id' => 'id1', 'retweeted_status' => {'id' => 'id2'}}]}
36
+ twitter.should_receive(:retweet).with('id2')
37
+ retweeter.run
38
+ end
39
+ end
40
+
41
+ context 'user timeline with already retweeted items' do
42
+ before(:each) do
43
+ twitter.stub(:user_timeline) {[{}, {'retweeted_status' => {'id' => 'id1'}}]}
44
+ end
45
+
46
+ it 'does not retweet already tweeted items' do
47
+ twitter.stub(:home_timeline) {[{'text' => 'MatchingKeyword', 'id' => 'id1'}]}
48
+ twitter.should_not_receive(:retweet)
49
+ Retweeter.new(twitter, :match => ['MatchingKeyword']).run
50
+ end
51
+ end
52
+ end
53
+
54
+ context 'match is empty' do
55
+ let(:retweeter) { Retweeter.new(twitter, :match => []) }
56
+
57
+ it 'retweets all items' do
58
+ twitter.stub(:user_timeline) {[]}
59
+ twitter.stub(:home_timeline) {[{'text' => 'OtherKeyword', 'id' => 'id1'}]}
60
+
61
+ twitter.should_receive(:retweet).with('id1')
62
+
63
+ retweeter.run
64
+ end
65
+ end
66
+
67
+ context 'retweet from list' do
68
+ let(:retweeter) { Retweeter.new(twitter, :retweet_from_list => 'MyList', :match => ['MatchingKeyword']) }
69
+
70
+ it 'retweets from the right list' do
71
+ twitter.stub(:user_timeline) {[]}
72
+ twitter.should_receive(:list_statuses).with('MyList', anything()).and_return([
73
+ {'text' => 'MatchingKeyword', 'id' => 'id1'},
74
+ {'text' => 'OtherKeyword', 'id' => 'id2'}
75
+ ])
76
+ twitter.should_receive(:retweet).with('id1')
77
+ retweeter.run
78
+ end
79
+ end
80
+
81
+ end
82
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: retjilp
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.2'
4
+ version: '0.3'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Remko Tronçon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-03-02 00:00:00.000000000 Z
11
+ date: 2013-03-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oauth
@@ -38,6 +38,62 @@ dependencies:
38
38
  - - '>='
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec-core
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec-mocks
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec-expectations
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
41
97
  description: Retjilp logs into your account, scans all the tweets from your following
42
98
  list or another defined list for a set of matching words, and retweets the ones
43
99
  that match (using the native retweet API).
@@ -47,15 +103,37 @@ executables:
47
103
  extensions: []
48
104
  extra_rdoc_files: []
49
105
  files:
50
- - AUTHORS
51
106
  - bin/retjilp
52
- - config
53
- - COPYING
54
- - lib/retjilp/options.rb
107
+ - lib/retjilp/log.rb
55
108
  - lib/retjilp/retweeter.rb
56
109
  - lib/retjilp/runner.rb
110
+ - lib/retjilp/twitter.rb
111
+ - doc/_index.html
112
+ - doc/class_list.html
113
+ - doc/css/common.css
114
+ - doc/css/full_list.css
115
+ - doc/css/style.css
116
+ - doc/file.AUTHORS.html
117
+ - doc/file.COPYING.html
118
+ - doc/file.README.html
119
+ - doc/file_list.html
120
+ - doc/frames.html
121
+ - doc/index.html
122
+ - doc/js/app.js
123
+ - doc/js/full_list.js
124
+ - doc/js/jquery.js
125
+ - doc/method_list.html
126
+ - doc/Retjilp/Options.html
127
+ - doc/Retjilp/Retweeter.html
128
+ - doc/Retjilp/Runner.html
129
+ - doc/Retjilp.html
130
+ - doc/top-level-namespace.html
131
+ - AUTHORS
132
+ - COPYING
133
+ - Rakefile
57
134
  - README.markdown
58
135
  - retjilp.gemspec
136
+ - test/spec/retweeter_spec.rb
59
137
  homepage: http://el-tramo.be/blog/retjilp
60
138
  licenses:
61
139
  - BSD
@@ -81,4 +159,5 @@ rubygems_version: 2.0.0
81
159
  signing_key:
82
160
  specification_version: 4
83
161
  summary: Automatically retweet tweets
84
- test_files: []
162
+ test_files:
163
+ - test/spec/retweeter_spec.rb
data/config DELETED
@@ -1,27 +0,0 @@
1
- /*
2
- * Retjilp configuration file.
3
- *
4
- * Change this to reflect your setup, and put it in ~/.retjilp
5
- */
6
- {
7
- /*
8
- * Consumer key and secret.
9
- * Get this by registering a new (desktop) application at
10
- * http://twitter.com/apps
11
- */
12
- "consumer_key": "abcdeFghIjklMnOpQrStUv",
13
- "consumer_secret": "abcdefgh123456789abcdefgh123456789abcdefg",
14
-
15
- /*
16
- * The strings that a tweet should be matched against.
17
- * These strings are matched in lower case.
18
- */
19
- "match": ["#sometag", "#someothertag", "someword"]
20
-
21
- /*
22
- * List name from which statuses are retweeted.
23
- * Set this config value if you want to retweet only from
24
- * this list instead of your following list.
25
- */
26
- /* "retweet_from_list": "auto-retweet" */
27
- }
@@ -1,24 +0,0 @@
1
- require 'optparse'
2
- require 'logger'
3
-
4
- module Retjilp
5
- class Options
6
- attr_reader :log_level
7
-
8
- def initialize(argv)
9
- @log_level = Logger::WARN
10
- OptionParser.new do |opts|
11
- opts.banner = "Usage: retjilp [ --help ] [ --verbose | --debug ]"
12
- opts.on("--verbose", "Run with verbose output") { @log_level = Logger::INFO }
13
- opts.on("--debug", "Run with debug output") { @log_level = Logger::DEBUG }
14
- opts.on_tail("-h", "--help", "Show this help") { puts opts ; exit }
15
- begin
16
- opts.parse!(argv)
17
- rescue => e
18
- STDERR.puts e.message, "\n", opts
19
- exit(-1)
20
- end
21
- end.parse!(argv)
22
- end
23
- end
24
- end