From 5b030967ea3ec207b9119ca61077520887880d88 Mon Sep 17 00:00:00 2001 From: Shohei Maeda Date: Thu, 5 Dec 2019 02:21:54 +0900 Subject: [PATCH 01/34] [bug fix] Stop manual URI parsing/escaping and use CGI::parse (#119) * Stop manual URI parsing/escaping and use URI::HTTP standard library. * Update .travis.yml * bump minimum Ruby version * Use CGI::parse - URI::decode_www_form can't handle multibyte characters * avoid using set_form_data() - as it doesn't seem to be escaping some of the special characters such as '*' (asterisk). Use CGI.escape instead. - do not parse/escape request POST body in case if "content-type" request header is specified. * Update .travis.yml - follow latest Ruby releases --- .travis.yml | 27 ++++++++++++++++++--------- lib/twurl/cli.rb | 19 +++++++++++-------- lib/twurl/oauth_client.rb | 9 ++++++++- twurl.gemspec | 3 +-- 4 files changed, 38 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index dfd89ff..977e3cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,23 @@ -bundler_args: --without development +dist: xenial language: ruby -rvm: - - 1.9.3 - - 2.0.0 - - jruby-head - - rbx-2 - - ruby-head + +before_install: + - bundle config without 'development' + +branches: + only: + - master + matrix: + fast_finish: true allow_failures: - rvm: jruby-head - rvm: ruby-head - fast_finish: true -sudo: false + - rvm: rbx-2 + include: + - rvm: 2.6.5 + - rvm: 2.5.7 + - rvm: 2.4.9 + - rvm: jruby-head + - rvm: ruby-head + - rvm: rbx-2 diff --git a/lib/twurl/cli.rb b/lib/twurl/cli.rb index c68210c..fa9327b 100644 --- a/lib/twurl/cli.rb +++ b/lib/twurl/cli.rb @@ -44,6 +44,7 @@ def parse_options(args) arguments = args.dup Twurl.options = Options.new + Twurl.options.args = arguments Twurl.options.trace = false Twurl.options.data = {} Twurl.options.headers = {} @@ -159,11 +160,9 @@ def extract_path!(arguments) end def escape_params(params) - split_params = params.split("&").map do |param| - key, value = param.split('=', 2) - CGI::escape(key) + '=' + CGI::escape(value) - end - split_params.join("&") + CGI::parse(params).map do |key, value| + "#{CGI.escape key}=#{CGI.escape value.first}" + end.join("&") end end @@ -230,9 +229,13 @@ def trace def data on('-d', '--data [data]', 'Sends the specified data in a POST request to the HTTP server.') do |data| - data.split('&').each do |pair| - key, value = pair.split('=', 2) - options.data[key] = value + if options.args.count { |item| /content-type: (.*)/i.match(item) } > 0 + options.data[data] = nil + else + data.split('&').each do |pair| + key, value = pair.split('=', 2) + options.data[key] = value + end end end end diff --git a/lib/twurl/oauth_client.rb b/lib/twurl/oauth_client.rb index 773e333..c04fcd6 100644 --- a/lib/twurl/oauth_client.rb +++ b/lib/twurl/oauth_client.rb @@ -111,7 +111,14 @@ def perform_request_from_options(options, &block) elsif request.content_type && options.data request.body = options.data.keys.first elsif options.data - request.set_form_data(options.data) + request.content_type = "application/x-www-form-urlencoded" + if options.data.length == 1 && options.data.values.first == nil + request.body = options.data.keys.first + else + request.body = options.data.map do |key, value| + "#{key}=#{CGI.escape value}" + end.join("&") + end end request.oauth!(consumer.http, consumer, access_token) diff --git a/twurl.gemspec b/twurl.gemspec index 77b7de8..8bea400 100644 --- a/twurl.gemspec +++ b/twurl.gemspec @@ -5,7 +5,6 @@ require 'twurl/version' Gem::Specification.new do |spec| spec.add_dependency 'oauth', '~> 0.4' - spec.add_development_dependency 'bundler', '~> 1.0' spec.authors = ["Marcel Molina", "Erik Michaels-Ober"] spec.description = %q{Curl for the Twitter API} spec.email = ['marcel@twitter.com'] @@ -17,7 +16,7 @@ Gem::Specification.new do |spec| spec.name = 'twurl' spec.rdoc_options = ['--title', 'twurl -- OAuth-enabled curl for the Twitter API', '--main', 'README', '--line-numbers', '--inline-source'] spec.require_paths = ['lib'] - spec.required_ruby_version = '>= 1.9.3' + spec.required_ruby_version = '>= 2.4.0' spec.rubyforge_project = 'twurl' spec.summary = spec.description spec.version = Twurl::Version From 1890ab98726a407381bac6551198fbd0e6a6f1e9 Mon Sep 17 00:00:00 2001 From: Shohei Maeda Date: Thu, 5 Dec 2019 02:26:40 +0900 Subject: [PATCH 02/34] make username option settable on requests (#125) * make username option settable on requests https://github.com/twitter/twurl/issues/10 * update deprecated name --- lib/twurl/oauth_client.rb | 4 +++- test/cli_test.rb | 2 +- test/oauth_client_test.rb | 12 +++++------- test/rcfile_test.rb | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/twurl/oauth_client.rb b/lib/twurl/oauth_client.rb index c04fcd6..4941f3b 100644 --- a/lib/twurl/oauth_client.rb +++ b/lib/twurl/oauth_client.rb @@ -11,7 +11,9 @@ def rcfile(reload = false) def load_from_options(options) if rcfile.has_oauth_profile_for_username_with_consumer_key?(options.username, options.consumer_key) load_client_for_username_and_consumer_key(options.username, options.consumer_key) - elsif options.username || (options.command == 'authorize') + elsif options.username + load_client_for_username(options.username) + elsif options.command == 'authorize' load_new_client_from_options(options) else load_default_client diff --git a/test/cli_test.rb b/test/cli_test.rb index 19012b7..ec77c62 100644 --- a/test/cli_test.rb +++ b/test/cli_test.rb @@ -226,7 +226,7 @@ def test_protocol_is_stripped_from_host module ProxyOptionTests def test_not_specifying_proxy_sets_it_to_nil options = Twurl::CLI.parse_options([TEST_PATH]) - assert_equal nil, options.proxy + assert_nil options.proxy end def test_setting_proxy_updates_to_requested_value diff --git a/test/oauth_client_test.rb b/test/oauth_client_test.rb index 94fd85b..9be5401 100644 --- a/test/oauth_client_test.rb +++ b/test/oauth_client_test.rb @@ -20,7 +20,7 @@ def teardown super Twurl.options = Twurl::Options.new # Make sure we don't do any disk IO in these tests - assert !File.exists?(Twurl::RCFile.file_path) + assert !File.exist?(Twurl::RCFile.file_path) end def test_nothing @@ -46,12 +46,10 @@ def test_forced_reloading end class Twurl::OAuthClient::ClientLoadingFromOptionsTest < Twurl::OAuthClient::AbstractOAuthClientTest - def test_if_username_is_supplied_and_no_profile_exists_for_username_then_new_client_is_created - mock(Twurl::OAuthClient).load_client_for_username(options.username).never - mock(Twurl::OAuthClient).load_new_client_from_options(options).times(1) - mock(Twurl::OAuthClient).load_default_client.never - - Twurl::OAuthClient.load_from_options(options) + def test_if_username_is_supplied_and_no_profile_exists_for_username_then_no_profile_error + assert_raises Twurl::Exception do + Twurl::OAuthClient.load_from_options(options) + end end def test_if_username_is_supplied_and_profile_exists_for_username_then_client_is_loaded diff --git a/test/rcfile_test.rb b/test/rcfile_test.rb index cf5cc31..cb4682b 100644 --- a/test/rcfile_test.rb +++ b/test/rcfile_test.rb @@ -136,7 +136,7 @@ def test_file_is_not_world_readable private def rcfile_exists? - File.exists?(Twurl::RCFile.file_path) + File.exist?(Twurl::RCFile.file_path) end def delete_rcfile From 65632de89450c21c8d39e01533533bb8379f6292 Mon Sep 17 00:00:00 2001 From: Shohei Maeda Date: Thu, 5 Dec 2019 02:37:42 +0900 Subject: [PATCH 03/34] Use IO.binread instead of IO.read (#110) --- lib/twurl/oauth_client.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/twurl/oauth_client.rb b/lib/twurl/oauth_client.rb index 4941f3b..7a9ab11 100644 --- a/lib/twurl/oauth_client.rb +++ b/lib/twurl/oauth_client.rb @@ -99,10 +99,10 @@ def perform_request_from_options(options, &block) multipart_body << "\r\n" if options.upload['base64'] - enc = Base64.encode64(File.read(filename)) + enc = Base64.encode64(File.binread(filename)) multipart_body << enc else - multipart_body << File.read(filename) + multipart_body << File.binread(filename) end } From 6fb3c081d16efcb88a84f97b7b11ea8b31d325a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20Ro=C5=A1t=C3=A1r?= Date: Fri, 6 Dec 2019 19:32:32 +0100 Subject: [PATCH 04/34] Fix PIN authorization (#111) * Use STDIN.gets for pin prompt * Fix invalid README reference in gemspec --- lib/twurl/oauth_client.rb | 2 +- twurl.gemspec | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/twurl/oauth_client.rb b/lib/twurl/oauth_client.rb index 7a9ab11..01b8486 100644 --- a/lib/twurl/oauth_client.rb +++ b/lib/twurl/oauth_client.rb @@ -144,7 +144,7 @@ def client_auth_parameters def perform_pin_authorize_workflow @request_token = consumer.get_request_token CLI.puts("Go to #{generate_authorize_url} and paste in the supplied PIN") - pin = gets + pin = STDIN.gets access_token = @request_token.get_access_token(:oauth_verifier => pin.chomp) {:oauth_token => access_token.token, :oauth_token_secret => access_token.secret} end diff --git a/twurl.gemspec b/twurl.gemspec index 8bea400..f66334d 100644 --- a/twurl.gemspec +++ b/twurl.gemspec @@ -9,12 +9,12 @@ Gem::Specification.new do |spec| spec.description = %q{Curl for the Twitter API} spec.email = ['marcel@twitter.com'] spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } - spec.extra_rdoc_files = %w(COPYING INSTALL README) + spec.extra_rdoc_files = %w(COPYING INSTALL README.md) spec.files = `git ls-files -z`.split("\x0").reject { |f| f.start_with?('test/') } spec.homepage = 'http://github.com/twitter/twurl' spec.licenses = ['MIT'] spec.name = 'twurl' - spec.rdoc_options = ['--title', 'twurl -- OAuth-enabled curl for the Twitter API', '--main', 'README', '--line-numbers', '--inline-source'] + spec.rdoc_options = ['--title', 'twurl -- OAuth-enabled curl for the Twitter API', '--main', 'README.md', '--line-numbers', '--inline-source'] spec.require_paths = ['lib'] spec.required_ruby_version = '>= 2.4.0' spec.rubyforge_project = 'twurl' From d037cf6866eb92e0d33ac37b873ba23e9a72e772 Mon Sep 17 00:00:00 2001 From: Shohei Maeda Date: Mon, 9 Dec 2019 00:15:12 +0900 Subject: [PATCH 05/34] Add twurl version to user agent header (#126) * Add twurl version to user agent header * Add test --- lib/twurl/oauth_client.rb | 6 ++++++ test/oauth_client_test.rb | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/twurl/oauth_client.rb b/lib/twurl/oauth_client.rb index 01b8486..5625ae0 100644 --- a/lib/twurl/oauth_client.rb +++ b/lib/twurl/oauth_client.rb @@ -124,9 +124,15 @@ def perform_request_from_options(options, &block) end request.oauth!(consumer.http, consumer, access_token) + request['user-agent'] = user_agent consumer.http.request(request, &block) end + def user_agent + "twurl version: #{Version} " \ + "platform: #{RUBY_ENGINE} #{RUBY_VERSION} (#{RUBY_PLATFORM})" + end + def exchange_credentials_for_access_token response = begin consumer.token_request(:post, consumer.access_token_path, nil, {}, client_auth_parameters) diff --git a/test/oauth_client_test.rb b/test/oauth_client_test.rb index 9be5401..a8e61c9 100644 --- a/test/oauth_client_test.rb +++ b/test/oauth_client_test.rb @@ -167,6 +167,20 @@ def test_content_type_is_set_to_form_encoded_if_not_set_and_data_in_options client.perform_request_from_options(options) end + + def test_user_agent_request_header_is_set + client = Twurl::OAuthClient.test_exemplar + expected_ua_string = "twurl version: #{Twurl::Version} platform: #{RUBY_ENGINE} #{RUBY_VERSION} (#{RUBY_PLATFORM})" + + mock(client.consumer.http).request( + satisfy { |req| + req.is_a?(Net::HTTP::Get) && + req['user-agent'] == expected_ua_string + } + ) + + client.perform_request_from_options(options) + end end class Twurl::OAuthClient::CredentialsForAccessTokenExchangeTest < Twurl::OAuthClient::AbstractOAuthClientTest From c872a7da4f6526ea37fcd4463b7f14e8a0bf4963 Mon Sep 17 00:00:00 2001 From: Shohei Maeda Date: Mon, 9 Dec 2019 21:46:57 +0900 Subject: [PATCH 06/34] Update Code-Of-Conduct / LICENSE (#127) * Add Code of Conduct v2.0 from https://github.com/twitter/code-of-conduct/blob/master/code-of-conduct.md * Update LICENSE * Update README --- CODE_OF_CONDUCT.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++ COPYING | 18 ----------------- LICENSE | 21 ++++++++++++++++++++ README.md | 4 +++- 4 files changed, 72 insertions(+), 19 deletions(-) create mode 100644 CODE_OF_CONDUCT.md delete mode 100644 COPYING create mode 100644 LICENSE diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..62347b8 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,48 @@ +# Code of Conduct v2.0 + +This code of conduct outlines our expectations for participants within the [@TwitterOSS](https://twitter.com/twitteross) community, as well as steps to reporting unacceptable behavior. We are committed to providing a welcoming and inspiring community for all and expect our code of conduct to be honored. Anyone who violates this code of conduct may be banned from the community. + +Our open source community strives to: + +* **Be friendly and patient.** +* **Be welcoming**: We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. +* **Be considerate**: Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language. +* **Be respectful**: Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. +* **Be careful in the words that you choose**: we are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to: + * Violent threats or language directed against another person. + * Discriminatory jokes and language. + * Posting sexually explicit or violent material. + * Posting (or threatening to post) other people's personally identifying information ("doxing"). + * Personal insults, especially those using racist or sexist terms. + * Unwelcome sexual attention. + * Advocating for, or encouraging, any of the above behavior. + * Repeated harassment of others. In general, if someone asks you to stop, then stop. +* **When we disagree, try to understand why**: Disagreements, both social and technical, happen all the time. It is important that we resolve disagreements and differing views constructively. Remember that we’re different. The strength of our community comes from its diversity, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. + +This code is not exhaustive or complete. It serves to distill our common understanding of a collaborative, shared environment, and goals. We expect it to be followed in spirit as much as in the letter. + +### Diversity Statement + +We encourage everyone to participate and are committed to building a community for all. Although we may not be able to satisfy everyone, we all agree that everyone is equal. Whenever a participant has made a mistake, we expect them to take responsibility for it. If someone has been harmed or offended, it is our responsibility to listen carefully and respectfully, and do our best to right the wrong. + +Although this list cannot be exhaustive, we explicitly honor diversity in age, gender, gender identity or expression, culture, ethnicity, language, national origin, political beliefs, profession, race, religion, sexual orientation, socioeconomic status, and technical ability. We will not tolerate discrimination based on any of the protected +characteristics above, including participants with disabilities. + +### Reporting Issues + +If you experience or witness unacceptable behavior—or have any other concerns—please report it by contacting us via [opensource+codeofconduct@twitter.com](mailto:opensource+codeofconduct@twitter.com). All reports will be handled with discretion. In your report please include: + +- Your contact information. +- Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional witnesses, please +include them as well. Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public IRC logger), please include a link. +- Any additional information that may be helpful. + +After filing a report, a representative will contact you personally. If the person who is harassing you is part of the response team, they will recuse themselves from handling your incident. A representative will then review the incident, follow up with any additional questions, and make a decision as to how to respond. We will respect confidentiality requests for the purpose of protecting victims of abuse. + +Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual engages in unacceptable behavior, the representative may take any action they deem appropriate, up to and including a permanent ban from our community without warning. + +## Thanks + +This code of conduct is based on the [Open Code of Conduct](https://github.com/todogroup/opencodeofconduct) from the [TODOGroup](http://todogroup.org). + +We are thankful for their work and all the communities who have paved the way with code of conducts. diff --git a/COPYING b/COPYING deleted file mode 100644 index 04dc088..0000000 --- a/COPYING +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) 2009 Marcel Molina , Twitter, Inc. -# -# 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: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# 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. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..59d11d8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2009 Marcel Molina , 2019 Twitter, Inc. + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. diff --git a/README.md b/README.md index 1fdb5fb..9d4a0e9 100644 --- a/README.md +++ b/README.md @@ -137,5 +137,7 @@ twurl accounts Contributors ------------ -Marcel Molina / @noradio +Marcel Molina / @noradio Erik Michaels-Ober / @sferik + +and there are many [more](https://github.com/twitter/twurl/graphs/contributors)! From ded1136919bb347bde41828c37e74a58071720d6 Mon Sep 17 00:00:00 2001 From: Shohei Maeda Date: Tue, 10 Dec 2019 19:22:56 +0900 Subject: [PATCH 07/34] Add --timeout and --connection-timeout options (#128) * Add --timeout and --connection-timeout options * Only set max_retries if Ruby version is >= 2.5) * Fix test (random failure) Since the Minitest run tests concurrently, sometimes the "DispatchWithOneAuthorizedAccountTest" class runs after the "DispatchWithOneUsernameThatHasAuthorizedMultipleAccountsTest" run. In this case, "Twurl::OAuthClient.rcfile" has multiple profiles data in there and hence it fails with its mock checks. --- lib/twurl/cli.rb | 14 ++++++++++++++ lib/twurl/oauth_client.rb | 4 ++++ test/account_information_controller_test.rb | 11 ++++++++--- test/cli_test.rb | 15 +++++++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/lib/twurl/cli.rb b/lib/twurl/cli.rb index fa9327b..0d24a11 100644 --- a/lib/twurl/cli.rb +++ b/lib/twurl/cli.rb @@ -89,6 +89,8 @@ def parse_options(args) file filefield base64 + timeout + connection_timeout end end @@ -326,6 +328,18 @@ def base64 options.upload['base64'] = base64 end end + + def timeout + on('--timeout [sec]', Integer, 'Number of seconds to wait for the request to be read (default: 60)') do |timeout| + options.timeout = timeout + end + end + + def connection_timeout + on('--connection-timeout [sec]', Integer, 'Number of seconds to wait for the connection to open (default: 60)') do |connection_timeout| + options.connection_timeout = connection_timeout + end + end end end diff --git a/lib/twurl/oauth_client.rb b/lib/twurl/oauth_client.rb index 5625ae0..f369288 100644 --- a/lib/twurl/oauth_client.rb +++ b/lib/twurl/oauth_client.rb @@ -206,6 +206,10 @@ def to_hash def configure_http! consumer.http.set_debug_output(Twurl.options.debug_output_io) if Twurl.options.trace + consumer.http.read_timeout = consumer.http.open_timeout = Twurl.options.timeout || 60 + consumer.http.open_timeout = Twurl.options.connection_timeout if Twurl.options.connection_timeout + # Only override if Net::HTTP support max_retries (since Ruby >= 2.5) + consumer.http.max_retries = 0 if consumer.http.respond_to?(:max_retries=) if Twurl.options.ssl? consumer.http.use_ssl = true consumer.http.verify_mode = OpenSSL::SSL::VERIFY_NONE diff --git a/test/account_information_controller_test.rb b/test/account_information_controller_test.rb index c232574..9a65be1 100644 --- a/test/account_information_controller_test.rb +++ b/test/account_information_controller_test.rb @@ -22,6 +22,7 @@ def setup @options = Twurl::Options.test_exemplar @client = Twurl::OAuthClient.load_new_client_from_options(options) mock(Twurl::OAuthClient.rcfile).save.times(1) + Twurl::OAuthClient.rcfile.profiles.clear Twurl::OAuthClient.rcfile << client @controller = Twurl::AccountInformationController.new(client, options) end @@ -51,10 +52,14 @@ def setup @controller = Twurl::AccountInformationController.new(other_client, other_client_options) end + def teardown + Twurl::OAuthClient.rcfile.profiles[default_client.username][other_client.consumer_key].clear + end + def test_authorized_account_is_displayed_and_marked_as_the_default - mock(Twurl::CLI).puts(default_client.username).times(1) - mock(Twurl::CLI).puts(" #{default_client.consumer_key} (default)").times(1) - mock(Twurl::CLI).puts(" #{other_client.consumer_key}").times(1) + mock(Twurl::CLI).puts(default_client.username).times(1).ordered + mock(Twurl::CLI).puts(" #{default_client.consumer_key} (default)").times(1).ordered + mock(Twurl::CLI).puts(" #{other_client.consumer_key}").times(1).ordered controller.dispatch end diff --git a/test/cli_test.rb b/test/cli_test.rb index ec77c62..ea6bbca 100644 --- a/test/cli_test.rb +++ b/test/cli_test.rb @@ -239,4 +239,19 @@ def test_setting_proxy_updates_to_requested_value end end include ProxyOptionTests + + module TimeoutOptionTests + def test_not_specifying_timeout_sets_it_to_nil + options = Twurl::CLI.parse_options([TEST_PATH]) + assert_nil options.timeout + assert_nil options.connection_timeout + end + + def test_setting_timeout_updates_to_requested_value + options = Twurl::CLI.parse_options([TEST_PATH, '--timeout', '10', '--connection-timeout', '5']) + assert_equal 10, options.timeout + assert_equal 5, options.connection_timeout + end + end + include TimeoutOptionTests end From 5a7cee493675cef02847e7d726dc62ce5f6dbab0 Mon Sep 17 00:00:00 2001 From: Flavio Martins Date: Tue, 10 Dec 2019 10:54:23 +0000 Subject: [PATCH 08/34] Make aliases work again Closes: #93 (#114) * Make aliases work again Closes: #93 * show usage when args are insufficient * Update lib/twurl/cli.rb Co-Authored-By: Shohei Maeda --- lib/twurl/cli.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/twurl/cli.rb b/lib/twurl/cli.rb index 0d24a11..0872547 100644 --- a/lib/twurl/cli.rb +++ b/lib/twurl/cli.rb @@ -41,10 +41,8 @@ def dispatch(options) end def parse_options(args) - arguments = args.dup - Twurl.options = Options.new - Twurl.options.args = arguments + Twurl.options.args = args.dup Twurl.options.trace = false Twurl.options.data = {} Twurl.options.headers = {} @@ -97,13 +95,13 @@ def parse_options(args) arguments = option_parser.parse!(args) Twurl.options.command = extract_command!(arguments) Twurl.options.path = extract_path!(arguments) - - if Twurl.options.command == DEFAULT_COMMAND and Twurl.options.path.nil? + Twurl.options.subcommands = arguments + + if Twurl.options.command == DEFAULT_COMMAND and Twurl.options.path.nil? and Twurl.options.args.empty? CLI.puts option_parser raise NoPathFound, "No path found" end - Twurl.options.subcommands = arguments Twurl.options end From 03984a20a2fc07e6a41ee979b266f893561d1823 Mon Sep 17 00:00:00 2001 From: Shohei Maeda Date: Tue, 10 Dec 2019 21:11:35 +0900 Subject: [PATCH 09/34] Release v0.9.4 (#129) * bump version * Handle timeout errors * Fix .gemspec --- lib/twurl/request_controller.rb | 8 ++++++++ lib/twurl/version.rb | 2 +- twurl.gemspec | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/twurl/request_controller.rb b/lib/twurl/request_controller.rb index 2b78cb8..71c9d5f 100644 --- a/lib/twurl/request_controller.rb +++ b/lib/twurl/request_controller.rb @@ -1,6 +1,10 @@ module Twurl class RequestController < AbstractCommandController NO_URI_MESSAGE = "No URI specified" + READ_TIMEOUT_MESSAGE = 'A timeout occurred (Net::ReadTimeout). ' \ + 'Please try again or increase the value using --timeout option.' + OPEN_TIMEOUT_MESSAGE = 'A timeout occurred (Net::OpenTimeout). ' \ + 'Please try again or increase the value using --connection-timeout option.' def dispatch if client.needs_to_authorize? raise Exception, "You need to authorize first." @@ -15,6 +19,10 @@ def perform_request } rescue URI::InvalidURIError CLI.puts NO_URI_MESSAGE + rescue Net::ReadTimeout + CLI.puts READ_TIMEOUT_MESSAGE + rescue Net::OpenTimeout + CLI.puts OPEN_TIMEOUT_MESSAGE end end end diff --git a/lib/twurl/version.rb b/lib/twurl/version.rb index d4593d7..a866516 100644 --- a/lib/twurl/version.rb +++ b/lib/twurl/version.rb @@ -2,7 +2,7 @@ module Twurl class Version MAJOR = 0 unless defined? Twurl::Version::MAJOR MINOR = 9 unless defined? Twurl::Version::MINOR - PATCH = 3 unless defined? Twurl::Version::PATCH + PATCH = 4 unless defined? Twurl::Version::PATCH PRE = nil unless defined? Twurl::Version::PRE # Time.now.to_i.to_s class << self diff --git a/twurl.gemspec b/twurl.gemspec index f66334d..be27b30 100644 --- a/twurl.gemspec +++ b/twurl.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |spec| spec.description = %q{Curl for the Twitter API} spec.email = ['marcel@twitter.com'] spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } - spec.extra_rdoc_files = %w(COPYING INSTALL README.md) + spec.extra_rdoc_files = %w(CODE_OF_CONDUCT.md INSTALL LICENSE README.md) spec.files = `git ls-files -z`.split("\x0").reject { |f| f.start_with?('test/') } spec.homepage = 'http://github.com/twitter/twurl' spec.licenses = ['MIT'] From d0822646c84c1f72ac17ca9dad86e55c745fc1ac Mon Sep 17 00:00:00 2001 From: Shohei Maeda Date: Sat, 14 Dec 2019 00:19:42 +0900 Subject: [PATCH 10/34] Support authorization options on request (#131) so users can make a request without ~/.twurlrc file if all options (-c, -s, -a, and -S) are provided. --- lib/twurl/oauth_client.rb | 14 ++++++++++++++ test/oauth_client_test.rb | 27 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/lib/twurl/oauth_client.rb b/lib/twurl/oauth_client.rb index f369288..0359604 100644 --- a/lib/twurl/oauth_client.rb +++ b/lib/twurl/oauth_client.rb @@ -15,11 +15,17 @@ def load_from_options(options) load_client_for_username(options.username) elsif options.command == 'authorize' load_new_client_from_options(options) + elsif options.command == 'request' && has_oauth_options?(options) + load_new_client_from_oauth_options(options) else load_default_client end end + def has_oauth_options?(options) + (options.consumer_key && options.consumer_secret && options.access_token && options.token_secret) ? true : false + end + def load_client_for_username_and_consumer_key(username, consumer_key) user_profiles = rcfile[username] if user_profiles && attributes = user_profiles[consumer_key] @@ -45,6 +51,14 @@ def load_new_client_from_options(options) new(options.oauth_client_options.merge('password' => options.password)) end + def load_new_client_from_oauth_options(options) + new(options.oauth_client_options.merge( + 'token' => options.access_token, + 'secret' => options.token_secret + ) + ) + end + def load_default_client raise Exception, "You must authorize first" unless rcfile.default_profile load_client_for_username_and_consumer_key(*rcfile.default_profile) diff --git a/test/oauth_client_test.rb b/test/oauth_client_test.rb index a8e61c9..c45ec67 100644 --- a/test/oauth_client_test.rb +++ b/test/oauth_client_test.rb @@ -72,6 +72,18 @@ def test_if_username_is_not_provided_then_the_default_client_is_loaded Twurl::OAuthClient.load_from_options(options) end + + def test_if_all_oauth_options_are_supplied_then_client_is_loaded_from_options + options.username = nil + options.command = 'request' + options.access_token = 'test_access_token' + options.token_secret = 'test_token_secret' + + mock(Twurl::OAuthClient).load_new_client_from_oauth_options(options).times(1) + mock(Twurl::OAuthClient).load_default_client.never + + Twurl::OAuthClient.load_from_options(options) + end end class Twurl::OAuthClient::ClientLoadingForUsernameTest < Twurl::OAuthClient::AbstractOAuthClientTest @@ -130,6 +142,21 @@ def test_oauth_options_are_passed_through end end +class Twurl::OAuthClient::NewClientLoadingFromOauthOptionsTest < Twurl::OAuthClient::AbstractOAuthClientTest + attr_reader :new_client + def setup + super + options.access_token = 'test_access_token' + options.token_secret = 'test_token_secret' + @new_client = Twurl::OAuthClient.load_new_client_from_oauth_options(options) + end + + def test_attributes_are_updated + assert_equal options.access_token, new_client.token + assert_equal options.token_secret, new_client.secret + end +end + class Twurl::OAuthClient::PerformingRequestsFromOptionsTest < Twurl::OAuthClient::AbstractOAuthClientTest def test_request_is_made_using_request_method_and_path_and_data_in_options client = Twurl::OAuthClient.test_exemplar From f81ec261b8cb5be6872fd470faf882cf2b5e993b Mon Sep 17 00:00:00 2001 From: Shohei Maeda Date: Tue, 17 Dec 2019 05:15:39 +0900 Subject: [PATCH 11/34] Add -j (--json-pretty) option (#130) * Add -j (--json-pretty) option * Fix test not all machines have the "TMPDIR" env variable set by default (e.g., Chrome OS) and hence Minitest actually removes user's "~/.twurlrc" file if the "TMPDIR" is not set. To remediate this, override the directory path in case if it's missing so running tests don't remove the user's file by accident. * Update .gitignore * Update INSTALL remove outdated info and use Markdown * Update INSTALL.md --- .gitignore | 1 + INSTALL | 23 --------------------- INSTALL.md | 36 +++++++++++++++++++++++++++++++++ lib/twurl.rb | 1 + lib/twurl/cli.rb | 7 +++++++ lib/twurl/request_controller.rb | 13 +++++++++++- test/request_controller_test.rb | 18 +++++++++++++++++ test/test_helper.rb | 2 +- twurl.gemspec | 2 +- 9 files changed, 77 insertions(+), 26 deletions(-) delete mode 100644 INSTALL create mode 100644 INSTALL.md diff --git a/.gitignore b/.gitignore index 5ee79fe..d01e512 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ test/version_tmp tmp tmtags tramp +vendor diff --git a/INSTALL b/INSTALL deleted file mode 100644 index 106127f..0000000 --- a/INSTALL +++ /dev/null @@ -1,23 +0,0 @@ -+-----------------------+ -| Install with RubyGems | -+-----------------------+ - - sudo gem i twurl --source http://rubygems.org - -+---------------------+ -| Build from source | -+---------------------+ - - rake build - -+---------------------+ -| Install from source | -+---------------------+ - - rake install - -+--------------+ -| Dependencies | -+--------------+ - - sudo gem i oauth diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..89404c8 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,36 @@ +# Install + +## Install with RubyGems (recommended) + +```sh +# installing the latest release +$ gem install twurl +``` + +```sh +# verify installation +$ twurl -v +0.9.4 +``` + +## Install from source + +In case if you haven't installed `bundler` you need to install it first: + +```sh +$ gem install bundler +``` + +```sh +$ git clone https://github.com/twitter/twurl +$ cd twurl +$ bundle install +``` + +If you don't want to install Twurl globally on your system, use `--path` [option](https://bundler.io/v2.0/bundle_install.html): + +``` +$ bundle install --path path/to/directory +$ bundle exec twurl -v +0.9.4 +``` diff --git a/lib/twurl.rb b/lib/twurl.rb index 124a2a0..0f50089 100644 --- a/lib/twurl.rb +++ b/lib/twurl.rb @@ -4,6 +4,7 @@ require 'ostruct' require 'stringio' require 'yaml' +require 'json' library_files = Dir[File.join(File.dirname(__FILE__), "/twurl/**/*.rb")].sort library_files.each do |file| diff --git a/lib/twurl/cli.rb b/lib/twurl/cli.rb index 0872547..c88ce7d 100644 --- a/lib/twurl/cli.rb +++ b/lib/twurl/cli.rb @@ -87,6 +87,7 @@ def parse_options(args) file filefield base64 + json_format timeout connection_timeout end @@ -327,6 +328,12 @@ def base64 end end + def json_format + on('-j', '--json-pretty', 'Format response body to JSON pretty style') do |json_format| + options.json_format = true + end + end + def timeout on('--timeout [sec]', Integer, 'Number of seconds to wait for the request to be read (default: 60)') do |timeout| options.timeout = timeout diff --git a/lib/twurl/request_controller.rb b/lib/twurl/request_controller.rb index 71c9d5f..2da33b6 100644 --- a/lib/twurl/request_controller.rb +++ b/lib/twurl/request_controller.rb @@ -15,7 +15,9 @@ def dispatch def perform_request client.perform_request_from_options(options) { |response| - response.read_body { |chunk| CLI.print chunk } + response.read_body { |body| + CLI.print options.json_format ? JsonFormatter.format(body) : body + } } rescue URI::InvalidURIError CLI.puts NO_URI_MESSAGE @@ -25,4 +27,13 @@ def perform_request CLI.puts OPEN_TIMEOUT_MESSAGE end end + + class JsonFormatter + def self.format(string) + json = JSON.parse(string) + (json.is_a?(Array) || json.is_a?(Hash)) ? JSON.pretty_generate(json) : string + rescue JSON::ParserError, TypeError + string + end + end end diff --git a/test/request_controller_test.rb b/test/request_controller_test.rb index 92427e0..39027ce 100644 --- a/test/request_controller_test.rb +++ b/test/request_controller_test.rb @@ -49,6 +49,24 @@ def test_request_response_is_written_to_output assert_equal expected_body, Twurl::CLI.output.string end + def test_request_response_is_json_formatted + response_body = '{"data": {"text": "this is a fake response"}}' + expected_body = "{\n" \ + " \"data\": {\n" \ + " \"text\": \"this is a fake response\"\n" \ + " }\n" \ + "}" + custom_options = options + custom_options.json_format = true + response = Object.new + mock(response).read_body { |block| block.call response_body } + mock(client).perform_request_from_options(custom_options).times(1) { |custom_options, block| block.call(response) } + + controller.perform_request + + assert_equal expected_body, Twurl::CLI.output.string + end + def test_invalid_or_unspecified_urls_report_error mock(Twurl::CLI).puts(Twurl::RequestController::NO_URI_MESSAGE).times(1) mock(client).perform_request_from_options(options).times(1) { raise URI::InvalidURIError } diff --git a/test/test_helper.rb b/test/test_helper.rb index e22e451..1ba7179 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -9,7 +9,7 @@ require 'minitest/autorun' require 'rr' -Twurl::RCFile.directory = ENV['TMPDIR'] +Twurl::RCFile.directory = ENV['TMPDIR'] || File.dirname(__FILE__) module Twurl class Options diff --git a/twurl.gemspec b/twurl.gemspec index be27b30..9fa2c52 100644 --- a/twurl.gemspec +++ b/twurl.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |spec| spec.description = %q{Curl for the Twitter API} spec.email = ['marcel@twitter.com'] spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } - spec.extra_rdoc_files = %w(CODE_OF_CONDUCT.md INSTALL LICENSE README.md) + spec.extra_rdoc_files = %w(CODE_OF_CONDUCT.md INSTALL.md LICENSE README.md) spec.files = `git ls-files -z`.split("\x0").reject { |f| f.start_with?('test/') } spec.homepage = 'http://github.com/twitter/twurl' spec.licenses = ['MIT'] From 1f6dcac19db0a3967801b716006e508dbccd0bc6 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Tue, 17 Dec 2019 16:17:02 +0000 Subject: [PATCH 12/34] Documentation improvements (#133) * Documentation improvements * Adding suggestions from #62 * fixing contributors in readme --- .gitignore | 3 ++- CONTRIBUTING.md | 25 +++++++++++++++++++ README.md | 65 +++++++++++++++++++++++++++++-------------------- twurl.gemspec | 5 ++-- 4 files changed, 68 insertions(+), 30 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/.gitignore b/.gitignore index d01e512..43b06ca 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ test/version_tmp tmp tmtags tramp -vendor +.vscode +vendor \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..119acb1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# How to Contribute + +We'd love to get patches from you! + +## Getting Started + +We follow the [GitHub Flow Workflow](https://guides.github.com/introduction/flow/) + +1. Fork the project +2. Check out the `master` branch +3. Create a feature branch +4. Write code and tests for your change +5. From your branch, make a pull request against `twitter/twurl/master` +6. Work with repo maintainers to get your change reviewed +7. Wait for your change to be pulled into `twitter/twurl/master` +8. Delete your feature branch + +## License + +By contributing your code, you agree to license your contribution under the +terms of the MIT License: https://github.com/twitter/twurl/blob/master/LICENSE + +## Code of Conduct + +Read our [Code of Conduct](CODE_OF_CONDUCT.md) for the project. diff --git a/README.md b/README.md index 9d4a0e9..56593f4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ Twurl ===== +[![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/twitter/twurl/blob/master/LICENSE) + [![Gem Version](https://badge.fury.io/rb/twurl.svg)](https://badge.fury.io/rb/twurl) + Twurl is like curl, but tailored specifically for the Twitter API. It knows how to grant an access token to a client application for a specified user and then sign all requests with that access token. @@ -10,34 +13,31 @@ as defining aliases for common requests, as well as support for multiple access tokens to easily switch between different client applications and Twitter accounts. - Installing Twurl ---------------- -Twurl can be installed using ruby gems: +Twurl can be installed using RubyGems: -``` +```sh gem install twurl ``` - Getting Started --------------- -If you haven't already, the first thing to do is apply for a developer account to access Twitter APIs: +If you haven't already, the first thing to do is apply for a developer account to access Twitter APIs: -``` +```text https://developer.twitter.com/en/apply-for-access ``` - -After you have that access you can create a Twitter app and generate a consumer key and secret. - + +After you have that access you can create a Twitter app and generate a consumer key and secret. When you have your consumer key and its secret you authorize -your Twitter account to make API requests with your consumer key +your Twitter account to make API requests with that consumer key and secret. -``` +```sh twurl authorize --consumer-key key \ --consumer-secret secret ``` @@ -47,14 +47,13 @@ Authenticate to Twitter, and then enter the returned PIN back into the terminal. Assuming all that works well, you will be authorized to make requests with the API. Twurl will tell you as much. - Making Requests --------------- The simplest request just requires that you specify the path you want to request. -``` +```sh twurl /1.1/statuses/home_timeline.json ``` @@ -63,47 +62,61 @@ Similar to curl, a GET request is performed by default. You can implicitly perform a POST request by passing the -d option, which specifies POST parameters. -``` +```sh twurl -d 'status=Testing twurl' /1.1/statuses/update.json ``` You can explicitly specify what request method to perform with the -X (or --request-method) option. -``` +```sh twurl -X POST /1.1/statuses/destroy/1234567890.json ``` +Accessing Different Hosts +------------------------- + +You can access different hosts for other Twitter APIs using the -H flag. + +```sh +twurl -H "ads-api.twitter.com" "/5/accounts" +``` + +Uploading Media +--------------- + +To upload binary files, you can format the call as a form post. Below, the binary is "/path/to/media.jpg" and the form field is "media": + +```sh +twurl -H "upload.twitter.com" -X POST "/1.1/media/upload.json" --file "/path/to/media.jpg" --file-field "media" +``` Creating aliases ---------------- -``` +```sh twurl alias h /1.1/statuses/home_timeline.json ``` You can then use "h" in place of the full path. -``` +```sh twurl h ``` -Paths that require additional options such as request parameters for example can -be used with aliases the same as with full explicit paths, just as you might -expect. +Paths that require additional options (such as request parameters, for example) can be used with aliases the same as with full explicit paths, just as you might expect. -``` +```sh twurl alias tweet /1.1/statuses/update.json twurl tweet -d "status=Aliases in twurl are convenient" ``` - Changing your default profile ----------------------------- The first time you authorize a client application to make requests on behalf of your account, twurl stores your access token information in its .twurlrc file. Subsequent requests will use this profile as the default profile. You can use the 'accounts' subcommand to see what client applications have been authorized for what user names: -``` +```sh twurl accounts noradio HQsAGcBm5MQT4n6j7qVJw @@ -114,7 +127,7 @@ twurl accounts Notice that one of those consumer keys is marked as the default. To change the default use the 'set' subcommand, passing then either just the username, if it's unambiguous, or the username and consumer key pair if it isn't unambiguous: -``` +```sh twurl set default testiverse twurl accounts noradio @@ -124,7 +137,7 @@ twurl accounts guT9RsJbNQgVe6AwoY9BA (default) ``` -``` +```sh twurl set default noradio HQsAGcBm5MQT4n6j7qVJw twurl accounts noradio @@ -137,7 +150,7 @@ twurl accounts Contributors ------------ -Marcel Molina / @noradio +Marcel Molina / @noradio Erik Michaels-Ober / @sferik and there are many [more](https://github.com/twitter/twurl/graphs/contributors)! diff --git a/twurl.gemspec b/twurl.gemspec index 9fa2c52..237c2c4 100644 --- a/twurl.gemspec +++ b/twurl.gemspec @@ -5,11 +5,10 @@ require 'twurl/version' Gem::Specification.new do |spec| spec.add_dependency 'oauth', '~> 0.4' - spec.authors = ["Marcel Molina", "Erik Michaels-Ober"] + spec.authors = ["Marcel Molina", "Erik Michaels-Ober", "@TwitterDev team"] spec.description = %q{Curl for the Twitter API} - spec.email = ['marcel@twitter.com'] spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } - spec.extra_rdoc_files = %w(CODE_OF_CONDUCT.md INSTALL.md LICENSE README.md) + spec.extra_rdoc_files = %w(CODE_OF_CONDUCT.md CONTRIBUTING.md INSTALL LICENSE README.md) spec.files = `git ls-files -z`.split("\x0").reject { |f| f.start_with?('test/') } spec.homepage = 'http://github.com/twitter/twurl' spec.licenses = ['MIT'] From 3a1a055c5ebc93b94d3a35b637a2156363c6aa47 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Tue, 17 Dec 2019 16:22:06 +0000 Subject: [PATCH 13/34] fix newline in README (#134) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 56593f4..ffdf9c2 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,7 @@ Contributors ------------ Marcel Molina / @noradio + Erik Michaels-Ober / @sferik and there are many [more](https://github.com/twitter/twurl/graphs/contributors)! From e9cb1a0b0d41ebc00566412affeffbee05b12491 Mon Sep 17 00:00:00 2001 From: Shohei Maeda Date: Thu, 26 Dec 2019 18:52:05 +0900 Subject: [PATCH 14/34] Support Bearer Token (#132) * Support Bearer Token * change method name * Add tests * rcfile.bearer_tokens is nil if .twurlrc doesn't exist * Add "oauth2_tokens" command and support --oauth2 with -c option so users can make an OAuth2 request without creating a user profile if they want... (e.g., a user who only use bearer token) * Change "--oauth2" option name to "--bearer" * Fix AppOnlyTokenInformationController * Fix NoMethodError * Update README * Fix Typo --- README.md | 58 +++++++---- lib/twurl/app_only_oauth_client.rb | 78 +++++++++++++++ .../app_only_token_information_controller.rb | 18 ++++ lib/twurl/cli.rb | 11 ++- lib/twurl/oauth_client.rb | 53 +++++++++- lib/twurl/rcfile.rb | 19 ++++ lib/twurl/request_controller.rb | 6 +- test/app_only_oauth_client_test.rb | 96 +++++++++++++++++++ test/authorization_controller_test.rb | 15 +++ test/cli_test.rb | 13 +++ test/oauth_client_test.rb | 41 +++++++- test/rcfile_test.rb | 21 ++++ test/request_controller_test.rb | 3 +- test/test_helper.rb | 23 +++++ 14 files changed, 422 insertions(+), 33 deletions(-) create mode 100644 lib/twurl/app_only_oauth_client.rb create mode 100644 lib/twurl/app_only_token_information_controller.rb create mode 100644 test/app_only_oauth_client_test.rb diff --git a/README.md b/README.md index ffdf9c2..f49a448 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -Twurl -===== +# Twurl [![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/twitter/twurl/blob/master/LICENSE) [![Gem Version](https://badge.fury.io/rb/twurl.svg)](https://badge.fury.io/rb/twurl) @@ -13,8 +12,7 @@ as defining aliases for common requests, as well as support for multiple access tokens to easily switch between different client applications and Twitter accounts. -Installing Twurl ----------------- +## Installing Twurl Twurl can be installed using RubyGems: @@ -22,8 +20,7 @@ Twurl can be installed using RubyGems: gem install twurl ``` -Getting Started ---------------- +## Getting Started If you haven't already, the first thing to do is apply for a developer account to access Twitter APIs: @@ -47,8 +44,7 @@ Authenticate to Twitter, and then enter the returned PIN back into the terminal. Assuming all that works well, you will be authorized to make requests with the API. Twurl will tell you as much. -Making Requests ---------------- +## Making Requests The simplest request just requires that you specify the path you want to request. @@ -73,8 +69,30 @@ the -X (or --request-method) option. twurl -X POST /1.1/statuses/destroy/1234567890.json ``` -Accessing Different Hosts -------------------------- +## Using Bearer Tokens (Application-only authentication) + +You can generate a bearer token using `--bearer` option in combination with the `authorize` subcommand. + +```sh +twurl authorize --bearer --consumer-key key \ + --consumer-secret secret +``` + +And then, you can make a request using a generated bearer token using `--bearer` request option. + +```sh +twurl --bearer '/1.1/search/tweets.json?q=hello' +``` + +To list your generated bearer tokens, you can use the `bearer_tokens` subcommand. + +```sh +twurl bearer_tokens +``` + +This will print a pair of consumer_key and its associated bearer token. Note, tokens are omitted from this output. + +## Accessing Different Hosts You can access different hosts for other Twitter APIs using the -H flag. @@ -82,8 +100,7 @@ You can access different hosts for other Twitter APIs using the -H flag. twurl -H "ads-api.twitter.com" "/5/accounts" ``` -Uploading Media ---------------- +## Uploading Media To upload binary files, you can format the call as a form post. Below, the binary is "/path/to/media.jpg" and the form field is "media": @@ -91,8 +108,7 @@ To upload binary files, you can format the call as a form post. Below, the binar twurl -H "upload.twitter.com" -X POST "/1.1/media/upload.json" --file "/path/to/media.jpg" --file-field "media" ``` -Creating aliases ----------------- +## Creating aliases ```sh twurl alias h /1.1/statuses/home_timeline.json @@ -111,10 +127,9 @@ twurl alias tweet /1.1/statuses/update.json twurl tweet -d "status=Aliases in twurl are convenient" ``` -Changing your default profile ------------------------------ +## Changing your default profile -The first time you authorize a client application to make requests on behalf of your account, twurl stores your access token information in its .twurlrc file. Subsequent requests will use this profile as the default profile. You can use the 'accounts' subcommand to see what client applications have been authorized for what user names: +The first time you authorize a client application to make requests on behalf of your account, twurl stores your access token information in its `~/.twurlrc` file. Subsequent requests will use this profile as the default profile. You can use the `accounts` subcommand to see what client applications have been authorized for what user names: ```sh twurl accounts @@ -125,7 +140,7 @@ twurl accounts guT9RsJbNQgVe6AwoY9BA ``` -Notice that one of those consumer keys is marked as the default. To change the default use the 'set' subcommand, passing then either just the username, if it's unambiguous, or the username and consumer key pair if it isn't unambiguous: +Notice that one of those consumer keys is marked as the default. To change the default use the `set` subcommand, passing then either just the username, if it's unambiguous, or the username and consumer key pair if it isn't unambiguous: ```sh twurl set default testiverse @@ -147,8 +162,11 @@ twurl accounts guT9RsJbNQgVe6AwoY9BA ``` -Contributors ------------- +### Profiles and Bearer Tokens + +While changing the default profile allows you to select which access token (OAuth1.0a) to use, bearer tokens don't link to any user profiles as the Application-only authentication doesn't require user context. That is, you can make an application-only request regardless of your default profile if you specify the `-c` (--consumer-key) option once you generate a bearer token with this consumer key. By default, twurl reads the current profile's consumer key and its associated bearer token from `~/.twurlrc` file. + +## Contributors Marcel Molina / @noradio diff --git a/lib/twurl/app_only_oauth_client.rb b/lib/twurl/app_only_oauth_client.rb new file mode 100644 index 0000000..a84b5c0 --- /dev/null +++ b/lib/twurl/app_only_oauth_client.rb @@ -0,0 +1,78 @@ +require 'base64' +require_relative 'oauth_client' + +module Twurl + class AppOnlyOAuthClient < Twurl::OAuthClient + + AUTHORIZATION_FAILED_MESSAGE = "Authorization failed. Check that your consumer key and secret are correct." + + attr_reader :consumer_key, :consumer_secret, :bearer_token + + def initialize(options = {}) + @consumer_key = options['consumer_key'] + @consumer_secret = options['consumer_secret'] + @bearer_token = options['bearer_token'] + end + + def save + self.class.rcfile.bearer_token(consumer_key, bearer_token) + end + + def exchange_credentials_for_access_token + response = fetch_oauth2_token + if response.nil? || response[:access_token].nil? + raise Exception, AUTHORIZATION_FAILED_MESSAGE + end + @bearer_token = response[:access_token] + end + + def perform_request_from_options(options, &block) + request = build_request_from_options(options) + request['user-agent'] = user_agent + request['authorization'] = "Bearer #{bearer_token}" + + http_client.request(request, &block) + end + + def needs_to_authorize? + bearer_token.nil? + end + + def request_data + {'grant_type' => 'client_credentials'} + end + + def http_client + uri = URI.parse(Twurl.options.base_url) + http = if Twurl.options.proxy + proxy_uri = URI.parse(Twurl.options.proxy) + Net::HTTP.new(uri.host, uri.port, proxy_uri.host, proxy_uri.port) + else + Net::HTTP.new(uri.host, uri.port) + end + set_http_client_options(http) + end + + def set_http_client_options(http) + http.set_debug_output(Twurl.options.debug_output_io) if Twurl.options.trace + http.read_timeout = http.open_timeout = Twurl.options.timeout || 60 + http.open_timeout = Twurl.options.connection_timeout if Twurl.options.connection_timeout + # Only override if Net::HTTP support max_retries (since Ruby >= 2.5) + http.max_retries = 0 if http.respond_to?(:max_retries=) + if Twurl.options.ssl? + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + http + end + + def fetch_oauth2_token + request = Net::HTTP::Post.new('/oauth2/token') + request.body = URI.encode_www_form(request_data) + request['user-agent'] = user_agent + request['authorization'] = "Basic #{Base64.strict_encode64("#{consumer_key}:#{consumer_secret}")}" + response = http_client.request(request).body + JSON.parse(response,:symbolize_names => true) + end + end +end diff --git a/lib/twurl/app_only_token_information_controller.rb b/lib/twurl/app_only_token_information_controller.rb new file mode 100644 index 0000000..333845e --- /dev/null +++ b/lib/twurl/app_only_token_information_controller.rb @@ -0,0 +1,18 @@ +module Twurl + class AppOnlyTokenInformationController < AbstractCommandController + NO_ISSUED_TOKENS_MESSAGE = "No issued application-only (Bearer) tokens" + + def dispatch + rcfile = OAuthClient.rcfile + if rcfile.empty? || rcfile.bearer_tokens.nil? + CLI.puts NO_ISSUED_TOKENS_MESSAGE + else + tokens = rcfile.bearer_tokens + CLI.puts "[consumer_key: bearer_token]" + tokens.each_key do |consumer_key| + CLI.puts "#{consumer_key}: (omitted)" + end + end + end + end +end diff --git a/lib/twurl/cli.rb b/lib/twurl/cli.rb index c88ce7d..304a883 100644 --- a/lib/twurl/cli.rb +++ b/lib/twurl/cli.rb @@ -1,6 +1,6 @@ module Twurl class CLI - SUPPORTED_COMMANDS = %w(authorize accounts alias set) + SUPPORTED_COMMANDS = %w(authorize accounts bearer_tokens alias set) DEFAULT_COMMAND = 'request' PATH_PATTERN = /^\/\w+/ PROTOCOL_PATTERN = /^\w+:\/\// @@ -28,6 +28,8 @@ def dispatch(options) AuthorizationController when 'accounts' AccountInformationController + when 'bearer_tokens' + AppOnlyTokenInformationController when 'alias' AliasesController when 'set' @@ -90,6 +92,7 @@ def parse_options(args) json_format timeout connection_timeout + app_only end end @@ -345,6 +348,12 @@ def connection_timeout options.connection_timeout = connection_timeout end end + + def app_only + on('--bearer', "Use application-only authentication (Bearer Token)") do |app_only| + options.app_only = true + end + end end end diff --git a/lib/twurl/oauth_client.rb b/lib/twurl/oauth_client.rb index 0359604..02ebc59 100644 --- a/lib/twurl/oauth_client.rb +++ b/lib/twurl/oauth_client.rb @@ -13,12 +13,16 @@ def load_from_options(options) load_client_for_username_and_consumer_key(options.username, options.consumer_key) elsif options.username load_client_for_username(options.username) + elsif options.command == 'authorize' && options.app_only + load_client_for_app_only_auth(options, options.consumer_key) elsif options.command == 'authorize' load_new_client_from_options(options) elsif options.command == 'request' && has_oauth_options?(options) load_new_client_from_oauth_options(options) + elsif options.command == 'request' && options.app_only && options.consumer_key + load_client_for_non_profile_app_only_auth(options) else - load_default_client + load_default_client(options) end end @@ -59,9 +63,44 @@ def load_new_client_from_oauth_options(options) ) end - def load_default_client - raise Exception, "You must authorize first" unless rcfile.default_profile - load_client_for_username_and_consumer_key(*rcfile.default_profile) + def load_client_for_app_only_auth(options, consumer_key) + if options.command == 'authorize' + AppOnlyOAuthClient.new(options) + else + AppOnlyOAuthClient.new( + options.oauth_client_options.merge( + 'bearer_token' => rcfile.bearer_tokens.to_hash[consumer_key] + ) + ) + end + end + + def load_client_for_non_profile_app_only_auth(options) + AppOnlyOAuthClient.new( + options.oauth_client_options.merge( + 'bearer_token' => rcfile.bearer_tokens.to_hash[options.consumer_key] + ) + ) + end + + def load_default_client(options) + return if options.command == 'bearer_tokens' + + exception_message = "You must authorize first." + app_only_exception_message = "To use --bearer option, you need to authorize (OAuth1.0a) and create at least one user profile (~/.twurlrc):\n\n" \ + "twurl authorize -c key -s secret\n" \ + "\nor, you can specify issued token's consumer_key directly:\n" \ + "(to see your issued tokens: 'twurl bearer_tokens')\n\n" \ + "twurl --bearer -c key '/path/to/api'" + + raise Exception, "#{options.app_only ? app_only_exception_message : exception_message}" unless rcfile.default_profile + if options.app_only + raise Exception, "No available bearer token found for consumer_key:#{rcfile.default_profile_consumer_key}" \ + unless rcfile.has_bearer_token_for_consumer_key?(rcfile.default_profile_consumer_key) + load_client_for_app_only_auth(options, rcfile.default_profile_consumer_key) + else + load_client_for_username_and_consumer_key(*rcfile.default_profile) + end end end @@ -88,7 +127,7 @@ def initialize(options = {}) :copy => Net::HTTP::Copy } - def perform_request_from_options(options, &block) + def build_request_from_options(options, &block) request_class = METHODS.fetch(options.request_method.to_sym) request = request_class.new(options.path, options.headers) @@ -136,7 +175,11 @@ def perform_request_from_options(options, &block) end.join("&") end end + request + end + def perform_request_from_options(options, &block) + request = build_request_from_options(options) request.oauth!(consumer.http, consumer, access_token) request['user-agent'] = user_agent consumer.http.request(request, &block) diff --git a/lib/twurl/rcfile.rb b/lib/twurl/rcfile.rb index d2b50d9..25baf98 100644 --- a/lib/twurl/rcfile.rb +++ b/lib/twurl/rcfile.rb @@ -52,6 +52,11 @@ def default_profile configuration['default_profile'] end + def default_profile_consumer_key + username, consumer_key = configuration['default_profile'] + consumer_key + end + def default_profile=(profile) configuration['default_profile'] = [profile.username, profile.consumer_key] end @@ -70,6 +75,16 @@ def aliases data['aliases'] end + def bearer_token(consumer_key, bearer_token) + data['bearer_tokens'] ||= {} + data['bearer_tokens'][consumer_key] = bearer_token + save + end + + def bearer_tokens + data['bearer_tokens'] + end + def alias_from_options(options) options.subcommands.each do |potential_alias| if path = alias_from_name(potential_alias) @@ -87,6 +102,10 @@ def has_oauth_profile_for_username_with_consumer_key?(username, consumer_key) !user_profiles.nil? && !user_profiles[consumer_key].nil? end + def has_bearer_token_for_consumer_key?(consumer_key) + bearer_tokens.nil? ? false : bearer_tokens.to_hash.has_key?(consumer_key) + end + def <<(oauth_client) client_from_file = self[oauth_client.username] || {} client_from_file[oauth_client.consumer_key] = oauth_client.to_hash diff --git a/lib/twurl/request_controller.rb b/lib/twurl/request_controller.rb index 2da33b6..cc81f13 100644 --- a/lib/twurl/request_controller.rb +++ b/lib/twurl/request_controller.rb @@ -1,6 +1,7 @@ module Twurl class RequestController < AbstractCommandController - NO_URI_MESSAGE = "No URI specified" + NO_URI_MESSAGE = 'No URI specified' + INVALID_URI_MESSAGE = 'Invalid URI detected' READ_TIMEOUT_MESSAGE = 'A timeout occurred (Net::ReadTimeout). ' \ 'Please try again or increase the value using --timeout option.' OPEN_TIMEOUT_MESSAGE = 'A timeout occurred (Net::OpenTimeout). ' \ @@ -10,6 +11,7 @@ def dispatch raise Exception, "You need to authorize first." end options.path ||= OAuthClient.rcfile.alias_from_options(options) + raise Exception, NO_URI_MESSAGE if options.path.empty? perform_request end @@ -20,7 +22,7 @@ def perform_request } } rescue URI::InvalidURIError - CLI.puts NO_URI_MESSAGE + CLI.puts INVALID_URI_MESSAGE rescue Net::ReadTimeout CLI.puts READ_TIMEOUT_MESSAGE rescue Net::OpenTimeout diff --git a/test/app_only_oauth_client_test.rb b/test/app_only_oauth_client_test.rb new file mode 100644 index 0000000..90edc37 --- /dev/null +++ b/test/app_only_oauth_client_test.rb @@ -0,0 +1,96 @@ +require File.dirname(__FILE__) + '/test_helper' + +class Twurl::AppOnlyOAuthClient::AbstractClientTest < Minitest::Test + attr_reader :client, :options + def setup + Twurl::OAuthClient.instance_variable_set(:@rcfile, nil) + + @options = Twurl::Options.test_app_only_exemplar + @client = Twurl::AppOnlyOAuthClient.test_app_only_exemplar + options.base_url = 'api.twitter.com' + options.request_method = 'get' + options.path = '/path/does/not/matter.json' + options.data = {} + options.headers = {} + + Twurl.options = options + end + + def teardown + super + Twurl.options = Twurl::Options.new + # Make sure we don't do any disk IO in these tests + assert !File.exist?(Twurl::RCFile.file_path) + end + + def test_nothing + # Appeasing test/unit + end +end + +class Twurl::AppOnlyOAuthClient::BasicMethods < Twurl::AppOnlyOAuthClient::AbstractClientTest + def test_needs_to_authorize? + client = Twurl::AppOnlyOAuthClient.new( + options.oauth_client_options.merge( + 'bearer_token' => nil + ) + ) + + fake_response = {:access_token => "test_bearer_token"} + mock(client).fetch_oauth2_token { fake_response } + + assert client.needs_to_authorize?, 'token should be nil' + client.exchange_credentials_for_access_token + assert !client.needs_to_authorize?, 'token should be exist' + end +end + +class Twurl::AppOnlyOAuthClient::ClientLoadingTest < Twurl::AppOnlyOAuthClient::AbstractClientTest + def test_attempting_to_load_a_bearer_token_from_non_authed_consumer_key_fails + mock(Twurl::OAuthClient.rcfile).save.times(any_times) + Twurl::OAuthClient.rcfile.bearer_token(options.consumer_key, options.bearer_token) + + assert Twurl::OAuthClient.rcfile.bearer_tokens.to_hash[options.consumer_key] + assert_nil Twurl::OAuthClient.rcfile.bearer_tokens.to_hash[:invalid_consumer_key] + + options.consumer_key = 'invalid_consumer_key' + assert_raises Twurl::Exception do + Twurl::OAuthClient.load_default_client(options) + end + end +end + +class Twurl::AppOnlyOAuthClient::PerformingRequestsFromAppOnlyClient < Twurl::AppOnlyOAuthClient::AbstractClientTest + def test_request_is_made_using_request_method_and_path_and_data_in_options + http = client.send(:http_client) + mock(client).http_client { http } + mock(http).request( + satisfy { |req| req.is_a?(Net::HTTP::Get) && (req.path == options.path) } + ) + client.perform_request_from_options(options) + end + + def test_user_agent_request_header_is_set + expected_ua_string = "twurl version: #{Twurl::Version} platform: #{RUBY_ENGINE} #{RUBY_VERSION} (#{RUBY_PLATFORM})" + + http = client.send(:http_client) + mock(client).http_client { http } + mock(http).request( + satisfy { |req| + req.is_a?(Net::HTTP::Get) && + req['user-agent'] == expected_ua_string + } + ) + client.perform_request_from_options(options) + end + + def test_request_options_are_setable + http = client.send(:http_client) + assert_equal 60, http.read_timeout + + options.timeout = 10 + http = client.send(:http_client) + + assert_equal 10, http.read_timeout + end +end diff --git a/test/authorization_controller_test.rb b/test/authorization_controller_test.rb index 9d2cba7..7e111f1 100644 --- a/test/authorization_controller_test.rb +++ b/test/authorization_controller_test.rb @@ -17,6 +17,21 @@ def test_successful_authentication_saves_retrieved_access_token controller.dispatch end + module AppOnlyAuth + def test_successful_app_only_authentication_saves_retrieved_access_token + app_only_client = Twurl::AppOnlyOAuthClient.test_app_only_exemplar + app_only_controller = Twurl::AuthorizationController.new(app_only_client, options) + + mock(app_only_client).exchange_credentials_for_access_token.times(1) + mock(app_only_client).save.times(1) + mock(app_only_controller).raise(Twurl::Exception, Twurl::AuthorizationController::AUTHORIZATION_FAILED_MESSAGE).never + mock(Twurl::CLI).puts(Twurl::AuthorizationController::AUTHORIZATION_SUCCEEDED_MESSAGE).times(1) + + app_only_controller.dispatch + end + end + include AppOnlyAuth + module ErrorCases def test_failed_authorization_does_not_save_client mock(client).exchange_credentials_for_access_token { raise OAuth::Unauthorized } diff --git a/test/cli_test.rb b/test/cli_test.rb index ea6bbca..7d6fcb3 100644 --- a/test/cli_test.rb +++ b/test/cli_test.rb @@ -254,4 +254,17 @@ def test_setting_timeout_updates_to_requested_value end end include TimeoutOptionTests + + module AppOnlyOptionTests + def test_not_specifying_app_only_sets_it_to_nil + options = Twurl::CLI.parse_options([TEST_PATH]) + assert_nil options.app_only + end + + def test_specifying_app_only_updates_to_requested_value + options = Twurl::CLI.parse_options([TEST_PATH, '--bearer']) + assert options.app_only + end + end + include AppOnlyOptionTests end diff --git a/test/oauth_client_test.rb b/test/oauth_client_test.rb index c45ec67..4c739e6 100644 --- a/test/oauth_client_test.rb +++ b/test/oauth_client_test.rb @@ -58,7 +58,7 @@ def test_if_username_is_supplied_and_profile_exists_for_username_then_client_is_ mock(Twurl::OAuthClient).load_client_for_username_and_consumer_key(options.username, options.consumer_key).times(1) mock(Twurl::OAuthClient).load_new_client_from_options(options).never - mock(Twurl::OAuthClient).load_default_client.never + mock(Twurl::OAuthClient).load_default_client(options).never Twurl::OAuthClient.load_from_options(options) end @@ -68,7 +68,7 @@ def test_if_username_is_not_provided_then_the_default_client_is_loaded mock(Twurl::OAuthClient).load_client_for_username(options.username).never mock(Twurl::OAuthClient).load_new_client_from_options(options).never - mock(Twurl::OAuthClient).load_default_client.times(1) + mock(Twurl::OAuthClient).load_default_client(options).times(1) Twurl::OAuthClient.load_from_options(options) end @@ -80,7 +80,40 @@ def test_if_all_oauth_options_are_supplied_then_client_is_loaded_from_options options.token_secret = 'test_token_secret' mock(Twurl::OAuthClient).load_new_client_from_oauth_options(options).times(1) + mock(Twurl::OAuthClient).load_default_client(options).never + + Twurl::OAuthClient.load_from_options(options) + end + + def test_if_authorize_app_only_client_is_loaded + options = Twurl::Options.test_app_only_exemplar + options.command = 'authorize' + mock(Twurl::OAuthClient).load_default_client.never + mock(Twurl::OAuthClient).load_client_for_app_only_auth(options, options.consumer_key).times(1) + + Twurl::OAuthClient.load_from_options(options) + end + + def test_if_app_only_options_is_supplied_on_request_then_app_only_client_is_loaded + options = Twurl::Options.test_app_only_exemplar + options.command = 'request' + + mock(Twurl::OAuthClient.rcfile).save.times(any_times) + Twurl::OAuthClient.rcfile << client + Twurl::OAuthClient.rcfile.bearer_token(options.consumer_key, options.bearer_token) + mock.proxy(Twurl::OAuthClient).load_default_client(options).times(1) + mock(Twurl::OAuthClient).load_client_for_app_only_auth(options, options.consumer_key).times(1) + + options.consumer_key = nil + Twurl::OAuthClient.load_from_options(options) + end + + def test_if_app_only_and_consumer_key_options_are_supplied_on_request_then_app_only_client_is_loaded + options = Twurl::Options.test_app_only_exemplar + options.command = 'request' + + mock(Twurl::OAuthClient).load_client_for_non_profile_app_only_auth(options).times(1) Twurl::OAuthClient.load_from_options(options) end @@ -110,7 +143,7 @@ def test_loading_default_client_when_there_is_none_fails assert_nil Twurl::OAuthClient.rcfile.default_profile assert_raises Twurl::Exception do - Twurl::OAuthClient.load_default_client + Twurl::OAuthClient.load_default_client(options) end end @@ -120,7 +153,7 @@ def test_loading_default_client_from_file Twurl::OAuthClient.rcfile << client assert_equal [client.username, client.consumer_key], Twurl::OAuthClient.rcfile.default_profile - client_from_file = Twurl::OAuthClient.load_default_client + client_from_file = Twurl::OAuthClient.load_default_client(options) assert_equal client.to_hash, client_from_file.to_hash end diff --git a/test/rcfile_test.rb b/test/rcfile_test.rb index cb4682b..22ff3b3 100644 --- a/test/rcfile_test.rb +++ b/test/rcfile_test.rb @@ -108,6 +108,27 @@ def test_adding_additional_clients_does_not_change_default_profile end end +class Twurl::RCFile::BearerTokenTest < Minitest::Test + attr_reader :rcfile + def setup + @rcfile = Twurl::RCFile.new + mock(rcfile).save.times(any_times) + end + + def test_save_bearer_token + options = Twurl::Options.test_app_only_exemplar + expected_response = { + options.consumer_key => options.bearer_token + } + rcfile << Twurl::OAuthClient.test_exemplar + rcfile.bearer_token(options.consumer_key, options.bearer_token) + + client = Twurl::AppOnlyOAuthClient.test_app_only_exemplar + + assert_equal expected_response, rcfile.bearer_tokens + end +end + class Twurl::RCFile::SavingTest < Minitest::Test attr_reader :rcfile def setup diff --git a/test/request_controller_test.rb b/test/request_controller_test.rb index 39027ce..ccb69f7 100644 --- a/test/request_controller_test.rb +++ b/test/request_controller_test.rb @@ -24,6 +24,7 @@ def test_request_will_be_made_if_client_is_authorized mock(client).needs_to_authorize? { false }.times(1) mock(controller).perform_request.times(1) + options.path = '/test/path' controller.dispatch end @@ -68,7 +69,7 @@ def test_request_response_is_json_formatted end def test_invalid_or_unspecified_urls_report_error - mock(Twurl::CLI).puts(Twurl::RequestController::NO_URI_MESSAGE).times(1) + mock(Twurl::CLI).puts(Twurl::RequestController::INVALID_URI_MESSAGE).times(1) mock(client).perform_request_from_options(options).times(1) { raise URI::InvalidURIError } controller.perform_request diff --git a/test/test_helper.rb b/test/test_helper.rb index 1ba7179..47c422c 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -23,6 +23,16 @@ def test_exemplar options.subcommands = [] options end + + def test_app_only_exemplar + options = new + options.app_only = true + options.consumer_key = '123456789' + options.consumer_secret = '987654321' + options.bearer_token = 'test_bearer_token' + options.subcommands = [] + options + end end end @@ -39,4 +49,17 @@ def test_exemplar(overrides = {}) end end end + + class AppOnlyOAuthClient + class << self + def test_app_only_exemplar + options = Twurl::Options.test_app_only_exemplar + Twurl::AppOnlyOAuthClient.new( + options.oauth_client_options.merge( + 'bearer_token' => options.bearer_token + ) + ) + end + end + end end From f276355fc66220018b007e07f5623547c20e84fe Mon Sep 17 00:00:00 2001 From: Shohei Maeda Date: Thu, 26 Dec 2019 22:10:15 +0900 Subject: [PATCH 15/34] bump version -> v0.9.5 (#135) --- lib/twurl/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/twurl/version.rb b/lib/twurl/version.rb index a866516..c9fe471 100644 --- a/lib/twurl/version.rb +++ b/lib/twurl/version.rb @@ -2,7 +2,7 @@ module Twurl class Version MAJOR = 0 unless defined? Twurl::Version::MAJOR MINOR = 9 unless defined? Twurl::Version::MINOR - PATCH = 4 unless defined? Twurl::Version::PATCH + PATCH = 5 unless defined? Twurl::Version::PATCH PRE = nil unless defined? Twurl::Version::PRE # Time.now.to_i.to_s class << self From b9818b8ff36551f95634011089d00b3f399fcebc Mon Sep 17 00:00:00 2001 From: Shohei Maeda Date: Thu, 26 Dec 2019 22:19:58 +0900 Subject: [PATCH 16/34] Fix .gemspec (#136) --- twurl.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twurl.gemspec b/twurl.gemspec index 237c2c4..2a44b4b 100644 --- a/twurl.gemspec +++ b/twurl.gemspec @@ -8,7 +8,7 @@ Gem::Specification.new do |spec| spec.authors = ["Marcel Molina", "Erik Michaels-Ober", "@TwitterDev team"] spec.description = %q{Curl for the Twitter API} spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } - spec.extra_rdoc_files = %w(CODE_OF_CONDUCT.md CONTRIBUTING.md INSTALL LICENSE README.md) + spec.extra_rdoc_files = %w(CODE_OF_CONDUCT.md CONTRIBUTING.md INSTALL.md LICENSE README.md) spec.files = `git ls-files -z`.split("\x0").reject { |f| f.start_with?('test/') } spec.homepage = 'http://github.com/twitter/twurl' spec.licenses = ['MIT'] From 4e8b5f905fe840652f5959d01417d4fd208be3e9 Mon Sep 17 00:00:00 2001 From: Shohei Maeda Date: Sat, 28 Dec 2019 06:39:02 +0900 Subject: [PATCH 17/34] Add Ruby 2.7.0 to .travis.yml (#137) * Add Ruby 2.7.0 to .travis.yml * Update contribute guide --- .travis.yml | 1 + CONTRIBUTING.md | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 977e3cf..0ae9be9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ matrix: - rvm: ruby-head - rvm: rbx-2 include: + - rvm: 2.7.0 - rvm: 2.6.5 - rvm: 2.5.7 - rvm: 2.4.9 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 119acb1..c38fa06 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,21 @@ We follow the [GitHub Flow Workflow](https://guides.github.com/introduction/flow 1. Fork the project 2. Check out the `master` branch 3. Create a feature branch -4. Write code and tests for your change +4. Write code and tests for your change + +```sh +$ cd ./twurl + +# install twurl from source +$ bundle install + +# run twurl from source +$ bundle exec twurl -v + +# run tests +$ bundle exec rake +``` + 5. From your branch, make a pull request against `twitter/twurl/master` 6. Work with repo maintainers to get your change reviewed 7. Wait for your change to be pulled into `twitter/twurl/master` From 23a837468d9ea92c04af13e332b554997d9db7a2 Mon Sep 17 00:00:00 2001 From: Shohei Maeda Date: Tue, 31 Dec 2019 03:54:58 +0900 Subject: [PATCH 18/34] Add VSCode remote container setting files (#138) * rubyforge_project option is deprecated * Adding Dockerfile for VSCode remote development * simplify .gemspec --- .devcontainer/Dockerfile | 6 ++++++ .devcontainer/devcontainer.json | 13 +++++++++++++ twurl.gemspec | 10 +++++----- 3 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..06f8b90 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,6 @@ +FROM ruby:latest + +WORKDIR /usr/src/app + +COPY . . +RUN bundle install diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..fa7186f --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,13 @@ +{ + "name": "twurl dev container", + "dockerFile": "Dockerfile", + "context": "..", + "workspaceFolder": "/usr/src/app", + "settings": { + "terminal.integrated.shell.linux": "/bin/bash" + }, + "shutdownAction": "none", + "extensions": [ + "rebornix.Ruby" + ] +} diff --git a/twurl.gemspec b/twurl.gemspec index 2a44b4b..739a2d0 100644 --- a/twurl.gemspec +++ b/twurl.gemspec @@ -7,16 +7,16 @@ Gem::Specification.new do |spec| spec.add_dependency 'oauth', '~> 0.4' spec.authors = ["Marcel Molina", "Erik Michaels-Ober", "@TwitterDev team"] spec.description = %q{Curl for the Twitter API} - spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } - spec.extra_rdoc_files = %w(CODE_OF_CONDUCT.md CONTRIBUTING.md INSTALL.md LICENSE README.md) - spec.files = `git ls-files -z`.split("\x0").reject { |f| f.start_with?('test/') } - spec.homepage = 'http://github.com/twitter/twurl' + spec.bindir = 'bin' + spec.executables << 'twurl' + spec.extra_rdoc_files = Dir["*.md", "LICENSE"] + spec.files = Dir["*.md", "LICENSE", "twurl.gemspec", "bin/*", "lib/**/*"] + spec.homepage = 'http://github.com/twitter/twurl' spec.licenses = ['MIT'] spec.name = 'twurl' spec.rdoc_options = ['--title', 'twurl -- OAuth-enabled curl for the Twitter API', '--main', 'README.md', '--line-numbers', '--inline-source'] spec.require_paths = ['lib'] spec.required_ruby_version = '>= 2.4.0' - spec.rubyforge_project = 'twurl' spec.summary = spec.description spec.version = Twurl::Version end From 0dd7c29273a4bd8327568f1ac6c437dded63fe58 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Mon, 30 Dec 2019 19:39:29 +0000 Subject: [PATCH 19/34] Doc fix (#140) * fix newline in README * fix version in install, add build badge --- INSTALL.md | 2 +- README.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/INSTALL.md b/INSTALL.md index 89404c8..736ac8b 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -10,7 +10,7 @@ $ gem install twurl ```sh # verify installation $ twurl -v -0.9.4 +0.9.5 ``` ## Install from source diff --git a/README.md b/README.md index f49a448..095e47d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/twitter/twurl/blob/master/LICENSE) [![Gem Version](https://badge.fury.io/rb/twurl.svg)](https://badge.fury.io/rb/twurl) + [![Build Status](https://travis-ci.com/twitter/twurl.svg?branch=master)](https://travis-ci.com/twitter/twurl) Twurl is like curl, but tailored specifically for the Twitter API. It knows how to grant an access token to a client application for From 837882da2240f332ac79fb97d670cecaddae2396 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Mon, 30 Dec 2019 19:40:23 +0000 Subject: [PATCH 20/34] Update INSTALL.md fix version --- INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/INSTALL.md b/INSTALL.md index 736ac8b..221e152 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -32,5 +32,5 @@ If you don't want to install Twurl globally on your system, use `--path` [option ``` $ bundle install --path path/to/directory $ bundle exec twurl -v -0.9.4 +0.9.5 ``` From 3c78a05238d2845bbb25b1f478cfa4ee46da4c6b Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Tue, 30 Jun 2020 14:13:42 +0100 Subject: [PATCH 21/34] Set theme jekyll-theme-modernist --- _config.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 _config.yml diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..cc35c1d --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-modernist \ No newline at end of file From 138c978ba1ce83f5a0e6fb6751cdc3483d3f07a1 Mon Sep 17 00:00:00 2001 From: Shohei Maeda Date: Thu, 27 Aug 2020 22:59:14 +0900 Subject: [PATCH 22/34] 2020-03 patch (#145) * handle invalid option exceptions * deprecate xauth * fix #142 * supress undefined method error #149 --- lib/twurl/cli.rb | 17 +++++++++-------- lib/twurl/oauth_client.rb | 21 ++++++++------------- lib/twurl/rcfile.rb | 2 +- test/oauth_client_test.rb | 10 +++------- test/test_helper.rb | 1 - 5 files changed, 21 insertions(+), 30 deletions(-) diff --git a/lib/twurl/cli.rb b/lib/twurl/cli.rb index 304a883..60b01db 100644 --- a/lib/twurl/cli.rb +++ b/lib/twurl/cli.rb @@ -67,7 +67,6 @@ def parse_options(args) o.section "Authorization options:" do username - password consumer_key consumer_secret access_token @@ -96,7 +95,15 @@ def parse_options(args) end end - arguments = option_parser.parse!(args) + begin + arguments = option_parser.parse!(args) + rescue OptionParser::InvalidOption + CLI.puts "ERROR: undefined option" + exit + rescue + CLI.puts "ERROR: invalid argument" + exit + end Twurl.options.command = extract_command!(arguments) Twurl.options.path = extract_path!(arguments) Twurl.options.subcommands = arguments @@ -219,12 +226,6 @@ def username end end - def password - on('-p', '--password [password]', 'Password of account to authorize (required)') do |password| - options.password = password ? password : CLI.prompt_for('Password') - end - end - def trace on('-t', '--[no-]trace', 'Trace request/response traffic (default: --no-trace)') do |trace| options.trace = trace diff --git a/lib/twurl/oauth_client.rb b/lib/twurl/oauth_client.rb index 02ebc59..482f90c 100644 --- a/lib/twurl/oauth_client.rb +++ b/lib/twurl/oauth_client.rb @@ -9,7 +9,11 @@ def rcfile(reload = false) end def load_from_options(options) - if rcfile.has_oauth_profile_for_username_with_consumer_key?(options.username, options.consumer_key) + if options.command == 'request' && has_oauth_options?(options) + load_new_client_from_oauth_options(options) + elsif options.command == 'request' && options.app_only && options.consumer_key + load_client_for_non_profile_app_only_auth(options) + elsif rcfile.has_oauth_profile_for_username_with_consumer_key?(options.username, options.consumer_key) load_client_for_username_and_consumer_key(options.username, options.consumer_key) elsif options.username load_client_for_username(options.username) @@ -17,10 +21,6 @@ def load_from_options(options) load_client_for_app_only_auth(options, options.consumer_key) elsif options.command == 'authorize' load_new_client_from_options(options) - elsif options.command == 'request' && has_oauth_options?(options) - load_new_client_from_oauth_options(options) - elsif options.command == 'request' && options.app_only && options.consumer_key - load_client_for_non_profile_app_only_auth(options) else load_default_client(options) end @@ -52,7 +52,7 @@ def load_client_for_username(username) end def load_new_client_from_options(options) - new(options.oauth_client_options.merge('password' => options.password)) + new(options.oauth_client_options) end def load_new_client_from_oauth_options(options) @@ -106,10 +106,9 @@ def load_default_client(options) OAUTH_CLIENT_OPTIONS = %w[username consumer_key consumer_secret token secret] attr_reader *OAUTH_CLIENT_OPTIONS - attr_reader :username, :password + attr_reader :username def initialize(options = {}) @username = options['username'] - @password = options['password'] @consumer_key = options['consumer_key'] @consumer_secret = options['consumer_secret'] @token = options['token'] @@ -192,7 +191,7 @@ def user_agent def exchange_credentials_for_access_token response = begin - consumer.token_request(:post, consumer.access_token_path, nil, {}, client_auth_parameters) + consumer.token_request(:post, consumer.access_token_path, nil, {}) rescue OAuth::Unauthorized perform_pin_authorize_workflow end @@ -200,10 +199,6 @@ def exchange_credentials_for_access_token @secret = response[:oauth_token_secret] end - def client_auth_parameters - {'x_auth_username' => username, 'x_auth_password' => password, 'x_auth_mode' => 'client_auth'} - end - def perform_pin_authorize_workflow @request_token = consumer.get_request_token CLI.puts("Go to #{generate_authorize_url} and paste in the supplied PIN") diff --git a/lib/twurl/rcfile.rb b/lib/twurl/rcfile.rb index 25baf98..d6173dd 100644 --- a/lib/twurl/rcfile.rb +++ b/lib/twurl/rcfile.rb @@ -72,7 +72,7 @@ def alias(name, path) end def aliases - data['aliases'] + data['aliases'] ||= {} end def bearer_token(consumer_key, bearer_token) diff --git a/test/oauth_client_test.rb b/test/oauth_client_test.rb index 4c739e6..6d86c05 100644 --- a/test/oauth_client_test.rb +++ b/test/oauth_client_test.rb @@ -166,10 +166,6 @@ def setup @new_client = Twurl::OAuthClient.load_new_client_from_options(options) end - def test_password_is_included - assert_equal options.password, new_client.password - end - def test_oauth_options_are_passed_through assert_equal client.to_hash, new_client.to_hash end @@ -248,15 +244,15 @@ def test_successful_exchange_parses_token_and_secret_from_response_body parsed_response = {:oauth_token => "123456789", :oauth_token_secret => "abcdefghi", :user_id => "3191321", - :screen_name => "noradio", - :x_auth_expires => "0"} + :screen_name => "noradio" + } mock(client.consumer). token_request(:post, client.consumer.access_token_path, nil, {}, - client.client_auth_parameters) { parsed_response } + ) { parsed_response } assert client.needs_to_authorize? client.exchange_credentials_for_access_token diff --git a/test/test_helper.rb b/test/test_helper.rb index 47c422c..782b26e 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -17,7 +17,6 @@ class << self def test_exemplar options = new options.username = 'exemplar_user_name' - options.password = 'secret' options.consumer_key = '123456789' options.consumer_secret = '987654321' options.subcommands = [] From f986f114b023741ecce1f0cf5fe4be4b5e1f3149 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Thu, 27 Aug 2020 15:04:58 +0100 Subject: [PATCH 23/34] prepare 0.9.6 release (#152) --- INSTALL.md | 4 ++-- README.md | 4 ++-- lib/twurl/version.rb | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 221e152..18a16ad 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -10,7 +10,7 @@ $ gem install twurl ```sh # verify installation $ twurl -v -0.9.5 +0.9.6 ``` ## Install from source @@ -32,5 +32,5 @@ If you don't want to install Twurl globally on your system, use `--path` [option ``` $ bundle install --path path/to/directory $ bundle exec twurl -v -0.9.5 +0.9.6 ``` diff --git a/README.md b/README.md index 095e47d..c1e6359 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ This will print a pair of consumer_key and its associated bearer token. Note, to You can access different hosts for other Twitter APIs using the -H flag. ```sh -twurl -H "ads-api.twitter.com" "/5/accounts" +twurl -H "ads-api.twitter.com" "/7/accounts" ``` ## Uploading Media @@ -165,7 +165,7 @@ twurl accounts ### Profiles and Bearer Tokens -While changing the default profile allows you to select which access token (OAuth1.0a) to use, bearer tokens don't link to any user profiles as the Application-only authentication doesn't require user context. That is, you can make an application-only request regardless of your default profile if you specify the `-c` (--consumer-key) option once you generate a bearer token with this consumer key. By default, twurl reads the current profile's consumer key and its associated bearer token from `~/.twurlrc` file. +While changing the default profile allows you to select which access token (OAuth1.0a) to use, bearer tokens don't link to any user profiles as the Application-only authentication doesn't require user context. That is, you can make an application-only request regardless of your default profile if you specify the `-c` (`--consumer-key`) option once you generate a bearer token with this consumer key. By default, twurl reads the current profile's consumer key and its associated bearer token from `~/.twurlrc` file. ## Contributors diff --git a/lib/twurl/version.rb b/lib/twurl/version.rb index c9fe471..f734d47 100644 --- a/lib/twurl/version.rb +++ b/lib/twurl/version.rb @@ -2,7 +2,7 @@ module Twurl class Version MAJOR = 0 unless defined? Twurl::Version::MAJOR MINOR = 9 unless defined? Twurl::Version::MINOR - PATCH = 5 unless defined? Twurl::Version::PATCH + PATCH = 6 unless defined? Twurl::Version::PATCH PRE = nil unless defined? Twurl::Version::PRE # Time.now.to_i.to_s class << self From 73593e5b5f5ae817af2a7c8ba1db9698bf12f8a3 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Fri, 11 Sep 2020 13:55:27 +0100 Subject: [PATCH 24/34] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. From f7c6a149dbaadb455a71b49a2efdc219bc89b465 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Fri, 11 Sep 2020 13:56:35 +0100 Subject: [PATCH 25/34] Create config.yml --- .github/ISSUE_TEMPLATE/config.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..57f7674 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Question about the Twitter API? Ask in the Twitter Developer Community + url: https://twittercommunity.com/ + about: For general API functionality questions, please ask in the developer forums. From 671ef6cf46e3d1dff713be9a6fe44b9e20e3ad45 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Fri, 11 Sep 2020 13:56:56 +0100 Subject: [PATCH 26/34] Delete ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index ef3b9bc..0000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,13 +0,0 @@ -One line summary of the issue here. - -### Expected behavior - -As concisely as possible, describe the expected behavior. - -### Actual behavior - -As concisely as possible, describe the observed behavior. - -### Steps to reproduce the behavior - -Please list all relevant steps to reproduce the observed behavior. \ No newline at end of file From f89553fcbe8db458e998ebd0303f9c8c5b610d6e Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Fri, 11 Sep 2020 14:01:34 +0100 Subject: [PATCH 27/34] Update issue templates --- .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 6c2f7bb69225d86e296b8bea94b3aaa4f63a2b87 Mon Sep 17 00:00:00 2001 From: Andy Piper Date: Fri, 11 Sep 2020 14:02:10 +0100 Subject: [PATCH 28/34] Update config.yml --- .github/ISSUE_TEMPLATE/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 57f7674..c6fed7f 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - - name: Question about the Twitter API? Ask in the Twitter Developer Community + - name: Question about the Twitter API? Ask the Twitter Developer Community url: https://twittercommunity.com/ about: For general API functionality questions, please ask in the developer forums. From 1dff739887d673288a0029aec72945ea8a903c4b Mon Sep 17 00:00:00 2001 From: Twitter Service Date: Thu, 11 Feb 2021 07:02:58 -0800 Subject: [PATCH 29/34] Add .github/workflows/cla.yml through file_replicator. (#162) --- .github/workflows/cla.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/cla.yml diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 0000000..b5eac6a --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,25 @@ +name: "CLA Assistant" +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened,closed,synchronize] + +jobs: + CLAssistant: + runs-on: ubuntu-latest + steps: + - name: "CLA Assistant" + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' + # Alpha Release + uses: cla-assistant/github-action@v2.0.2-alpha + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PERSONAL_ACCESS_TOKEN : ${{ secrets.CLA_PAT }} + with: + remote-organization-name: twitter + remote-repository-name: .github-private + path-to-signatures: 'cla/signatures.json' + path-to-document: 'https://gist.github.com/twitter-service/a1ad5818c024dc4265f8b60e6d043f26' + custom-allsigned-prcomment: 'All Contributors have signed the CLA. If the commit check is not passing, a maintainer must go the Checks tab of this PR and rerun the GitHub Action.' + branch: 'main' From 96ee80d77b22248c5515316138f60b670369d8d3 Mon Sep 17 00:00:00 2001 From: mishina <32959831+mishina2228@users.noreply.github.com> Date: Sat, 16 Apr 2022 04:47:18 +0900 Subject: [PATCH 30/34] Use https for GitHub link (#174) and remove a magic comment --- twurl.gemspec | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/twurl.gemspec b/twurl.gemspec index 739a2d0..db31e5e 100644 --- a/twurl.gemspec +++ b/twurl.gemspec @@ -1,4 +1,3 @@ -# coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'twurl/version' @@ -11,7 +10,7 @@ Gem::Specification.new do |spec| spec.executables << 'twurl' spec.extra_rdoc_files = Dir["*.md", "LICENSE"] spec.files = Dir["*.md", "LICENSE", "twurl.gemspec", "bin/*", "lib/**/*"] - spec.homepage = 'http://github.com/twitter/twurl' + spec.homepage = 'https://github.com/twitter/twurl' spec.licenses = ['MIT'] spec.name = 'twurl' spec.rdoc_options = ['--title', 'twurl -- OAuth-enabled curl for the Twitter API', '--main', 'README.md', '--line-numbers', '--inline-source'] From 3604e655ad3da2f6a901db20d3529d5f95bed090 Mon Sep 17 00:00:00 2001 From: mishina <32959831+mishina2228@users.noreply.github.com> Date: Sat, 16 Apr 2022 04:49:45 +0900 Subject: [PATCH 31/34] Migrate CI from Travis CI to GitHub Actions (#173) * Add GitHub Actions Workflow * Remove Coveralls as it is not being used https://github.com/twitter/twurl/pull/161#issuecomment-765267294 * Address changes in RR v3.0.0 Now we need to take the block as a proc, not as an argument. refs: https://github.com/rr/rr/commit/d6da2090f935cd60cefc7556bcbce9f7964e2be1#diff-a20b4cc1aac26c32a40206ad0776d4ff528201e97b737b7b3f0be1eb0e12e93dL44-L49 * Require RR v3 * Require OpenStruct v0.3.3+ The following test cases fail when running CI in Ruby 3.0. ``` 1) Failure: Twurl::CLI::OptionParsingTest#test_setting_host_updates_to_requested_value [/home/runner/work/twurl/twurl/test/cli_test.rb:214]: Expected: "localhost:3000" Actual: "api.twitter.com" 2) Failure: Twurl::CLI::OptionParsingTest#test_setting_proxy_updates_to_requested_value [/home/runner/work/twurl/twurl/test/cli_test.rb:237]: Expected: "localhost:80" Actual: nil 3) Failure: Twurl::CLI::OptionParsingTest#test_passing_no_ssl_option_disables_ssl [/home/runner/work/twurl/twurl/test/cli_test.rb:196]: Expected false to be truthy. 4) Failure: Twurl::CLI::OptionParsingTest#test_specifying_a_request_method_extracts_and_normalizes_request_method [/home/runner/work/twurl/twurl/test/cli_test.rb:54]: Expected: "put" Actual: "get" 5) Failure: Twurl::CLI::OptionParsingTest#test_protocol_is_stripped_from_host [/home/runner/work/twurl/twurl/test/cli_test.rb:221]: Expected: "localhost:3000" Actual: "api.twitter.com" 6) Failure: Twurl::CLI::OptionParsingTest#test_passing_data_and_an_explicit_request_method_uses_the_specified_method [/home/runner/work/twurl/twurl/test/cli_test.rb:148]: Expected: "delete" Actual: "post" 7) Failure: Twurl::Options::Test#test_ssl_is_enabled_if_the_protocol_is_https [/home/runner/work/twurl/twurl/test/cli_options_test.rb:18]: Expected false to be truthy. 8) Failure: Twurl::Options::Test#test_base_url_is_built_from_protocol_and_host [/home/runner/work/twurl/twurl/test/cli_options_test.rb:13]: Expected: "http://api.twitter.com" Actual: "https://api.twitter.com" ``` According to https://github.com/twitter/twurl/issues/159#issuecomment-762511975, the cause appears to be a bug in OpenStruct. The bug was fixed in https://github.com/ruby/ostruct/issues/23, and released as v0.3.3. * Drop support for Ruby 2.4 - OpenStruct v0.3.3+ requires Ruby 2.5+. - It's already reached EOL on 2020-03-31. * Migrate CI from Travis CI to GitHub Actions --- .github/workflows/ci.yml | 30 ++++++++++++++++++++++++++++++ .travis.yml | 24 ------------------------ Gemfile | 3 +-- README.md | 4 ++-- test/request_controller_test.rb | 8 ++++---- test/test_helper.rb | 3 +-- twurl.gemspec | 3 ++- 7 files changed, 40 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..201865c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + - push + - pull_request + +jobs: + test: + strategy: + fail-fast: false + matrix: + ruby: + - '2.5' + - '2.6' + - '2.7' + - '3.0' + - '3.1' + - 'ruby-head' + - 'jruby-head' + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Run unit tests + run: bundle exec rake diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0ae9be9..0000000 --- a/.travis.yml +++ /dev/null @@ -1,24 +0,0 @@ -dist: xenial -language: ruby - -before_install: - - bundle config without 'development' - -branches: - only: - - master - -matrix: - fast_finish: true - allow_failures: - - rvm: jruby-head - - rvm: ruby-head - - rvm: rbx-2 - include: - - rvm: 2.7.0 - - rvm: 2.6.5 - - rvm: 2.5.7 - - rvm: 2.4.9 - - rvm: jruby-head - - rvm: ruby-head - - rvm: rbx-2 diff --git a/Gemfile b/Gemfile index 9534487..98251a3 100644 --- a/Gemfile +++ b/Gemfile @@ -4,9 +4,8 @@ gem 'jruby-openssl', :platforms => :jruby gem 'rake' group :test do - gem 'coveralls' gem 'minitest', '>= 5' - gem 'rr', '>= 1.1' + gem 'rr', '~> 3.0.9' gem 'simplecov', '>= 0.9' end diff --git a/README.md b/README.md index c1e6359..fa9f0fc 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Twurl [![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/twitter/twurl/blob/master/LICENSE) - [![Gem Version](https://badge.fury.io/rb/twurl.svg)](https://badge.fury.io/rb/twurl) - [![Build Status](https://travis-ci.com/twitter/twurl.svg?branch=master)](https://travis-ci.com/twitter/twurl) +[![Gem Version](https://badge.fury.io/rb/twurl.svg)](https://badge.fury.io/rb/twurl) +[![CI](https://github.com/twitter/twurl/actions/workflows/ci.yml/badge.svg)](https://github.com/twitter/twurl/actions/workflows/ci.yml) Twurl is like curl, but tailored specifically for the Twitter API. It knows how to grant an access token to a client application for diff --git a/test/request_controller_test.rb b/test/request_controller_test.rb index ccb69f7..0133005 100644 --- a/test/request_controller_test.rb +++ b/test/request_controller_test.rb @@ -42,8 +42,8 @@ class Twurl::RequestController::RequestTest < Twurl::RequestController::Abstract def test_request_response_is_written_to_output expected_body = 'this is a fake response body' response = Object.new - mock(response).read_body { |block| block.call expected_body } - mock(client).perform_request_from_options(options).times(1) { |options, block| block.call(response) } + mock(response).read_body { |&block| block.call expected_body } + mock(client).perform_request_from_options(options).times(1) { |_options, &block| block.call(response) } controller.perform_request @@ -60,8 +60,8 @@ def test_request_response_is_json_formatted custom_options = options custom_options.json_format = true response = Object.new - mock(response).read_body { |block| block.call response_body } - mock(client).perform_request_from_options(custom_options).times(1) { |custom_options, block| block.call(response) } + mock(response).read_body { |&block| block.call response_body } + mock(client).perform_request_from_options(custom_options).times(1) { |_custom_options, &block| block.call(response) } controller.perform_request diff --git a/test/test_helper.rb b/test/test_helper.rb index 782b26e..0230b68 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,7 +1,6 @@ require 'simplecov' -require 'coveralls' -SimpleCov.formatters = [SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter] +SimpleCov.formatters = [SimpleCov::Formatter::HTMLFormatter] SimpleCov.start diff --git a/twurl.gemspec b/twurl.gemspec index db31e5e..688a5f4 100644 --- a/twurl.gemspec +++ b/twurl.gemspec @@ -4,6 +4,7 @@ require 'twurl/version' Gem::Specification.new do |spec| spec.add_dependency 'oauth', '~> 0.4' + spec.add_dependency 'ostruct', '>= 0.3.3' spec.authors = ["Marcel Molina", "Erik Michaels-Ober", "@TwitterDev team"] spec.description = %q{Curl for the Twitter API} spec.bindir = 'bin' @@ -15,7 +16,7 @@ Gem::Specification.new do |spec| spec.name = 'twurl' spec.rdoc_options = ['--title', 'twurl -- OAuth-enabled curl for the Twitter API', '--main', 'README.md', '--line-numbers', '--inline-source'] spec.require_paths = ['lib'] - spec.required_ruby_version = '>= 2.4.0' + spec.required_ruby_version = '>= 2.5.0' spec.summary = spec.description spec.version = Twurl::Version end From 3d8a4901cde53ea93b81c379aa69bbf26148e647 Mon Sep 17 00:00:00 2001 From: Shohei Maeda <11495867+smaeda-ks@users.noreply.github.com> Date: Wed, 26 Apr 2023 16:22:35 +0900 Subject: [PATCH 32/34] 0.9.7 (#161) * update VSCode remote container settings Use docker-compose and mount volume between host and container properly * "-v" option to include Ruby runtime version * Support of Ruby 2.4 has ended https://www.ruby-lang.org/en/news/2020/04/05/support-of-ruby-2-4-has-ended/ * #157 fix parse rule and better error message * remove --no-ssl option * do not allow alias to use reserved command names * max_retries is available since Ruby 2.5 and now we require spec.required_ruby_version = '>= 2.5.0' so no need to have this condition anymore * don't exit * --raw-data option is fake Although the description says "Sends the specified data as it is..." it's actually parsing input data using CGI.parse() method. This isn't ideal, especially when sending JSON data, for instance. This commit changes this behavior and makes that option a real thing, so users can also send JSON data using -r option by setting the content-type request header using -A option. For backword-compatibility, we still allow -d option to send JSON data as long as "content-type: application/json" request header is set. Also, since -r takes input data as-is, it doesn't really make sense to allow combine use with -d option or adding -r option more than once, so we treat them as an error going forward. * error message should use Exception over CLI.puts * workaround for OpenStruct bug in Ruby 3.0.0 This commit can be reverted once new Ruby 3 patch/minor version has released. * update .travis.yml * remove coveralls * Revert "workaround for OpenStruct bug in Ruby 3.0.0" see https://github.com/twitter/twurl/pull/173 * Update devcontainer.json * prevent panic message on Ctrl-C in PIN auth flow --- .devcontainer/devcontainer.json | 12 ++++--- .devcontainer/docker-compose.yml | 9 +++++ lib/twurl/aliases_controller.rb | 8 +++-- lib/twurl/app_only_oauth_client.rb | 9 ++--- lib/twurl/cli.rb | 49 +++++++++++--------------- lib/twurl/oauth_client.rb | 37 +++++++++++--------- lib/twurl/request_controller.rb | 6 ++-- test/alias_controller_test.rb | 10 +++--- test/cli_options_test.rb | 13 ++----- test/cli_test.rb | 56 ++++++++---------------------- test/oauth_client_test.rb | 26 ++++++++++---- test/request_controller_test.rb | 7 ++-- 12 files changed, 117 insertions(+), 125 deletions(-) create mode 100644 .devcontainer/docker-compose.yml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index fa7186f..bca8d96 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,12 +1,16 @@ { "name": "twurl dev container", - "dockerFile": "Dockerfile", - "context": "..", + "dockerComposeFile": "./docker-compose.yml", + "service": "twurl", "workspaceFolder": "/usr/src/app", "settings": { - "terminal.integrated.shell.linux": "/bin/bash" + "terminal.integrated.profiles.linux": { + "bash": { + "path": "bash", + "args": ["-l"] + } + } }, - "shutdownAction": "none", "extensions": [ "rebornix.Ruby" ] diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..652a66a --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3" +services: + twurl: + build: + context: .. + dockerfile: ./.devcontainer/Dockerfile + command: /bin/sh -c "while sleep 1000; do :; done" + volumes: + - ../:/usr/src/app diff --git a/lib/twurl/aliases_controller.rb b/lib/twurl/aliases_controller.rb index 6d9ebc0..4dd6b96 100644 --- a/lib/twurl/aliases_controller.rb +++ b/lib/twurl/aliases_controller.rb @@ -15,9 +15,13 @@ def dispatch end when 1 if options.path - OAuthClient.rcfile.alias(options.subcommands.first, options.path) + if Twurl::CLI::SUPPORTED_COMMANDS.include?(options.subcommands.first) + raise Exception, "ERROR: '#{options.subcommands.first}' is reserved for commands. Please use different alias name." + else + OAuthClient.rcfile.alias(options.subcommands.first, options.path) + end else - CLI.puts NO_PATH_PROVIDED_MESSAGE + raise Exception, NO_PATH_PROVIDED_MESSAGE end end end diff --git a/lib/twurl/app_only_oauth_client.rb b/lib/twurl/app_only_oauth_client.rb index a84b5c0..bd3133f 100644 --- a/lib/twurl/app_only_oauth_client.rb +++ b/lib/twurl/app_only_oauth_client.rb @@ -57,12 +57,9 @@ def set_http_client_options(http) http.set_debug_output(Twurl.options.debug_output_io) if Twurl.options.trace http.read_timeout = http.open_timeout = Twurl.options.timeout || 60 http.open_timeout = Twurl.options.connection_timeout if Twurl.options.connection_timeout - # Only override if Net::HTTP support max_retries (since Ruby >= 2.5) - http.max_retries = 0 if http.respond_to?(:max_retries=) - if Twurl.options.ssl? - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - end + http.max_retries = 0 + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE http end diff --git a/lib/twurl/cli.rb b/lib/twurl/cli.rb index 60b01db..ad26a0a 100644 --- a/lib/twurl/cli.rb +++ b/lib/twurl/cli.rb @@ -6,8 +6,6 @@ class CLI PROTOCOL_PATTERN = /^\w+:\/\// README = File.dirname(__FILE__) + '/../../README.md' @output ||= STDOUT - class NoPathFound < Exception - end class << self attr_accessor :output @@ -15,8 +13,8 @@ class << self def run(args) begin options = parse_options(args) - rescue NoPathFound => e - exit + rescue Twurl::Exception => exception + abort(exception.message) end dispatch(options) end @@ -80,7 +78,6 @@ def parse_options(args) headers host quiet - disable_ssl request_method help version @@ -98,11 +95,11 @@ def parse_options(args) begin arguments = option_parser.parse!(args) rescue OptionParser::InvalidOption - CLI.puts "ERROR: undefined option" - exit + raise Exception "ERROR: undefined option" + rescue Twurl::Exception + raise rescue - CLI.puts "ERROR: invalid argument" - exit + raise Exception "ERROR: invalid argument" end Twurl.options.command = extract_command!(arguments) Twurl.options.path = extract_path!(arguments) @@ -110,7 +107,7 @@ def parse_options(args) if Twurl.options.command == DEFAULT_COMMAND and Twurl.options.path.nil? and Twurl.options.args.empty? CLI.puts option_parser - raise NoPathFound, "No path found" + raise Exception, "No path found" end Twurl.options @@ -172,7 +169,7 @@ def extract_path!(arguments) def escape_params(params) CGI::parse(params).map do |key, value| - "#{CGI.escape key}=#{CGI.escape value.first}" + "#{CGI.escape(key)}=#{CGI.escape(value.first)}" end.join("&") end end @@ -234,12 +231,12 @@ def trace def data on('-d', '--data [data]', 'Sends the specified data in a POST request to the HTTP server.') do |data| - if options.args.count { |item| /content-type: (.*)/i.match(item) } > 0 - options.data[data] = nil + if options.args.count { |item| /^content-type:\s+application\/json/i.match(item) } > 0 + options.json_data = true + options.data = data else - data.split('&').each do |pair| - key, value = pair.split('=', 2) - options.data[key] = value + CGI.parse(data).each_pair do |key, value| + options.data[key] = value.first end end end @@ -247,9 +244,13 @@ def data def raw_data on('-r', '--raw-data [data]', 'Sends the specified data as it is in a POST request to the HTTP server.') do |data| - CGI::parse(data).each_pair do |key, value| - options.data[key] = value.first + if options.raw_data + raise Exception, "ERROR: can't specify '-r' option more than once" + elsif options.args.include?('-d') || options.args.include?('--data') + raise Exception, "ERROR: can't use '-r' and '-d' options together" end + options.raw_data = true + options.data = data end end @@ -277,12 +278,6 @@ def quiet end end - def disable_ssl - on('-U', '--no-ssl', 'Disable SSL (default: SSL is enabled)') do |use_ssl| - options.protocol = 'http' - end - end - def request_method on('-X', '--request-method [method]', 'Request method (default: GET)') do |request_method| options.request_method = request_method.downcase @@ -298,7 +293,7 @@ def help def version on_tail("-v", "--version", "Show version") do - CLI.puts Version + CLI.puts "twurl version: #{Version}\nplatform: #{RUBY_ENGINE} #{RUBY_VERSION} (#{RUBY_PLATFORM})" exit end end @@ -374,10 +369,6 @@ def base_url "#{protocol}://#{host}" end - def ssl? - protocol == 'https' - end - def debug_output_io super || STDERR end diff --git a/lib/twurl/oauth_client.rb b/lib/twurl/oauth_client.rb index 482f90c..c87226b 100644 --- a/lib/twurl/oauth_client.rb +++ b/lib/twurl/oauth_client.rb @@ -162,16 +162,20 @@ def build_request_from_options(options, &block) request.body = multipart_body.join request.content_type = "multipart/form-data, boundary=\"#{boundary}\"" - elsif request.content_type && options.data - request.body = options.data.keys.first + elsif options.json_data + request.body = options.data elsif options.data - request.content_type = "application/x-www-form-urlencoded" - if options.data.length == 1 && options.data.values.first == nil - request.body = options.data.keys.first + request.content_type = "application/x-www-form-urlencoded" unless request.content_type + if options.raw_data + request.body = options.data else - request.body = options.data.map do |key, value| - "#{key}=#{CGI.escape value}" - end.join("&") + begin + request.body = options.data.map do |key, value| + "#{key}" + (value.nil? ? "" : "=#{CGI.escape(value)}") + end.join("&") + rescue + raise Exception, "ERROR: failed to parse POST request body" + end end end request @@ -202,7 +206,11 @@ def exchange_credentials_for_access_token def perform_pin_authorize_workflow @request_token = consumer.get_request_token CLI.puts("Go to #{generate_authorize_url} and paste in the supplied PIN") - pin = STDIN.gets + begin + pin = STDIN.gets.chomp + rescue SystemExit, Interrupt + raise Exception, "Operation cancelled" + end access_token = @request_token.get_access_token(:oauth_verifier => pin.chomp) {:oauth_token => access_token.token, :oauth_token_secret => access_token.secret} end @@ -212,7 +220,7 @@ def generate_authorize_url params = request['Authorization'].sub(/^OAuth\s+/, '').split(/,\s+/).map { |p| k, v = p.split('=') v =~ /"(.*?)"/ - "#{k}=#{CGI::escape($1)}" + "#{k}=#{CGI.escape($1)}" }.join('&') "#{Twurl.options.base_url}#{request.path}?#{params}" end @@ -260,12 +268,9 @@ def configure_http! consumer.http.set_debug_output(Twurl.options.debug_output_io) if Twurl.options.trace consumer.http.read_timeout = consumer.http.open_timeout = Twurl.options.timeout || 60 consumer.http.open_timeout = Twurl.options.connection_timeout if Twurl.options.connection_timeout - # Only override if Net::HTTP support max_retries (since Ruby >= 2.5) - consumer.http.max_retries = 0 if consumer.http.respond_to?(:max_retries=) - if Twurl.options.ssl? - consumer.http.use_ssl = true - consumer.http.verify_mode = OpenSSL::SSL::VERIFY_NONE - end + consumer.http.max_retries = 0 + consumer.http.use_ssl = true + consumer.http.verify_mode = OpenSSL::SSL::VERIFY_NONE end def consumer diff --git a/lib/twurl/request_controller.rb b/lib/twurl/request_controller.rb index cc81f13..b3e9c70 100644 --- a/lib/twurl/request_controller.rb +++ b/lib/twurl/request_controller.rb @@ -22,11 +22,11 @@ def perform_request } } rescue URI::InvalidURIError - CLI.puts INVALID_URI_MESSAGE + raise Exception, INVALID_URI_MESSAGE rescue Net::ReadTimeout - CLI.puts READ_TIMEOUT_MESSAGE + raise Exception, READ_TIMEOUT_MESSAGE rescue Net::OpenTimeout - CLI.puts OPEN_TIMEOUT_MESSAGE + raise Exception, OPEN_TIMEOUT_MESSAGE end end diff --git a/test/alias_controller_test.rb b/test/alias_controller_test.rb index 3dda3a1..f896e6d 100644 --- a/test/alias_controller_test.rb +++ b/test/alias_controller_test.rb @@ -41,13 +41,15 @@ def test_when_alias_and_value_are_provided_they_are_added controller.dispatch end - def test_when_no_path_is_provided_nothing_happens + def test_error_if_no_path_is_provided options.subcommands = ['a'] assert_nil options.path - mock(Twurl::CLI).puts(Twurl::AliasesController::NO_PATH_PROVIDED_MESSAGE).times(1) + e = assert_raises Twurl::Exception do + controller = Twurl::AliasesController.new(client, options) + controller.dispatch + end - controller = Twurl::AliasesController.new(client, options) - controller.dispatch + assert_equal Twurl::AliasesController::NO_PATH_PROVIDED_MESSAGE, e.message end end diff --git a/test/cli_options_test.rb b/test/cli_options_test.rb index 53c3aa8..5db43ec 100644 --- a/test/cli_options_test.rb +++ b/test/cli_options_test.rb @@ -7,17 +7,8 @@ def setup end def test_base_url_is_built_from_protocol_and_host - options.protocol = 'http' - options.host = 'api.twitter.com' + options = Twurl::CLI.parse_options(['-H', 'ads-api.twitter.com']) - assert_equal 'http://api.twitter.com', options.base_url - end - - def test_ssl_is_enabled_if_the_protocol_is_https - options.protocol = 'http' - assert !options.ssl? - - options.protocol = 'https' - assert options.ssl? + assert_equal 'https://ads-api.twitter.com', options.base_url end end diff --git a/test/cli_test.rb b/test/cli_test.rb index 7d6fcb3..34830c6 100644 --- a/test/cli_test.rb +++ b/test/cli_test.rb @@ -26,9 +26,10 @@ def test_unsupported_command_specified_sets_default_command module PathParsingTests def test_missing_path_throws_no_path_found stub(Twurl::CLI).puts - assert_raises Twurl::CLI::NoPathFound do + e = assert_raises Twurl::Exception do Twurl::CLI.parse_options([]) end + assert_equal 'No path found', e.message end def test_uri_params_are_encoded @@ -124,17 +125,12 @@ def test_extracting_an_empty_key_value_pair include DataParsingTests module RawDataParsingTests - def test_extracting_a_single_key_value_pair - options = Twurl::CLI.parse_options([TEST_PATH, '-r', 'key=value']) - assert_equal({'key' => 'value'}, options.data) + def test_raw_data_option_should_not_use_parser + options = Twurl::CLI.parse_options([TEST_PATH, '-r', 'key=foo%26bar']) + assert_equal('key=foo%26bar', options.data) - options = Twurl::CLI.parse_options([TEST_PATH, '--raw-data', 'key=value']) - assert_equal({'key' => 'value'}, options.data) - end - - def test_with_special_to_url_characters_in_value - options = Twurl::CLI.parse_options([TEST_PATH, '-r', 'key=a+%26%26+b+%2B%2B+c']) - assert_equal({'key' => 'a && b ++ c'}, options.data) + options = Twurl::CLI.parse_options([TEST_PATH, '--raw-data', 'key=foo%26bar']) + assert_equal('key=foo%26bar', options.data) end def test_passing_data_and_no_explicit_request_method_defaults_request_method_to_post @@ -144,26 +140,19 @@ def test_passing_data_and_no_explicit_request_method_defaults_request_method_to_ def test_passing_data_and_an_explicit_request_method_uses_the_specified_method options = Twurl::CLI.parse_options([TEST_PATH, '-r', 'key=value', '-X', 'DELETE']) - assert_equal({'key' => 'value'}, options.data) assert_equal 'delete', options.request_method end - def test_multiple_pairs_when_option_is_specified_multiple_times_on_command_line_collects_all - options = Twurl::CLI.parse_options([TEST_PATH, '-r', 'key=value', '-d', 'another=pair']) - assert_equal({'key' => 'value', 'another' => 'pair'}, options.data) - end - - def test_multiple_pairs_separated_by_ampersand_are_all_captured - options = Twurl::CLI.parse_options([TEST_PATH, '-r', 'key=value+%26+value&another=pair']) - assert_equal({'key' => 'value & value', 'another' => 'pair'}, options.data) + def test_error_when_option_is_specified_multiple_times + assert_raises Twurl::Exception do + Twurl::CLI.parse_options([TEST_PATH, '-r', 'key1=value1', '-r', 'key2=value2']) + end end - def test_extracting_an_empty_key_value_pair - options = Twurl::CLI.parse_options([TEST_PATH, '-r', 'key=']) - assert_equal({'key' => ''}, options.data) - - options = Twurl::CLI.parse_options([TEST_PATH, '--raw-data', 'key=']) - assert_equal({'key' => ''}, options.data) + def test_error_when_option_is_specified_with_data_option + assert_raises Twurl::Exception do + Twurl::CLI.parse_options([TEST_PATH, '-r', 'key1=value1', '-d', 'key2=value2']) + end end end include RawDataParsingTests @@ -184,21 +173,6 @@ def test_multiple_headers_when_option_is_specified_multiple_times_on_command_lin end include HeaderParsingTests - module SSLDisablingTests - def test_ssl_is_on_by_default - options = Twurl::CLI.parse_options([TEST_PATH]) - assert options.ssl? - end - - def test_passing_no_ssl_option_disables_ssl - ['-U', '--no-ssl'].each do |switch| - options = Twurl::CLI.parse_options([TEST_PATH, switch]) - assert !options.ssl? - end - end - end - include SSLDisablingTests - module HostOptionTests def test_not_specifying_host_sets_it_to_the_default options = Twurl::CLI.parse_options([TEST_PATH]) diff --git a/test/oauth_client_test.rb b/test/oauth_client_test.rb index 6d86c05..84bf1ab 100644 --- a/test/oauth_client_test.rb +++ b/test/oauth_client_test.rb @@ -1,6 +1,8 @@ require File.dirname(__FILE__) + '/test_helper' class Twurl::OAuthClient::AbstractOAuthClientTest < Minitest::Test + TEST_PATH = '/1.1/url/does/not/matter.json' + attr_reader :client, :options def setup Twurl::OAuthClient.instance_variable_set(:@rcfile, nil) @@ -199,10 +201,7 @@ def test_request_is_made_using_request_method_and_path_and_data_in_options def test_content_type_is_not_overridden_if_set_and_data_in_options client = Twurl::OAuthClient.test_exemplar - - options.request_method = 'post' - options.data = { '{ "foo": "bar" }' => nil } - options.headers = { 'Content-Type' => 'application/json' } + options = Twurl::CLI.parse_options([TEST_PATH, '-d', '{ "foo": "bar" }', '-A', 'Content-Type: application/json']) mock(client.consumer.http).request( satisfy { |req| req.is_a?(Net::HTTP::Post) && req.content_type == 'application/json' } @@ -213,9 +212,7 @@ def test_content_type_is_not_overridden_if_set_and_data_in_options def test_content_type_is_set_to_form_encoded_if_not_set_and_data_in_options client = Twurl::OAuthClient.test_exemplar - - options.request_method = 'post' - options.data = { '{ "foo": "bar" }' => nil } + options = Twurl::CLI.parse_options([TEST_PATH, '-d', 'foo=bar']) mock(client.consumer.http).request( satisfy { |req| req.is_a?(Net::HTTP::Post) && req.content_type == 'application/x-www-form-urlencoded' } @@ -224,6 +221,21 @@ def test_content_type_is_set_to_form_encoded_if_not_set_and_data_in_options client.perform_request_from_options(options) end + def test_post_body_is_parsed_and_escaped_properly_through_request_builder + client = Twurl::OAuthClient.test_exemplar + options = Twurl::CLI.parse_options([TEST_PATH, '-d', 'foo=text%26text']) + + mock(client.consumer.http).request( + satisfy { |req| + req.is_a?(Net::HTTP::Post) && + req.content_type == 'application/x-www-form-urlencoded' && + req.body == 'foo=text%26text' + } + ) + + client.perform_request_from_options(options) + end + def test_user_agent_request_header_is_set client = Twurl::OAuthClient.test_exemplar expected_ua_string = "twurl version: #{Twurl::Version} platform: #{RUBY_ENGINE} #{RUBY_VERSION} (#{RUBY_PLATFORM})" diff --git a/test/request_controller_test.rb b/test/request_controller_test.rb index 0133005..9720575 100644 --- a/test/request_controller_test.rb +++ b/test/request_controller_test.rb @@ -69,9 +69,12 @@ def test_request_response_is_json_formatted end def test_invalid_or_unspecified_urls_report_error - mock(Twurl::CLI).puts(Twurl::RequestController::INVALID_URI_MESSAGE).times(1) mock(client).perform_request_from_options(options).times(1) { raise URI::InvalidURIError } - controller.perform_request + e = assert_raises Twurl::Exception do + controller.perform_request + end + + assert_equal Twurl::RequestController::INVALID_URI_MESSAGE, e.message end end From c256756dc89e8507cd77495d3ff9e0b6e49ae339 Mon Sep 17 00:00:00 2001 From: Shohei Maeda <11495867+smaeda-ks@users.noreply.github.com> Date: Wed, 26 Apr 2023 16:38:40 +0900 Subject: [PATCH 33/34] bump version -> v0.9.7 --- INSTALL.md | 4 ++-- lib/twurl/version.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 18a16ad..961ed5c 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -10,7 +10,7 @@ $ gem install twurl ```sh # verify installation $ twurl -v -0.9.6 +0.9.7 ``` ## Install from source @@ -32,5 +32,5 @@ If you don't want to install Twurl globally on your system, use `--path` [option ``` $ bundle install --path path/to/directory $ bundle exec twurl -v -0.9.6 +0.9.7 ``` diff --git a/lib/twurl/version.rb b/lib/twurl/version.rb index f734d47..69ac97f 100644 --- a/lib/twurl/version.rb +++ b/lib/twurl/version.rb @@ -2,7 +2,7 @@ module Twurl class Version MAJOR = 0 unless defined? Twurl::Version::MAJOR MINOR = 9 unless defined? Twurl::Version::MINOR - PATCH = 6 unless defined? Twurl::Version::PATCH + PATCH = 7 unless defined? Twurl::Version::PATCH PRE = nil unless defined? Twurl::Version::PRE # Time.now.to_i.to_s class << self From 9747b0da50ea541358c7e846bf6f67cbac3bb019 Mon Sep 17 00:00:00 2001 From: richbloodfuckin Date: Fri, 20 Sep 2024 16:36:31 +0300 Subject: [PATCH 34/34] Rename cla.yml to cla.ympythonengage --- .github/workflows/{cla.yml => cla.ympython} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{cla.yml => cla.ympython} (100%) diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.ympython similarity index 100% rename from .github/workflows/cla.yml rename to .github/workflows/cla.ympython