├── .jrubyrc ├── .rspec ├── spec ├── fixtures │ ├── already_posted.json │ ├── count.json │ ├── ids_list_new_cursor2.json │ ├── not_found.json │ ├── already_retweeted.json │ ├── bearer_token.json │ ├── me.jpeg │ ├── pbjt.gif │ ├── 1080p.mp4 │ ├── forbidden.json │ ├── already_favorited.json │ ├── wildcomet2.jpe │ ├── we_concept_bg2.png │ ├── saved_search.json │ ├── upload.json │ ├── saved_searches.json │ ├── account_activity_create_webhook.json │ ├── chunk_upload_init.json │ ├── ids_list.json │ ├── ids_list2.json │ ├── locations.json │ ├── chunk_upload_finalize_succeeded.json │ ├── ids_list_new_cursor.json │ ├── chunk_upload_finalize_pending.json │ ├── welcome_message_rule.json │ ├── chunk_upload_status_pending.json │ ├── chunk_upload_status_succeeded.json │ ├── friendships.json │ ├── account_activity_list_webhook.json │ ├── chunk_upload_status_failed.json │ ├── request_token.txt │ ├── welcome_message_rules.json │ ├── search_malformed.json │ ├── direct_message_event.json │ ├── not_following.json │ ├── following.json │ ├── settings.json │ ├── languages.json │ ├── welcome_message.json │ ├── profile_banner.json │ ├── ids.json │ ├── welcome_message_with_name.json │ ├── oembed.json │ ├── suggestions.json │ ├── user_search.json │ ├── place.json │ ├── users_list.json │ ├── users_list2.json │ ├── category.json │ ├── welcome_messages.json │ ├── list.json │ ├── configuration.json │ ├── welcome_message_with_entities.json │ ├── user_timeline.json │ ├── matching_trends.json │ ├── memberships.json │ ├── memberships2.json │ ├── lists.json │ ├── pengwynn.json │ ├── members.json │ ├── subscriptions.json │ ├── subscriptions2.json │ ├── followers_list.json │ ├── followers_list2.json │ ├── contributees.json │ ├── friends_list.json │ ├── friends_list2.json │ ├── direct_messages.json │ ├── users.json │ ├── premium_search.json │ └── retweets.json ├── twitter │ ├── streaming │ │ ├── friend_list_spec.rb │ │ ├── response_spec.rb │ │ ├── deleted_tweet_spec.rb │ │ └── message_parser_spec.rb │ ├── media │ │ ├── photo_spec.rb │ │ ├── video_spec.rb │ │ ├── animated_gif_spec.rb │ │ └── video_info_spec.rb │ ├── profile_banner_spec.rb │ ├── geo_factory_spec.rb │ ├── configuration_spec.rb │ ├── media_factory_spec.rb │ ├── basic_user_spec.rb │ ├── source_user_spec.rb │ ├── target_user_spec.rb │ ├── base_spec.rb │ ├── identifiable_spec.rb │ ├── size_spec.rb │ ├── rest │ │ ├── spam_reporting_spec.rb │ │ ├── search_spec.rb │ │ ├── help_spec.rb │ │ ├── suggested_users_spec.rb │ │ ├── client_spec.rb │ │ ├── oauth_spec.rb │ │ └── trends_spec.rb │ ├── variant_spec.rb │ ├── geo_results_spec.rb │ ├── version_spec.rb │ ├── settings_spec.rb │ ├── geo │ │ ├── point_spec.rb │ │ └── polygon_spec.rb │ ├── geo_spec.rb │ ├── suggestion_spec.rb │ ├── saved_search_spec.rb │ ├── relationship_spec.rb │ ├── trend_spec.rb │ ├── error_spec.rb │ ├── utils_spec.rb │ ├── client_spec.rb │ ├── rate_limit_spec.rb │ ├── search_results_spec.rb │ ├── entity │ │ └── uri_spec.rb │ └── cursor_spec.rb └── helper.rb ├── lib ├── twitter │ ├── streaming │ │ ├── friend_list.rb │ │ ├── stall_warning.rb │ │ ├── deleted_tweet.rb │ │ ├── response.rb │ │ ├── message_parser.rb │ │ ├── event.rb │ │ └── connection.rb │ ├── geo │ │ ├── polygon.rb │ │ └── point.rb │ ├── media │ │ ├── animated_gif.rb │ │ ├── video_info.rb │ │ ├── photo.rb │ │ └── video.rb │ ├── target_user.rb │ ├── entity.rb │ ├── language.rb │ ├── metadata.rb │ ├── entity │ │ ├── symbol.rb │ │ ├── hashtag.rb │ │ ├── uri.rb │ │ └── user_mention.rb │ ├── basic_user.rb │ ├── variant.rb │ ├── geo.rb │ ├── saved_search.rb │ ├── size.rb │ ├── source_user.rb │ ├── trend.rb │ ├── direct_messages │ │ ├── welcome_message_rule.rb │ │ ├── welcome_message.rb │ │ ├── welcome_message_rule_wrapper.rb │ │ └── welcome_message_wrapper.rb │ ├── arguments.rb │ ├── oembed.rb │ ├── relationship.rb │ ├── profile_banner.rb │ ├── direct_message.rb │ ├── geo_factory.rb │ ├── identity.rb │ ├── rest │ │ ├── client.rb │ │ ├── spam_reporting.rb │ │ ├── undocumented.rb │ │ ├── api.rb │ │ ├── suggested_users.rb │ │ ├── help.rb │ │ └── premium_search.rb │ ├── suggestion.rb │ ├── media_factory.rb │ ├── creatable.rb │ ├── factory.rb │ ├── geo_results.rb │ ├── version.rb │ ├── enumerable.rb │ ├── rate_limit.rb │ ├── settings.rb │ ├── configuration.rb │ ├── utils.rb │ ├── list.rb │ ├── place.rb │ ├── null_object.rb │ ├── direct_message_event.rb │ ├── trend_results.rb │ ├── client.rb │ ├── headers.rb │ ├── premium_search_results.rb │ ├── cursor.rb │ ├── tweet.rb │ ├── search_results.rb │ └── entities.rb └── twitter.rb ├── .gitignore ├── .codeclimate.yml ├── .yardopts ├── Gemfile ├── .travis.yml ├── examples ├── Streaming.md ├── README.md ├── Search.md ├── RateLimiting.md ├── AllTweets.md └── Update.md ├── Rakefile ├── LICENSE.md ├── .rubocop.yml ├── twitter.gemspec ├── etc └── erd.rb └── CONTRIBUTING.md /.jrubyrc: -------------------------------------------------------------------------------- 1 | debug.fullTrace=true 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --order random 3 | -------------------------------------------------------------------------------- /spec/fixtures/already_posted.json: -------------------------------------------------------------------------------- 1 | {"errors":"Status is a duplicate."} -------------------------------------------------------------------------------- /spec/fixtures/count.json: -------------------------------------------------------------------------------- 1 | {"count":13845465,"url":"http:\/\/twitter.com\/"} -------------------------------------------------------------------------------- /spec/fixtures/ids_list_new_cursor2.json: -------------------------------------------------------------------------------- 1 | {"ids":[20009713,22469930,351223419]} 2 | -------------------------------------------------------------------------------- /spec/fixtures/not_found.json: -------------------------------------------------------------------------------- 1 | {"error":"Not Found","request":"/1/statuses/show/1.json"} -------------------------------------------------------------------------------- /spec/fixtures/already_retweeted.json: -------------------------------------------------------------------------------- 1 | {"errors":"You have already retweeted this Tweet."} 2 | -------------------------------------------------------------------------------- /spec/fixtures/bearer_token.json: -------------------------------------------------------------------------------- 1 | {"token_type":"bearer","access_token":"AAAA%2FAAA%3DAAAAAAAA"} -------------------------------------------------------------------------------- /spec/fixtures/me.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feedbin/twitter/master/spec/fixtures/me.jpeg -------------------------------------------------------------------------------- /spec/fixtures/pbjt.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feedbin/twitter/master/spec/fixtures/pbjt.gif -------------------------------------------------------------------------------- /spec/fixtures/1080p.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feedbin/twitter/master/spec/fixtures/1080p.mp4 -------------------------------------------------------------------------------- /spec/fixtures/forbidden.json: -------------------------------------------------------------------------------- 1 | {"errors":[{"code":160,"message":"You've already requested to follow %s."}]} -------------------------------------------------------------------------------- /spec/fixtures/already_favorited.json: -------------------------------------------------------------------------------- 1 | {"errors":[{"message":"You have already favorited this status.","code":139}]} -------------------------------------------------------------------------------- /spec/fixtures/wildcomet2.jpe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feedbin/twitter/master/spec/fixtures/wildcomet2.jpe -------------------------------------------------------------------------------- /spec/fixtures/we_concept_bg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feedbin/twitter/master/spec/fixtures/we_concept_bg2.png -------------------------------------------------------------------------------- /lib/twitter/streaming/friend_list.rb: -------------------------------------------------------------------------------- 1 | module Twitter 2 | module Streaming 3 | class FriendList < Array 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *~ 3 | .bundle 4 | .rvmrc 5 | .yardoc 6 | Gemfile.lock 7 | coverage/* 8 | doc/* 9 | log/* 10 | measurement/* 11 | pkg/* 12 | -------------------------------------------------------------------------------- /lib/twitter/geo/polygon.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/geo' 2 | 3 | module Twitter 4 | class Geo 5 | class Polygon < Twitter::Geo 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/saved_search.json: -------------------------------------------------------------------------------- 1 | {"position":null,"query":"twitter","created_at":"Tue Oct 26 21:49:01 +0000 2010","id_str":"16129012","name":"twitter","id":16129012} -------------------------------------------------------------------------------- /spec/fixtures/upload.json: -------------------------------------------------------------------------------- 1 | {"image":{"w":428,"h":428,"image_type":"image\/png"},"media_id":470030289822314497,"media_id_string":"470030289822314497","size":68900} -------------------------------------------------------------------------------- /spec/fixtures/saved_searches.json: -------------------------------------------------------------------------------- 1 | [{"position":null,"query":"twitter","created_at":"Tue Oct 26 21:49:01 +0000 2010","id_str":"16129012","name":"twitter","id":16129012}] -------------------------------------------------------------------------------- /lib/twitter/media/animated_gif.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/media/video' 2 | 3 | module Twitter 4 | module Media 5 | class AnimatedGif < Video 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/account_activity_create_webhook.json: -------------------------------------------------------------------------------- 1 | { "id": "1234567890", "url": "https://your_domain.com/webhook/twitter", "valid": true, "created_at": "2016-06-02T23:54:02Z" } 2 | -------------------------------------------------------------------------------- /lib/twitter/target_user.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/basic_user' 2 | 3 | module Twitter 4 | class TargetUser < Twitter::BasicUser 5 | predicate_attr_reader :followed_by 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/chunk_upload_init.json: -------------------------------------------------------------------------------- 1 | {"media_id":710511363345354753,"media_id_string":"710511363345354753","size":10240,"expires_after_secs":86400,"video":{"video_type":"video/mp4"}} 2 | -------------------------------------------------------------------------------- /spec/fixtures/ids_list.json: -------------------------------------------------------------------------------- 1 | {"previous_cursor":0,"next_cursor_str":"1305102810874389703","ids":[20009713,22469930,351223419],"previous_cursor_str":"0","next_cursor":1305102810874389703} -------------------------------------------------------------------------------- /spec/fixtures/ids_list2.json: -------------------------------------------------------------------------------- 1 | {"previous_cursor":-1305101990888327757,"next_cursor_str":"0","ids":[14100886,23621851,14509199],"previous_cursor_str":"-1305101990888327757","next_cursor":0} -------------------------------------------------------------------------------- /lib/twitter/entity.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/base' 2 | 3 | module Twitter 4 | class Entity < Twitter::Base 5 | # @return [Array] 6 | attr_reader :indices 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/twitter/language.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/base' 2 | 3 | module Twitter 4 | class Language < Twitter::Base 5 | # @return [String] 6 | attr_reader :code, :name, :status 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/locations.json: -------------------------------------------------------------------------------- 1 | [{"url":"http://where.yahooapis.com/v1/place/23424803","woeid":23424803,"placeType":{"code":12,"name":"Country"},"name":"Ireland","country":"Ireland","countryCode":"IE"}] -------------------------------------------------------------------------------- /spec/fixtures/chunk_upload_finalize_succeeded.json: -------------------------------------------------------------------------------- 1 | {"media_id":710511363345354753,"media_id_string":"710511363345354753","size":11065,"expires_after_secs":86400,"video":{"video_type":"video/mp4"}} 2 | -------------------------------------------------------------------------------- /lib/twitter/streaming/stall_warning.rb: -------------------------------------------------------------------------------- 1 | module Twitter 2 | module Streaming 3 | class StallWarning < Twitter::Base 4 | attr_reader :code, :message, :percent_full 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/ids_list_new_cursor.json: -------------------------------------------------------------------------------- 1 | {"previous_cursor":0,"next_cursor_str":"1305102810874389703","ids":[20009713,22469930,351223419],"previous_cursor_str":"0","next_cursor":"ODU2NDc3NzEwNTk1NjI0OTYz"} 2 | -------------------------------------------------------------------------------- /lib/twitter/metadata.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/base' 2 | 3 | module Twitter 4 | class Metadata < Twitter::Base 5 | # @return [String] 6 | attr_reader :iso_language_code, :result_type 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/twitter/streaming/deleted_tweet.rb: -------------------------------------------------------------------------------- 1 | module Twitter 2 | module Streaming 3 | class DeletedTweet < Twitter::Identity 4 | # @return [Integer] 5 | attr_reader :user_id 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/fixtures/chunk_upload_finalize_pending.json: -------------------------------------------------------------------------------- 1 | {"media_id":710511363345354753,"media_id_string":"710511363345354753","expires_after_secs":86400,"size":10240,"processing_info":{"state":"pending","check_after_secs":5}} 2 | -------------------------------------------------------------------------------- /spec/fixtures/welcome_message_rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome_message_rule": { 3 | "id": "1073279057817731072", 4 | "created_timestamp": "1544724642601", 5 | "welcome_message_id": "1073273784206012421" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /spec/fixtures/chunk_upload_status_pending.json: -------------------------------------------------------------------------------- 1 | {"media_id":710511363345354753,"media_id_string":"710511363345354753","expires_after_secs":3595,"processing_info":{"state":"in_progress","check_after_secs":10,"progress_percent":8}} 2 | -------------------------------------------------------------------------------- /lib/twitter/entity/symbol.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/entity' 2 | 3 | module Twitter 4 | class Entity 5 | class Symbol < Twitter::Entity 6 | # @return [String] 7 | attr_reader :text 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/twitter/entity/hashtag.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/entity' 2 | 3 | module Twitter 4 | class Entity 5 | class Hashtag < Twitter::Entity 6 | # @return [String] 7 | attr_reader :text 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/fixtures/chunk_upload_status_succeeded.json: -------------------------------------------------------------------------------- 1 | {"media_id":710511363345354753,"media_id_string":"710511363345354753","expires_after_secs":3593,"video":{"video_type":"video/mp4"},"processing_info":{"state":"succeeded","progress_percent":100}} 2 | -------------------------------------------------------------------------------- /spec/fixtures/friendships.json: -------------------------------------------------------------------------------- 1 | [{"id_str":"7505382","name":"Erik Berlin","screen_name":"sferik","id":7505382,"connections":["none"]},{"id_str":"14100886","name":"Wynn Netherland","screen_name":"pengwynn","id":14100886,"connections":["followed_by"]}] -------------------------------------------------------------------------------- /lib/twitter/entity/uri.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/entity' 2 | 3 | module Twitter 4 | class Entity 5 | class URI < Twitter::Entity 6 | display_uri_attr_reader 7 | uri_attr_reader :expanded_uri, :uri 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/fixtures/account_activity_list_webhook.json: -------------------------------------------------------------------------------- 1 | { "environments": [ { "environment_name": "env_name", "webhooks": [ { "id": "1234567890", "url": "https://your_domain.com/webhook/twitter", "valid": true, "created_at": "2017-06-02T23:54:02Z" } ] } ] } 2 | -------------------------------------------------------------------------------- /spec/fixtures/chunk_upload_status_failed.json: -------------------------------------------------------------------------------- 1 | {"media_id":710511363345354753,"media_id_string":"710511363345354753","processing_info":{"state":"failed","progress_percent":12,"error":{"code":1,"name":"InvalidMedia","message":"Unsupported video format"}}} 2 | -------------------------------------------------------------------------------- /spec/fixtures/request_token.txt: -------------------------------------------------------------------------------- 1 | OAuth oauth_signature="AAAAAAAAA", 2 | oauth_timestamp="1380473364", oauth_version="1.0", 3 | oauth_token="BBBBBBBBB", 4 | oauth_signature_method="HMAC-SHA1", 5 | oauth_consumer_key="CCCCCCCCCCC", 6 | oauth_nonce="DDDDDDDDDDD" 7 | -------------------------------------------------------------------------------- /spec/fixtures/welcome_message_rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome_message_rules": [ 3 | { 4 | "id": "1073279057817731072", 5 | "created_timestamp": "1544724642601", 6 | "welcome_message_id": "1073273784206012421" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /spec/fixtures/search_malformed.json: -------------------------------------------------------------------------------- 1 | {"max_id":28857935752,"since_id":0,"refresh_url":"?since_id=28857935752&q=twitter","next_page":"?page=2&max_id=28857935752&q=twitter","count":15,"page":1,"completed_in":0.017349,"since_id_str":"0","max_id_str":"28857935752","query":"twitter"} -------------------------------------------------------------------------------- /lib/twitter/basic_user.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/identity' 2 | require 'twitter/utils' 3 | 4 | module Twitter 5 | class BasicUser < Twitter::Identity 6 | # @return [String] 7 | attr_reader :screen_name 8 | predicate_attr_reader :following 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/twitter/variant.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/base' 2 | 3 | module Twitter 4 | class Variant < Twitter::Base 5 | # @return [Integer] 6 | attr_reader :bitrate 7 | 8 | # @return [String] 9 | attr_reader :content_type 10 | uri_attr_reader :uri 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/twitter/geo.rb: -------------------------------------------------------------------------------- 1 | require 'equalizer' 2 | require 'twitter/base' 3 | 4 | module Twitter 5 | class Geo < Twitter::Base 6 | include Equalizer.new(:coordinates) 7 | # @return [Array] 8 | attr_reader :coordinates 9 | alias coords coordinates 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/twitter/saved_search.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/creatable' 2 | require 'twitter/identity' 3 | 4 | module Twitter 5 | class SavedSearch < Twitter::Identity 6 | include Twitter::Creatable 7 | # @return [String] 8 | attr_reader :name, :position, :query 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | rubocop: 3 | enabled: true 4 | duplication: 5 | enabled: true 6 | config: 7 | languages: 8 | - ruby 9 | 10 | ratings: 11 | paths: 12 | - "**.rb" 13 | 14 | exclude_paths: 15 | - etc/**/* 16 | - examples/**/* 17 | - spec/**/* 18 | -------------------------------------------------------------------------------- /lib/twitter/entity/user_mention.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/entity' 2 | 3 | module Twitter 4 | class Entity 5 | class UserMention < Twitter::Entity 6 | # @return [Integer] 7 | attr_reader :id 8 | # @return [String] 9 | attr_reader :name, :screen_name 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/fixtures/direct_message_event.json: -------------------------------------------------------------------------------- 1 | {"event":{"type":"message_create","id":"1006278767680131076","created_timestamp":"1528750528627","message_create":{"target":{"recipient_id":"58983"},"sender_id":"124294236","message_data":{"text":"testing","entities":{"hashtags":[],"symbols":[],"user_mentions":[],"urls":[]}}}}} 2 | -------------------------------------------------------------------------------- /spec/twitter/streaming/friend_list_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Streaming::FriendList do 4 | it 'is an array' do 5 | friend_list = Twitter::Streaming::FriendList.new([1, 2, 3]) 6 | expect(friend_list).to be_an Array 7 | expect(friend_list.first).to eq(1) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/twitter/size.rb: -------------------------------------------------------------------------------- 1 | require 'equalizer' 2 | require 'twitter/base' 3 | 4 | module Twitter 5 | class Size < Twitter::Base 6 | include Equalizer.new(:h, :w) 7 | # @return [Integer] 8 | attr_reader :h, :w 9 | # @return [String] 10 | attr_reader :resize 11 | alias height h 12 | alias width w 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/twitter/source_user.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/basic_user' 2 | 3 | module Twitter 4 | class SourceUser < Twitter::BasicUser 5 | predicate_attr_reader :all_replies, :blocking, :can_dm, :followed_by, 6 | :marked_spam, :muting, :notifications_enabled, 7 | :want_retweets 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/twitter/trend.rb: -------------------------------------------------------------------------------- 1 | require 'equalizer' 2 | require 'twitter/base' 3 | 4 | module Twitter 5 | class Trend < Twitter::Base 6 | include Equalizer.new(:name) 7 | # @return [String] 8 | attr_reader :events, :name, :query, :tweet_volume 9 | predicate_attr_reader :promoted_content 10 | uri_attr_reader :uri 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/fixtures/not_following.json: -------------------------------------------------------------------------------- 1 | {"relationship":{"target":{"followed_by":false,"id_str":"14100886","following":true,"screen_name":"sferik","id":7505382},"source":{"marked_spam":false,"notifications_enabled":false,"followed_by":true,"want_retweets":true,"id_str":"7505382","blocking":false,"all_replies":false,"following":false,"screen_name":"pengwynn","id":14100886}}} -------------------------------------------------------------------------------- /lib/twitter/direct_messages/welcome_message_rule.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/creatable' 2 | require 'twitter/identity' 3 | 4 | module Twitter 5 | module DirectMessages 6 | class WelcomeMessageRule < Twitter::Identity 7 | include Twitter::Creatable 8 | # @return [Integer] 9 | attr_reader :welcome_message_id 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/fixtures/following.json: -------------------------------------------------------------------------------- 1 | {"relationship":{"target":{"followed_by":true,"id_str":"14100886","following":false,"screen_name":"pengwynn","id":14100886},"source":{"marked_spam":false,"notifications_enabled":false,"muting":false,"followed_by":false,"want_retweets":true,"id_str":"7505382","blocking":false,"all_replies":false,"following":true,"screen_name":"sferik","id":7505382}}} -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | --protected 3 | --tag rate_limited:"Rate Limited?" 4 | --tag authentication:"Authentication" 5 | --markup markdown 6 | - 7 | CHANGELOG.md 8 | CONTRIBUTING.md 9 | LICENSE.md 10 | README.md 11 | examples/AllTweets.md 12 | examples/Configuration.md 13 | examples/RateLimiting.md 14 | examples/Search.md 15 | examples/Streaming.md 16 | examples/Update.md 17 | -------------------------------------------------------------------------------- /lib/twitter/arguments.rb: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class Arguments < Array 3 | # @return [Hash] 4 | attr_reader :options 5 | 6 | # Initializes a new Arguments object 7 | # 8 | # @return [Twitter::Arguments] 9 | def initialize(args) 10 | @options = args.last.is_a?(::Hash) ? args.pop : {} 11 | super(args.flatten) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/twitter/oembed.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/base' 2 | 3 | module Twitter 4 | class OEmbed < Twitter::Base 5 | # @return [Integer] 6 | attr_reader :height, :width 7 | # @return [String] 8 | attr_reader :author_name, :cache_age, :html, :provider_name, :type, 9 | :version 10 | uri_attr_reader :author_uri, :provider_uri, :uri 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'jruby-openssl', platforms: :jruby 4 | gem 'rake' 5 | gem 'yard' 6 | 7 | group :development do 8 | gem 'pry' 9 | end 10 | 11 | group :test do 12 | gem 'coveralls', '>= 0.8.23' 13 | gem 'rspec', '>= 2.14' 14 | gem 'rubocop', '>= 0.46' 15 | gem 'simplecov', '>= 0.16' 16 | gem 'timecop' 17 | gem 'webmock' 18 | gem 'yardstick' 19 | end 20 | 21 | gemspec 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | cache: bundler 2 | 3 | language: ruby 4 | 5 | rvm: 6 | - 2.4 7 | - 2.5 8 | - 2.6 9 | - 2.7 10 | - jruby-head 11 | - ruby-head 12 | 13 | sudo: false 14 | 15 | bundler_args: --without development --retry=3 --jobs=3 16 | 17 | env: 18 | global: 19 | - JRUBY_OPTS="$JRUBY_OPTS --debug" 20 | 21 | matrix: 22 | allow_failures: 23 | - rvm: jruby-head 24 | - rvm: ruby-head 25 | fast_finish: true 26 | -------------------------------------------------------------------------------- /lib/twitter/geo/point.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/geo' 2 | 3 | module Twitter 4 | class Geo 5 | class Point < Twitter::Geo 6 | # @return [Integer] 7 | def latitude 8 | coordinates[0] 9 | end 10 | alias lat latitude 11 | 12 | # @return [Integer] 13 | def longitude 14 | coordinates[1] 15 | end 16 | alias long longitude 17 | alias lng longitude 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/twitter/relationship.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/base' 2 | 3 | module Twitter 4 | class Relationship < Twitter::Base 5 | object_attr_reader :SourceUser, :source 6 | object_attr_reader :TargetUser, :target 7 | 8 | # Initializes a new object 9 | # 10 | # @param attrs [Hash] 11 | # @return [Twitter::Relationship] 12 | def initialize(attrs = {}) 13 | @attrs = attrs[:relationship] 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/twitter/profile_banner.rb: -------------------------------------------------------------------------------- 1 | require 'memoizable' 2 | require 'twitter/base' 3 | 4 | module Twitter 5 | class ProfileBanner < Twitter::Base 6 | include Memoizable 7 | 8 | # Returns an array of photo sizes 9 | # 10 | # @return [Array] 11 | def sizes 12 | @attrs.fetch(:sizes, []).each_with_object({}) do |(key, value), object| 13 | object[key] = Size.new(value) 14 | end 15 | end 16 | memoize :sizes 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/twitter/direct_messages/welcome_message.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/creatable' 2 | require 'twitter/entities' 3 | require 'twitter/identity' 4 | 5 | module Twitter 6 | module DirectMessages 7 | class WelcomeMessage < Twitter::Identity 8 | include Twitter::Creatable 9 | include Twitter::Entities 10 | # @return [String] 11 | attr_reader :text 12 | # @return [String] 13 | attr_reader :name 14 | alias full_text text 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /examples/Streaming.md: -------------------------------------------------------------------------------- 1 | # Streaming 2 | 3 | This example assumes you have a configured Twitter Streaming `client`. 4 | Instructions on how to configure a client can be found in 5 | [examples/Configuration.md][cfg]. 6 | 7 | [cfg]: https://github.com/sferik/twitter/blob/master/examples/Configuration.md 8 | 9 | Here's a simple example of how to stream tweets from San Francisco: 10 | 11 | ```ruby 12 | client.filter(locations: "-122.75,36.8,-121.75,37.8") do |tweet| 13 | puts tweet.text 14 | end 15 | ``` 16 | -------------------------------------------------------------------------------- /lib/twitter/direct_message.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/creatable' 2 | require 'twitter/entities' 3 | require 'twitter/identity' 4 | 5 | module Twitter 6 | class DirectMessage < Twitter::Identity 7 | include Twitter::Creatable 8 | include Twitter::Entities 9 | # @return [String] 10 | attr_reader :text 11 | attr_reader :sender_id 12 | attr_reader :recipient_id 13 | alias full_text text 14 | object_attr_reader :User, :recipient 15 | object_attr_reader :User, :sender 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/twitter/media/photo_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Media::Photo do 4 | it_behaves_like 'a Twitter::Media object' 5 | 6 | describe '#type' do 7 | it 'returns true when the type is set' do 8 | photo = Twitter::Media::Photo.new(id: 1, type: 'photo') 9 | expect(photo.type).to eq('photo') 10 | end 11 | it 'returns false when the type is not set' do 12 | photo = Twitter::Media::Photo.new(id: 1) 13 | expect(photo.type).to be_nil 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/twitter/profile_banner_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::ProfileBanner do 4 | describe '#sizes' do 5 | it 'returns a hash of Sizes when sizes is set' do 6 | sizes = Twitter::ProfileBanner.new(sizes: {small: {h: 226, w: 340, resize: 'fit'}, large: {h: 466, w: 700, resize: 'fit'}, medium: {h: 399, w: 600, resize: 'fit'}, thumb: {h: 150, w: 150, resize: 'crop'}}).sizes 7 | expect(sizes).to be_a Hash 8 | expect(sizes[:small]).to be_a Twitter::Size 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/twitter/geo_factory.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/factory' 2 | require 'twitter/geo/point' 3 | require 'twitter/geo/polygon' 4 | 5 | module Twitter 6 | class GeoFactory < Twitter::Factory 7 | class << self 8 | # Construct a new geo object 9 | # 10 | # @param attrs [Hash] 11 | # @raise [IndexError] Error raised when supplied argument is missing a :type key. 12 | # @return [Twitter::Geo] 13 | def new(attrs = {}) 14 | super(:type, Geo, attrs) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/twitter/identity.rb: -------------------------------------------------------------------------------- 1 | require 'equalizer' 2 | require 'twitter/base' 3 | 4 | module Twitter 5 | class Identity < Twitter::Base 6 | include Equalizer.new(:id) 7 | # @return [Integer] 8 | attr_reader :id 9 | 10 | # Initializes a new object 11 | # 12 | # @param attrs [Hash] 13 | # @raise [ArgumentError] Error raised when supplied argument is missing an :id key. 14 | # @return [Twitter::Identity] 15 | def initialize(attrs = {}) 16 | attrs.fetch(:id) 17 | super 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Examples Index 2 | ==================== 3 | 4 | - [All Tweets](AllTweets.md) 5 | - [Configuration](Configuration.md#configuration) 6 | - [Application-only Authentication](Configuration.md#application-only-authentication) 7 | - [Single-user Authentication](Configuration.md#single-user-authentication) 8 | - [Streaming Clients](Configuration.md#streaming-clients) 9 | - [Rate Limits](RateLimiting.md#rate-limits) 10 | - [Search](Search.md#search) 11 | - [Streaming](Streaming.md#streaming) 12 | - [Update (Tweet)](Update.md#update) 13 | -------------------------------------------------------------------------------- /lib/twitter/rest/client.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/client' 2 | require 'twitter/rest/api' 3 | require 'twitter/rest/request' 4 | require 'twitter/rest/utils' 5 | 6 | module Twitter 7 | module REST 8 | class Client < Twitter::Client 9 | include Twitter::REST::API 10 | attr_accessor :bearer_token 11 | 12 | # @return [Boolean] 13 | def bearer_token? 14 | !!bearer_token 15 | end 16 | 17 | # @return [Boolean] 18 | def credentials? 19 | super || bearer_token? 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/twitter/suggestion.rb: -------------------------------------------------------------------------------- 1 | require 'equalizer' 2 | require 'memoizable' 3 | require 'twitter/base' 4 | 5 | module Twitter 6 | class Suggestion < Twitter::Base 7 | include Equalizer.new(:slug) 8 | include Memoizable 9 | 10 | # @return [Integer] 11 | attr_reader :size 12 | # @return [String] 13 | attr_reader :name, :slug 14 | 15 | # @return [Array] 16 | def users 17 | @attrs.fetch(:users, []).collect do |user| 18 | User.new(user) 19 | end 20 | end 21 | memoize :users 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /examples/Search.md: -------------------------------------------------------------------------------- 1 | # Search 2 | 3 | This example assumes you have a configured Twitter REST `client`. Instructions 4 | on how to configure a client can be found in [examples/Configuration.md][cfg]. 5 | 6 | [cfg]: https://github.com/sferik/twitter/blob/master/examples/Configuration.md 7 | 8 | Here's a simple example of how to search for tweets. This query will return the 9 | three most recent marriage proposals to @justinbieber. 10 | 11 | ```ruby 12 | client.search("to:justinbieber marry me", result_type: "recent").take(3).each do |tweet| 13 | puts tweet.text 14 | end 15 | ``` 16 | -------------------------------------------------------------------------------- /lib/twitter/media_factory.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/factory' 2 | require 'twitter/media/animated_gif' 3 | require 'twitter/media/photo' 4 | require 'twitter/media/video' 5 | 6 | module Twitter 7 | class MediaFactory < Twitter::Factory 8 | class << self 9 | # Construct a new media object 10 | # 11 | # @param attrs [Hash] 12 | # @raise [IndexError] Error raised when supplied argument is missing a :type key. 13 | # @return [Twitter::Media] 14 | def new(attrs = {}) 15 | super(:type, Media, attrs) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/twitter/geo_factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::GeoFactory do 4 | describe '.new' do 5 | it 'generates a Point' do 6 | geo = Twitter::GeoFactory.new(type: 'Point') 7 | expect(geo).to be_a Twitter::Geo::Point 8 | end 9 | it 'generates a Polygon' do 10 | geo = Twitter::GeoFactory.new(type: 'Polygon') 11 | expect(geo).to be_a Twitter::Geo::Polygon 12 | end 13 | it 'raises an IndexError when type is not specified' do 14 | expect { Twitter::GeoFactory.new }.to raise_error(IndexError) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/twitter/creatable.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | require 'memoizable' 3 | 4 | module Twitter 5 | module Creatable 6 | include Memoizable 7 | 8 | # Time when the object was created on Twitter 9 | # 10 | # @return [Time] 11 | def created_at 12 | time = @attrs[:created_at] 13 | return if time.nil? 14 | 15 | time = Time.parse(time) unless time.is_a?(Time) 16 | time.utc 17 | end 18 | memoize :created_at 19 | 20 | # @return [Boolean] 21 | def created? 22 | !!@attrs[:created_at] 23 | end 24 | memoize :created? 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/fixtures/settings.json: -------------------------------------------------------------------------------- 1 | {"time_zone":{"name":"Berlin","utc_offset":7200,"tzinfo_name":"Europe\/Berlin"},"protected":false,"screen_name":"sferik","always_use_https":true,"use_cookie_personalization":true,"sleep_time":{"enabled":false,"end_time":null,"start_time":null},"geo_enabled":true,"language":"en","discoverable_by_email":true,"discoverable_by_mobile_phone":true,"display_sensitive_media":true,"trend_location":[{"name":"San Francisco","countryCode":"US","url":"http:\/\/where.yahooapis.com\/v1\/place\/2487956","woeid":2487956,"placeType":{"name":"Town","code":7},"parentid":23424977,"country":"United States"}]} -------------------------------------------------------------------------------- /lib/twitter/factory.rb: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class Factory 3 | class << self 4 | # Construct a new object 5 | # 6 | # @param method [Symbol] 7 | # @param klass [Class] 8 | # @param attrs [Hash] 9 | # @raise [IndexError] Error raised when supplied argument is missing a key. 10 | # @return [Twitter::Base] 11 | def new(method, klass, attrs = {}) 12 | type = attrs.fetch(method.to_sym) 13 | const_name = type.split('_').collect(&:capitalize).join 14 | klass.const_get(const_name.to_sym).new(attrs) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/fixtures/languages.json: -------------------------------------------------------------------------------- 1 | [{"name":"Portuguese","status":"production","code":"pt"},{"name":"Indonesian","status":"production","code":"id"},{"name":"Italian","status":"production","code":"it"},{"name":"Spanish","status":"production","code":"es"},{"name":"Turkish","status":"production","code":"tr"},{"name":"English","status":"production","code":"en"},{"name":"Korean","status":"production","code":"ko"},{"name":"French","status":"production","code":"fr"},{"name":"Dutch","status":"production","code":"nl"},{"name":"Russian","status":"production","code":"ru"},{"name":"German","status":"production","code":"de"},{"name":"Japanese","status":"production","code":"ja"}] -------------------------------------------------------------------------------- /spec/fixtures/welcome_message.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome_message": { 3 | "id": "1073273784206012421", 4 | "created_timestamp": "1544723385274", 5 | "message_data": { 6 | "text": "Welcome message text", 7 | "entities": { 8 | "hashtags": [ 9 | 10 | ], 11 | "symbols": [ 12 | 13 | ], 14 | "user_mentions": [ 15 | 16 | ], 17 | "urls": [ 18 | 19 | ] 20 | } 21 | }, 22 | "source_app_id": "1111111" 23 | }, 24 | "apps": { 25 | "1111111": { 26 | "id": "1111111", 27 | "name": "Foobar", 28 | "url": "http://example.com" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/twitter/media/video_info.rb: -------------------------------------------------------------------------------- 1 | require 'memoizable' 2 | require 'twitter/variant' 3 | 4 | module Twitter 5 | module Media 6 | class VideoInfo < Twitter::Base 7 | include Memoizable 8 | 9 | # @return [Array] 18 | def variants 19 | @attrs.fetch(:variants, []).collect do |variant| 20 | Variant.new(variant) 21 | end 22 | end 23 | memoize :variants 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/fixtures/profile_banner.json: -------------------------------------------------------------------------------- 1 | {"sizes":{"mobile_retina":{"h":320,"w":640,"url":"https:\/\/si0.twimg.com\/profile_banners\/7505382\/1349499693\/mobile_retina"},"ipad":{"h":313,"w":626,"url":"https:\/\/si0.twimg.com\/profile_banners\/7505382\/1349499693\/ipad"},"mobile":{"h":160,"w":320,"url":"https:\/\/si0.twimg.com\/profile_banners\/7505382\/1349499693\/mobile"},"ipad_retina":{"h":626,"w":1252,"url":"https:\/\/si0.twimg.com\/profile_banners\/7505382\/1349499693\/ipad_retina"},"web_retina":{"h":520,"w":1040,"url":"https:\/\/si0.twimg.com\/profile_banners\/7505382\/1349499693\/web_retina"},"web":{"h":260,"w":520,"url":"https:\/\/si0.twimg.com\/profile_banners\/7505382\/1349499693\/web"}}} -------------------------------------------------------------------------------- /spec/fixtures/ids.json: -------------------------------------------------------------------------------- 1 | [47,48431692,1438261,949521,12241752,780561,63846421,12025282,97715094,14100886,110520327,666073,3191321,8285392,80983,15209501,14239131,23828637,74543,158396884,823408,721623,14353952,8033832,68753655,46123040,15866539,186116459,30364057,183749519,2404341,5728652,17293897,16017475,7440462,14328758,15263394,2735631,14990751,102782288,22699508,6707392,10230812,10255262,6238622,224,11620792,687613,9380652,125416024,125144921,14881422,51165048,10273252,15792047,5502392,2049071,67207650,792690,774010,3468841,9980812,14471007,9267332,2379441,13370272,10583402,6253282,8526432,13192,14372143,779112,6834002,14163141,7890522,14561327,12520552,18713,59593,641433,10609,528,30923,1919231,33423,10113122,813286,783214] -------------------------------------------------------------------------------- /spec/twitter/streaming/response_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Streaming::Response do 4 | subject { Twitter::Streaming::Response.new } 5 | 6 | describe '#on_headers_complete' do 7 | it 'should not error if status code is 200' do 8 | expect do 9 | subject << "HTTP/1.1 200 OK\r\nSome-Header: Woo\r\n\r\n" 10 | end.to_not raise_error 11 | end 12 | 13 | Twitter::Error::ERRORS.each do |code, klass| 14 | it "should raise an exception of type #{klass} for status code #{code}" do 15 | expect do 16 | subject << "HTTP/1.1 #{code} NOK\r\nSome-Header: Woo\r\n\r\n" 17 | end.to raise_error(klass) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/fixtures/welcome_message_with_name.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome_message": { 3 | "id": "1073276982106996741", 4 | "created_timestamp": "1544724147713", 5 | "message_data": { 6 | "text": "A second welcome message with a name", 7 | "entities": { 8 | "hashtags": [ 9 | 10 | ], 11 | "symbols": [ 12 | 13 | ], 14 | "user_mentions": [ 15 | 16 | ], 17 | "urls": [ 18 | 19 | ] 20 | } 21 | }, 22 | "source_app_id": "1111111", 23 | "name": "welcome_message_name" 24 | }, 25 | "apps": { 26 | "1111111": { 27 | "id": "1111111", 28 | "name": "Foobar", 29 | "url": "http://example.com" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /spec/twitter/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Configuration do 4 | describe '#photo_sizes' do 5 | it 'returns a hash of sizes when photo_sizes is set' do 6 | photo_sizes = Twitter::Configuration.new(photo_sizes: {small: {h: 226, w: 340, resize: 'fit'}, large: {h: 466, w: 700, resize: 'fit'}, medium: {h: 399, w: 600, resize: 'fit'}, thumb: {h: 150, w: 150, resize: 'crop'}}).photo_sizes 7 | expect(photo_sizes).to be_a Hash 8 | expect(photo_sizes[:small]).to be_a Twitter::Size 9 | end 10 | it 'is empty when photo_sizes is not set' do 11 | photo_sizes = Twitter::Configuration.new.photo_sizes 12 | expect(photo_sizes).to be_empty 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /examples/RateLimiting.md: -------------------------------------------------------------------------------- 1 | # Rate Limits 2 | 3 | This example assumes you have a configured Twitter REST `client`. Instructions 4 | on how to configure a client can be found in [examples/Configuration.md][cfg]. 5 | 6 | [cfg]: https://github.com/sferik/twitter/blob/master/examples/Configuration.md 7 | 8 | Here's an example of how to handle rate limits: 9 | 10 | ```ruby 11 | follower_ids = client.follower_ids('justinbieber') 12 | begin 13 | follower_ids.to_a 14 | rescue Twitter::Error::TooManyRequests => error 15 | # NOTE: Your process could go to sleep for up to 15 minutes but if you 16 | # retry any sooner, it will almost certainly fail with the same exception. 17 | sleep error.rate_limit.reset_in + 1 18 | retry 19 | end 20 | ``` 21 | -------------------------------------------------------------------------------- /lib/twitter/geo_results.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/enumerable' 2 | require 'twitter/utils' 3 | 4 | module Twitter 5 | class GeoResults 6 | include Twitter::Enumerable 7 | include Twitter::Utils 8 | # @return [Hash] 9 | attr_reader :attrs 10 | alias to_h attrs 11 | alias to_hash to_h 12 | 13 | # Initializes a new GeoResults object 14 | # 15 | # @param attrs [Hash] 16 | # @return [Twitter::GeoResults] 17 | def initialize(attrs = {}) 18 | @attrs = attrs 19 | @collection = @attrs[:result].fetch(:places, []).collect do |place| 20 | Place.new(place) 21 | end 22 | end 23 | 24 | # @return [String] 25 | def token 26 | @attrs[:token] 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/twitter/media/photo.rb: -------------------------------------------------------------------------------- 1 | require 'memoizable' 2 | require 'twitter/identity' 3 | 4 | module Twitter 5 | module Media 6 | class Photo < Twitter::Identity 7 | include Memoizable 8 | 9 | # @return [Array] 10 | attr_reader :indices 11 | 12 | # @return [String] 13 | attr_reader :type 14 | 15 | display_uri_attr_reader 16 | uri_attr_reader :expanded_uri, :media_uri, :media_uri_https, :uri 17 | 18 | # Returns an array of photo sizes 19 | # 20 | # @return [Array] 21 | def sizes 22 | @attrs.fetch(:sizes, []).each_with_object({}) do |(key, value), object| 23 | object[key] = Size.new(value) 24 | end 25 | end 26 | memoize :sizes 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/twitter/version.rb: -------------------------------------------------------------------------------- 1 | module Twitter 2 | module Version 3 | module_function 4 | 5 | # @return [Integer] 6 | def major 7 | 7 8 | end 9 | 10 | # @return [Integer] 11 | def minor 12 | 0 13 | end 14 | 15 | # @return [Integer] 16 | def patch 17 | 0 18 | end 19 | 20 | # @return [Integer, NilClass] 21 | def pre 22 | nil 23 | end 24 | 25 | # @return [Hash] 26 | def to_h 27 | { 28 | major: major, 29 | minor: minor, 30 | patch: patch, 31 | pre: pre, 32 | } 33 | end 34 | 35 | # @return [Array] 36 | def to_a 37 | [major, minor, patch, pre].compact 38 | end 39 | 40 | # @return [String] 41 | def to_s 42 | to_a.join('.') 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/twitter/media_factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::MediaFactory do 4 | describe '.new' do 5 | it 'generates a photo' do 6 | media = Twitter::MediaFactory.new(id: 1, type: 'photo') 7 | expect(media).to be_a Twitter::Media::Photo 8 | end 9 | it 'generates an animated GIF' do 10 | media = Twitter::MediaFactory.new(id: 1, type: 'animated_gif') 11 | expect(media).to be_a Twitter::Media::AnimatedGif 12 | end 13 | it 'generates a video' do 14 | media = Twitter::MediaFactory.new(id: 1, type: 'video') 15 | expect(media).to be_a Twitter::Media::Video 16 | end 17 | it 'raises an IndexError when type is not specified' do 18 | expect { Twitter::MediaFactory.new }.to raise_error(IndexError) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | task :erd do 5 | FORMAT = 'svg'.freeze 6 | `bundle exec ruby ./etc/erd.rb > ./etc/erd.dot` 7 | `dot -T #{FORMAT} ./etc/erd.dot -o ./etc/erd.#{FORMAT}` 8 | end 9 | 10 | require 'rspec/core/rake_task' 11 | RSpec::Core::RakeTask.new(:spec) 12 | 13 | task test: :spec 14 | 15 | require 'rubocop/rake_task' 16 | RuboCop::RakeTask.new 17 | 18 | require 'yard' 19 | YARD::Rake::YardocTask.new 20 | 21 | require 'yardstick/rake/measurement' 22 | Yardstick::Rake::Measurement.new do |measurement| 23 | measurement.output = 'measurement/report.txt' 24 | end 25 | 26 | require 'yardstick/rake/verify' 27 | Yardstick::Rake::Verify.new do |verify| 28 | verify.threshold = 57.8 29 | end 30 | 31 | task default: %i[spec rubocop verify_measurements] 32 | -------------------------------------------------------------------------------- /lib/twitter/enumerable.rb: -------------------------------------------------------------------------------- 1 | module Twitter 2 | module Enumerable 3 | include ::Enumerable 4 | 5 | # @return [Enumerator] 6 | def each(start = 0, &block) 7 | return to_enum(:each, start) unless block_given? 8 | 9 | Array(@collection[start..-1]).each do |element| 10 | yield(element) 11 | end 12 | unless finished? 13 | start = [@collection.size, start].max 14 | fetch_next_page 15 | each(start, &block) 16 | end 17 | self 18 | end 19 | 20 | private 21 | 22 | # @return [Boolean] 23 | def last? 24 | true 25 | end 26 | 27 | # @return [Boolean] 28 | def reached_limit? 29 | false 30 | end 31 | 32 | # @return [Boolean] 33 | def finished? 34 | last? || reached_limit? 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/twitter/basic_user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::BasicUser do 4 | describe '#==' do 5 | it 'returns true when objects IDs are the same' do 6 | saved_search = Twitter::BasicUser.new(id: 1, name: 'foo') 7 | other = Twitter::BasicUser.new(id: 1, name: 'bar') 8 | expect(saved_search == other).to be true 9 | end 10 | it 'returns false when objects IDs are different' do 11 | saved_search = Twitter::BasicUser.new(id: 1) 12 | other = Twitter::BasicUser.new(id: 2) 13 | expect(saved_search == other).to be false 14 | end 15 | it 'returns false when classes are different' do 16 | saved_search = Twitter::BasicUser.new(id: 1) 17 | other = Twitter::Identity.new(id: 1) 18 | expect(saved_search == other).to be false 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/twitter/rate_limit.rb: -------------------------------------------------------------------------------- 1 | require 'memoizable' 2 | 3 | module Twitter 4 | class RateLimit < Twitter::Base 5 | include Memoizable 6 | 7 | # @return [Integer] 8 | def limit 9 | limit = @attrs['x-rate-limit-limit'] 10 | limit&.to_i 11 | end 12 | memoize :limit 13 | 14 | # @return [Integer] 15 | def remaining 16 | remaining = @attrs['x-rate-limit-remaining'] 17 | remaining&.to_i 18 | end 19 | memoize :remaining 20 | 21 | # @return [Time] 22 | def reset_at 23 | reset = @attrs['x-rate-limit-reset'] 24 | Time.at(reset.to_i).utc if reset 25 | end 26 | memoize :reset_at 27 | 28 | # @return [Integer] 29 | def reset_in 30 | [(reset_at - Time.now).ceil, 0].max if reset_at 31 | end 32 | alias retry_after reset_in 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/twitter/source_user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::SourceUser do 4 | describe '#==' do 5 | it 'returns true when objects IDs are the same' do 6 | saved_search = Twitter::SourceUser.new(id: 1, name: 'foo') 7 | other = Twitter::SourceUser.new(id: 1, name: 'bar') 8 | expect(saved_search == other).to be true 9 | end 10 | it 'returns false when objects IDs are different' do 11 | saved_search = Twitter::SourceUser.new(id: 1) 12 | other = Twitter::SourceUser.new(id: 2) 13 | expect(saved_search == other).to be false 14 | end 15 | it 'returns false when classes are different' do 16 | saved_search = Twitter::SourceUser.new(id: 1) 17 | other = Twitter::Identity.new(id: 1) 18 | expect(saved_search == other).to be false 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/twitter/target_user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::TargetUser do 4 | describe '#==' do 5 | it 'returns true when objects IDs are the same' do 6 | saved_search = Twitter::TargetUser.new(id: 1, name: 'foo') 7 | other = Twitter::TargetUser.new(id: 1, name: 'bar') 8 | expect(saved_search == other).to be true 9 | end 10 | it 'returns false when objects IDs are different' do 11 | saved_search = Twitter::TargetUser.new(id: 1) 12 | other = Twitter::TargetUser.new(id: 2) 13 | expect(saved_search == other).to be false 14 | end 15 | it 'returns false when classes are different' do 16 | saved_search = Twitter::TargetUser.new(id: 1) 17 | other = Twitter::Identity.new(id: 1) 18 | expect(saved_search == other).to be false 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/fixtures/oembed.json: -------------------------------------------------------------------------------- 1 | {"cache_age":"3153600000","url":"https:\/\/twitter.com\/sferik\/statuses\/540897316908331009","height":null,"provider_url":"https:\/\/twitter.com","provider_name":"Twitter","author_name":"Erik Berlin","version":"1.0","author_url":"https:\/\/twitter.com\/sferik","type":"rich","html":"\u003Cblockquote class=\"twitter-tweet\"\u003E\u003Cp\u003EPowerful cartoon by \u003Ca href=\"https:\/\/twitter.com\/BillBramhall\"\u003E@BillBramhall\u003C\/a\u003E: \u003Ca href=\"http:\/\/t.co\/IOEbc5QoES\"\u003Epic.twitter.com\/IOEbc5QoES\u003C\/a\u003E\u003C\/p\u003E— Erik Berlin (@sferik) \u003Ca href=\"https:\/\/twitter.com\/sferik\/status\/540897316908331009\"\u003EDecember 5, 2014\u003C\/a\u003E\u003C\/blockquote\u003E\n\u003Cscript async src=\"\/\/platform.twitter.com\/widgets.js\" charset=\"utf-8\"\u003E\u003C\/script\u003E","width":550} -------------------------------------------------------------------------------- /spec/twitter/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Base do 4 | before do 5 | @base = Twitter::Base.new(id: 1) 6 | end 7 | 8 | describe '#[]' do 9 | it 'calls methods using [] with symbol' do 10 | capture_warning do 11 | expect(@base[:object_id]).to be_an Integer 12 | end 13 | end 14 | it 'calls methods using [] with string' do 15 | capture_warning do 16 | expect(@base['object_id']).to be_an Integer 17 | end 18 | end 19 | it 'returns nil for missing method' do 20 | capture_warning do 21 | expect(@base[:foo]).to be_nil 22 | expect(@base['foo']).to be_nil 23 | end 24 | end 25 | end 26 | 27 | describe '#attrs' do 28 | it 'returns a hash of attributes' do 29 | expect(@base.attrs).to eq(id: 1) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/twitter/settings.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/base' 2 | 3 | module Twitter 4 | class Settings < Twitter::Base 5 | # @return [Hash] 6 | attr_reader :sleep_time, :time_zone 7 | # @return [String] 8 | attr_reader :language, :screen_name 9 | object_attr_reader :Place, :trend_location 10 | predicate_attr_reader :allow_contributor_request, 11 | :allow_dm_groups_from, 12 | :allow_dms_from, 13 | :always_use_https, 14 | :discoverable_by_email, 15 | :discoverable_by_mobile_phone, 16 | :display_sensitive_media, 17 | :geo_enabled, 18 | :protected, 19 | :show_all_inline_media, 20 | :use_cookie_personalization 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/twitter/streaming/deleted_tweet_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Streaming::DeletedTweet do 4 | describe '#==' do 5 | it 'returns true when objects IDs are the same' do 6 | deleted_tweet = Twitter::Streaming::DeletedTweet.new(id: 1) 7 | other = Twitter::Streaming::DeletedTweet.new(id: 1) 8 | expect(deleted_tweet == other).to be true 9 | end 10 | it 'returns false when objects IDs are different' do 11 | deleted_tweet = Twitter::Streaming::DeletedTweet.new(id: 1) 12 | other = Twitter::Streaming::DeletedTweet.new(id: 2) 13 | expect(deleted_tweet == other).to be false 14 | end 15 | it 'returns false when classes are different' do 16 | deleted_tweet = Twitter::Streaming::DeletedTweet.new(id: 1) 17 | other = Twitter::Identity.new(id: 1) 18 | expect(deleted_tweet == other).to be false 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/twitter/configuration.rb: -------------------------------------------------------------------------------- 1 | require 'memoizable' 2 | require 'twitter/base' 3 | 4 | module Twitter 5 | class Configuration < Twitter::Base 6 | include Memoizable 7 | 8 | # @return [Array] 9 | attr_reader :non_username_paths 10 | # @return [Integer] 11 | attr_reader :characters_reserved_per_media, :dm_text_character_limit, 12 | :max_media_per_upload, :photo_size_limit, :short_url_length, 13 | :short_url_length_https 14 | alias short_uri_length short_url_length 15 | alias short_uri_length_https short_url_length_https 16 | 17 | # Returns an array of photo sizes 18 | # 19 | # @return [Array] 20 | def photo_sizes 21 | @attrs.fetch(:photo_sizes, []).each_with_object({}) do |(key, value), object| 22 | object[key] = Size.new(value) 23 | end 24 | end 25 | memoize :photo_sizes 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/fixtures/suggestions.json: -------------------------------------------------------------------------------- 1 | [{"size":56,"slug":"art-design","name":"Art & Design"},{"size":72,"slug":"books","name":"Books"},{"size":65,"slug":"business","name":"Business"},{"size":82,"slug":"charity","name":"Charity"},{"size":32,"slug":"deals-discounts","name":"Deals & Discounts"},{"size":125,"slug":"entertainment","name":"Entertainment"},{"size":55,"slug":"family","name":"Family"},{"size":56,"slug":"fashion","name":"Fashion"},{"size":81,"slug":"food-drink","name":"Food & Drink"},{"size":69,"slug":"funny","name":"Funny"},{"size":51,"slug":"health","name":"Health"},{"size":117,"slug":"music","name":"Music"},{"size":59,"slug":"news","name":"News"},{"size":81,"slug":"politics","name":"Politics"},{"size":53,"slug":"science","name":"Science"},{"size":114,"slug":"sports","name":"Sports"},{"size":96,"slug":"staff-picks","name":"Staff Picks"},{"size":60,"slug":"technology","name":"Technology"},{"size":56,"slug":"travel","name":"Travel"},{"size":16,"slug":"twitter","name":"Twitter"}] -------------------------------------------------------------------------------- /lib/twitter/streaming/response.rb: -------------------------------------------------------------------------------- 1 | require 'buftok' 2 | require 'http' 3 | require 'json' 4 | require 'twitter/error' 5 | 6 | module Twitter 7 | module Streaming 8 | class Response 9 | # Initializes a new Response object 10 | # 11 | # @return [Twitter::Streaming::Response] 12 | def initialize(&block) 13 | @block = block 14 | @parser = Http::Parser.new(self) 15 | @tokenizer = BufferedTokenizer.new("\r\n") 16 | end 17 | 18 | def <<(data) 19 | @parser << data 20 | end 21 | 22 | def on_headers_complete(_headers) 23 | error = Twitter::Error::ERRORS[@parser.status_code] 24 | raise error if error 25 | end 26 | 27 | def on_body(data) 28 | @tokenizer.extract(data).each do |line| 29 | next if line.empty? 30 | 31 | @block.call(JSON.parse(line, symbolize_names: true)) 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/twitter/media/video_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Media::Video do 4 | it_behaves_like 'a Twitter::Media object' 5 | 6 | describe '#video_info' do 7 | it 'returns a Twitter::Media::VideoInfo when the video is set' do 8 | video = Twitter::Media::Video.new(id: 1, video_info: {}) 9 | expect(video.video_info).to be_a Twitter::Media::VideoInfo 10 | end 11 | it 'returns nil when the display_url is not set' do 12 | video = Twitter::Media::Video.new(id: 1, video_info: nil) 13 | expect(video.video_info).to be_nil 14 | end 15 | end 16 | describe '#type' do 17 | it 'returns true when the type is set' do 18 | video = Twitter::Media::Video.new(id: 1, type: 'video') 19 | expect(video.type).to eq('video') 20 | end 21 | it 'returns false when the type is not set' do 22 | video = Twitter::Media::Video.new(id: 1) 23 | expect(video.type).to be_nil 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/twitter/identifiable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Identity do 4 | describe '#initialize' do 5 | it 'raises an IndexError when id is not specified' do 6 | expect { Twitter::Identity.new }.to raise_error(IndexError) 7 | end 8 | end 9 | 10 | describe '#==' do 11 | it 'returns true when objects IDs are the same' do 12 | one = Twitter::Identity.new(id: 1, screen_name: 'sferik') 13 | two = Twitter::Identity.new(id: 1, screen_name: 'garybernhardt') 14 | expect(one == two).to be true 15 | end 16 | it 'returns false when objects IDs are different' do 17 | one = Twitter::Identity.new(id: 1) 18 | two = Twitter::Identity.new(id: 2) 19 | expect(one == two).to be false 20 | end 21 | it 'returns false when classes are different' do 22 | one = Twitter::Identity.new(id: 1) 23 | two = Twitter::Base.new(id: 1) 24 | expect(one == two).to be false 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/twitter/size_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Size do 4 | describe '#==' do 5 | it 'returns true for empty objects' do 6 | size = Twitter::Size.new 7 | other = Twitter::Size.new 8 | expect(size == other).to be true 9 | end 10 | it 'returns true when objects height and width are the same' do 11 | size = Twitter::Size.new(h: 1, w: 1, resize: true) 12 | other = Twitter::Size.new(h: 1, w: 1, resize: false) 13 | expect(size == other).to be true 14 | end 15 | it 'returns false when objects height or width are different' do 16 | size = Twitter::Size.new(h: 1, w: 1) 17 | other = Twitter::Size.new(h: 1, w: 2) 18 | expect(size == other).to be false 19 | end 20 | it 'returns false when classes are different' do 21 | size = Twitter::Size.new(h: 1, w: 1) 22 | other = Twitter::Base.new(h: 1, w: 1) 23 | expect(size == other).to be false 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/twitter/rest/spam_reporting_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::REST::SpamReporting do 4 | before do 5 | @client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', access_token: 'AT', access_token_secret: 'AS') 6 | end 7 | 8 | describe '#report_spam' do 9 | before do 10 | stub_post('/1.1/users/report_spam.json').with(body: {screen_name: 'sferik'}).to_return(body: fixture('sferik.json'), headers: {content_type: 'application/json; charset=utf-8'}) 11 | end 12 | it 'requests the correct resource' do 13 | @client.report_spam('sferik') 14 | expect(a_post('/1.1/users/report_spam.json').with(body: {screen_name: 'sferik'})).to have_been_made 15 | end 16 | it 'returns an array of users' do 17 | users = @client.report_spam('sferik') 18 | expect(users).to be_an Array 19 | expect(users.first).to be_a Twitter::User 20 | expect(users.first.id).to eq(7_505_382) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/twitter/variant_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Variant do 4 | describe '#uri' do 5 | it 'returns a URI when the url is set' do 6 | variant = Twitter::Variant.new(id: 1, url: 'https://video.twimg.com/media/BQD6MPOCEAAbCH0.mp4') 7 | expect(variant.uri).to be_an Addressable::URI 8 | expect(variant.uri.to_s).to eq('https://video.twimg.com/media/BQD6MPOCEAAbCH0.mp4') 9 | end 10 | it 'returns nil when the url is not set' do 11 | variant = Twitter::Variant.new({}) 12 | expect(variant.uri).to be_nil 13 | end 14 | end 15 | 16 | describe '#uri?' do 17 | it 'returns true when the url is set' do 18 | variant = Twitter::Variant.new(id: 1, url: 'https://video.twimg.com/media/BQD6MPOCEAAbCH0.mp4') 19 | expect(variant.uri?).to be true 20 | end 21 | it 'returns false when the url is not set' do 22 | variant = Twitter::Variant.new({}) 23 | expect(variant.uri?).to be false 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/fixtures/user_search.json: -------------------------------------------------------------------------------- 1 | [{"geo_enabled":true,"time_zone":"Pacific Time (US & Canada)","description":"Adventures in hunger and foolishness.","profile_sidebar_fill_color":"DDEEF6","followers_count":898,"verified":false,"notifications":false,"follow_request_sent":false,"profile_use_background_image":true,"profile_sidebar_border_color":"C0DEED","url":null,"profile_background_image_url":"http:\/\/a3.twimg.com\/profile_background_images\/162641967\/we_concept_bg2.png","lang":"en","created_at":"Mon Jul 16 12:59:01 +0000 2007","profile_background_color":"000000","location":"San Francisco","listed_count":29,"profile_background_tile":false,"friends_count":88,"protected":false,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/323331048\/me_normal.jpg","statuses_count":2962,"profile_text_color":"333333","name":"Erik Berlin","show_all_inline_media":true,"following":true,"favourites_count":727,"screen_name":"sferik","id":7505382,"id_str":"7505382","contributors_enabled":false,"utc_offset":-28800,"profile_link_color":"0084B4"}] -------------------------------------------------------------------------------- /spec/twitter/media/animated_gif_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Media::AnimatedGif do 4 | it_behaves_like 'a Twitter::Media object' 5 | 6 | describe '#video_info' do 7 | it 'returns a Twitter::Media::VideoInfo when the video is set' do 8 | image = Twitter::Media::AnimatedGif.new(id: 1, video_info: {}) 9 | expect(image.video_info).to be_a Twitter::Media::VideoInfo 10 | end 11 | it 'returns nil when the display_url is not set' do 12 | image = Twitter::Media::AnimatedGif.new(id: 1, video_info: nil) 13 | expect(image.video_info).to be_nil 14 | end 15 | end 16 | describe '#type' do 17 | it 'returns true when the type is set' do 18 | image = Twitter::Media::AnimatedGif.new(id: 1, type: 'animated_gif') 19 | expect(image.type).to eq('animated_gif') 20 | end 21 | it 'returns false when the type is not set' do 22 | image = Twitter::Media::AnimatedGif.new(id: 1) 23 | expect(image.type).to be_nil 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/fixtures/place.json: -------------------------------------------------------------------------------- 1 | {"country_code":"US","place_type":"poi","url":"http:\/\/api.twitter.com\/1\/geo\/id\/247f43d441defc03.json","polylines":[],"country":"The United States of America","geometry":{"type":"Point","coordinates":[-122.400612831116,37.7821120598956]},"bounding_box":{"type":"Polygon","coordinates":[[[-122.400612831116,37.7821120598956],[-122.400612831116,37.7821120598956],[-122.400612831116,37.7821120598956],[-122.400612831116,37.7821120598956]]]},"attributes":{"street_address":"795 Folsom St","twitter":"twitter","623:id":"210176"},"full_name":"Twitter HQ, San Francisco","name":"Twitter HQ","id":"247f43d441defc03","contained_within":[{"country_code":"US","place_type":"city","url":"http:\/\/api.twitter.com\/1\/geo\/id\/5a110d312052166f.json","country":"The United States of America","bounding_box":{"type":"Polygon","coordinates":[[[-122.51368188,37.70813196],[-122.35845384,37.70813196],[-122.35845384,37.83245301],[-122.51368188,37.83245301]]]},"attributes":{},"full_name":"San Francisco, CA","name":"San Francisco","id":"5a110d312052166f"}]} -------------------------------------------------------------------------------- /lib/twitter/streaming/message_parser.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/direct_message' 2 | require 'twitter/streaming/deleted_tweet' 3 | require 'twitter/streaming/event' 4 | require 'twitter/streaming/friend_list' 5 | require 'twitter/streaming/stall_warning' 6 | require 'twitter/tweet' 7 | 8 | module Twitter 9 | module Streaming 10 | class MessageParser 11 | def self.parse(data) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity 12 | if data[:id] 13 | Tweet.new(data) 14 | elsif data[:event] 15 | Event.new(data) 16 | elsif data[:direct_message] 17 | DirectMessage.new(data[:direct_message]) 18 | elsif data[:friends] 19 | FriendList.new(data[:friends]) 20 | elsif data[:delete] && data[:delete][:status] 21 | DeletedTweet.new(data[:delete][:status]) 22 | elsif data[:warning] 23 | StallWarning.new(data[:warning]) 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/twitter/media/video.rb: -------------------------------------------------------------------------------- 1 | require 'memoizable' 2 | require 'twitter/identity' 3 | require 'twitter/media/video_info' 4 | 5 | module Twitter 6 | module Media 7 | class Video < Twitter::Identity 8 | include Memoizable 9 | 10 | # @return [Array] 11 | attr_reader :indices 12 | 13 | # @return [String] 14 | attr_reader :type 15 | 16 | display_uri_attr_reader 17 | uri_attr_reader :expanded_uri, :media_uri, :media_uri_https, :uri 18 | 19 | # Returns an array of photo sizes 20 | # 21 | # @return [Array] 22 | def sizes 23 | @attrs.fetch(:sizes, []).each_with_object({}) do |(key, value), object| 24 | object[key] = Size.new(value) 25 | end 26 | end 27 | memoize :sizes 28 | 29 | # Returns video info 30 | # 31 | # @return [Twitter::Media::VideoInfo] 32 | def video_info 33 | VideoInfo.new(@attrs[:video_info]) unless @attrs[:video_info].nil? 34 | end 35 | memoize :video_info 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/twitter/geo_results_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::GeoResults do 4 | describe '#each' do 5 | before do 6 | @geo_results = Twitter::GeoResults.new(result: {places: [{id: 1}, {id: 2}, {id: 3}, {id: 4}, {id: 5}, {id: 6}]}) 7 | end 8 | it 'iterates' do 9 | count = 0 10 | @geo_results.each { count += 1 } 11 | expect(count).to eq(6) 12 | end 13 | context 'with start' do 14 | it 'iterates' do 15 | count = 0 16 | @geo_results.each(5) { count += 1 } 17 | expect(count).to eq(1) 18 | end 19 | end 20 | end 21 | 22 | describe '#token' do 23 | it 'returns a String when token is set' do 24 | geo_results = Twitter::GeoResults.new(result: {}, token: 'abc123') 25 | expect(geo_results.token).to be_a String 26 | expect(geo_results.token).to eq('abc123') 27 | end 28 | it 'returns nil when token is not set' do 29 | geo_results = Twitter::GeoResults.new(result: {}) 30 | expect(geo_results.token).to be_nil 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/twitter/direct_messages/welcome_message_rule_wrapper.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/creatable' 2 | require 'twitter/identity' 3 | 4 | module Twitter 5 | module DirectMessages 6 | class WelcomeMessageRuleWrapper < Twitter::Identity 7 | attr_reader :created_timestamp 8 | 9 | object_attr_reader 'DirectMessages::WelcomeMessageRule', :welcome_message_rule 10 | 11 | def initialize(attrs) 12 | attrs = read_from_response(attrs) 13 | 14 | attrs[:welcome_message_rule] = build_welcome_message_rule(attrs) 15 | super 16 | end 17 | 18 | private 19 | 20 | # @return [Hash] Normalized hash of attrs 21 | def read_from_response(attrs) 22 | return attrs[:welcome_message_rule] unless attrs[:welcome_message_rule].nil? 23 | 24 | attrs 25 | end 26 | 27 | def build_welcome_message_rule(attrs) 28 | { 29 | id: attrs[:id].to_i, 30 | created_at: Time.at(attrs[:created_timestamp].to_i / 1000.0), 31 | welcome_message_id: attrs[:welcome_message_id].to_i, 32 | } 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/twitter/utils.rb: -------------------------------------------------------------------------------- 1 | module Twitter 2 | module Utils 3 | module_function 4 | 5 | # Returns a new array with the concatenated results of running block once for every element in enumerable. 6 | # If no block is given, an enumerator is returned instead. 7 | # 8 | # @param enumerable [Enumerable] 9 | # @return [Array, Enumerator] 10 | def flat_pmap(enumerable, &block) 11 | return to_enum(:flat_pmap, enumerable) unless block_given? 12 | 13 | pmap(enumerable, &block).flatten(1) 14 | end 15 | 16 | # Returns a new array with the results of running block once for every element in enumerable. 17 | # If no block is given, an enumerator is returned instead. 18 | # 19 | # @param enumerable [Enumerable] 20 | # @return [Array, Enumerator] 21 | def pmap(enumerable) 22 | return to_enum(:pmap, enumerable) unless block_given? 23 | 24 | if enumerable.count == 1 25 | enumerable.collect { |object| yield(object) } 26 | else 27 | enumerable.collect { |object| Thread.new { yield(object) } }.collect(&:value) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/fixtures/users_list.json: -------------------------------------------------------------------------------- 1 | {"users":[{"show_all_inline_media":true,"time_zone":"Pacific Time (US & Canada)","favourites_count":727,"description":"Adventures in hunger and foolishness.","contributors_enabled":false,"profile_sidebar_fill_color":"DDEEF6","followers_count":897,"geo_enabled":true,"notifications":false,"profile_use_background_image":true,"profile_sidebar_border_color":"C0DEED","verified":false,"url":null,"follow_request_sent":false,"profile_background_image_url":"http:\/\/a3.twimg.com\/profile_background_images\/162641967\/we_concept_bg2.png","lang":"en","created_at":"Mon Jul 16 12:59:01 +0000 2007","profile_background_color":"000000","location":"San Francisco","profile_background_tile":false,"protected":false,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/323331048\/me_normal.jpg","profile_text_color":"333333","name":"Erik Berlin","listed_count":28,"following":false,"friends_count":88,"screen_name":"sferik","id":7505382,"id_str":"7505382","statuses_count":2964,"utc_offset":-28800,"profile_link_color":"0084B4"}], "next_cursor":1322801608223717003, "previous_cursor":0, "next_cursor_str":"1322801608223717003", "previous_cursor_str":"0"} -------------------------------------------------------------------------------- /spec/fixtures/users_list2.json: -------------------------------------------------------------------------------- 1 | {"users":[{"show_all_inline_media":true,"time_zone":"Pacific Time (US & Canada)","favourites_count":727,"description":"Adventures in hunger and foolishness.","contributors_enabled":false,"profile_sidebar_fill_color":"DDEEF6","followers_count":897,"geo_enabled":true,"notifications":false,"profile_use_background_image":true,"profile_sidebar_border_color":"C0DEED","verified":false,"url":null,"follow_request_sent":false,"profile_background_image_url":"http:\/\/a3.twimg.com\/profile_background_images\/162641967\/we_concept_bg2.png","lang":"en","created_at":"Mon Jul 16 12:59:01 +0000 2007","profile_background_color":"000000","location":"San Francisco","profile_background_tile":false,"protected":false,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/323331048\/me_normal.jpg","profile_text_color":"333333","name":"Erik Berlin","listed_count":28,"following":false,"friends_count":88,"screen_name":"sferik","id":7505382,"id_str":"7505382","statuses_count":2964,"utc_offset":-28800,"profile_link_color":"0084B4"}], "next_cursor":0, "previous_cursor":1322801608223717003, "next_cursor_str":"0", "previous_cursor_str":"1322801608223717003"} -------------------------------------------------------------------------------- /lib/twitter/streaming/event.rb: -------------------------------------------------------------------------------- 1 | module Twitter 2 | module Streaming 3 | class Event 4 | LIST_EVENTS = %i[ 5 | list_created list_destroyed list_updated list_member_added 6 | list_member_added list_member_removed list_user_subscribed 7 | list_user_subscribed list_user_unsubscribed list_user_unsubscribed 8 | ].freeze 9 | 10 | TWEET_EVENTS = %i[ 11 | favorite unfavorite quoted_tweet 12 | ].freeze 13 | 14 | attr_reader :name, :source, :target, :target_object 15 | 16 | # @param data [Hash] 17 | def initialize(data) 18 | @name = data[:event].to_sym 19 | @source = Twitter::User.new(data[:source]) 20 | @target = Twitter::User.new(data[:target]) 21 | @target_object = target_object_factory(@name, data[:target_object]) 22 | end 23 | 24 | private 25 | 26 | def target_object_factory(event_name, data) 27 | if LIST_EVENTS.include?(event_name) 28 | Twitter::List.new(data) 29 | elsif TWEET_EVENTS.include?(event_name) 30 | Twitter::Tweet.new(data) 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/fixtures/category.json: -------------------------------------------------------------------------------- 1 | {"categories":[],"users":[{"geo_enabled":false,"time_zone":"Pacific Time (US & Canada)","description":"Designer, thinker, speaker, Creative Director at Twitter. Previously at Google, Stopdesign, and Wired.","profile_sidebar_fill_color":"bddaf7","followers_count":87831,"verified":false,"notifications":false,"follow_request_sent":false,"profile_use_background_image":true,"profile_sidebar_border_color":"eeeeee","url":"http:\/\/stopdesign.com","profile_background_image_url":"http:\/\/a3.twimg.com\/profile_background_images\/149914329\/bg-stop.png","lang":"en","created_at":"Sun Mar 11 19:50:16 +0000 2007","profile_background_color":"6092d2","location":"San Francisco, CA","listed_count":2454,"profile_background_tile":true,"friends_count":351,"protected":false,"profile_image_url":"http:\/\/a2.twimg.com\/profile_images\/552708586\/doug-profile_normal.jpg","statuses_count":2533,"profile_text_color":"333333","name":"Doug Bowman","show_all_inline_media":true,"following":true,"favourites_count":302,"screen_name":"stop","id":949521,"id_str":"949521","contributors_enabled":false,"utc_offset":-28800,"profile_link_color":"3387cc"}],"slug":"art-design","name":"Art & Design"} -------------------------------------------------------------------------------- /examples/AllTweets.md: -------------------------------------------------------------------------------- 1 | # All Tweets 2 | 3 | This example assumes you have a configured Twitter REST `client`. Instructions 4 | on how to configure a client can be found in [examples/Configuration.md][cfg]. 5 | 6 | [cfg]: https://github.com/sferik/twitter/blob/master/examples/Configuration.md 7 | 8 | You can fetch up to 3,200 tweets for a user, 200 at a time. 9 | 10 | Here is an example of recursively getting pages of 200 Tweets until you receive 11 | an empty response. 12 | 13 | **Note: This may result in [rate limiting][].** 14 | 15 | [rate limiting]: https://github.com/sferik/twitter/blob/master/examples/RateLimiting.md 16 | 17 | ```ruby 18 | def collect_with_max_id(collection=[], max_id=nil, &block) 19 | response = yield(max_id) 20 | collection += response 21 | response.empty? ? collection.flatten : collect_with_max_id(collection, response.last.id - 1, &block) 22 | end 23 | 24 | def client.get_all_tweets(user) 25 | collect_with_max_id do |max_id| 26 | options = {count: 200, include_rts: true} 27 | options[:max_id] = max_id unless max_id.nil? 28 | user_timeline(user, options) 29 | end 30 | end 31 | 32 | client.get_all_tweets("sferik") 33 | ``` 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006-2020 Erik Berlin, John Nunemaker, Wynn Netherland, Steve Richert, Steve Agalloco 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /spec/twitter/version_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Version do 4 | before do 5 | allow(Twitter::Version).to receive(:major).and_return(1) 6 | allow(Twitter::Version).to receive(:minor).and_return(2) 7 | allow(Twitter::Version).to receive(:patch).and_return(3) 8 | allow(Twitter::Version).to receive(:pre).and_return(nil) 9 | end 10 | 11 | describe '.to_h' do 12 | it 'returns a hash with the right values' do 13 | expect(Twitter::Version.to_h).to be_a Hash 14 | expect(Twitter::Version.to_h[:major]).to eq(1) 15 | expect(Twitter::Version.to_h[:minor]).to eq(2) 16 | expect(Twitter::Version.to_h[:patch]).to eq(3) 17 | expect(Twitter::Version.to_h[:pre]).to eq(nil) 18 | end 19 | end 20 | 21 | describe '.to_a' do 22 | it 'returns an array with the right values' do 23 | expect(Twitter::Version.to_a).to be_an Array 24 | expect(Twitter::Version.to_a).to eq([1, 2, 3]) 25 | end 26 | end 27 | 28 | describe '.to_s' do 29 | it 'returns a string with the right value' do 30 | expect(Twitter::Version.to_s).to be_a String 31 | expect(Twitter::Version.to_s).to eq('1.2.3') 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/twitter/rest/spam_reporting.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/rest/utils' 2 | require 'twitter/user' 3 | 4 | module Twitter 5 | module REST 6 | module SpamReporting 7 | include Twitter::REST::Utils 8 | 9 | # The users specified are blocked by the authenticated user and reported as spammers 10 | # 11 | # @see https://dev.twitter.com/rest/reference/post/users/report_spam 12 | # @rate_limited Yes 13 | # @authentication Requires user context 14 | # @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid. 15 | # @return [Array] The reported users. 16 | # @overload report_spam(*users) 17 | # @param users [Enumerable] A collection of Twitter user IDs, screen names, or objects. 18 | # @overload report_spam(*users, options) 19 | # @param users [Enumerable] A collection of Twitter user IDs, screen names, or objects. 20 | # @param options [Hash] A customizable set of options. 21 | def report_spam(*args) 22 | parallel_users_from_response(:post, '/1.1/users/report_spam.json', args) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/fixtures/welcome_messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome_messages": [ 3 | { 4 | "id": "1073273784206012421", 5 | "created_timestamp": "1544723385274", 6 | "message_data": { 7 | "text": "Welcome message text updated", 8 | "entities": { 9 | "hashtags": [ 10 | 11 | ], 12 | "symbols": [ 13 | 14 | ], 15 | "user_mentions": [ 16 | 17 | ], 18 | "urls": [ 19 | 20 | ] 21 | } 22 | }, 23 | "source_app_id": "1111111" 24 | }, 25 | { 26 | "id": "1073276982106996741", 27 | "created_timestamp": "1544724147713", 28 | "message_data": { 29 | "text": "A second welcome message with a name", 30 | "entities": { 31 | "hashtags": [ 32 | 33 | ], 34 | "symbols": [ 35 | 36 | ], 37 | "user_mentions": [ 38 | 39 | ], 40 | "urls": [ 41 | 42 | ] 43 | } 44 | }, 45 | "source_app_id": "1111111", 46 | "name": "welcome_message_name" 47 | } 48 | ], 49 | "apps": { 50 | "1111111": { 51 | "id": "1111111", 52 | "name": "Foobar", 53 | "url": "http://example.com" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/twitter/list.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/creatable' 2 | require 'twitter/identity' 3 | 4 | module Twitter 5 | class List < Twitter::Identity 6 | include Twitter::Creatable 7 | # @return [Integer] 8 | attr_reader :member_count, :subscriber_count 9 | # @return [String] 10 | attr_reader :description, :full_name, :mode, :name, :slug 11 | object_attr_reader :User, :user 12 | predicate_attr_reader :following 13 | 14 | # @return [Addressable::URI] The URI to the list members. 15 | def members_uri 16 | Addressable::URI.parse("#{uri}/members") if uri? 17 | end 18 | memoize :members_uri 19 | alias members_url members_uri 20 | 21 | # @return [Addressable::URI] The URI to the list subscribers. 22 | def subscribers_uri 23 | Addressable::URI.parse("#{uri}/subscribers") if uri? 24 | end 25 | memoize :subscribers_uri 26 | alias subscribers_url subscribers_uri 27 | 28 | # @return [Addressable::URI] The URI to the list. 29 | def uri 30 | Addressable::URI.parse("https://twitter.com/#{user.screen_name}/#{slug}") if slug? && user.screen_name? 31 | end 32 | memoize :uri 33 | alias url uri 34 | 35 | def uri? 36 | !!uri 37 | end 38 | memoize :uri? 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/fixtures/list.json: -------------------------------------------------------------------------------- 1 | {"mode":"public","description":"Presidents of the United States of America","id_str":"8863586","member_count":2,"uri":"\/sferik\/presidents","subscriber_count":0,"full_name":"@sferik\/presidents","user":{"profile_background_image_url":"http:\/\/a3.twimg.com\/profile_background_images\/162641967\/we_concept_bg2.png","profile_link_color":"0084B4","description":"Adventures in hunger and foolishness.","screen_name":"sferik","verified":false,"id_str":"7505382","follow_request_sent":false,"profile_background_tile":false,"profile_sidebar_fill_color":"DDEEF6","favourites_count":742,"profile_sidebar_border_color":"C0DEED","followers_count":911,"url":null,"listed_count":29,"lang":"en","time_zone":"Pacific Time (US & Canada)","created_at":"Mon Jul 16 12:59:01 +0000 2007","location":"San Francisco","statuses_count":3018,"profile_background_color":"000000","protected":false,"show_all_inline_media":true,"friends_count":86,"name":"Erik Berlin","contributors_enabled":false,"following":false,"profile_use_background_image":true,"profile_text_color":"333333","profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/323331048\/me_normal.jpg","id":7505382,"geo_enabled":true,"notifications":false,"utc_offset":-28800},"name":"presidents","following":false,"slug":"presidents","id":8863586} -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.4 3 | 4 | Layout/AccessModifierIndentation: 5 | EnforcedStyle: outdent 6 | 7 | Layout/LineLength: 8 | AllowURI: true 9 | Enabled: false 10 | 11 | Layout/SpaceInsideHashLiteralBraces: 12 | EnforcedStyle: no_space 13 | 14 | Lint/Void: 15 | Enabled: false 16 | 17 | Metrics/BlockLength: 18 | Max: 36 19 | Exclude: 20 | - spec/**/*.rb 21 | 22 | Metrics/BlockNesting: 23 | Max: 2 24 | 25 | Metrics/MethodLength: 26 | CountComments: false 27 | Max: 10 28 | 29 | Metrics/ModuleLength: 30 | Max: 150 # TODO: Lower to 100 31 | 32 | Metrics/ParameterLists: 33 | Max: 5 34 | CountKeywordArgs: true 35 | 36 | Style/CollectionMethods: 37 | Enabled: true 38 | PreferredMethods: 39 | map: 'collect' 40 | map!: 'collect!' 41 | reduce: 'inject' 42 | find: 'detect' 43 | find_all: 'select' 44 | 45 | Style/Documentation: 46 | Enabled: false 47 | 48 | Style/DoubleNegation: 49 | Enabled: false 50 | 51 | Style/FrozenStringLiteralComment: 52 | Enabled: false 53 | 54 | Style/NumericPredicate: 55 | Enabled: false 56 | 57 | Style/TrailingCommaInArrayLiteral: 58 | EnforcedStyleForMultiline: 'comma' 59 | 60 | Style/TrailingCommaInHashLiteral: 61 | EnforcedStyleForMultiline: 'comma' 62 | -------------------------------------------------------------------------------- /twitter.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('lib', __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'twitter/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.add_dependency 'addressable', '~> 2.3' 7 | spec.add_dependency 'buftok', '~> 0.2.0' 8 | spec.add_dependency 'equalizer', '~> 0.0.11' 9 | spec.add_dependency 'http', '~> 4.0' 10 | spec.add_dependency 'http-form_data', '~> 2.0' 11 | spec.add_dependency 'http_parser.rb', '~> 0.6.0' 12 | spec.add_dependency 'memoizable', '~> 0.4.0' 13 | spec.add_dependency 'multipart-post', '~> 2.0' 14 | spec.add_dependency 'naught', '~> 1.0' 15 | spec.add_dependency 'simple_oauth', '~> 0.3.0' 16 | spec.authors = ['Erik Berlin', 'John Nunemaker', 'Wynn Netherland', 'Steve Richert', 'Steve Agalloco'] 17 | spec.description = 'A Ruby interface to the Twitter API.' 18 | spec.email = %w[sferik@gmail.com] 19 | spec.files = %w[.yardopts CHANGELOG.md CONTRIBUTING.md LICENSE.md README.md twitter.gemspec] + Dir['lib/**/*.rb'] 20 | spec.homepage = 'http://sferik.github.com/twitter/' 21 | spec.licenses = %w[MIT] 22 | spec.name = 'twitter' 23 | spec.require_paths = %w[lib] 24 | spec.required_ruby_version = '>= 2.4' 25 | spec.summary = spec.description 26 | spec.version = Twitter::Version 27 | end 28 | -------------------------------------------------------------------------------- /spec/twitter/settings_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Settings do 4 | describe '#trend_location' do 5 | it 'returns a Twitter::Place when trend_location is set' do 6 | settings = Twitter::Settings.new(trend_location: {countryCode: 'US', name: 'San Francisco', country: 'United States', placeType: {name: 'Town', code: 7}, woeid: 2_487_956, parentid: 23_424_977, url: 'http://where.yahooapis.com/v1/place/2487956'}) 7 | expect(settings.trend_location).to be_a Twitter::Place 8 | end 9 | it 'returns nil when trend_location is not set' do 10 | settings = Twitter::Settings.new 11 | expect(settings.trend_location).to be_nil 12 | end 13 | end 14 | 15 | describe '#trend_location?' do 16 | it 'returns true when trend_location is set' do 17 | settings = Twitter::Settings.new(trend_location: {countryCode: 'US', name: 'San Francisco', country: 'United States', placeType: {name: 'Town', code: 7}, woeid: 2_487_956, parentid: 23_424_977, url: 'http://where.yahooapis.com/v1/place/2487956'}) 18 | expect(settings.trend_location?).to be true 19 | end 20 | it 'returns false when trend_location is not set' do 21 | settings = Twitter::Settings.new 22 | expect(settings.trend_location?).to be false 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/fixtures/configuration.json: -------------------------------------------------------------------------------- 1 | {"photo_sizes":{"medium":{"h":1200,"w":600,"resize":"fit"},"small":{"h":480,"w":340,"resize":"fit"},"large":{"h":2048,"w":1024,"resize":"fit"},"thumb":{"h":150,"w":150,"resize":"crop"}},"non_username_paths":["about","account","accounts","activity","all","announcements","anywhere","api_rules","api_terms","apirules","apps","auth","badges","blog","business","buttons","contacts","devices","direct_messages","download","downloads","edit_announcements","faq","favorites","find_sources","find_users","followers","following","friend_request","friendrequest","friends","goodies","help","home","im_account","inbox","invitations","invite","jobs","list","login","logout","me","mentions","messages","mockview","newtwitter","notifications","nudge","oauth","phoenix_search","positions","privacy","public_timeline","related_tweets","replies","retweeted_of_mine","retweets","retweets_by_others","rules","saved_searches","search","sent","settings","share","signup","signin","similar_to","statistics","terms","tos","translate","trends","tweetbutton","twttr","update_discoverability","users","welcome","who_to_follow","widgets","zendesk_auth","media_signup","t1_qunit_tests","phoenix_qunit_tests"],"photo_size_limit":3145728,"max_media_per_upload":1,"short_url_length":19,"short_url_length_https":20,"characters_reserved_per_media":20} -------------------------------------------------------------------------------- /lib/twitter.rb: -------------------------------------------------------------------------------- 1 | require 'addressable/uri' 2 | require 'twitter/configuration' 3 | require 'twitter/cursor' 4 | require 'twitter/direct_message' 5 | require 'twitter/entity' 6 | require 'twitter/entity/hashtag' 7 | require 'twitter/entity/symbol' 8 | require 'twitter/entity/uri' 9 | require 'twitter/entity/user_mention' 10 | require 'twitter/geo_factory' 11 | require 'twitter/language' 12 | require 'twitter/list' 13 | require 'twitter/media_factory' 14 | require 'twitter/metadata' 15 | require 'twitter/oembed' 16 | require 'twitter/place' 17 | require 'twitter/profile_banner' 18 | require 'twitter/rate_limit' 19 | require 'twitter/relationship' 20 | require 'twitter/rest/client' 21 | require 'twitter/saved_search' 22 | require 'twitter/search_results' 23 | require 'twitter/premium_search_results' 24 | require 'twitter/settings' 25 | require 'twitter/size' 26 | require 'twitter/source_user' 27 | require 'twitter/streaming/client' 28 | require 'twitter/suggestion' 29 | require 'twitter/target_user' 30 | require 'twitter/trend' 31 | require 'twitter/tweet' 32 | require 'twitter/user' 33 | require 'twitter/direct_messages/welcome_message' 34 | require 'twitter/direct_messages/welcome_message_rule' 35 | require 'twitter/direct_messages/welcome_message_rule_wrapper' 36 | require 'twitter/direct_messages/welcome_message_wrapper' 37 | -------------------------------------------------------------------------------- /lib/twitter/place.rb: -------------------------------------------------------------------------------- 1 | require 'memoizable' 2 | require 'twitter/identity' 3 | 4 | module Twitter 5 | class Place < Twitter::Identity 6 | include Memoizable 7 | 8 | # @return [Hash] 9 | attr_reader :attributes 10 | # @return [String] 11 | attr_reader :country, :full_name, :name 12 | alias woe_id id 13 | alias woeid id 14 | object_attr_reader :GeoFactory, :bounding_box 15 | object_attr_reader :Place, :contained_within 16 | alias contained? contained_within? 17 | uri_attr_reader :uri 18 | 19 | # Initializes a new place 20 | # 21 | # @param attrs [Hash] 22 | # @raise [ArgumentError] Error raised when supplied argument is missing a :woeid key. 23 | # @return [Twitter::Place] 24 | def initialize(attrs = {}) 25 | attrs[:id] ||= attrs.fetch(:woeid) 26 | super 27 | end 28 | 29 | # @return [String] 30 | def country_code 31 | @attrs[:country_code] || @attrs[:countryCode] 32 | end 33 | memoize :country_code 34 | 35 | # @return [Integer] 36 | def parent_id 37 | @attrs[:parentid] 38 | end 39 | memoize :parent_id 40 | 41 | # @return [String] 42 | def place_type 43 | @attrs[:place_type] || @attrs[:placeType] && @attrs[:placeType][:name] 44 | end 45 | memoize :place_type 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/twitter/direct_messages/welcome_message_wrapper.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/creatable' 2 | require 'twitter/identity' 3 | 4 | module Twitter 5 | module DirectMessages 6 | class WelcomeMessageWrapper < Twitter::Identity 7 | attr_reader :created_timestamp 8 | 9 | object_attr_reader 'DirectMessages::WelcomeMessage', :welcome_message 10 | 11 | def initialize(attrs) 12 | attrs = read_from_response(attrs) 13 | text = attrs.dig(:message_data, :text) 14 | urls = attrs.dig(:message_data, :entities, :urls) 15 | 16 | text.gsub!(urls[0][:url], urls[0][:expanded_url]) if urls.any? 17 | 18 | attrs[:welcome_message] = build_welcome_message(attrs, text) 19 | super 20 | end 21 | 22 | private 23 | 24 | # @return [Hash] Normalized hash of attrs 25 | def read_from_response(attrs) 26 | return attrs[:welcome_message] unless attrs[:welcome_message].nil? 27 | 28 | attrs 29 | end 30 | 31 | def build_welcome_message(attrs, text) 32 | { 33 | id: attrs[:id].to_i, 34 | created_at: Time.at(attrs[:created_timestamp].to_i / 1000.0), 35 | text: text, 36 | name: attrs[:name], 37 | entities: attrs.dig(:message_data, :entities), 38 | } 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/twitter/geo/point_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Geo::Point do 4 | before do 5 | @point = Twitter::Geo::Point.new(coordinates: [-122.399983, 37.788299]) 6 | end 7 | 8 | describe '#==' do 9 | it 'returns true for empty objects' do 10 | point = Twitter::Geo::Point.new 11 | other = Twitter::Geo::Point.new 12 | expect(point == other).to be true 13 | end 14 | it 'returns true when objects coordinates are the same' do 15 | other = Twitter::Geo::Point.new(coordinates: [-122.399983, 37.788299]) 16 | expect(@point == other).to be true 17 | end 18 | it 'returns false when objects coordinates are different' do 19 | other = Twitter::Geo::Point.new(coordinates: [37.788299, -122.399983]) 20 | expect(@point == other).to be false 21 | end 22 | it 'returns false when classes are different' do 23 | other = Twitter::Geo.new(coordinates: [-122.399983, 37.788299]) 24 | expect(@point == other).to be false 25 | end 26 | end 27 | 28 | describe '#latitude' do 29 | it 'returns the latitude' do 30 | expect(@point.latitude).to eq(-122.399983) 31 | end 32 | end 33 | 34 | describe '#longitude' do 35 | it 'returns the longitude' do 36 | expect(@point.longitude).to eq(37.788299) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/twitter/null_object.rb: -------------------------------------------------------------------------------- 1 | require 'naught' 2 | 3 | module Twitter 4 | NullObject = Naught.build do |config| # rubocop:disable Metrics/BlockLength 5 | include Comparable 6 | 7 | config.black_hole 8 | config.define_explicit_conversions 9 | config.define_implicit_conversions 10 | config.predicates_return false 11 | 12 | def ! 13 | true 14 | end 15 | 16 | def respond_to?(*) 17 | true 18 | end 19 | 20 | def instance_of?(klass) 21 | raise(TypeError, 'class or module required') unless klass.is_a?(Class) 22 | 23 | self.class == klass 24 | end 25 | 26 | def kind_of?(mod) 27 | raise(TypeError, 'class or module required') unless mod.is_a?(Module) 28 | 29 | self.class.ancestors.include?(mod) 30 | end 31 | 32 | alias_method :is_a?, :kind_of? 33 | 34 | def <=>(other) 35 | if other.is_a?(self.class) 36 | 0 37 | else 38 | -1 39 | end 40 | end 41 | 42 | def nil? 43 | true 44 | end 45 | 46 | def as_json(*) 47 | 'null' 48 | end 49 | 50 | def to_json(*args) 51 | nil.to_json(*args) 52 | end 53 | 54 | def presence 55 | nil 56 | end 57 | 58 | def blank? 59 | true 60 | end 61 | 62 | def present? 63 | false 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/twitter/geo_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Geo do 4 | before do 5 | @geo = Twitter::Geo.new(coordinates: [[[-122.40348192, 37.77752898], [-122.387436, 37.77752898], [-122.387436, 37.79448597], [-122.40348192, 37.79448597]]]) 6 | end 7 | 8 | describe '#==' do 9 | it 'returns true for empty objects' do 10 | geo = Twitter::Geo.new 11 | other = Twitter::Geo.new 12 | expect(geo == other).to be true 13 | end 14 | it 'returns true when objects coordinates are the same' do 15 | other = Twitter::Geo.new(coordinates: [[[-122.40348192, 37.77752898], [-122.387436, 37.77752898], [-122.387436, 37.79448597], [-122.40348192, 37.79448597]]]) 16 | expect(@geo == other).to be true 17 | end 18 | it 'returns false when objects coordinates are different' do 19 | other = Twitter::Geo.new(coordinates: [[[37.77752898, -122.40348192], [37.77752898, -122.387436], [37.79448597, -122.387436], [37.79448597, -122.40348192]]]) 20 | expect(@geo == other).to be false 21 | end 22 | it 'returns true when classes are different' do 23 | other = Twitter::Geo::Polygon.new(coordinates: [[[-122.40348192, 37.77752898], [-122.387436, 37.77752898], [-122.387436, 37.79448597], [-122.40348192, 37.79448597]]]) 24 | expect(@geo == other).to be true 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/twitter/geo/polygon_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Geo::Polygon do 4 | before do 5 | @polygon = Twitter::Geo::Polygon.new(coordinates: [[[-122.40348192, 37.77752898], [-122.387436, 37.77752898], [-122.387436, 37.79448597], [-122.40348192, 37.79448597]]]) 6 | end 7 | 8 | describe '#==' do 9 | it 'returns true for empty objects' do 10 | polygon = Twitter::Geo::Polygon.new 11 | other = Twitter::Geo::Polygon.new 12 | expect(polygon == other).to be true 13 | end 14 | it 'returns true when objects coordinates are the same' do 15 | other = Twitter::Geo::Polygon.new(coordinates: [[[-122.40348192, 37.77752898], [-122.387436, 37.77752898], [-122.387436, 37.79448597], [-122.40348192, 37.79448597]]]) 16 | expect(@polygon == other).to be true 17 | end 18 | it 'returns false when objects coordinates are different' do 19 | other = Twitter::Geo::Polygon.new(coordinates: [[[37.77752898, -122.40348192], [37.77752898, -122.387436], [37.79448597, -122.387436], [37.79448597, -122.40348192]]]) 20 | expect(@polygon == other).to be false 21 | end 22 | it 'returns false when classes are different' do 23 | other = Twitter::Geo.new(coordinates: [[[-122.40348192, 37.77752898], [-122.387436, 37.77752898], [-122.387436, 37.79448597], [-122.40348192, 37.79448597]]]) 24 | expect(@polygon == other).to be false 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/fixtures/welcome_message_with_entities.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome_message": { 3 | "id": "1073522416838733829", 4 | "created_timestamp": "1544782663913", 5 | "message_data": { 6 | "text": "Url: https://t.co/Fv7l28yris and #hashtag and @TwitterSupport", 7 | "entities": { 8 | "hashtags": [ 9 | { 10 | "text": "hashtag", 11 | "indices": [ 12 | 33, 13 | 41 14 | ] 15 | } 16 | ], 17 | "symbols": [ 18 | 19 | ], 20 | "user_mentions": [ 21 | { 22 | "screen_name": "TwitterSupport", 23 | "name": "Twitter Support", 24 | "id": 17874544, 25 | "id_str": "17874544", 26 | "indices": [ 27 | 46, 28 | 61 29 | ] 30 | } 31 | ], 32 | "urls": [ 33 | { 34 | "url": "https://t.co/Fv7l28yris", 35 | "expanded_url": "http://example.com/expanded", 36 | "display_url": "example.com/expanded", 37 | "indices": [ 38 | 5, 39 | 28 40 | ] 41 | } 42 | ] 43 | } 44 | }, 45 | "source_app_id": "1111111" 46 | }, 47 | "apps": { 48 | "1111111": { 49 | "id": "1111111", 50 | "name": "Foobar", 51 | "url": "http://example.com" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /spec/twitter/suggestion_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Suggestion do 4 | describe '#==' do 5 | it 'returns true for empty objects' do 6 | suggestion = Twitter::Suggestion.new 7 | other = Twitter::Suggestion.new 8 | expect(suggestion == other).to be true 9 | end 10 | it 'returns true when objects slugs are the same' do 11 | suggestion = Twitter::Suggestion.new(slug: 1, name: 'foo') 12 | other = Twitter::Suggestion.new(slug: 1, name: 'bar') 13 | expect(suggestion == other).to be true 14 | end 15 | it 'returns false when objects slugs are different' do 16 | suggestion = Twitter::Suggestion.new(slug: 1) 17 | other = Twitter::Suggestion.new(slug: 2) 18 | expect(suggestion == other).to be false 19 | end 20 | it 'returns false when classes are different' do 21 | suggestion = Twitter::Suggestion.new(slug: 1) 22 | other = Twitter::Base.new(slug: 1) 23 | expect(suggestion == other).to be false 24 | end 25 | end 26 | 27 | describe '#users' do 28 | it 'returns a User when user is set' do 29 | users = Twitter::Suggestion.new(users: [{id: 7_505_382}]).users 30 | expect(users).to be_an Array 31 | expect(users.first).to be_a Twitter::User 32 | end 33 | it 'is empty when not set' do 34 | users = Twitter::Suggestion.new.users 35 | expect(users).to be_empty 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/twitter/direct_message_event.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/creatable' 2 | require 'twitter/entities' 3 | require 'twitter/identity' 4 | 5 | module Twitter 6 | class DirectMessageEvent < Twitter::Identity 7 | include Twitter::Creatable 8 | include Twitter::Entities 9 | 10 | attr_reader :created_timestamp 11 | 12 | object_attr_reader :DirectMessage, :direct_message 13 | 14 | def initialize(attrs) 15 | attrs = read_from_response(attrs) 16 | text = attrs.dig(:message_create, :message_data, :text) 17 | urls = attrs.dig(:message_create, :message_data, :entities, :urls) 18 | 19 | text.gsub!(urls[0][:url], urls[0][:expanded_url]) if urls.any? 20 | 21 | attrs[:direct_message] = build_direct_message(attrs, text) 22 | super 23 | end 24 | 25 | private 26 | 27 | # @return [Hash] Normalized hash of attrs 28 | def read_from_response(attrs) 29 | attrs[:event].nil? ? attrs : attrs[:event] 30 | end 31 | 32 | def build_direct_message(attrs, text) 33 | recipient_id = attrs[:message_create][:target][:recipient_id].to_i 34 | sender_id = attrs[:message_create][:sender_id].to_i 35 | {id: attrs[:id].to_i, 36 | created_at: Time.at(attrs[:created_timestamp].to_i / 1000.0), 37 | sender: {id: sender_id}, 38 | sender_id: sender_id, 39 | recipient: {id: recipient_id}, 40 | recipient_id: recipient_id, 41 | text: text} 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/twitter/trend_results.rb: -------------------------------------------------------------------------------- 1 | require 'memoizable' 2 | require 'twitter/creatable' 3 | require 'twitter/enumerable' 4 | require 'twitter/null_object' 5 | require 'twitter/utils' 6 | 7 | module Twitter 8 | class TrendResults 9 | include Twitter::Creatable 10 | include Twitter::Enumerable 11 | include Twitter::Utils 12 | include Memoizable 13 | # @return [Hash] 14 | attr_reader :attrs 15 | alias to_h attrs 16 | alias to_hash to_h 17 | 18 | # Initializes a new TrendResults object 19 | # 20 | # @param attrs [Hash] 21 | # @return [Twitter::TrendResults] 22 | def initialize(attrs = {}) 23 | @attrs = attrs 24 | @collection = @attrs.fetch(:trends, []).collect do |trend| 25 | Trend.new(trend) 26 | end 27 | end 28 | 29 | # Time when the object was created on Twitter 30 | # 31 | # @return [Time] 32 | def as_of 33 | Time.parse(@attrs[:as_of]).utc unless @attrs[:as_of].nil? 34 | end 35 | memoize :as_of 36 | 37 | def as_of? 38 | !!@attrs[:as_of] 39 | end 40 | memoize :as_of? 41 | 42 | # @return [Twitter::Place, NullObject] 43 | def location 44 | location? ? Place.new(@attrs[:locations].first) : NullObject.new 45 | end 46 | memoize :location 47 | 48 | # @return [Boolean] 49 | def location? 50 | !@attrs[:locations].nil? && !@attrs[:locations].first.nil? 51 | end 52 | memoize :location? 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/twitter/client.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/error' 2 | require 'twitter/utils' 3 | require 'twitter/version' 4 | 5 | module Twitter 6 | class Client 7 | include Twitter::Utils 8 | attr_accessor :access_token, :access_token_secret, :consumer_key, :consumer_secret, :proxy, :timeouts, :dev_environment 9 | attr_writer :user_agent 10 | 11 | # Initializes a new Client object 12 | # 13 | # @param options [Hash] 14 | # @return [Twitter::Client] 15 | def initialize(options = {}) 16 | options.each do |key, value| 17 | instance_variable_set("@#{key}", value) 18 | end 19 | yield(self) if block_given? 20 | end 21 | 22 | # @return [Boolean] 23 | def user_token? 24 | !(blank_string?(access_token) || blank_string?(access_token_secret)) 25 | end 26 | 27 | # @return [String] 28 | def user_agent 29 | @user_agent ||= "TwitterRubyGem/#{Twitter::Version}" 30 | end 31 | 32 | # @return [Hash] 33 | def credentials 34 | { 35 | consumer_key: consumer_key, 36 | consumer_secret: consumer_secret, 37 | token: access_token, 38 | token_secret: access_token_secret, 39 | } 40 | end 41 | 42 | # @return [Boolean] 43 | def credentials? 44 | credentials.values.none? { |v| blank_string?(v) } 45 | end 46 | 47 | private 48 | 49 | def blank_string?(string) 50 | string.respond_to?(:empty?) ? string.empty? : !string 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/fixtures/user_timeline.json: -------------------------------------------------------------------------------- 1 | [{"place":null,"geo":null,"truncated":false,"favorited":false,"source":"Echofon","contributors":null,"in_reply_to_screen_name":"chriskottom","created_at":"Fri Oct 22 15:50:35 +0000 2010","user":{"description":"Adventures in hunger and foolishness.","profile_use_background_image":true,"profile_sidebar_border_color":"C0DEED","listed_count":29,"notifications":false,"friends_count":88,"statuses_count":2961,"profile_background_image_url":"http://a3.twimg.com/profile_background_images/162641967/we_concept_bg2.png","show_all_inline_media":true,"favourites_count":724,"profile_background_color":"000000","url":null,"contributors_enabled":false,"profile_background_tile":false,"lang":"en","geo_enabled":true,"created_at":"Mon Jul 16 12:59:01 +0000 2007","profile_text_color":"333333","location":"San Francisco","protected":false,"profile_image_url":"http://a0.twimg.com/profile_images/323331048/me_normal.jpg","verified":false,"profile_link_color":"0084B4","followers_count":899,"name":"Erik Berlin","follow_request_sent":false,"following":true,"time_zone":"Pacific Time (US & Canada)","screen_name":"sferik","id":7505382,"utc_offset":-28800,"profile_sidebar_fill_color":"DDEEF6"},"retweet_count":null,"coordinates":null,"in_reply_to_status_id":28416708070,"id":28416898759,"retweeted":false,"in_reply_to_user_id":14565733,"text":"@chriskottom Thanks for the heads up. Doing some major refactoring. I'll fix it as soon as that's done. /cc @jnunemaker @pengwynn"}] -------------------------------------------------------------------------------- /etc/erd.rb: -------------------------------------------------------------------------------- 1 | require 'twitter' 2 | 3 | COLON = ':'.freeze 4 | UNDERSCORE = '_'.freeze 5 | TAB = "\t".freeze 6 | NAMESPACE = 'Twitter::'.freeze 7 | 8 | # Colons are invalid characters in DOT nodes. 9 | # Replace them with underscores. 10 | # http://www.graphviz.org/doc/info/lang.html 11 | def nodize(klass) 12 | klass.name.tr(COLON, UNDERSCORE) 13 | end 14 | 15 | nodes = {} 16 | edges = {} 17 | 18 | twitter_objects = ObjectSpace.each_object(Class).select do |klass| 19 | klass.name.to_s.start_with?(NAMESPACE) 20 | end 21 | 22 | twitter_objects.each do |klass| 23 | loop do 24 | unless klass.nil? || klass.superclass.nil? || klass.name.empty? 25 | nodes[nodize(klass)] = klass.name 26 | edges[nodize(klass)] = nodize(klass.superclass) 27 | end 28 | klass = klass.superclass 29 | break if klass.nil? 30 | end 31 | end 32 | 33 | edges.delete(nil) 34 | 35 | @indent = 0 36 | 37 | def indent 38 | @indent += 1 39 | yield 40 | @indent -= 1 41 | end 42 | 43 | def puts(string) 44 | super(TAB * @indent + string) 45 | end 46 | 47 | puts 'digraph classes {' 48 | # Add or remove DOT formatting options here 49 | indent do 50 | puts 'graph [rotate=0, rankdir="LR"]' 51 | puts 'node [fillcolor="#c4ddec", style="filled", fontname="Helvetica Neue"]' 52 | puts 'edge [color="#444444"]' 53 | nodes.sort.each do |node, label| 54 | puts "#{node} [label=\"#{label}\"]" 55 | end 56 | edges.sort.each do |child, parent| 57 | puts "#{child} -> #{parent}" 58 | end 59 | end 60 | puts '}' 61 | -------------------------------------------------------------------------------- /spec/fixtures/matching_trends.json: -------------------------------------------------------------------------------- 1 | [{"as_of":"2010-10-25T14:49:50Z","created_at":"2010-10-25T14:41:13Z","trends":[{"promoted_content":null,"query":"%23sevenwordsaftersex","url":"http:\/\/search.twitter.com\/search?q=%23sevenwordsaftersex","name":"#sevenwordsaftersex","events":null},{"promoted_content":null,"query":"Isaacs","url":"http:\/\/search.twitter.com\/search?q=Isaacs","name":"Isaacs","events":null},{"promoted_content":null,"query":"%23speaknow","url":"http:\/\/search.twitter.com\/search?q=%23speaknow","name":"#speaknow","events":null},{"promoted_content":null,"query":"Walkman","url":"http:\/\/search.twitter.com\/search?q=Walkman","name":"Walkman","events":null},{"promoted_content":null,"query":"%23dia31vote13","url":"http:\/\/search.twitter.com\/search?q=%23dia31vote13","name":"#dia31vote13","events":null},{"promoted_content":null,"query":"RIP+Gregory","url":"http:\/\/search.twitter.com\/search?q=RIP+Gregory","name":"RIP Gregory","events":null},{"promoted_content":null,"query":"Allen+Iverson","url":"http:\/\/search.twitter.com\/search?q=Allen+Iverson","name":"Allen Iverson","events":null},{"promoted_content":null,"query":"Issacs","url":"http:\/\/search.twitter.com\/search?q=Issacs","name":"Issacs","events":null},{"promoted_content":null,"query":"Night+Nurse","url":"http:\/\/search.twitter.com\/search?q=Night+Nurse","name":"Night Nurse","events":null},{"promoted_content":null,"query":"Jay+Bum","url":"http:\/\/search.twitter.com\/search?q=Jay+Bum","name":"Jay Bum","events":null}],"locations":[{"woeid":1,"name":"Worldwide"}]}] -------------------------------------------------------------------------------- /spec/twitter/media/video_info_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Media::VideoInfo do 4 | describe '#aspect_ratio' do 5 | it 'returns a String when the aspect_ratio is set' do 6 | info = Twitter::Media::VideoInfo.new(aspect_ratio: [16, 9]) 7 | expect(info.aspect_ratio).to be_an Array 8 | expect(info.aspect_ratio).to eq([16, 9]) 9 | end 10 | it 'returns nil when the aspect_ratio is not set' do 11 | info = Twitter::Media::VideoInfo.new({}) 12 | expect(info.aspect_ratio).to be_nil 13 | end 14 | end 15 | 16 | describe '#duration_millis' do 17 | it 'returns a Integer when the duration_millis is set' do 18 | info = Twitter::Media::VideoInfo.new(duration_millis: 30_033) 19 | expect(info.duration_millis).to be_a Integer 20 | expect(info.duration_millis).to eq(30_033) 21 | end 22 | it 'returns nil when the duration_millis is not set' do 23 | info = Twitter::Media::VideoInfo.new({}) 24 | expect(info.duration_millis).to be_nil 25 | end 26 | end 27 | 28 | describe '#variants' do 29 | it 'returns a hash of Variants when variants is set' do 30 | variants = Twitter::Media::VideoInfo.new(variants: [{bitrate: 2_176_000, content_type: 'video/mp4', url: 'http://video.twimg.com/c4E56sl91ZB7cpYi.mp4'}]).variants 31 | expect(variants).to be_an Array 32 | expect(variants.first).to be_a Twitter::Variant 33 | end 34 | it 'is empty when variants is not set' do 35 | variants = Twitter::Media::VideoInfo.new({}).variants 36 | expect(variants).to be_empty 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/fixtures/memberships.json: -------------------------------------------------------------------------------- 1 | {"lists":[{"uri":"\/DavidBahia\/developer","name":"developer","full_name":"@DavidBahia\/developer","description":"","mode":"public","user":{"id":20647833,"profile_background_image_url_https":"https:\/\/si0.twimg.com\/profile_background_images\/16339914\/twit3.JPG","time_zone":"London","location":"Rochester, Medway Towns, Kent","profile_background_image_url":"http:\/\/a0.twimg.com\/profile_background_images\/16339914\/twit3.JPG","id_str":"20647833","entities":{"description":{"urls":[]}},"profile_link_color":"1F98C7","geo_enabled":false,"default_profile":false,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/272062257\/Irandb4_normal.JPG","utc_offset":0,"profile_use_background_image":true,"statuses_count":4920,"name":"David Bahia","follow_request_sent":false,"profile_text_color":"663B12","lang":"en","screen_name":"DavidBahia","listed_count":31,"protected":false,"followers_count":953,"profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/272062257\/Irandb4_normal.JPG","profile_sidebar_border_color":"C6E2EE","description":"I wonder. why?","profile_background_tile":false,"following":false,"profile_sidebar_fill_color":"DAECF4","default_profile_image":false,"url":null,"is_translator":false,"favourites_count":15128,"created_at":"Thu Feb 12 02:11:49 +0000 2009","friends_count":1997,"verified":false,"notifications":false,"profile_background_color":"C6E2EE","contributors_enabled":false},"following":false,"created_at":"Fri Apr 01 09:16:02 +0000 2011","member_count":382,"id_str":"41944715","subscriber_count":2,"slug":"developer","id":41944715}], "next_cursor":1401037770457540712, "previous_cursor":0, "next_cursor_str":"1401037770457540712", "previous_cursor_str":"0"} -------------------------------------------------------------------------------- /spec/fixtures/memberships2.json: -------------------------------------------------------------------------------- 1 | {"lists":[{"uri":"\/DavidBahia\/developer","name":"developer","full_name":"@DavidBahia\/developer","description":"","mode":"public","user":{"id":20647833,"profile_background_image_url_https":"https:\/\/si0.twimg.com\/profile_background_images\/16339914\/twit3.JPG","time_zone":"London","location":"Rochester, Medway Towns, Kent","profile_background_image_url":"http:\/\/a0.twimg.com\/profile_background_images\/16339914\/twit3.JPG","id_str":"20647833","entities":{"description":{"urls":[]}},"profile_link_color":"1F98C7","geo_enabled":false,"default_profile":false,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/272062257\/Irandb4_normal.JPG","utc_offset":0,"profile_use_background_image":true,"statuses_count":4920,"name":"David Bahia","follow_request_sent":false,"profile_text_color":"663B12","lang":"en","screen_name":"DavidBahia","listed_count":31,"protected":false,"followers_count":953,"profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/272062257\/Irandb4_normal.JPG","profile_sidebar_border_color":"C6E2EE","description":"I wonder. why?","profile_background_tile":false,"following":false,"profile_sidebar_fill_color":"DAECF4","default_profile_image":false,"url":null,"is_translator":false,"favourites_count":15128,"created_at":"Thu Feb 12 02:11:49 +0000 2009","friends_count":1997,"verified":false,"notifications":false,"profile_background_color":"C6E2EE","contributors_enabled":false},"following":false,"created_at":"Fri Apr 01 09:16:02 +0000 2011","member_count":382,"id_str":"41944715","subscriber_count":2,"slug":"developer","id":41944715}], "next_cursor":0, "previous_cursor":1401037770457540712, "next_cursor_str":"0", "previous_cursor_str":"1401037770457540712"} -------------------------------------------------------------------------------- /spec/fixtures/lists.json: -------------------------------------------------------------------------------- 1 | [{"uri":"\/pengwynn\/rubyists","name":"Rubyists","full_name":"@pengwynn\/rubyists","description":"","mode":"public","user":{"id":14100886,"profile_background_image_url_https":"https:\/\/si0.twimg.com\/images\/themes\/theme1\/bg.png","time_zone":"Central Time (US & Canada)","location":"Denton, TX","profile_background_image_url":"http:\/\/a0.twimg.com\/images\/themes\/theme1\/bg.png","id_str":"14100886","profile_link_color":"0084B4","geo_enabled":true,"default_profile":false,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/2221455972\/wynn-mic-bw_normal.jpg","utc_offset":-21600,"profile_use_background_image":false,"statuses_count":7384,"name":"Wynn Netherland","follow_request_sent":false,"profile_text_color":"333333","lang":"en","screen_name":"pengwynn","listed_count":397,"protected":false,"is_translator":false,"followers_count":6182,"profile_sidebar_border_color":"FFFFFF","description":"Christian, husband, father, GitHubber, Co-host of @thechangelog, Co-author of Sass, Compass, #CSS book http:\/\/wynn.fm\/sass-meap","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/2221455972\/wynn-mic-bw_normal.jpg","profile_background_tile":false,"following":true,"profile_sidebar_fill_color":"DDEEF6","default_profile_image":false,"url":"http:\/\/wynnnetherland.com","profile_banner_url":"https:\/\/si0.twimg.com\/profile_banners\/14100886\/1347987369","favourites_count":338,"created_at":"Sat Mar 08 16:34:22 +0000 2008","friends_count":3528,"verified":false,"notifications":false,"profile_background_color":"292929","contributors_enabled":false},"following":true,"created_at":"Fri Oct 30 14:39:25 +0000 2009","member_count":499,"id_str":"1129440","subscriber_count":39,"slug":"rubyists","id":1129440}] -------------------------------------------------------------------------------- /lib/twitter/streaming/connection.rb: -------------------------------------------------------------------------------- 1 | require 'http/parser' 2 | require 'openssl' 3 | require 'resolv' 4 | 5 | module Twitter 6 | module Streaming 7 | class Connection 8 | attr_reader :tcp_socket_class, :ssl_socket_class 9 | 10 | def initialize(options = {}) 11 | @tcp_socket_class = options.fetch(:tcp_socket_class) { TCPSocket } 12 | @ssl_socket_class = options.fetch(:ssl_socket_class) { OpenSSL::SSL::SSLSocket } 13 | @using_ssl = options.fetch(:using_ssl) { false } 14 | @write_pipe = nil 15 | end 16 | 17 | def stream(request, response) # rubocop:disable Metrics/MethodLength 18 | client = connect(request) 19 | request.stream(client) 20 | read_pipe, @write_pipe = IO.pipe 21 | loop do 22 | read_ios, _write_ios, _exception_ios = IO.select([read_pipe, client]) 23 | case read_ios.first 24 | when client 25 | response << client.readpartial(1024) 26 | when read_pipe 27 | break 28 | end 29 | end 30 | client.close 31 | end 32 | 33 | def connect(request) 34 | client = new_tcp_socket(request.socket_host, request.socket_port) 35 | return client if !@using_ssl && request.using_proxy? 36 | 37 | client_context = OpenSSL::SSL::SSLContext.new 38 | ssl_client = @ssl_socket_class.new(client, client_context) 39 | ssl_client.connect 40 | end 41 | 42 | def close 43 | @write_pipe&.write('q') 44 | end 45 | 46 | private 47 | 48 | def new_tcp_socket(host, port) 49 | @tcp_socket_class.new(Resolv.getaddress(host), port) 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/twitter/headers.rb: -------------------------------------------------------------------------------- 1 | require 'addressable/uri' 2 | require 'base64' 3 | require 'simple_oauth' 4 | 5 | module Twitter 6 | class Headers 7 | def initialize(client, request_method, url, options = {}) 8 | @client = client 9 | @request_method = request_method.to_sym 10 | @uri = Addressable::URI.parse(url) 11 | @bearer_token_request = options.delete(:bearer_token_request) 12 | @options = options 13 | end 14 | 15 | def bearer_token_request? 16 | !!@bearer_token_request 17 | end 18 | 19 | def oauth_auth_header 20 | SimpleOAuth::Header.new(@request_method, @uri, @options, @client.credentials.merge(ignore_extra_keys: true)) 21 | end 22 | 23 | def request_headers 24 | headers = {} 25 | headers[:user_agent] = @client.user_agent 26 | if bearer_token_request? 27 | headers[:accept] = '*/*' 28 | headers[:authorization] = bearer_token_credentials_auth_header 29 | else 30 | headers[:authorization] = auth_header 31 | end 32 | headers 33 | end 34 | 35 | private 36 | 37 | def auth_header 38 | if @client.user_token? 39 | oauth_auth_header.to_s 40 | else 41 | @client.bearer_token = @client.token unless @client.bearer_token? 42 | bearer_auth_header 43 | end 44 | end 45 | 46 | # @return [String] 47 | def bearer_auth_header 48 | "Bearer #{@client.bearer_token}" 49 | end 50 | 51 | # Generates authentication header for a bearer token request 52 | # 53 | # @return [String] 54 | def bearer_token_credentials_auth_header 55 | "Basic #{Base64.strict_encode64("#{@client.consumer_key}:#{@client.consumer_secret}")}" 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/twitter/rest/undocumented.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/arguments' 2 | require 'twitter/cursor' 3 | require 'twitter/rest/utils' 4 | require 'twitter/tweet' 5 | require 'twitter/user' 6 | 7 | module Twitter 8 | module REST 9 | module Undocumented 10 | include Twitter::REST::Utils 11 | 12 | # @note Undocumented 13 | # @rate_limited Yes 14 | # @authentication Requires user context 15 | # @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid. 16 | # @return [Twitter::Cursor] 17 | # @overload following_followers_of(options = {}) 18 | # Returns users following followers of the specified user 19 | # 20 | # @param options [Hash] A customizable set of options. 21 | # @overload following_followers_of(user, options = {}) 22 | # Returns users following followers of the authenticated user 23 | # 24 | # @param user [Integer, String, Twitter::User] A Twitter user ID, screen name, URI, or object. 25 | # @param options [Hash] A customizable set of options. 26 | def following_followers_of(*args) 27 | cursor_from_response_with_user(:users, Twitter::User, '/users/following_followers_of.json', args) 28 | end 29 | 30 | # Returns Tweets count for a URI 31 | # 32 | # @note Undocumented 33 | # @rate_limited No 34 | # @authentication Not required 35 | # @return [Integer] 36 | # @param url [String, URI] A URL. 37 | # @param options [Hash] A customizable set of options. 38 | def tweet_count(url, options = {}) 39 | HTTP.get('https://cdn.api.twitter.com/1/urls/count.json', params: options.merge(url: url.to_s)).parse['count'] 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/fixtures/pengwynn.json: -------------------------------------------------------------------------------- 1 | {"profile_background_color":"efefef","listed_count":201,"lang":"en","verified":false,"profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/61741268\/twitter-small.png","created_at":"Sat Mar 08 16:34:22 +0000 2008","description":"Christian husband and father. Dev Experience @ HP Cloud Services. Co-host of the @changelogshow. Mashup of design & development.","screen_name":"pengwynn","status":{"in_reply_to_user_id_str":null,"text":"Flatstache is to Mustache what Zepto is to jQuery, from @natevw http:\/\/t.co\/gOjxJ1E","coordinates":null,"retweeted":false,"retweet_count":0,"created_at":"Sun Jan 16 21:01:10 +0000 2011","in_reply_to_user_id":null,"place":null,"source":"\u003Ca href=\"http:\/\/itunes.apple.com\/us\/app\/twitter\/id409789998?mt=12\" rel=\"nofollow\"\u003ETwitter for Mac\u003C\/a\u003E","in_reply_to_status_id":null,"truncated":false,"favorited":false,"in_reply_to_status_id_str":null,"id_str":"26745802235842561","geo":null,"id":26745802235842561,"contributors":null,"in_reply_to_screen_name":null},"url":"http:\/\/wynnnetherland.com","is_translator":false,"show_all_inline_media":false,"geo_enabled":true,"profile_text_color":"666666","followers_count":2902,"contributors_enabled":false,"following":true,"favourites_count":67,"profile_sidebar_fill_color":"dddddd","location":"Dallas, TX","profile_background_tile":false,"time_zone":"Central Time (US & Canada)","profile_link_color":"35abe9","protected":false,"follow_request_sent":false,"statuses_count":4160,"profile_sidebar_border_color":"cccccc","name":"Wynn Netherland","id_str":"14100886","friends_count":1710,"id":14100886,"notifications":false,"profile_use_background_image":true,"utc_offset":-21600,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/1180321093\/komikazee_normal.png"} -------------------------------------------------------------------------------- /spec/twitter/saved_search_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::SavedSearch do 4 | describe '#==' do 5 | it 'returns true when objects IDs are the same' do 6 | saved_search = Twitter::SavedSearch.new(id: 1, name: 'foo') 7 | other = Twitter::SavedSearch.new(id: 1, name: 'bar') 8 | expect(saved_search == other).to be true 9 | end 10 | it 'returns false when objects IDs are different' do 11 | saved_search = Twitter::SavedSearch.new(id: 1) 12 | other = Twitter::SavedSearch.new(id: 2) 13 | expect(saved_search == other).to be false 14 | end 15 | it 'returns false when classes are different' do 16 | saved_search = Twitter::SavedSearch.new(id: 1) 17 | other = Twitter::Identity.new(id: 1) 18 | expect(saved_search == other).to be false 19 | end 20 | end 21 | 22 | describe '#created_at' do 23 | it 'returns a Time when created_at is set' do 24 | saved_search = Twitter::SavedSearch.new(id: 16_129_012, created_at: 'Mon Jul 16 12:59:01 +0000 2007') 25 | expect(saved_search.created_at).to be_a Time 26 | expect(saved_search.created_at).to be_utc 27 | end 28 | it 'returns nil when created_at is not set' do 29 | saved_search = Twitter::SavedSearch.new(id: 16_129_012) 30 | expect(saved_search.created_at).to be_nil 31 | end 32 | end 33 | 34 | describe '#created?' do 35 | it 'returns true when created_at is set' do 36 | saved_search = Twitter::SavedSearch.new(id: 16_129_012, created_at: 'Mon Jul 16 12:59:01 +0000 2007') 37 | expect(saved_search.created?).to be true 38 | end 39 | it 'returns false when created_at is not set' do 40 | saved_search = Twitter::SavedSearch.new(id: 16_129_012) 41 | expect(saved_search.created?).to be false 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/twitter/relationship_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Relationship do 4 | describe '#source' do 5 | it 'returns a User when source is set' do 6 | relationship = Twitter::Relationship.new(relationship: {source: {id: 7_505_382}}) 7 | expect(relationship.source).to be_a Twitter::SourceUser 8 | end 9 | it 'returns nil when source is not set' do 10 | relationship = Twitter::Relationship.new(relationship: {}) 11 | expect(relationship.source).to be_nil 12 | end 13 | end 14 | 15 | describe '#source?' do 16 | it 'returns true when source is set' do 17 | relationship = Twitter::Relationship.new(relationship: {source: {id: 7_505_382}}) 18 | expect(relationship.source?).to be true 19 | end 20 | it 'returns false when source is not set' do 21 | relationship = Twitter::Relationship.new(relationship: {}) 22 | expect(relationship.source?).to be false 23 | end 24 | end 25 | 26 | describe '#target' do 27 | it 'returns a User when target is set' do 28 | relationship = Twitter::Relationship.new(relationship: {target: {id: 7_505_382}}) 29 | expect(relationship.target).to be_a Twitter::TargetUser 30 | end 31 | it 'returns nil when target is not set' do 32 | relationship = Twitter::Relationship.new(relationship: {}) 33 | expect(relationship.target).to be_nil 34 | end 35 | end 36 | 37 | describe '#target?' do 38 | it 'returns true when target is set' do 39 | relationship = Twitter::Relationship.new(relationship: {target: {id: 7_505_382}}) 40 | expect(relationship.target?).to be true 41 | end 42 | it 'returns false when target is not set' do 43 | relationship = Twitter::Relationship.new(relationship: {}) 44 | expect(relationship.target?).to be false 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'coveralls' 3 | 4 | SimpleCov.formatters = [SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter] 5 | 6 | SimpleCov.start do 7 | add_filter '/spec/' 8 | add_filter '/vendor/' 9 | minimum_coverage(99.78) 10 | end 11 | 12 | require 'twitter' 13 | require 'rspec' 14 | require 'stringio' 15 | require 'tempfile' 16 | require 'timecop' 17 | require 'webmock/rspec' 18 | 19 | require_relative 'support/media_object_examples' 20 | 21 | WebMock.disable_net_connect!(allow: 'coveralls.io') 22 | 23 | RSpec.configure do |config| 24 | config.expect_with :rspec do |c| 25 | c.syntax = :expect 26 | end 27 | end 28 | 29 | def a_delete(path) 30 | a_request(:delete, Twitter::REST::Request::BASE_URL + path) 31 | end 32 | 33 | def a_get(path) 34 | a_request(:get, Twitter::REST::Request::BASE_URL + path) 35 | end 36 | 37 | def a_post(path) 38 | a_request(:post, Twitter::REST::Request::BASE_URL + path) 39 | end 40 | 41 | def a_put(path) 42 | a_request(:put, Twitter::REST::Request::BASE_URL + path) 43 | end 44 | 45 | def stub_delete(path) 46 | stub_request(:delete, Twitter::REST::Request::BASE_URL + path) 47 | end 48 | 49 | def stub_get(path) 50 | stub_request(:get, Twitter::REST::Request::BASE_URL + path) 51 | end 52 | 53 | def stub_post(path) 54 | stub_request(:post, Twitter::REST::Request::BASE_URL + path) 55 | end 56 | 57 | def stub_put(path) 58 | stub_request(:put, Twitter::REST::Request::BASE_URL + path) 59 | end 60 | 61 | def fixture_path 62 | File.expand_path('fixtures', __dir__) 63 | end 64 | 65 | def fixture(file) 66 | File.new(fixture_path + '/' + file) 67 | end 68 | 69 | def capture_warning 70 | begin 71 | old_stderr = $stderr 72 | $stderr = StringIO.new 73 | yield 74 | result = $stderr.string 75 | ensure 76 | $stderr = old_stderr 77 | end 78 | result 79 | end 80 | -------------------------------------------------------------------------------- /spec/twitter/trend_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Trend do 4 | describe '#==' do 5 | it 'returns true for empty objects' do 6 | trend = Twitter::Trend.new 7 | other = Twitter::Trend.new 8 | expect(trend == other).to be true 9 | end 10 | it 'returns true when objects names are the same' do 11 | trend = Twitter::Trend.new(name: '#sevenwordsaftersex', query: 'foo') 12 | other = Twitter::Trend.new(name: '#sevenwordsaftersex', query: 'bar') 13 | expect(trend == other).to be true 14 | end 15 | it 'returns false when objects names are different' do 16 | trend = Twitter::Trend.new(name: '#sevenwordsaftersex') 17 | other = Twitter::Trend.new(name: '#sixwordsaftersex') 18 | expect(trend == other).to be false 19 | end 20 | it 'returns false when classes are different' do 21 | trend = Twitter::Trend.new(name: '#sevenwordsaftersex') 22 | other = Twitter::Base.new(name: '#sevenwordsaftersex') 23 | expect(trend == other).to be false 24 | end 25 | end 26 | 27 | describe '#uri' do 28 | it 'returns a URI when the url is set' do 29 | trend = Twitter::Trend.new(url: 'http://twitter.com/search/?q=%23sevenwordsaftersex') 30 | expect(trend.uri).to be_an Addressable::URI 31 | expect(trend.uri.to_s).to eq('http://twitter.com/search/?q=%23sevenwordsaftersex') 32 | end 33 | it 'returns nil when the url is not set' do 34 | trend = Twitter::Trend.new 35 | expect(trend.uri).to be_nil 36 | end 37 | end 38 | 39 | describe '#uri?' do 40 | it 'returns true when the url is set' do 41 | trend = Twitter::Trend.new(url: 'https://api.twitter.com/1.1/geo/id/247f43d441defc03.json') 42 | expect(trend.uri?).to be true 43 | end 44 | it 'returns false when the url is not set' do 45 | trend = Twitter::Trend.new 46 | expect(trend.uri?).to be false 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/twitter/error_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Error do 4 | before do 5 | @client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', access_token: 'AT', access_token_secret: 'AS') 6 | end 7 | 8 | describe '#code' do 9 | it 'returns the error code' do 10 | error = Twitter::Error.new('execution expired', {}, 123) 11 | expect(error.code).to eq(123) 12 | end 13 | end 14 | 15 | describe '#message' do 16 | it 'returns the error message' do 17 | error = Twitter::Error.new('execution expired') 18 | expect(error.message).to eq('execution expired') 19 | end 20 | end 21 | 22 | describe '#rate_limit' do 23 | it 'returns a rate limit object' do 24 | error = Twitter::Error.new('execution expired') 25 | expect(error.rate_limit).to be_a Twitter::RateLimit 26 | end 27 | end 28 | 29 | %w[error errors].each do |key| 30 | context "when JSON body contains #{key}" do 31 | before do 32 | body = "{\"#{key}\":\"Internal Server Error\"}" 33 | stub_get('/1.1/statuses/user_timeline.json').with(query: {screen_name: 'sferik'}).to_return(status: 500, body: body, headers: {content_type: 'application/json; charset=utf-8'}) 34 | end 35 | it 'raises an exception with the proper message' do 36 | expect { @client.user_timeline('sferik') }.to raise_error(Twitter::Error::InternalServerError) 37 | end 38 | end 39 | end 40 | 41 | Twitter::Error::ERRORS.each do |status, exception| 42 | context "when HTTP status is #{status}" do 43 | before do 44 | stub_get('/1.1/statuses/user_timeline.json').with(query: {screen_name: 'sferik'}).to_return(status: status, body: '{}', headers: {content_type: 'application/json; charset=utf-8'}) 45 | end 46 | it "raises #{exception}" do 47 | expect { @client.user_timeline('sferik') }.to raise_error(exception) 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/twitter/rest/api.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/rest/account_activity' 2 | require 'twitter/rest/direct_messages' 3 | require 'twitter/rest/direct_messages/welcome_messages' 4 | require 'twitter/rest/favorites' 5 | require 'twitter/rest/friends_and_followers' 6 | require 'twitter/rest/help' 7 | require 'twitter/rest/lists' 8 | require 'twitter/rest/oauth' 9 | require 'twitter/rest/places_and_geo' 10 | require 'twitter/rest/saved_searches' 11 | require 'twitter/rest/search' 12 | require 'twitter/rest/premium_search' 13 | require 'twitter/rest/spam_reporting' 14 | require 'twitter/rest/suggested_users' 15 | require 'twitter/rest/timelines' 16 | require 'twitter/rest/trends' 17 | require 'twitter/rest/tweets' 18 | require 'twitter/rest/undocumented' 19 | require 'twitter/rest/users' 20 | 21 | module Twitter 22 | module REST 23 | # @note All methods have been separated into modules and follow the same grouping used in {http://dev.twitter.com/doc the Twitter API Documentation}. 24 | # @see https://dev.twitter.com/overview/general/things-every-developer-should-know 25 | module API 26 | include Twitter::REST::AccountActivity 27 | include Twitter::REST::DirectMessages 28 | include Twitter::REST::DirectMessages::WelcomeMessages 29 | include Twitter::REST::Favorites 30 | include Twitter::REST::FriendsAndFollowers 31 | include Twitter::REST::Help 32 | include Twitter::REST::Lists 33 | include Twitter::REST::OAuth 34 | include Twitter::REST::PlacesAndGeo 35 | include Twitter::REST::PremiumSearch 36 | include Twitter::REST::SavedSearches 37 | include Twitter::REST::Search 38 | include Twitter::REST::SpamReporting 39 | include Twitter::REST::SuggestedUsers 40 | include Twitter::REST::Timelines 41 | include Twitter::REST::Trends 42 | include Twitter::REST::Tweets 43 | include Twitter::REST::Undocumented 44 | include Twitter::REST::Users 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/fixtures/members.json: -------------------------------------------------------------------------------- 1 | [{"profile_sidebar_fill_color":"C0DFEC","protected":false,"id_str":"13","notifications":false,"profile_background_tile":false,"screen_name":"biz","name":"Biz Stone","display_url":"bizstone.com","listed_count":15392,"location":"San Francisco, CA","expanded_url":"http:\/\/www.bizstone.com","show_all_inline_media":true,"contributors_enabled":false,"following":false,"geo_enabled":true,"utc_offset":-28800,"profile_link_color":"0084B4","description":"Co-founder of Twitter, Inc.","profile_sidebar_border_color":"a8c7f7","url":"http:\/\/t.co\/bdlNWgB","time_zone":"Pacific Time (US & Canada)","status":{"id_str":"110109632814518272","in_reply_to_status_id":null,"truncated":false,"favorited":false,"possibly_sensitive":false,"in_reply_to_status_id_str":null,"geo":null,"in_reply_to_screen_name":null,"in_reply_to_user_id_str":null,"coordinates":null,"in_reply_to_user_id":null,"source":"\u003Ca href=\"http:\/\/flickr.com\/services\/twitter\/\" rel=\"nofollow\"\u003EFlickr\u003C\/a\u003E","created_at":"Sat Sep 03 21:59:16 +0000 2011","contributors":null,"retweeted":false,"retweet_count":4,"id":110109632814518272,"place":null,"text":"Cavallo Point Dandelion http:\/\/t.co\/nv0gcpL"},"default_profile_image":false,"statuses_count":4466,"profile_use_background_image":true,"verified":true,"favourites_count":869,"friends_count":530,"profile_background_color":"022330","is_translator":false,"profile_background_image_url":"http:\/\/a0.twimg.com\/images\/themes\/theme15\/bg.png","created_at":"Tue Mar 21 20:51:43 +0000 2006","followers_count":1765283,"entities":{"user_mentions":[],"urls":[],"hashtags":[]},"default_profile":false,"follow_request_sent":false,"lang":"en","profile_background_image_url_https":"https:\/\/si0.twimg.com\/images\/themes\/theme15\/bg.png","id":13,"profile_text_color":"333333","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/513819852\/biz_stone_normal.jpg","profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/513819852\/biz_stone_normal.jpg"}] -------------------------------------------------------------------------------- /lib/twitter/premium_search_results.rb: -------------------------------------------------------------------------------- 1 | require 'cgi' 2 | require 'twitter/enumerable' 3 | require 'twitter/rest/request' 4 | require 'twitter/utils' 5 | require 'uri' 6 | 7 | module Twitter 8 | class PremiumSearchResults 9 | include Twitter::Enumerable 10 | include Twitter::Utils 11 | # @return [Hash] 12 | attr_reader :attrs 13 | alias to_h attrs 14 | alias to_hash to_h 15 | 16 | # Initializes a new SearchResults object 17 | # 18 | # @param request [Twitter::REST::Request] 19 | # @return [Twitter::PremiumSearchResults] 20 | def initialize(request, request_config = {}) 21 | @client = request.client 22 | @request_method = request.verb 23 | @path = request.path 24 | @options = request.options 25 | @request_config = request_config 26 | @collection = [] 27 | self.attrs = request.perform 28 | end 29 | 30 | private 31 | 32 | # @return [Boolean] 33 | def last? 34 | !next_page? 35 | end 36 | 37 | # @return [Boolean] 38 | def next_page? 39 | !!@attrs[:next] 40 | end 41 | 42 | # Returns a Hash of query parameters for the next result in the search 43 | # 44 | # @note Returned Hash can be merged into the previous search options list to easily access the next page. 45 | # @return [Hash] The parameters needed to fetch the next page. 46 | def next_page 47 | {next: @attrs[:next]} if next_page? 48 | end 49 | 50 | # @return [Hash] 51 | def fetch_next_page 52 | request = @client.premium_search(@options[:query], (@options.reject { |k| k == :query } || {}).merge(next_page), @request_config) 53 | 54 | self.attrs = request.attrs 55 | end 56 | 57 | # @param attrs [Hash] 58 | # @return [Hash] 59 | def attrs=(attrs) 60 | @attrs = attrs 61 | @attrs.fetch(:results, []).collect do |tweet| 62 | @collection << Tweet.new(tweet) 63 | end 64 | @attrs 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/fixtures/subscriptions.json: -------------------------------------------------------------------------------- 1 | {"lists":[{"uri":"\/pengwynn\/rubyists","name":"Rubyists","full_name":"@pengwynn\/rubyists","description":"","mode":"public","user":{"id":14100886,"profile_background_image_url_https":"https:\/\/si0.twimg.com\/images\/themes\/theme1\/bg.png","time_zone":"Central Time (US & Canada)","location":"Denton, TX","profile_background_image_url":"http:\/\/a0.twimg.com\/images\/themes\/theme1\/bg.png","id_str":"14100886","entities":{"url":{"urls":[{"url":"http:\/\/wynnnetherland.com","display_url":null,"indices":[0,25],"expanded_url":null}]},"description":{"urls":[]}},"profile_link_color":"0084B4","geo_enabled":true,"default_profile":false,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/2221455972\/wynn-mic-bw_normal.jpg","utc_offset":-21600,"profile_use_background_image":false,"statuses_count":7384,"name":"Wynn Netherland","follow_request_sent":false,"profile_text_color":"333333","lang":"en","screen_name":"pengwynn","listed_count":397,"protected":false,"is_translator":false,"followers_count":6182,"profile_sidebar_border_color":"FFFFFF","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/2221455972\/wynn-mic-bw_normal.jpg","description":"Christian, husband, father, GitHubber, Co-host of @thechangelog, Co-author of Sass, Compass, #CSS book http:\/\/wynn.fm\/sass-meap","profile_background_tile":false,"following":true,"profile_sidebar_fill_color":"DDEEF6","default_profile_image":false,"url":"http:\/\/wynnnetherland.com","profile_banner_url":"https:\/\/si0.twimg.com\/profile_banners\/14100886\/1347987369","favourites_count":338,"created_at":"Sat Mar 08 16:34:22 +0000 2008","friends_count":3528,"verified":false,"notifications":false,"profile_background_color":"292929","contributors_enabled":false},"following":true,"created_at":"Fri Oct 30 14:39:25 +0000 2009","member_count":499,"id_str":"1129440","subscriber_count":39,"slug":"rubyists","id":1129440}], "next_cursor":1401037770457540712, "previous_cursor":0, "next_cursor_str":"1401037770457540712", "previous_cursor_str":"0"} -------------------------------------------------------------------------------- /spec/fixtures/subscriptions2.json: -------------------------------------------------------------------------------- 1 | {"lists":[{"uri":"\/pengwynn\/rubyists","name":"Rubyists","full_name":"@pengwynn\/rubyists","description":"","mode":"public","user":{"id":14100886,"profile_background_image_url_https":"https:\/\/si0.twimg.com\/images\/themes\/theme1\/bg.png","time_zone":"Central Time (US & Canada)","location":"Denton, TX","profile_background_image_url":"http:\/\/a0.twimg.com\/images\/themes\/theme1\/bg.png","id_str":"14100886","entities":{"url":{"urls":[{"url":"http:\/\/wynnnetherland.com","display_url":null,"indices":[0,25],"expanded_url":null}]},"description":{"urls":[]}},"profile_link_color":"0084B4","geo_enabled":true,"default_profile":false,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/2221455972\/wynn-mic-bw_normal.jpg","utc_offset":-21600,"profile_use_background_image":false,"statuses_count":7384,"name":"Wynn Netherland","follow_request_sent":false,"profile_text_color":"333333","lang":"en","screen_name":"pengwynn","listed_count":397,"protected":false,"is_translator":false,"followers_count":6182,"profile_sidebar_border_color":"FFFFFF","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/2221455972\/wynn-mic-bw_normal.jpg","description":"Christian, husband, father, GitHubber, Co-host of @thechangelog, Co-author of Sass, Compass, #CSS book http:\/\/wynn.fm\/sass-meap","profile_background_tile":false,"following":true,"profile_sidebar_fill_color":"DDEEF6","default_profile_image":false,"url":"http:\/\/wynnnetherland.com","profile_banner_url":"https:\/\/si0.twimg.com\/profile_banners\/14100886\/1347987369","favourites_count":338,"created_at":"Sat Mar 08 16:34:22 +0000 2008","friends_count":3528,"verified":false,"notifications":false,"profile_background_color":"292929","contributors_enabled":false},"following":true,"created_at":"Fri Oct 30 14:39:25 +0000 2009","member_count":499,"id_str":"1129440","subscriber_count":39,"slug":"rubyists","id":1129440}], "next_cursor":0, "previous_cursor":1401037770457540712, "next_cursor_str":"0", "previous_cursor_str":"1401037770457540712"} -------------------------------------------------------------------------------- /spec/fixtures/followers_list.json: -------------------------------------------------------------------------------- 1 | {"users":[{"id":979919484,"id_str":"979919484","name":"nylews","screen_name":"SwelynD","location":"","description":"","url":null,"entities":{"description":{"urls":[]}},"protected":false,"followers_count":0,"friends_count":8,"listed_count":0,"created_at":"Fri Nov 30 07:01:27 +0000 2012","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":1,"lang":"en","status":{"created_at":"Fri Nov 30 07:03:06 +0000 2012","id":274408192602157056,"id_str":"274408192602157056","text":"Hey","source":"\u003ca href=\"http:\/\/twitter.com\/download\/android\" rel=\"nofollow\"\u003eTwitter for Android\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":0,"entities":{"hashtags":[],"urls":[],"user_mentions":[]},"favorited":false,"retweeted":false},"contributors_enabled":false,"is_translator":false,"profile_background_color":"C0DEED","profile_background_image_url":"http:\/\/a0.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_image_url_https":"https:\/\/si0.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_tile":false,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/2913780400\/b8c461be768a6bdc42c1aee37105c4fc_normal.jpeg","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/2913780400\/b8c461be768a6bdc42c1aee37105c4fc_normal.jpeg","profile_banner_url":"https:\/\/si0.twimg.com\/profile_banners\/979919484\/1354259270","profile_link_color":"0084B4","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"default_profile":true,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false}],"next_cursor":1419103567112105362,"next_cursor_str":"1419103567112105362","previous_cursor":0,"previous_cursor_str":"0"} -------------------------------------------------------------------------------- /spec/fixtures/followers_list2.json: -------------------------------------------------------------------------------- 1 | {"users":[{"id":979919484,"id_str":"979919484","name":"nylews","screen_name":"SwelynD","location":"","description":"","url":null,"entities":{"description":{"urls":[]}},"protected":false,"followers_count":0,"friends_count":8,"listed_count":0,"created_at":"Fri Nov 30 07:01:27 +0000 2012","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":1,"lang":"en","status":{"created_at":"Fri Nov 30 07:03:06 +0000 2012","id":274408192602157056,"id_str":"274408192602157056","text":"Hey","source":"\u003ca href=\"http:\/\/twitter.com\/download\/android\" rel=\"nofollow\"\u003eTwitter for Android\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":0,"entities":{"hashtags":[],"urls":[],"user_mentions":[]},"favorited":false,"retweeted":false},"contributors_enabled":false,"is_translator":false,"profile_background_color":"C0DEED","profile_background_image_url":"http:\/\/a0.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_image_url_https":"https:\/\/si0.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_tile":false,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/2913780400\/b8c461be768a6bdc42c1aee37105c4fc_normal.jpeg","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/2913780400\/b8c461be768a6bdc42c1aee37105c4fc_normal.jpeg","profile_banner_url":"https:\/\/si0.twimg.com\/profile_banners\/979919484\/1354259270","profile_link_color":"0084B4","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"default_profile":true,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false}],"next_cursor":0,"next_cursor_str":"0","previous_cursor":1419103567112105362,"previous_cursor_str":"1419103567112105362"} -------------------------------------------------------------------------------- /spec/twitter/rest/search_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::REST::Search do 4 | before do 5 | @client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', access_token: 'AT', access_token_secret: 'AS') 6 | end 7 | 8 | describe '#search' do 9 | context 'without count specified' do 10 | before do 11 | stub_get('/1.1/search/tweets.json').with(query: {q: '#freebandnames', count: '100'}).to_return(body: fixture('search.json'), headers: {content_type: 'application/json; charset=utf-8'}) 12 | end 13 | it 'requests the correct resource' do 14 | @client.search('#freebandnames') 15 | expect(a_get('/1.1/search/tweets.json').with(query: {q: '#freebandnames', count: '100'})).to have_been_made 16 | end 17 | it 'returns recent Tweets related to a query with images and videos embedded' do 18 | search = @client.search('#freebandnames') 19 | expect(search).to be_a Twitter::SearchResults 20 | expect(search.first).to be_a Twitter::Tweet 21 | expect(search.first.text).to eq('@Just_Reboot #FreeBandNames mono surround') 22 | end 23 | end 24 | context 'with count specified' do 25 | before do 26 | stub_get('/1.1/search/tweets.json').with(query: {q: '#freebandnames', count: '3'}).to_return(body: fixture('search.json'), headers: {content_type: 'application/json; charset=utf-8'}) 27 | end 28 | it 'requests the correct resource' do 29 | @client.search('#freebandnames', count: 3) 30 | expect(a_get('/1.1/search/tweets.json').with(query: {q: '#freebandnames', count: '3'})).to have_been_made 31 | end 32 | it 'returns recent Tweets related to a query with images and videos embedded' do 33 | search = @client.search('#freebandnames', count: 3) 34 | expect(search).to be_a Twitter::SearchResults 35 | expect(search.first).to be_a Twitter::Tweet 36 | expect(search.first.text).to eq('@Just_Reboot #FreeBandNames mono surround') 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/twitter/cursor.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/enumerable' 2 | require 'twitter/rest/request' 3 | require 'twitter/utils' 4 | 5 | module Twitter 6 | class Cursor 7 | include Twitter::Enumerable 8 | include Twitter::Utils 9 | # @return [Hash] 10 | attr_reader :attrs 11 | alias to_h attrs 12 | alias to_hash to_h 13 | 14 | # Initializes a new Cursor 15 | # 16 | # @param key [String, Symbol] The key to fetch the data from the response 17 | # @param klass [Class] The class to instantiate objects in the response 18 | # @param request [Twitter::REST::Request] 19 | # @param limit [Integer] After reaching the limit, we stop fetching next page 20 | # @return [Twitter::Cursor] 21 | def initialize(key, klass, request, limit = nil) 22 | @key = key.to_sym 23 | @klass = klass 24 | @client = request.client 25 | @request_method = request.verb 26 | @path = request.path 27 | @options = request.options 28 | @collection = [] 29 | @limit = limit 30 | self.attrs = request.perform 31 | end 32 | 33 | private 34 | 35 | # @return [Integer] 36 | def next_cursor 37 | @attrs[:next_cursor] 38 | end 39 | alias next next_cursor 40 | 41 | # @return [Boolean] 42 | def last? 43 | return false if next_cursor.is_a?(String) 44 | return true if next_cursor.nil? 45 | 46 | next_cursor.zero? 47 | end 48 | 49 | # @return [Boolean] 50 | def reached_limit? 51 | @limit && @limit <= attrs[@key].count 52 | end 53 | 54 | # @return [Hash] 55 | def fetch_next_page 56 | response = Twitter::REST::Request.new(@client, @request_method, @path, @options.merge(cursor: next_cursor)).perform 57 | self.attrs = response 58 | end 59 | 60 | # @param attrs [Hash] 61 | # @return [Hash] 62 | def attrs=(attrs) 63 | @attrs = attrs 64 | @attrs.fetch(@key, []).each do |element| 65 | @collection << (@klass ? @klass.new(element) : element) 66 | end 67 | @attrs 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/twitter/utils_spec.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark' 2 | require 'helper' 3 | 4 | describe Twitter::Utils do 5 | describe '#pmap' do 6 | it 'returns an array' do 7 | expect(subject.flat_pmap([], &:reverse)).to be_an(Array) 8 | end 9 | 10 | it 'behaves like map' do 11 | array = (0..9).to_a 12 | block = proc { |x| x + 1 } 13 | expect(subject.pmap(array, &block)).to eq(array.collect(&block)) 14 | end 15 | 16 | it 'maps in parallel' do 17 | delay = 0.1 18 | array = (0..9).to_a 19 | size = array.size 20 | block = proc { |x| sleep(delay) && x + 1 } 21 | block_without_sleep = proc { |x| x + 1 } 22 | expected = array.collect(&block_without_sleep) 23 | elapsed_time = Benchmark.realtime do 24 | expect(subject.pmap(array, &block)).to eq(expected) 25 | end 26 | expect(elapsed_time).to be_between(delay, delay * size) 27 | end 28 | end 29 | 30 | describe '#flat_pmap' do 31 | it 'always returns an array' do 32 | expect(subject.flat_pmap([], &:reverse)).to be_an(Array) 33 | end 34 | 35 | it 'behaves like map for a flat array' do 36 | array = (0..9).to_a 37 | block = proc { |x| x + 1 } 38 | expect(subject.flat_pmap(array, &block)).to eq(array.collect(&block)) 39 | end 40 | 41 | it 'behaves like flat_map' do 42 | array = (0..4).to_a.combination(2).to_a 43 | block = proc { |x| x.reverse } 44 | expect(subject.flat_pmap(array, &block)).to eq(array.flat_map(&block)) 45 | end 46 | 47 | it 'flat maps in parallel' do 48 | delay = 0.1 49 | array = (0..4).to_a.combination(2).to_a 50 | size = array.size 51 | block = proc { |x| sleep(delay) && x.reverse } 52 | block_without_sleep = proc(&:reverse) 53 | expected = array.collect(&block_without_sleep).flatten!(1) 54 | elapsed_time = Benchmark.realtime do 55 | expect(subject.flat_pmap(array, &block)).to eq(expected) 56 | end 57 | expect(elapsed_time).to be_between(delay, delay * size) 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/fixtures/contributees.json: -------------------------------------------------------------------------------- 1 | [{"time_zone":"Pacific Time (US & Canada)","protected":false,"profile_use_background_image":true,"name":"Twitter API","contributors_enabled":true,"created_at":"Wed May 23 06:01:13 +0000 2007","profile_background_color":"e8f2f7","expanded_url":null,"listed_count":9032,"profile_background_image_url":"http:\/\/a2.twimg.com\/profile_background_images\/229557229\/twitterapi-bg.png","utc_offset":-28800,"description":"The Real Twitter API. I tweet about API changes, service issues and happily answer questions about Twitter and our API. Don't get an answer? It's on my website.","display_url":null,"verified":true,"profile_image_url":"http:\/\/a2.twimg.com\/profile_images\/1438634086\/avatar_normal.png","id_str":"6253282","entities":{"user_mentions":[],"urls":[],"hashtags":[]},"lang":"en","profile_background_image_url_https":"https:\/\/si0.twimg.com\/profile_background_images\/229557229\/twitterapi-bg.png","favourites_count":22,"profile_text_color":"437792","status":{"truncated":false,"created_at":"Sun Aug 21 15:47:24 +0000 2011","geo":null,"in_reply_to_user_id":null,"in_reply_to_status_id":null,"favorited":false,"in_reply_to_status_id_str":null,"coordinates":null,"id_str":"105305005493452801","in_reply_to_screen_name":null,"in_reply_to_user_id_str":null,"place":null,"contributors":[819797],"retweeted":false,"retweet_count":27,"source":"web","id":105305005493452801,"text":"dev.twitter.com is still inaccessible from some locations. We're working to restore availability to everyone again. ^TS"},"default_profile":false,"friends_count":30,"profile_sidebar_fill_color":"a9d9f1","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/1438634086\/avatar_normal.png","screen_name":"twitterapi","default_profile_image":false,"show_all_inline_media":false,"geo_enabled":true,"profile_background_tile":false,"location":"San Francisco, CA","notifications":null,"is_translator":false,"profile_link_color":"0094C2","url":"http:\/\/dev.twitter.com","id":6253282,"follow_request_sent":null,"statuses_count":3044,"following":null,"profile_sidebar_border_color":"0094C2","followers_count":633992}] -------------------------------------------------------------------------------- /examples/Update.md: -------------------------------------------------------------------------------- 1 | # Update 2 | 3 | These examples assume you have a configured Twitter REST `client`. 4 | Instructions on how to configure a client can be found in 5 | [examples/Configuration.md][cfg]. 6 | 7 | [cfg]: https://github.com/sferik/twitter/blob/master/examples/Configuration.md 8 | 9 | If the authenticated user has granted read/write permission to your 10 | application, you may tweet as them. 11 | 12 | ```ruby 13 | client.update("I'm tweeting with @gem!") 14 | ``` 15 | 16 | Post an update in reply to another tweet. 17 | 18 | ```ruby 19 | client.update("I'm tweeting with @gem!", in_reply_to_status_id: 402712877960019968) 20 | ``` 21 | 22 | Post an update with precise coordinates. 23 | 24 | ```ruby 25 | client.update("I'm tweeting with @gem!", lat: 37.7821120598956, long: -122.400612831116, display_coordinates: true) 26 | ``` 27 | 28 | Post an update from a specific place. Place IDs can be retrieved using the 29 | [`#reverse_geocode`][reverse_geocode] method. 30 | 31 | [reverse_geocode]: http://rdoc.info/gems/twitter/Twitter/REST/API/PlacesAndGeo#reverse_geocode-instance_method 32 | 33 | ```ruby 34 | client.update("I'm tweeting with @gem!", place_id: "df51dec6f4ee2b2c") 35 | ``` 36 | 37 | Post an update with an image. 38 | 39 | ```ruby 40 | client.update_with_media("I'm tweeting with @gem!", File.new("/path/to/media.png")) 41 | ``` 42 | 43 | Post an update with a possibly-sensitive image. 44 | 45 | ```ruby 46 | client.update_with_media("I'm tweeting with @gem!", File.new("/path/to/sensitive-media.png"), possibly_sensitive: true) 47 | ``` 48 | 49 | Post an update with multiple images. 50 | 51 | ```ruby 52 | media = %w(/path/to/media1.png /path/to/media2.png).map { |filename| File.new(filename) } 53 | client.update_with_media("I'm tweeting with @gem!", media) 54 | ``` 55 | 56 | For more information, see the documentation for the [`#update`][update] and 57 | [`#update_with_media`][update_with_media] methods. 58 | 59 | [update]: http://rdoc.info/gems/twitter/Twitter/REST/Tweets#update-instance_method 60 | [update_with_media]: http://rdoc.info/gems/twitter/Twitter/REST/Tweets#update_with_media-instance_method 61 | -------------------------------------------------------------------------------- /spec/twitter/client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Client do 4 | describe '#user_agent' do 5 | it 'defaults TwitterRubyGem/version' do 6 | expect(subject.user_agent).to eq("TwitterRubyGem/#{Twitter::Version}") 7 | end 8 | end 9 | 10 | describe '#user_agent=' do 11 | it 'overwrites the User-Agent string' do 12 | subject.user_agent = 'MyTwitterClient/1.0.0' 13 | expect(subject.user_agent).to eq('MyTwitterClient/1.0.0') 14 | end 15 | end 16 | 17 | describe '#user_token?' do 18 | it 'returns true if the user token/secret are present' do 19 | client = Twitter::REST::Client.new(access_token: 'AT', access_token_secret: 'AS') 20 | expect(client.user_token?).to be true 21 | end 22 | it 'returns false if the user token/secret are not completely present' do 23 | client = Twitter::REST::Client.new(access_token: 'AT') 24 | expect(client.user_token?).to be false 25 | end 26 | it 'returns false if any user token/secret is blank' do 27 | client = Twitter::REST::Client.new(access_token: '', access_token_secret: 'AS') 28 | expect(client.user_token?).to be false 29 | 30 | client = Twitter::REST::Client.new(access_token: 'AT', access_token_secret: '') 31 | expect(client.user_token?).to be false 32 | end 33 | end 34 | 35 | describe '#credentials?' do 36 | it 'returns true if all credentials are present' do 37 | client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', access_token: 'AT', access_token_secret: 'AS') 38 | expect(client.credentials?).to be true 39 | end 40 | it 'returns false if any credentials are missing' do 41 | client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', access_token: 'AT') 42 | expect(client.credentials?).to be false 43 | end 44 | it 'returns false if any credential is blank' do 45 | client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', access_token: 'AT', access_token_secret: '') 46 | expect(client.credentials?).to be false 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/twitter/streaming/message_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Streaming::MessageParser do 4 | subject do 5 | Twitter::Streaming::MessageParser 6 | end 7 | 8 | describe '.parse' do 9 | it 'returns a tweet if the data has an id' do 10 | data = {id: 1} 11 | object = subject.parse(data) 12 | expect(object).to be_a Twitter::Tweet 13 | expect(object.id).to eq(1) 14 | end 15 | it 'returns an event if the data has an event' do 16 | data = {event: 'favorite', source: {id: 1}, target: {id: 2}, target_object: {id: 1}} 17 | object = subject.parse(data) 18 | expect(object).to be_a Twitter::Streaming::Event 19 | expect(object.name).to eq(:favorite) 20 | expect(object.source).to be_a Twitter::User 21 | expect(object.source.id).to eq(1) 22 | expect(object.target).to be_a Twitter::User 23 | expect(object.target.id).to eq(2) 24 | expect(object.target_object).to be_a Twitter::Tweet 25 | expect(object.target_object.id).to eq(1) 26 | end 27 | it 'returns a direct message if the data has a direct_message' do 28 | data = {direct_message: {id: 1}} 29 | object = subject.parse(data) 30 | expect(object).to be_a Twitter::DirectMessage 31 | expect(object.id).to eq(1) 32 | end 33 | it 'returns a friend list if the data has friends' do 34 | data = {friends: [1]} 35 | object = subject.parse(data) 36 | expect(object).to be_a Twitter::Streaming::FriendList 37 | expect(object.first).to eq(1) 38 | end 39 | it 'returns a deleted tweet if the data has a deleted status' do 40 | data = {delete: {status: {id: 1}}} 41 | object = subject.parse(data) 42 | expect(object).to be_a Twitter::Streaming::DeletedTweet 43 | expect(object.id).to eq(1) 44 | end 45 | it 'returns a stall warning if the data has a warning' do 46 | data = {warning: {code: 'FALLING_BEHIND'}} 47 | object = subject.parse(data) 48 | expect(object).to be_a Twitter::Streaming::StallWarning 49 | expect(object.code).to eq('FALLING_BEHIND') 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | In the spirit of [free software][free-sw], **everyone** is encouraged to help 3 | improve this project. Here are some ways *you* can contribute: 4 | 5 | [free-sw]: http://www.fsf.org/licensing/essays/free-sw.html 6 | 7 | * Use alpha, beta, and pre-release versions. 8 | * Report bugs. 9 | * Suggest new features. 10 | * Write or edit documentation. 11 | * Write specifications. 12 | * Write code (**no patch is too small**: fix typos, add comments, clean up 13 | inconsistent whitespace). 14 | * Refactor code. 15 | * Fix [issues][]. 16 | * Review patches. 17 | * Financially pledge using [gittip][]. 18 | 19 | [issues]: https://github.com/sferik/twitter/issues 20 | [gittip]: https://www.gittip.com/sferik/ 21 | 22 | ## Submitting an Issue 23 | We use the [GitHub issue tracker][issues] to track bugs and features. Before 24 | submitting a bug report or feature request, check to make sure it hasn't 25 | already been submitted. When submitting a bug report, please include a [Gist][] 26 | that includes a stack trace and any details that may be necessary to reproduce 27 | the bug, including your gem version, Ruby version, and operating system. 28 | Ideally, a bug report should include a pull request with failing specs. 29 | 30 | [gist]: https://gist.github.com/ 31 | 32 | ## Submitting a Pull Request 33 | 1. [Fork the repository.][fork] 34 | 2. [Create a topic branch.][branch] 35 | 3. Add specs for your unimplemented feature or bug fix. 36 | 4. Run `bundle exec rake spec`. If your specs pass, return to step 3. 37 | 5. Implement your feature or bug fix. 38 | 6. Run `bundle exec rake`. If your specs fail, return to step 5. 39 | 7. Run `open coverage/index.html`. If your changes are not completely covered 40 | by your tests, return to step 3. 41 | 8. Add documentation for your feature or bug fix. 42 | 9. Run `bundle exec rake verify_measurements`. If your changes are not 100% 43 | documented, go back to step 8. 44 | 10. Commit and push your changes. 45 | 11. [Submit a pull request.][pr] 46 | 47 | [fork]: http://help.github.com/fork-a-repo/ 48 | [branch]: http://learn.github.com/p/branching.html 49 | [pr]: http://help.github.com/send-pull-requests/ 50 | -------------------------------------------------------------------------------- /lib/twitter/tweet.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/creatable' 2 | require 'twitter/entities' 3 | require 'twitter/identity' 4 | 5 | module Twitter 6 | class Tweet < Twitter::Identity 7 | include Twitter::Creatable 8 | include Twitter::Entities 9 | # @return [String] 10 | attr_reader :filter_level, :in_reply_to_screen_name, :lang, :source, :text 11 | # @return [Integer] 12 | attr_reader :favorite_count, :in_reply_to_status_id, :in_reply_to_user_id, 13 | :quote_count, :reply_count, :retweet_count 14 | alias in_reply_to_tweet_id in_reply_to_status_id 15 | alias reply? in_reply_to_user_id? 16 | object_attr_reader :GeoFactory, :geo 17 | object_attr_reader :Metadata, :metadata 18 | object_attr_reader :Place, :place 19 | object_attr_reader :Tweet, :retweeted_status 20 | object_attr_reader :Tweet, :quoted_status 21 | object_attr_reader :Tweet, :current_user_retweet 22 | alias retweeted_tweet retweeted_status 23 | alias retweet? retweeted_status? 24 | alias retweeted_tweet? retweeted_status? 25 | alias quoted_tweet quoted_status 26 | alias quote? quoted_status? 27 | alias quoted_tweet? quoted_status? 28 | object_attr_reader :User, :user, :status 29 | predicate_attr_reader :favorited, :possibly_sensitive, :retweeted, 30 | :truncated 31 | 32 | # Initializes a new object 33 | # 34 | # @param attrs [Hash] 35 | # @return [Twitter::Tweet] 36 | def initialize(attrs = {}) 37 | attrs[:text] = attrs[:full_text] if attrs[:text].nil? && !attrs[:full_text].nil? 38 | super 39 | end 40 | 41 | # @note May be > 280 characters. 42 | # @return [String] 43 | def full_text 44 | if retweet? 45 | prefix = text[/\A(RT @[a-z0-9_]{1,20}: )/i, 1] 46 | [prefix, retweeted_status.text].compact.join 47 | else 48 | text 49 | end 50 | end 51 | memoize :full_text 52 | 53 | # @return [Addressable::URI] The URL to the tweet. 54 | def uri 55 | Addressable::URI.parse("https://twitter.com/#{user.screen_name}/status/#{id}") if user? 56 | end 57 | memoize :uri 58 | alias url uri 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/twitter/rest/suggested_users.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/arguments' 2 | require 'twitter/rest/utils' 3 | require 'twitter/suggestion' 4 | require 'twitter/user' 5 | 6 | module Twitter 7 | module REST 8 | module SuggestedUsers 9 | include Twitter::REST::Utils 10 | 11 | # @return [Array] 12 | # @rate_limited Yes 13 | # @authentication Requires user context 14 | # @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid. 15 | # @overload suggestions(options = {}) 16 | # Returns the list of suggested user categories 17 | # 18 | # @see https://dev.twitter.com/rest/reference/get/users/suggestions 19 | # @param options [Hash] A customizable set of options. 20 | # @overload suggestions(slug, options = {}) 21 | # Returns the users in a given category 22 | # 23 | # @see https://dev.twitter.com/rest/reference/get/users/suggestions/:slug 24 | # @param slug [String] The short name of list or a category. 25 | # @param options [Hash] A customizable set of options. 26 | def suggestions(*args) 27 | arguments = Twitter::Arguments.new(args) 28 | if arguments.last 29 | perform_get_with_object("/1.1/users/suggestions/#{arguments.pop}.json", arguments.options, Twitter::Suggestion) 30 | else 31 | perform_get_with_objects('/1.1/users/suggestions.json', arguments.options, Twitter::Suggestion) 32 | end 33 | end 34 | 35 | # Access the users in a given category of the Twitter suggested user list and return their most recent Tweet if they are not a protected user 36 | # 37 | # @see https://dev.twitter.com/rest/reference/get/users/suggestions/:slug/members 38 | # @rate_limited Yes 39 | # @authentication Requires user context 40 | # @param slug [String] The short name of list or a category. 41 | # @param options [Hash] A customizable set of options. 42 | # @return [Array] 43 | def suggest_users(slug, options = {}) 44 | perform_get_with_objects("/1.1/users/suggestions/#{slug}/members.json", options, Twitter::User) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/twitter/rate_limit_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::RateLimit do 4 | describe '#limit' do 5 | it 'returns an Integer when x-rate-limit-limit header is set' do 6 | rate_limit = Twitter::RateLimit.new('x-rate-limit-limit' => '150') 7 | expect(rate_limit.limit).to be_an Integer 8 | expect(rate_limit.limit).to eq(150) 9 | end 10 | it 'returns nil when x-rate-limit-limit header is not set' do 11 | rate_limit = Twitter::RateLimit.new 12 | expect(rate_limit.limit).to be_nil 13 | end 14 | end 15 | 16 | describe '#remaining' do 17 | it 'returns an Integer when x-rate-limit-remaining header is set' do 18 | rate_limit = Twitter::RateLimit.new('x-rate-limit-remaining' => '149') 19 | expect(rate_limit.remaining).to be_an Integer 20 | expect(rate_limit.remaining).to eq(149) 21 | end 22 | it 'returns nil when x-rate-limit-remaining header is not set' do 23 | rate_limit = Twitter::RateLimit.new 24 | expect(rate_limit.remaining).to be_nil 25 | end 26 | end 27 | 28 | describe '#reset_at' do 29 | it 'returns a Time when x-rate-limit-reset header is set' do 30 | rate_limit = Twitter::RateLimit.new('x-rate-limit-reset' => '1339019097') 31 | expect(rate_limit.reset_at).to be_a Time 32 | expect(rate_limit.reset_at).to be_utc 33 | expect(rate_limit.reset_at).to eq(Time.at(1_339_019_097)) 34 | end 35 | it 'returns nil when x-rate-limit-reset header is not set' do 36 | rate_limit = Twitter::RateLimit.new 37 | expect(rate_limit.reset_at).to be_nil 38 | end 39 | end 40 | 41 | describe '#reset_in' do 42 | before do 43 | Timecop.freeze(Time.utc(2012, 6, 6, 17, 22, 0)) 44 | end 45 | after do 46 | Timecop.return 47 | end 48 | it 'returns an Integer when x-rate-limit-reset header is set' do 49 | rate_limit = Twitter::RateLimit.new('x-rate-limit-reset' => '1339019097') 50 | expect(rate_limit.reset_in).to be_an Integer 51 | expect(rate_limit.reset_in).to eq(15_777) 52 | end 53 | it 'returns nil when x-rate-limit-reset header is not set' do 54 | rate_limit = Twitter::RateLimit.new 55 | expect(rate_limit.reset_in).to be_nil 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/fixtures/friends_list.json: -------------------------------------------------------------------------------- 1 | {"users":[{"id":730805892,"id_str":"730805892","name":"Will Ahrens","screen_name":"WillAhrens","location":"Long Island, New York","description":"guitarist for Nick Tangorra, Paging Grace, Trish Torrales","url":null,"entities":{"description":{"urls":[]}},"protected":false,"followers_count":2592,"friends_count":2712,"listed_count":2,"created_at":"Wed Aug 01 14:40:58 +0000 2012","favourites_count":1482,"utc_offset":null,"time_zone":null,"geo_enabled":true,"verified":false,"statuses_count":3888,"lang":"en","status":{"created_at":"Thu Nov 29 19:33:44 +0000 2012","id":274234710501244929,"id_str":"274234710501244929","text":"@MandyJay7 catch ya later! :)","source":"web","truncated":false,"in_reply_to_status_id":274234209831354368,"in_reply_to_status_id_str":"274234209831354368","in_reply_to_user_id":463877152,"in_reply_to_user_id_str":"463877152","in_reply_to_screen_name":"MandyJay7","geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":0,"entities":{"hashtags":[],"urls":[],"user_mentions":[{"screen_name":"MandyJay7","name":"Mandy Jay","id":463877152,"id_str":"463877152","indices":[0,10]}]},"favorited":false,"retweeted":false},"contributors_enabled":false,"is_translator":false,"profile_background_color":"1A1B1F","profile_background_image_url":"http:\/\/a0.twimg.com\/profile_background_images\/679274925\/9f28628390683b63fb9784e34cc82190.jpeg","profile_background_image_url_https":"https:\/\/si0.twimg.com\/profile_background_images\/679274925\/9f28628390683b63fb9784e34cc82190.jpeg","profile_background_tile":true,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/2896578010\/5b406ead0cbf9784ba228e34e1ccc698_normal.jpeg","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/2896578010\/5b406ead0cbf9784ba228e34e1ccc698_normal.jpeg","profile_banner_url":"https:\/\/si0.twimg.com\/profile_banners\/730805892\/1353223929","profile_link_color":"1AA196","profile_sidebar_border_color":"FFFFFF","profile_sidebar_fill_color":"252429","profile_text_color":"666666","profile_use_background_image":true,"default_profile":false,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false}],"next_cursor":1418947360875712729,"next_cursor_str":"1418947360875712729","previous_cursor":0,"previous_cursor_str":"0"} -------------------------------------------------------------------------------- /spec/fixtures/friends_list2.json: -------------------------------------------------------------------------------- 1 | {"users":[{"id":730805892,"id_str":"730805892","name":"Will Ahrens","screen_name":"WillAhrens","location":"Long Island, New York","description":"guitarist for Nick Tangorra, Paging Grace, Trish Torrales","url":null,"entities":{"description":{"urls":[]}},"protected":false,"followers_count":2592,"friends_count":2712,"listed_count":2,"created_at":"Wed Aug 01 14:40:58 +0000 2012","favourites_count":1482,"utc_offset":null,"time_zone":null,"geo_enabled":true,"verified":false,"statuses_count":3888,"lang":"en","status":{"created_at":"Thu Nov 29 19:33:44 +0000 2012","id":274234710501244929,"id_str":"274234710501244929","text":"@MandyJay7 catch ya later! :)","source":"web","truncated":false,"in_reply_to_status_id":274234209831354368,"in_reply_to_status_id_str":"274234209831354368","in_reply_to_user_id":463877152,"in_reply_to_user_id_str":"463877152","in_reply_to_screen_name":"MandyJay7","geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":0,"entities":{"hashtags":[],"urls":[],"user_mentions":[{"screen_name":"MandyJay7","name":"Mandy Jay","id":463877152,"id_str":"463877152","indices":[0,10]}]},"favorited":false,"retweeted":false},"contributors_enabled":false,"is_translator":false,"profile_background_color":"1A1B1F","profile_background_image_url":"http:\/\/a0.twimg.com\/profile_background_images\/679274925\/9f28628390683b63fb9784e34cc82190.jpeg","profile_background_image_url_https":"https:\/\/si0.twimg.com\/profile_background_images\/679274925\/9f28628390683b63fb9784e34cc82190.jpeg","profile_background_tile":true,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/2896578010\/5b406ead0cbf9784ba228e34e1ccc698_normal.jpeg","profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/2896578010\/5b406ead0cbf9784ba228e34e1ccc698_normal.jpeg","profile_banner_url":"https:\/\/si0.twimg.com\/profile_banners\/730805892\/1353223929","profile_link_color":"1AA196","profile_sidebar_border_color":"FFFFFF","profile_sidebar_fill_color":"252429","profile_text_color":"666666","profile_use_background_image":true,"default_profile":false,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false}],"next_cursor":0,"next_cursor_str":"0","previous_cursor":1418947360875712729,"previous_cursor_str":"1418947360875712729"} -------------------------------------------------------------------------------- /spec/fixtures/direct_messages.json: -------------------------------------------------------------------------------- 1 | [{"recipient_id":6238622,"recipient":{"verified":false,"profile_background_tile":false,"profile_sidebar_fill_color":"DDEEF6","description":"Erasmus @ Lille, FII Student, Freelance web developer, Ruby & Rails fan","follow_request_sent":false,"notifications":false,"profile_sidebar_border_color":"C0DEED","time_zone":"Paris","url":"http:\/\/purl.org\/net\/bogdan.gaza","listed_count":11,"friends_count":196,"profile_background_color":"C0DEED","lang":"en","statuses_count":1448,"created_at":"Tue May 22 17:13:45 +0000 2007","location":"Lille, France","show_all_inline_media":false,"profile_use_background_image":true,"favourites_count":1,"profile_text_color":"333333","protected":false,"profile_image_url":"http:\/\/a3.twimg.com\/profile_images\/1099173727\/DSC_0261_normal.png","id_str":"6238622","contributors_enabled":false,"name":"Bogdan Gaza","following":true,"geo_enabled":true,"profile_background_image_url":"http:\/\/s.twimg.com\/a\/1286916367\/images\/themes\/theme1\/bg.png","profile_link_color":"0084B4","screen_name":"hurrycane","id":6238622,"utc_offset":3600,"followers_count":276},"sender_screen_name":"sferik","created_at":"Sun Oct 17 20:48:55 +0000 2010","recipient_screen_name":"hurrycane","id_str":"1773478249","sender":{"verified":false,"profile_background_tile":false,"profile_sidebar_fill_color":"DDEEF6","description":"Adventures in hunger and foolishness.","follow_request_sent":false,"notifications":false,"profile_sidebar_border_color":"C0DEED","time_zone":"Pacific Time (US & Canada)","url":null,"listed_count":28,"friends_count":88,"profile_background_color":"000000","lang":"en","statuses_count":2970,"created_at":"Mon Jul 16 12:59:01 +0000 2007","location":"San Francisco","show_all_inline_media":true,"profile_use_background_image":true,"favourites_count":729,"profile_text_color":"333333","protected":false,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/323331048\/me_normal.jpg","id_str":"7505382","contributors_enabled":false,"name":"Erik Berlin","following":false,"geo_enabled":true,"profile_background_image_url":"http:\/\/a3.twimg.com\/profile_background_images\/162641967\/we_concept_bg2.png","profile_link_color":"0084B4","screen_name":"sferik","id":7505382,"utc_offset":-28800,"followers_count":898},"sender_id":7505382,"id":1773478249,"text":"Sounds good. Meeting Tuesday is fine."}] -------------------------------------------------------------------------------- /lib/twitter/rest/help.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/configuration' 2 | require 'twitter/language' 3 | require 'twitter/rest/request' 4 | require 'twitter/rest/utils' 5 | 6 | module Twitter 7 | module REST 8 | module Help 9 | include Twitter::REST::Utils 10 | 11 | # Returns the current configuration used by Twitter 12 | # 13 | # @see https://dev.twitter.com/rest/reference/get/help/configuration 14 | # @rate_limited Yes 15 | # @authentication Requires user context 16 | # @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid. 17 | # @return [Twitter::Configuration] Twitter's configuration. 18 | def configuration(options = {}) 19 | perform_get_with_object('/1.1/help/configuration.json', options, Twitter::Configuration) 20 | end 21 | 22 | # Returns the list of languages supported by Twitter 23 | # 24 | # @see https://dev.twitter.com/rest/reference/get/help/languages 25 | # @rate_limited Yes 26 | # @authentication Requires user context 27 | # @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid. 28 | # @return [Array] 29 | def languages(options = {}) 30 | perform_get_with_objects('/1.1/help/languages.json', options, Twitter::Language) 31 | end 32 | 33 | # Returns {https://twitter.com/privacy Twitter's Privacy Policy} 34 | # 35 | # @see https://dev.twitter.com/rest/reference/get/help/privacy 36 | # @rate_limited Yes 37 | # @authentication Requires user context 38 | # @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid. 39 | # @return [String] 40 | def privacy(options = {}) 41 | perform_get('/1.1/help/privacy.json', options)[:privacy] 42 | end 43 | 44 | # Returns {https://twitter.com/tos Twitter's Terms of Service} 45 | # 46 | # @see https://dev.twitter.com/rest/reference/get/help/tos 47 | # @rate_limited Yes 48 | # @authentication Requires user context 49 | # @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid. 50 | # @return [String] 51 | def tos(options = {}) 52 | perform_get('/1.1/help/tos.json', options)[:tos] 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/twitter/rest/premium_search.rb: -------------------------------------------------------------------------------- 1 | require 'twitter/rest/request' 2 | require 'twitter/premium_search_results' 3 | 4 | module Twitter 5 | module REST 6 | module PremiumSearch 7 | MAX_TWEETS_PER_REQUEST = 100 8 | 9 | # Returns tweets from the 30-Day API that match a specified query. 10 | # 11 | # @see https://developer.twitter.com/en/docs/tweets/search/overview/premium 12 | # @see https://developer.twitter.com/en/docs/tweets/search/api-reference/premium-search.html#DataEndpoint 13 | # @rate_limited Yes 14 | # @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid. 15 | # @param query [String] A search term. 16 | # @param options [Hash] A customizable set of options. 17 | # @option options [String] :tag Tags can be used to segregate rules and their matching data into different logical groups. 18 | # @option options [Integer] :maxResults The maximum number of search results to be returned by a request. A number between 10 and the system limit (currently 500, 100 for Sandbox environments). By default, a request response will return 100 results 19 | # @option options [String] :fromDate The oldest UTC timestamp (from most recent 30 days) from which the Tweets will be provided. Date should be formatted as yyyymmddhhmm. 20 | # @option options [String] :toDate The latest, most recent UTC timestamp to which the activities will be provided. Date should be formatted as yyyymmddhhmm. 21 | # @option request_config [String] :product Indicates the search endpoint you are making requests to, either 30day or fullarchive. Default 30day 22 | # @return [Twitter::PremiumSearchResults] Return tweets that match a specified query with search metadata 23 | def premium_search(query, options = {}, request_config = {}) 24 | options = options.clone 25 | options[:maxResults] ||= MAX_TWEETS_PER_REQUEST 26 | request_config[:request_method] = :json_post if request_config[:request_method].nil? || request_config[:request_method] == :post 27 | request_config[:product] ||= '30day' 28 | path = "/1.1/tweets/search/#{request_config[:product]}/#{dev_environment}.json" 29 | request = Twitter::REST::Request.new(self, request_config[:request_method], path, options.merge(query: query)) 30 | Twitter::PremiumSearchResults.new(request, request_config) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/twitter/search_results.rb: -------------------------------------------------------------------------------- 1 | require 'cgi' 2 | require 'twitter/enumerable' 3 | require 'twitter/rest/request' 4 | require 'twitter/utils' 5 | require 'uri' 6 | 7 | module Twitter 8 | class SearchResults 9 | include Twitter::Enumerable 10 | include Twitter::Utils 11 | # @return [Hash] 12 | attr_reader :attrs, :rate_limit 13 | alias to_h attrs 14 | alias to_hash to_h 15 | 16 | # Initializes a new SearchResults object 17 | # 18 | # @param request [Twitter::REST::Request] 19 | # @return [Twitter::SearchResults] 20 | def initialize(request) 21 | @client = request.client 22 | @request_method = request.verb 23 | @path = request.path 24 | @options = request.options 25 | @collection = [] 26 | self.attrs = request.perform 27 | end 28 | 29 | private 30 | 31 | # @return [Boolean] 32 | def last? 33 | !next_page? 34 | end 35 | 36 | # @return [Boolean] 37 | def next_page? 38 | !!@attrs[:search_metadata][:next_results] unless @attrs[:search_metadata].nil? 39 | end 40 | 41 | # Returns a Hash of query parameters for the next result in the search 42 | # 43 | # @note Returned Hash can be merged into the previous search options list to easily access the next page. 44 | # @return [Hash] The parameters needed to fetch the next page. 45 | def next_page 46 | query_string_to_hash(@attrs[:search_metadata][:next_results]) if next_page? 47 | end 48 | 49 | # @return [Hash] 50 | def fetch_next_page 51 | response = Twitter::REST::Request.new(@client, @request_method, @path, @options.merge(next_page)) 52 | self.attrs = response.perform 53 | @rate_limit = response.rate_limit 54 | end 55 | 56 | # @param attrs [Hash] 57 | # @return [Hash] 58 | def attrs=(attrs) 59 | @attrs = attrs 60 | @attrs.fetch(:statuses, []).collect do |tweet| 61 | @collection << Tweet.new(tweet) 62 | end 63 | @attrs 64 | end 65 | 66 | # Converts query string to a hash 67 | # 68 | # @param query_string [String] The query string of a URL. 69 | # @return [Hash] The query string converted to a hash (with symbol keys). 70 | # @example Convert query string to a hash 71 | # query_string_to_hash("foo=bar&baz=qux") #=> {:foo=>"bar", :baz=>"qux"} 72 | def query_string_to_hash(query_string) 73 | query = CGI.parse(URI.parse(query_string).query) 74 | Hash[query.collect { |key, value| [key.to_sym, value.first] }] 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/twitter/rest/help_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::REST::Help do 4 | before do 5 | @client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', access_token: 'AT', access_token_secret: 'AS') 6 | end 7 | 8 | describe '#configuration' do 9 | before do 10 | stub_get('/1.1/help/configuration.json').to_return(body: fixture('configuration.json'), headers: {content_type: 'application/json; charset=utf-8'}) 11 | end 12 | it 'requests the correct resource' do 13 | @client.configuration 14 | expect(a_get('/1.1/help/configuration.json')).to have_been_made 15 | end 16 | it 'returns the Twitter configuration' do 17 | configuration = @client.configuration 18 | expect(configuration).to be_a Twitter::Configuration 19 | expect(configuration.characters_reserved_per_media).to eq(20) 20 | end 21 | end 22 | 23 | describe '#languages' do 24 | before do 25 | stub_get('/1.1/help/languages.json').to_return(body: fixture('languages.json'), headers: {content_type: 'application/json; charset=utf-8'}) 26 | end 27 | it 'requests the correct resource' do 28 | @client.languages 29 | expect(a_get('/1.1/help/languages.json')).to have_been_made 30 | end 31 | it 'returns the list of languages supported by Twitter' do 32 | languages = @client.languages 33 | expect(languages).to be_an Array 34 | expect(languages.first).to be_a Twitter::Language 35 | expect(languages.first.name).to eq('Portuguese') 36 | end 37 | end 38 | 39 | describe '#privacy' do 40 | before do 41 | stub_get('/1.1/help/privacy.json').to_return(body: fixture('privacy.json'), headers: {content_type: 'application/json; charset=utf-8'}) 42 | end 43 | it 'requests the correct resource' do 44 | @client.privacy 45 | expect(a_get('/1.1/help/privacy.json')).to have_been_made 46 | end 47 | it 'returns the Twitter Privacy Policy' do 48 | privacy = @client.privacy 49 | expect(privacy.split.first).to eq('Twitter') 50 | end 51 | end 52 | 53 | describe '#tos' do 54 | before do 55 | stub_get('/1.1/help/tos.json').to_return(body: fixture('tos.json'), headers: {content_type: 'application/json; charset=utf-8'}) 56 | end 57 | it 'requests the correct resource' do 58 | @client.tos 59 | expect(a_get('/1.1/help/tos.json')).to have_been_made 60 | end 61 | it 'returns the Twitter Terms of Service' do 62 | tos = @client.tos 63 | expect(tos.split.first).to eq('Terms') 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/twitter/search_results_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::SearchResults do 4 | describe '#each' do 5 | before do 6 | @client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', access_token: 'AT', access_token_secret: 'AS') 7 | stub_get('/1.1/search/tweets.json').with(query: {q: '#freebandnames', count: '100'}).to_return(body: fixture('search.json'), headers: {content_type: 'application/json; charset=utf-8'}) 8 | stub_get('/1.1/search/tweets.json').with(query: {q: '#freebandnames', count: '3', include_entities: '1', max_id: '414071361066532863'}).to_return(body: fixture('search2.json'), headers: {content_type: 'application/json; charset=utf-8'}) 9 | end 10 | it 'requests the correct resources' do 11 | @client.search('#freebandnames').each {} 12 | expect(a_get('/1.1/search/tweets.json').with(query: {q: '#freebandnames', count: '100'})).to have_been_made 13 | expect(a_get('/1.1/search/tweets.json').with(query: {q: '#freebandnames', count: '3', include_entities: '1', max_id: '414071361066532863'})).to have_been_made 14 | end 15 | it 'iterates' do 16 | count = 0 17 | search_results = @client.search('#freebandnames') 18 | search_results.each { count += 1 } 19 | expect(count).to eq(6) 20 | expect(search_results.rate_limit).to be_a(Twitter::RateLimit) 21 | end 22 | it 'passes through parameters to the next request' do 23 | stub_get('/1.1/search/tweets.json').with(query: {q: '#freebandnames', since_id: '414071360078878542', count: '100'}).to_return(body: fixture('search.json'), headers: {content_type: 'application/json; charset=utf-8'}) 24 | stub_get('/1.1/search/tweets.json').with(query: {q: '#freebandnames', since_id: '414071360078878542', count: '3', include_entities: '1', max_id: '414071361066532863'}).to_return(body: fixture('search2.json'), headers: {content_type: 'application/json; charset=utf-8'}) 25 | @client.search('#freebandnames', since_id: 414_071_360_078_878_542).each {} 26 | expect(a_get('/1.1/search/tweets.json').with(query: {q: '#freebandnames', since_id: '414071360078878542', count: '100'})).to have_been_made 27 | expect(a_get('/1.1/search/tweets.json').with(query: {q: '#freebandnames', since_id: '414071360078878542', count: '3', include_entities: '1', max_id: '414071361066532863'})).to have_been_made 28 | end 29 | context 'with start' do 30 | it 'iterates' do 31 | count = 0 32 | @client.search('#freebandnames').each(5) { count += 1 } 33 | expect(count).to eq(1) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/twitter/entity/uri_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Entity::URI do 4 | describe '#display_uri' do 5 | it 'returns a String when the display_url is set' do 6 | uri = Twitter::Entity::URI.new(display_url: 'example.com/expanded...') 7 | expect(uri.display_uri).to be_a String 8 | expect(uri.display_uri).to eq('example.com/expanded...') 9 | end 10 | it 'returns nil when the display_url is not set' do 11 | uri = Twitter::Entity::URI.new 12 | expect(uri.display_uri).to be_nil 13 | end 14 | end 15 | 16 | describe '#display_uri?' do 17 | it 'returns true when the display_url is set' do 18 | uri = Twitter::Entity::URI.new(display_url: 'example.com/expanded...') 19 | expect(uri.display_uri?).to be true 20 | end 21 | it 'returns false when the display_url is not set' do 22 | uri = Twitter::Entity::URI.new 23 | expect(uri.display_uri?).to be false 24 | end 25 | end 26 | 27 | describe '#expanded_uri' do 28 | it 'returns a URI when the expanded_url is set' do 29 | uri = Twitter::Entity::URI.new(expanded_url: 'https://github.com/sferik') 30 | expect(uri.expanded_uri).to be_an Addressable::URI 31 | expect(uri.expanded_uri.to_s).to eq('https://github.com/sferik') 32 | end 33 | it 'returns nil when the expanded_url is not set' do 34 | uri = Twitter::Entity::URI.new 35 | expect(uri.expanded_uri).to be_nil 36 | end 37 | end 38 | 39 | describe '#expanded_uri?' do 40 | it 'returns true when the expanded_url is set' do 41 | uri = Twitter::Entity::URI.new(expanded_url: 'https://github.com/sferik') 42 | expect(uri.expanded_uri?).to be true 43 | end 44 | it 'returns false when the expanded_url is not set' do 45 | uri = Twitter::Entity::URI.new 46 | expect(uri.expanded_uri?).to be false 47 | end 48 | end 49 | 50 | describe '#uri' do 51 | it 'returns a URI when the url is set' do 52 | uri = Twitter::Entity::URI.new(url: 'https://github.com/sferik') 53 | expect(uri.uri).to be_an Addressable::URI 54 | expect(uri.uri.to_s).to eq('https://github.com/sferik') 55 | end 56 | it 'returns nil when the url is not set' do 57 | uri = Twitter::Entity::URI.new 58 | expect(uri.uri).to be_nil 59 | end 60 | end 61 | 62 | describe '#uri?' do 63 | it 'returns true when the url is set' do 64 | uri = Twitter::Entity::URI.new(url: 'https://github.com/sferik') 65 | expect(uri.uri?).to be true 66 | end 67 | it 'returns false when the url is not set' do 68 | uri = Twitter::Entity::URI.new 69 | expect(uri.uri?).to be false 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/fixtures/users.json: -------------------------------------------------------------------------------- 1 | [{"geo_enabled":true,"time_zone":"Pacific Time (US & Canada)","description":"Adventures in hunger and foolishness.","profile_sidebar_fill_color":"DDEEF6","followers_count":898,"verified":false,"notifications":false,"follow_request_sent":false,"profile_use_background_image":true,"profile_sidebar_border_color":"C0DEED","url":null,"profile_background_image_url":"http:\/\/a3.twimg.com\/profile_background_images\/162641967\/we_concept_bg2.png","lang":"en","created_at":"Mon Jul 16 12:59:01 +0000 2007","profile_background_color":"000000","location":"San Francisco","listed_count":29,"profile_background_tile":false,"friends_count":88,"protected":false,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/323331048\/me_normal.jpg","statuses_count":2962,"profile_text_color":"333333","name":"Erik Berlin","show_all_inline_media":true,"following":true,"favourites_count":727,"screen_name":"sferik","id":7505382,"id_str":"7505382","contributors_enabled":false,"utc_offset":-28800,"profile_link_color":"0084B4"},{"geo_enabled":true,"time_zone":"Central Time (US & Canada)","description":"Christian husband and father. Dev Experience @ HP Cloud Services. Co-host of the @changelogshow. Mashup of design & development.","profile_sidebar_fill_color":"dddddd","followers_count":2767,"status":{"place":null,"retweet_count":null,"geo":null,"retweeted":false,"in_reply_to_status_id":28008649044,"source":"\u003Ca href=\"http:\/\/twitter.com\" rel=\"nofollow\"\u003ETweetie for Mac\u003C\/a\u003E","truncated":false,"in_reply_to_status_id_str":"28008649044","created_at":"Thu Oct 21 10:33:15 +0000 2010","in_reply_to_user_id":11502142,"favorited":false,"in_reply_to_user_id_str":"11502142","contributors":null,"coordinates":null,"in_reply_to_screen_name":"benfawkes","id":28014236998,"id_str":"28014236998","text":"@benfawkes Surely. Just email wynn at the changelog dot com."},"verified":false,"notifications":false,"follow_request_sent":false,"profile_use_background_image":true,"profile_sidebar_border_color":"cccccc","url":"http:\/\/wynnnetherland.com","profile_background_image_url":"http:\/\/a1.twimg.com\/profile_background_images\/61741268\/twitter-small.png","lang":"en","created_at":"Sat Mar 08 16:34:22 +0000 2008","profile_background_color":"efefef","location":"Dallas, TX","listed_count":185,"profile_background_tile":false,"friends_count":1871,"protected":false,"profile_image_url":"http:\/\/a2.twimg.com\/profile_images\/485575482\/komikazee_normal.png","statuses_count":3913,"profile_text_color":"666666","name":"Wynn Netherland","show_all_inline_media":false,"following":true,"favourites_count":32,"screen_name":"pengwynn","id":14100886,"id_str":"14100886","contributors_enabled":false,"utc_offset":-21600,"profile_link_color":"35abe9"}] -------------------------------------------------------------------------------- /spec/twitter/rest/suggested_users_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::REST::SuggestedUsers do 4 | before do 5 | @client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', access_token: 'AT', access_token_secret: 'AS') 6 | end 7 | 8 | describe '#suggestions' do 9 | context 'with a category slug passed' do 10 | before do 11 | stub_get('/1.1/users/suggestions/art-design.json').to_return(body: fixture('category.json'), headers: {content_type: 'application/json; charset=utf-8'}) 12 | end 13 | it 'requests the correct resource' do 14 | @client.suggestions('art-design') 15 | expect(a_get('/1.1/users/suggestions/art-design.json')).to have_been_made 16 | end 17 | it 'returns the users in a given category of the Twitter suggested user list' do 18 | suggestion = @client.suggestions('art-design') 19 | expect(suggestion).to be_a Twitter::Suggestion 20 | expect(suggestion.name).to eq('Art & Design') 21 | expect(suggestion.users).to be_an Array 22 | expect(suggestion.users.first).to be_a Twitter::User 23 | end 24 | end 25 | context 'without arguments passed' do 26 | before do 27 | stub_get('/1.1/users/suggestions.json').to_return(body: fixture('suggestions.json'), headers: {content_type: 'application/json; charset=utf-8'}) 28 | end 29 | it 'requests the correct resource' do 30 | @client.suggestions 31 | expect(a_get('/1.1/users/suggestions.json')).to have_been_made 32 | end 33 | it 'returns the list of suggested user categories' do 34 | suggestions = @client.suggestions 35 | expect(suggestions).to be_an Array 36 | expect(suggestions.first).to be_a Twitter::Suggestion 37 | expect(suggestions.first.name).to eq('Art & Design') 38 | end 39 | end 40 | end 41 | 42 | describe '#suggest_users' do 43 | before do 44 | stub_get('/1.1/users/suggestions/art-design/members.json').to_return(body: fixture('members.json'), headers: {content_type: 'application/json; charset=utf-8'}) 45 | end 46 | it 'requests the correct resource' do 47 | @client.suggest_users('art-design') 48 | expect(a_get('/1.1/users/suggestions/art-design/members.json')).to have_been_made 49 | end 50 | it 'returns users in a given category of the Twitter suggested user list and return their most recent status if they are not a protected user' do 51 | suggest_users = @client.suggest_users('art-design') 52 | expect(suggest_users).to be_an Array 53 | expect(suggest_users.first).to be_a Twitter::User 54 | expect(suggest_users.first.id).to eq(13) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/twitter/rest/client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::REST::Client do 4 | before do 5 | @client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', access_token: 'AT', access_token_secret: 'AS') 6 | end 7 | 8 | describe '#bearer_token?' do 9 | it 'returns true if the app token is present' do 10 | client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', bearer_token: 'BT') 11 | expect(client.bearer_token?).to be true 12 | end 13 | it 'returns false if the bearer_token is not present' do 14 | client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS') 15 | expect(client.bearer_token?).to be false 16 | end 17 | end 18 | 19 | describe '#credentials?' do 20 | it 'returns true if only bearer_token is supplied' do 21 | client = Twitter::REST::Client.new(bearer_token: 'BT') 22 | expect(client.credentials?).to be true 23 | end 24 | it 'returns true if all OAuth credentials are present' do 25 | client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', access_token: 'AT', access_token_secret: 'AS') 26 | expect(client.credentials?).to be true 27 | end 28 | it 'returns false if any credentials are missing' do 29 | client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', access_token: 'AT') 30 | expect(client.credentials?).to be false 31 | end 32 | end 33 | 34 | describe '#user_id' do 35 | it 'caches the user ID' do 36 | stub_get('/1.1/account/verify_credentials.json').with(query: {skip_status: 'true'}).to_return(body: fixture('sferik.json'), headers: {content_type: 'application/json; charset=utf-8'}) 37 | client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', access_token: 'AT', access_token_secret: 'AS') 38 | 2.times { client.send(:user_id) } 39 | expect(a_get('/1.1/account/verify_credentials.json').with(query: {skip_status: 'true'})).to have_been_made.times(1) 40 | end 41 | 42 | it 'does not cache the user ID across clients' do 43 | stub_get('/1.1/account/verify_credentials.json').with(query: {skip_status: 'true'}).to_return(body: fixture('sferik.json'), headers: {content_type: 'application/json; charset=utf-8'}) 44 | Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', access_token: 'AT', access_token_secret: 'AS').send(:user_id) 45 | Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', access_token: 'AT', access_token_secret: 'AS').send(:user_id) 46 | expect(a_get('/1.1/account/verify_credentials.json').with(query: {skip_status: 'true'})).to have_been_made.times(2) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/twitter/entities.rb: -------------------------------------------------------------------------------- 1 | require 'memoizable' 2 | require 'twitter/entity/hashtag' 3 | require 'twitter/entity/symbol' 4 | require 'twitter/entity/uri' 5 | require 'twitter/entity/user_mention' 6 | require 'twitter/media_factory' 7 | 8 | module Twitter 9 | module Entities 10 | include Memoizable 11 | 12 | # @return [Boolean] 13 | def entities? 14 | !@attrs[:entities].nil? && @attrs[:entities].any? { |_, array| array.any? } 15 | end 16 | memoize :entities? 17 | 18 | # @note Must include entities in your request for this method to work 19 | # @return [Array] 20 | def hashtags 21 | entities(Entity::Hashtag, :hashtags) 22 | end 23 | memoize :hashtags 24 | 25 | # @return [Boolean] 26 | def hashtags? 27 | hashtags.any? 28 | end 29 | memoize :hashtags? 30 | 31 | # @note Must include entities in your request for this method to work 32 | # @return [Array] 33 | def media 34 | extended_entities = entities(MediaFactory, :media, :extended_entities) 35 | extended_entities.empty? ? entities(MediaFactory, :media) : extended_entities 36 | end 37 | memoize :media 38 | 39 | # @return [Boolean] 40 | def media? 41 | media.any? 42 | end 43 | memoize :media? 44 | 45 | # @note Must include entities in your request for this method to work 46 | # @return [Array] 47 | def symbols 48 | entities(Entity::Symbol, :symbols) 49 | end 50 | memoize :symbols 51 | 52 | # @return [Boolean] 53 | def symbols? 54 | symbols.any? 55 | end 56 | memoize :symbols? 57 | 58 | # @note Must include entities in your request for this method to work 59 | # @return [Array] 60 | def uris 61 | entities(Entity::URI, :urls) 62 | end 63 | memoize :uris 64 | alias urls uris 65 | 66 | # @return [Boolean] 67 | def uris? 68 | uris.any? 69 | end 70 | alias urls? uris? 71 | 72 | # @note Must include entities in your request for this method to work 73 | # @return [Array] 74 | def user_mentions 75 | entities(Entity::UserMention, :user_mentions) 76 | end 77 | memoize :user_mentions 78 | 79 | # @return [Boolean] 80 | def user_mentions? 81 | user_mentions.any? 82 | end 83 | memoize :user_mentions? 84 | 85 | private 86 | 87 | # @param klass [Class] 88 | # @param key2 [Symbol] 89 | # @param key1 [Symbol] 90 | def entities(klass, key2, key1 = :entities) 91 | @attrs.fetch(key1.to_sym, {}).fetch(key2.to_sym, []).collect do |entity| 92 | klass.new(entity) 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /spec/twitter/cursor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::Cursor do 4 | describe '#each' do 5 | before do 6 | @client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', access_token: 'AT', access_token_secret: 'AS') 7 | stub_get('/1.1/followers/ids.json').with(query: {cursor: '-1', screen_name: 'sferik'}).to_return(body: fixture('ids_list.json'), headers: {content_type: 'application/json; charset=utf-8'}) 8 | stub_get('/1.1/followers/ids.json').with(query: {cursor: '1305102810874389703', screen_name: 'sferik'}).to_return(body: fixture('ids_list2.json'), headers: {content_type: 'application/json; charset=utf-8'}) 9 | end 10 | it 'requests the correct resources' do 11 | @client.follower_ids('sferik').each {} 12 | expect(a_get('/1.1/followers/ids.json').with(query: {cursor: '-1', screen_name: 'sferik'})).to have_been_made 13 | expect(a_get('/1.1/followers/ids.json').with(query: {cursor: '1305102810874389703', screen_name: 'sferik'})).to have_been_made 14 | end 15 | it 'iterates' do 16 | count = 0 17 | @client.follower_ids('sferik').each { count += 1 } 18 | expect(count).to eq(6) 19 | end 20 | context 'with start' do 21 | it 'iterates' do 22 | count = 0 23 | @client.follower_ids('sferik').each(5) { count += 1 } 24 | expect(count).to eq(1) 25 | end 26 | end 27 | end 28 | 29 | describe '#cursor new format' do 30 | before do 31 | @client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', access_token: 'AT', access_token_secret: 'AS') 32 | stub_get('/1.1/followers/ids.json').with(query: {cursor: '-1', screen_name: 'sferik'}).to_return(body: fixture('ids_list_new_cursor.json'), headers: {content_type: 'application/json; charset=utf-8'}) 33 | stub_get('/1.1/followers/ids.json').with(query: {cursor: 'ODU2NDc3NzEwNTk1NjI0OTYz', screen_name: 'sferik'}).to_return(body: fixture('ids_list_new_cursor2.json'), headers: {content_type: 'application/json; charset=utf-8'}) 34 | end 35 | 36 | it 'requests the correct resources' do 37 | @client.follower_ids('sferik').each {} 38 | expect(a_get('/1.1/followers/ids.json').with(query: {cursor: '-1', screen_name: 'sferik'})).to have_been_made 39 | expect(a_get('/1.1/followers/ids.json').with(query: {cursor: 'ODU2NDc3NzEwNTk1NjI0OTYz', screen_name: 'sferik'})).to have_been_made 40 | end 41 | 42 | it 'iterates' do 43 | count = 0 44 | @client.follower_ids('sferik').each { count += 1 } 45 | expect(count).to eq(6) 46 | end 47 | context 'with start' do 48 | it 'iterates' do 49 | count = 0 50 | @client.follower_ids('sferik').each(5) { count += 1 } 51 | expect(count).to eq(1) 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/twitter/rest/oauth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::REST::OAuth do 4 | before do 5 | @client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS') 6 | end 7 | 8 | describe '#token' do 9 | before do 10 | stub_post('/oauth2/token').with(body: {grant_type: 'client_credentials'}).to_return(body: fixture('bearer_token.json'), headers: {content_type: 'application/json; charset=utf-8'}) 11 | end 12 | it 'requests the correct resource' do 13 | @client.token 14 | expect(a_post('/oauth2/token').with(body: {grant_type: 'client_credentials'}, headers: {authorization: 'Basic Q0s6Q1M=', content_type: 'application/x-www-form-urlencoded', accept: '*/*'})).to have_been_made 15 | end 16 | it 'returns the bearer token' do 17 | bearer_token = @client.token 18 | expect(bearer_token).to be_a String 19 | expect(bearer_token).to eq('AAAA%2FAAA%3DAAAAAAAA') 20 | end 21 | end 22 | 23 | describe '#invalidate_token' do 24 | before do 25 | stub_post('/oauth2/invalidate_token').with(body: {access_token: 'AAAA%2FAAA%3DAAAAAAAA'}).to_return(body: '{"access_token":"AAAA%2FAAA%3DAAAAAAAA"}', headers: {content_type: 'application/json; charset=utf-8'}) 26 | @client.bearer_token = 'AAAA%2FAAA%3DAAAAAAAA' 27 | end 28 | it 'requests the correct resource' do 29 | @client.invalidate_token('AAAA%2FAAA%3DAAAAAAAA') 30 | expect(a_post('/oauth2/invalidate_token').with(body: {access_token: 'AAAA%2FAAA%3DAAAAAAAA'})).to have_been_made 31 | end 32 | it 'returns the invalidated token' do 33 | token = @client.invalidate_token('AAAA%2FAAA%3DAAAAAAAA') 34 | expect(token).to be_a String 35 | expect(token).to eq('AAAA%2FAAA%3DAAAAAAAA') 36 | end 37 | context 'with a token' do 38 | it 'requests the correct resource' do 39 | token = 'AAAA%2FAAA%3DAAAAAAAA' 40 | @client.invalidate_token(token) 41 | expect(a_post('/oauth2/invalidate_token').with(body: {access_token: 'AAAA%2FAAA%3DAAAAAAAA'})).to have_been_made 42 | end 43 | end 44 | end 45 | 46 | describe '#reverse_token' do 47 | before do 48 | # WebMock treats Basic Auth differently so we have to check against the full URL with credentials. 49 | @oauth_request_token_url = 'https://api.twitter.com/oauth/request_token?x_auth_mode=reverse_auth' 50 | stub_request(:post, @oauth_request_token_url).to_return(body: fixture('request_token.txt'), headers: {content_type: 'text/html; charset=utf-8'}) 51 | end 52 | it 'requests the correct resource' do 53 | @client.reverse_token 54 | expect(a_request(:post, @oauth_request_token_url).with(query: {x_auth_mode: 'reverse_auth'})).to have_been_made 55 | end 56 | it 'requests the correct resource' do 57 | expect(@client.reverse_token).to eql fixture('request_token.txt').read 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/fixtures/premium_search.json: -------------------------------------------------------------------------------- 1 | {"results":[{"created_at":"Sun Oct 29 21:21:37 +0000 2017","id":924748080133627904,"id_str":"924748080133627904","text":"@Simondoomband #freebandnames #freejamespattersontitles","display_text_range":[15,55],"source":"\u003ca href=\"http:\/\/twitter.com\/download\/iphone\" rel=\"nofollow\"\u003eTwitter for iPhone\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":922560845531377664,"in_reply_to_status_id_str":"922560845531377664","in_reply_to_user_id":2548533541,"in_reply_to_user_id_str":"2548533541","in_reply_to_screen_name":"Simondoomband","user":{"id":177966443,"id_str":"177966443","name":"Christian Lewis","screen_name":"CWCLewis","location":"Brooklyn, NY","url":"http:\/\/thebrotherpod.com","description":"co-host of Brother Brother Brother @thebrotherpod & Southeast Asia political economy analyst","translator_type":"none","protected":false,"verified":false,"followers_count":1412,"friends_count":1071,"listed_count":69,"favourites_count":735,"statuses_count":4049,"created_at":"Fri Aug 13 14:36:46 +0000 2010","utc_offset":-18000,"time_zone":"Eastern Time (US & Canada)","geo_enabled":true,"lang":"en","contributors_enabled":false,"is_translator":false,"profile_background_color":"000000","profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme2\/bg.gif","profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme2\/bg.gif","profile_background_tile":false,"profile_link_color":"FF691F","profile_sidebar_border_color":"000000","profile_sidebar_fill_color":"000000","profile_text_color":"000000","profile_use_background_image":false,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/809132739001810946\/2ieV1Vt0_normal.jpg","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/809132739001810946\/2ieV1Vt0_normal.jpg","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/177966443\/1475473099","default_profile":false,"default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"geo":null,"coordinates":null,"place":{"id":"011add077f4d2da3","url":"https:\/\/api.twitter.com\/1.1\/geo\/id\/011add077f4d2da3.json","place_type":"city","name":"Brooklyn","full_name":"Brooklyn, NY","country_code":"US","country":"United States","bounding_box":{"type":"Polygon","coordinates":[[[-74.041878,40.570842],[-74.041878,40.739434],[-73.855673,40.739434],[-73.855673,40.570842]]]},"attributes":{}},"contributors":null,"is_quote_status":false,"quote_count":0,"reply_count":0,"retweet_count":0,"favorite_count":0,"entities":{"hashtags":[{"text":"freebandnames","indices":[15,29]},{"text":"freejamespattersontitles","indices":[30,55]}],"urls":[],"user_mentions":[{"screen_name":"Simondoomband","name":"Simon Doom","id":2548533541,"id_str":"2548533541","indices":[0,14]}],"symbols":[]},"favorited":false,"retweeted":false,"filter_level":"low","lang":"und","matching_rules":[{"tag":null}]}],"requestParameters":{"maxResults":100,"fromDate":"201710200000","toDate":"201711190152"}} -------------------------------------------------------------------------------- /spec/twitter/rest/trends_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe Twitter::REST::Trends do 4 | before do 5 | @client = Twitter::REST::Client.new(consumer_key: 'CK', consumer_secret: 'CS', access_token: 'AT', access_token_secret: 'AS') 6 | end 7 | 8 | describe '#trends' do 9 | context 'with woeid passed' do 10 | before do 11 | stub_get('/1.1/trends/place.json').with(query: {id: '2487956'}).to_return(body: fixture('matching_trends.json'), headers: {content_type: 'application/json; charset=utf-8'}) 12 | end 13 | it 'requests the correct resource' do 14 | @client.trends(2_487_956) 15 | expect(a_get('/1.1/trends/place.json').with(query: {id: '2487956'})).to have_been_made 16 | end 17 | it 'returns the top 10 trending topics for a specific WOEID' do 18 | matching_trends = @client.trends(2_487_956) 19 | expect(matching_trends).to be_a Twitter::TrendResults 20 | expect(matching_trends.first).to be_a Twitter::Trend 21 | expect(matching_trends.first.name).to eq('#sevenwordsaftersex') 22 | end 23 | end 24 | context 'without arguments passed' do 25 | before do 26 | stub_get('/1.1/trends/place.json').with(query: {id: '1'}).to_return(body: fixture('matching_trends.json'), headers: {content_type: 'application/json; charset=utf-8'}) 27 | end 28 | it 'requests the correct resource' do 29 | @client.trends 30 | expect(a_get('/1.1/trends/place.json').with(query: {id: '1'})).to have_been_made 31 | end 32 | end 33 | end 34 | 35 | describe '#trends_available' do 36 | before do 37 | stub_get('/1.1/trends/available.json').to_return(body: fixture('locations.json'), headers: {content_type: 'application/json; charset=utf-8'}) 38 | end 39 | it 'requests the correct resource' do 40 | @client.trends_available 41 | expect(a_get('/1.1/trends/available.json')).to have_been_made 42 | end 43 | it 'returns the locations that Twitter has trending topic information for' do 44 | locations = @client.trends_available 45 | expect(locations).to be_an Array 46 | expect(locations.first).to be_a Twitter::Place 47 | expect(locations.first.name).to eq('Ireland') 48 | end 49 | end 50 | 51 | describe '#trends_closest' do 52 | before do 53 | stub_get('/1.1/trends/closest.json').to_return(body: fixture('locations.json'), headers: {content_type: 'application/json; charset=utf-8'}) 54 | end 55 | it 'requests the correct resource' do 56 | @client.trends_closest 57 | expect(a_get('/1.1/trends/closest.json')).to have_been_made 58 | end 59 | it 'returns the locations that Twitter has trending topic information for' do 60 | locations = @client.trends_closest 61 | expect(locations).to be_an Array 62 | expect(locations.first).to be_a Twitter::Place 63 | expect(locations.first.name).to eq('Ireland') 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/fixtures/retweets.json: -------------------------------------------------------------------------------- 1 | [{"retweeted_status":{"place":null,"retweet_count":null,"geo":null,"retweeted":false,"in_reply_to_status_id":null,"source":"\u003Ca href=\"http:\/\/twitter.com\/\" rel=\"nofollow\"\u003ETwitter for iPhone\u003C\/a\u003E","truncated":false,"in_reply_to_status_id_str":null,"created_at":"Sun Oct 24 03:46:25 +0000 2010","in_reply_to_user_id":null,"favorited":false,"in_reply_to_user_id_str":null,"user":{"statuses_count":10488,"time_zone":"Eastern Time (US & Canada)","description":"Raconteur.","show_all_inline_media":false,"favourites_count":8601,"profile_sidebar_fill_color":"dddddd","followers_count":91400,"contributors_enabled":false,"notifications":false,"geo_enabled":false,"profile_use_background_image":false,"profile_sidebar_border_color":"5d5d5d","url":"http:\/\/daringfireball.net","verified":false,"profile_background_image_url":"http:\/\/a3.twimg.com\/profile_background_images\/3150433\/Rumsfeld__Kissinger__Nixon_at_1974_NATO_meeting.jpg","follow_request_sent":false,"lang":"en","created_at":"Fri Dec 01 02:52:40 +0000 2006","profile_background_color":"5D5D5D","location":"Philadelphia","profile_background_tile":false,"protected":false,"profile_image_url":"http:\/\/a3.twimg.com\/profile_images\/546338003\/gruber-sxsw-final_normal.png","profile_text_color":"000000","name":"John Gruber","following":true,"screen_name":"gruber","id":33423,"id_str":"33423","listed_count":5095,"utc_offset":-18000,"friends_count":452,"profile_link_color":"2626C3"},"contributors":null,"coordinates":null,"in_reply_to_screen_name":null,"id":28561922516,"id_str":"28561922516","text":"As for the Series, I'm for the Giants. Fuck Texas, fuck Nolan Ryan, fuck George Bush."},"place":null,"retweet_count":null,"geo":null,"retweeted":false,"in_reply_to_status_id":null,"source":"\u003Ca href=\"http:\/\/apps.facebook.com\/the-run-around\/\" rel=\"nofollow\"\u003EThe Run Around\u003C\/a\u003E","truncated":false,"in_reply_to_status_id_str":null,"created_at":"Mon Oct 25 07:39:31 +0000 2010","in_reply_to_user_id":null,"favorited":false,"in_reply_to_user_id_str":null,"user":{"statuses_count":2968,"time_zone":"Pacific Time (US & Canada)","description":"Adventures in hunger and foolishness.","show_all_inline_media":true,"favourites_count":727,"profile_sidebar_fill_color":"DDEEF6","followers_count":898,"contributors_enabled":false,"notifications":false,"geo_enabled":true,"profile_use_background_image":true,"profile_sidebar_border_color":"C0DEED","url":null,"verified":false,"profile_background_image_url":"http:\/\/a3.twimg.com\/profile_background_images\/162641967\/we_concept_bg2.png","follow_request_sent":false,"lang":"en","created_at":"Mon Jul 16 12:59:01 +0000 2007","profile_background_color":"000000","location":"San Francisco","profile_background_tile":false,"protected":false,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/323331048\/me_normal.jpg","profile_text_color":"333333","name":"Erik Berlin","following":false,"screen_name":"sferik","id":7505382,"id_str":"7505382","listed_count":28,"utc_offset":-28800,"friends_count":88,"profile_link_color":"0084B4"},"contributors":null,"coordinates":null,"in_reply_to_screen_name":null,"id":28669560363,"id_str":"28669560363","text":"RT @gruber: As for the Series, I'm for the Giants. Fuck Texas, fuck Nolan Ryan, fuck George Bush."}] --------------------------------------------------------------------------------