├── docs └── .keep ├── spec ├── mock │ ├── 1.1 │ │ ├── account │ │ │ ├── update_profile_banner.json │ │ │ ├── settings.json │ │ │ ├── update_profile.json │ │ │ ├── update_profile_background_image.json │ │ │ ├── update_profile_image.json │ │ │ └── verify_credentials.json │ │ ├── blocks │ │ │ ├── ids.json │ │ │ ├── create.json │ │ │ ├── destroy.json │ │ │ └── list.json │ │ ├── followers │ │ │ └── ids.json │ │ ├── friends │ │ │ └── ids.json │ │ ├── friendships │ │ │ ├── lookup.json │ │ │ ├── create.json │ │ │ └── destroy.json │ │ ├── mutes │ │ │ └── users │ │ │ │ ├── create.json │ │ │ │ └── destroy.json │ │ ├── statuses │ │ │ ├── update.json │ │ │ ├── destroy.json │ │ │ ├── retweets_of_me.json │ │ │ ├── unretweet.json │ │ │ ├── show.json │ │ │ ├── retweet.json │ │ │ ├── mentions_timeline.json │ │ │ └── home_timeline.json │ │ ├── direct_messages │ │ │ ├── show.json │ │ │ └── sent.json │ │ ├── users │ │ │ ├── show.json │ │ │ ├── lookup.json │ │ │ └── search.json │ │ ├── direct_messages.json │ │ └── search │ │ │ └── tweets.json │ └── client.cr ├── helper.cr ├── twitter │ ├── rest │ │ ├── search_spec.cr │ │ ├── direct_messages_spec.cr │ │ ├── client_spec.cr │ │ ├── friends_and_followers_spec.cr │ │ ├── timelines_spec.cr │ │ ├── tweets_spec.cr │ │ └── users_spec.cr │ └── user_spec.cr └── fixtures │ └── users.json ├── src ├── twitter │ ├── version.cr │ ├── errors │ │ ├── client_error.cr │ │ └── server_error.cr │ ├── serializations │ │ ├── errors.cr │ │ ├── user_url_entity.cr │ │ ├── user_description_entity.cr │ │ ├── error.cr │ │ ├── poll_option.cr │ │ ├── hashtag_entity.cr │ │ ├── symbol_entity.cr │ │ ├── user_entities.cr │ │ ├── coordinates.cr │ │ ├── poll_entiry.cr │ │ ├── bounding_box.cr │ │ ├── url_entity.cr │ │ ├── cursor.cr │ │ ├── relationship.cr │ │ ├── user_mention_entity.cr │ │ ├── delete.cr │ │ ├── tweet_entities.cr │ │ ├── place.cr │ │ ├── direct_message.cr │ │ ├── location.cr │ │ ├── status.cr │ │ ├── user.cr │ │ ├── settings.cr │ │ └── tweet.cr │ ├── streaming │ │ ├── api.cr │ │ ├── statuses.cr │ │ └── client.cr │ ├── ext │ │ └── json │ │ │ └── from_json.cr │ └── rest │ │ ├── geo.cr │ │ ├── search.cr │ │ ├── api.cr │ │ ├── favorites.cr │ │ ├── direct_messages.cr │ │ ├── timelines.cr │ │ ├── tweets.cr │ │ ├── client.cr │ │ ├── friends_and_followers.cr │ │ └── users.cr └── twitter-crystal.cr ├── shard.yml ├── .gitignore ├── .ameba.yml ├── .github └── workflows │ ├── documentation.yml │ └── crystal.yml ├── README.md └── LICENSE.md /docs/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/mock/1.1/account/update_profile_banner.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/twitter-crystal" 3 | require "./mock/client" 4 | -------------------------------------------------------------------------------- /src/twitter/version.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | VERSION = {{ `shards version #{__DIR__}`.chomp.stringify }} 3 | end 4 | -------------------------------------------------------------------------------- /src/twitter-crystal.cr: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "uri" 3 | require "http/client" 4 | require "oauth" 5 | 6 | require "./**" 7 | -------------------------------------------------------------------------------- /src/twitter/errors/client_error.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | struct Errors 3 | class ClientError < Exception 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /src/twitter/errors/server_error.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | struct Errors 3 | class ServerError < Exception 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /src/twitter/serializations/errors.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | struct Errors 3 | include JSON::Serializable 4 | 5 | property errors : Array(Twitter::Error) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /src/twitter/serializations/user_url_entity.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class UserUrlEntity 3 | include JSON::Serializable 4 | 5 | property urls : Array(UrlEntity?) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/mock/1.1/blocks/ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "ids": [123, 7505382, 776284343173906432], 3 | "next_cursor": 0, 4 | "next_cursor_str": "0", 5 | "previous_cursor": 0, 6 | "previous_cursor_str": "0" 7 | } 8 | -------------------------------------------------------------------------------- /src/twitter/serializations/user_description_entity.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class UserDescriptionEntity 3 | include JSON::Serializable 4 | 5 | property urls : Array(UrlEntity?) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /src/twitter/streaming/api.cr: -------------------------------------------------------------------------------- 1 | require "./statuses" 2 | 3 | module Twitter 4 | module Streaming 5 | module API 6 | include Twitter::Streaming::Statuses 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /src/twitter/serializations/error.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | struct Error 3 | include JSON::Serializable 4 | 5 | property message : String 6 | 7 | property code : Int32 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /src/twitter/serializations/poll_option.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class PollOption 3 | include JSON::Serializable 4 | 5 | property position : Int32 6 | 7 | property text : String 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: twitter-crystal 2 | version: 0.5.4 3 | 4 | authors: 5 | - Erik Michaels-Ober 6 | - Anton Maminov 7 | 8 | crystal: ">= 1.0.0" 9 | 10 | license: Apache 11 | -------------------------------------------------------------------------------- /src/twitter/serializations/hashtag_entity.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class HashtagEntity 3 | include JSON::Serializable 4 | 5 | property text : String 6 | 7 | property indices : Array(Int32) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /src/twitter/serializations/symbol_entity.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class SymbolEntity 3 | include JSON::Serializable 4 | 5 | property text : String 6 | 7 | property indices : Array(Int32) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /src/twitter/ext/json/from_json.cr: -------------------------------------------------------------------------------- 1 | # Converter for string timestamp_ms 2 | module Time::EpochMillisConverterString 3 | def self.from_json(value : JSON::PullParser) : Time 4 | Time.unix_ms(value.read_string.to_i64) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /src/twitter/serializations/user_entities.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class UserEntities 3 | include JSON::Serializable 4 | 5 | property description : UserDescriptionEntity 6 | 7 | property url : UserUrlEntity? 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /lib/ 3 | /bin/ 4 | /.shards/ 5 | /.idea/ 6 | /tmp/ 7 | /samples/ 8 | /docs/ 9 | /.vscode 10 | 11 | # Libraries don't need dependency lock 12 | # Dependencies will be locked in application that uses them 13 | /shard.lock 14 | -------------------------------------------------------------------------------- /src/twitter/serializations/coordinates.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class Coordinates 3 | include JSON::Serializable 4 | 5 | @[JSON::Field(key: "type")] 6 | property coordinates_type : String 7 | 8 | property coordinates : Array(Float64) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /src/twitter/rest/geo.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | module REST 3 | module Geo 4 | def location(place_id : String) 5 | response = get("/1.1/geo/id/#{place_id}.json") 6 | 7 | Twitter::Location.from_json(response) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /src/twitter/serializations/poll_entiry.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class PollEntity 3 | include JSON::Serializable 4 | 5 | property options : Array(PollOption) 6 | 7 | property end_datetime : String 8 | 9 | property duration_minutes : Int32 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /src/twitter/serializations/bounding_box.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class BoundingBox 3 | include JSON::Serializable 4 | 5 | @[JSON::Field(key: "type")] 6 | property bounding_box_type : String 7 | 8 | property coordinates : Array(Array(Array(Float64))) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /src/twitter/serializations/url_entity.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class UrlEntity 3 | include JSON::Serializable 4 | 5 | property url : String 6 | 7 | property expanded_url : String? 8 | 9 | property display_url : String? 10 | 11 | property indices : Array(Int32) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /src/twitter/serializations/cursor.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class Cursor 3 | include JSON::Serializable 4 | 5 | property next_cursor : Int64 6 | 7 | property next_cursor_str : String 8 | 9 | property previous_cursor : Int64 10 | 11 | property previous_cursor_str : String 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /src/twitter/serializations/relationship.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class Relationship 3 | include JSON::Serializable 4 | 5 | property name : String 6 | 7 | property screen_name : String 8 | 9 | property id : Int64 10 | 11 | property id_str : String 12 | 13 | property connections : Array(String) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /src/twitter/serializations/user_mention_entity.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class UserMentionEntity 3 | include JSON::Serializable 4 | 5 | property screen_name : String 6 | 7 | property name : String 8 | 9 | property id : Int64 10 | 11 | property id_str : String 12 | 13 | property indices : Array(Int32) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /src/twitter/serializations/delete.cr: -------------------------------------------------------------------------------- 1 | require "../ext/json/from_json" 2 | require "../serializations/status" 3 | 4 | module Twitter 5 | class Delete 6 | include JSON::Serializable 7 | 8 | property status : Status 9 | 10 | @[JSON::Field(converter: Time::EpochMillisConverterString)] 11 | property timestamp_ms : Time 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /src/twitter/rest/search.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | module REST 3 | module Search 4 | def search(q : String, options = {} of String => String) : Array(Twitter::Tweet) 5 | response = get("/1.1/search/tweets.json", options.merge({"q" => q})) 6 | Array(Twitter::Tweet).from_json(JSON.parse(response)["statuses"].to_json) 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /src/twitter/serializations/tweet_entities.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class TweetEntities 3 | include JSON::Serializable 4 | 5 | property hashtags : Array(HashtagEntity?) 6 | 7 | property urls : Array(UrlEntity)? 8 | 9 | property user_mentions : Array(UserMentionEntity?) 10 | 11 | property symbols : Array(SymbolEntity?) 12 | 13 | property polls : Array(PollEntity?)? 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /src/twitter/serializations/place.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class Place 3 | include JSON::Serializable 4 | 5 | property id : String 6 | 7 | property url : String 8 | 9 | property place_type : String 10 | 11 | property name : String 12 | 13 | property full_name : String 14 | 15 | property country_code : String 16 | 17 | property country : String 18 | 19 | property bounding_box : BoundingBox? 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /src/twitter/serializations/direct_message.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class DirectMessage 3 | include JSON::Serializable 4 | 5 | property id : Int64 6 | 7 | property id_str : String 8 | 9 | property recipient : Twitter::User 10 | 11 | property sender_id : Int64 12 | 13 | property sender_screen_name : String 14 | 15 | property text : String 16 | 17 | @[JSON::Field(converter: Time::Format.new("%a %b %d %T +0000 %Y"))] 18 | property created_at : Time 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/twitter/rest/search_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../helper" 2 | 3 | describe Twitter::REST::Search do 4 | client = Mock::Client.new("CK", "CS", "AT", "AS", "UA") 5 | 6 | describe "#search" do 7 | context "called with String" do 8 | tweets = client.search("freebandnames") 9 | it "returns Array(Twitter::Tweet)" do 10 | tweets.should be_a Array(Twitter::Tweet) 11 | tweets[0].id.should eq 250075927172759552 12 | tweets[1].id.should eq 249292149810667520 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /.ameba.yml: -------------------------------------------------------------------------------- 1 | # This configuration file was generated by `ameba --gen-config` 2 | # on 2023-01-03 13:03:07 UTC using Ameba version 1.3.1. 3 | # The point is for the user to remove these configuration records 4 | # one by one as the reported problems are removed from the code base. 5 | 6 | # Problems found: 7 7 | # Run `ameba --only Lint/NotNil` for details 8 | Lint/NotNil: 9 | Description: Identifies usage of `not_nil!` calls 10 | Excluded: 11 | - src/twitter/streaming/statuses.cr 12 | - spec/twitter/rest/tweets_spec.cr 13 | Enabled: true 14 | Severity: Warning 15 | -------------------------------------------------------------------------------- /src/twitter/rest/api.cr: -------------------------------------------------------------------------------- 1 | require "./timelines" 2 | require "./users" 3 | require "./friends_and_followers" 4 | require "./tweets" 5 | require "./search" 6 | require "./direct_messages" 7 | require "./geo" 8 | require "./favorites" 9 | 10 | module Twitter 11 | module REST 12 | module API 13 | include Twitter::REST::Users 14 | include Twitter::REST::Timelines 15 | include Twitter::REST::FriendsAndFollowers 16 | include Twitter::REST::Tweets 17 | include Twitter::REST::Search 18 | include Twitter::REST::DirectMessages 19 | include Twitter::REST::Geo 20 | include Twitter::REST::Favorites 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/mock/1.1/followers/ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "previous_cursor": 0, 3 | "ids": [ 4 | 788892, 5 | 3895958, 6 | 2891211, 7 | 9019482, 8 | 1488353, 9 | 1170202, 10 | 12249, 11 | 2295745, 12 | 65793, 13 | 1249881, 14 | 1927800, 15 | 1523501, 16 | 2548447, 17 | 15062340, 18 | 18709371, 19 | 133031077, 20 | 1774544, 21 | 777925, 22 | 425731, 23 | 2764040, 24 | 976402, 25 | 785262, 26 | 819797, 27 | 140181, 28 | 828539, 29 | 960152, 30 | 79649, 31 | 391321, 32 | 78324 33 | ], 34 | "previous_cursor_str": "0", 35 | "next_cursor": 0, 36 | "next_cursor_str": "0" 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: website 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | container: 11 | image: crystallang/crystal 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Install dependencies 15 | run: shards install 16 | - name: Generate documentation 17 | run: crystal docs 18 | - 19 | name: Deploy to GitHub Pages 20 | if: success() 21 | uses: crazy-max/ghaction-github-pages@v3 22 | with: 23 | target_branch: gh-pages 24 | build_dir: docs 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /spec/mock/1.1/account/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "sleep_time": { 3 | "enabled": false 4 | }, 5 | "time_zone": { 6 | "name": "Pacific Time (US & Canada)", 7 | "utc_offset": -28800, 8 | "tzinfo_name": "America/Los_Angeles" 9 | }, 10 | "protected": false, 11 | "screen_name": "theSeanCook", 12 | "always_use_https": true, 13 | "use_cookie_personalization": true, 14 | "geo_enabled": true, 15 | "language": "uk", 16 | "discoverable_by_email": false, 17 | "discoverable_by_mobile_phone": false, 18 | "display_sensitive_media": true, 19 | "allow_contributor_request": "all", 20 | "allow_dms_from": "following", 21 | "allow_dm_groups_from": "following", 22 | "translator_type": "none" 23 | } 24 | -------------------------------------------------------------------------------- /spec/mock/1.1/friends/ids.json: -------------------------------------------------------------------------------- 1 | { 2 | "previous_cursor": 0, 3 | "ids": [ 4 | 657693, 5 | 183709371, 6 | 7588892, 7 | 38895958, 8 | 22891211, 9 | 9019482, 10 | 14488353, 11 | 11750202, 12 | 12249, 13 | 22915745, 14 | 1249881, 15 | 14927800, 16 | 1523501, 17 | 22548447, 18 | 15062340, 19 | 133031077, 20 | 17874544, 21 | 777925, 22 | 4265731, 23 | 27674040, 24 | 26123649, 25 | 9576402, 26 | 821958, 27 | 7852612, 28 | 819797, 29 | 1401881, 30 | 8285392, 31 | 9160152, 32 | 795649, 33 | 3191321, 34 | 783214 35 | ], 36 | "previous_cursor_str": "0", 37 | "next_cursor": 0, 38 | "next_cursor_str": "0" 39 | } 40 | -------------------------------------------------------------------------------- /src/twitter/serializations/location.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class Location 3 | include JSON::Serializable 4 | 5 | property id : String 6 | 7 | property url : String 8 | 9 | property place_type : String 10 | 11 | property name : String 12 | 13 | property full_name : String 14 | 15 | property country_code : String 16 | 17 | property country : String 18 | 19 | property contained_within : Array(Location)? 20 | 21 | property geometry : BoundingBox? 22 | 23 | property polylines : Array(String)? 24 | 25 | property centroid : Array(Float64)? 26 | 27 | property bounding_box : BoundingBox? 28 | 29 | property attributes : Hash(String, String) 30 | 31 | def_equals id 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /src/twitter/serializations/status.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class Status 3 | include JSON::Serializable 4 | 5 | @[JSON::Field(converter: Time::Format.new("%a %b %d %T +0000 %Y"))] 6 | property created_at : Time 7 | 8 | property favorite_count : Int32? 9 | 10 | property? favorited : Bool 11 | 12 | property id : Float64 13 | 14 | property in_reply_to_screen_name : String? 15 | 16 | property in_reply_to_status_id : Float64? 17 | 18 | property in_reply_to_user_id : Float64? 19 | 20 | property lang : String? 21 | 22 | property retweet_count : Int32 23 | 24 | property? retweeted : Bool 25 | 26 | property source : String 27 | 28 | property text : String 29 | 30 | property? truncated : Bool 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /src/twitter/rest/favorites.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | module REST 3 | module Favorites 4 | # Favorites (likes) the `Tweet` specified in the *id* parameter as the authenticating user. 5 | # Returns the favorite `Tweet` when successful. 6 | def like(id : Int64) : Twitter::Tweet 7 | response = post("/1.1/favorites/create.json", {"id" => id.to_s}) 8 | Twitter::Tweet.from_json(response) 9 | end 10 | 11 | # Unfavorites (un-likes) the `Tweet` specified in the *id* parameter as the authenticating user. 12 | # Returns the un-liked `Tweet` when successful. 13 | def unlike(id : Int64) : Twitter::Tweet 14 | response = post("/1.1/favorites/destroy.json", {"id" => id.to_s}) 15 | Twitter::Tweet.from_json(response) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /src/twitter/rest/direct_messages.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | module REST 3 | module DirectMessages 4 | def direct_messages_received(options = {} of String => String) : Array(Twitter::DirectMessage) 5 | response = get("/1.1/direct_messages.json", options) 6 | Array(Twitter::DirectMessage).from_json(response) 7 | end 8 | 9 | def direct_messages_sent(options = {} of String => String) : Array(Twitter::DirectMessage) 10 | response = get("/1.1/direct_messages/sent.json", options) 11 | Array(Twitter::DirectMessage).from_json(response) 12 | end 13 | 14 | def direct_message(id : Int32 | Int64, options = {} of String => String) : Twitter::DirectMessage 15 | options["id"] = id.to_s 16 | response = get("/1.1/direct_messages/show.json", options) 17 | Twitter::DirectMessage.from_json(response) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/mock/1.1/friendships/lookup.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "andy piper (pipes)", 4 | "screen_name": "andypiper", 5 | "id": 786491, 6 | "id_str": "786491", 7 | "connections": ["following"] 8 | }, 9 | { 10 | "name": "λ🥑. 🍞", 11 | "screen_name": "binary_aaron", 12 | "id": 165837734, 13 | "id_str": "165837734", 14 | "connections": ["following", "followed_by"] 15 | }, 16 | { 17 | "name": "Twitter Dev", 18 | "screen_name": "TwitterDev", 19 | "id": 2244994945, 20 | "id_str": "2244994945", 21 | "connections": ["following"] 22 | }, 23 | { 24 | "name": "Emily Sheehan 🏕", 25 | "screen_name": "happycamper", 26 | "id": 63046977, 27 | "id_str": "63046977", 28 | "connections": ["none"] 29 | }, 30 | { 31 | "name": "Harrison Test", 32 | "screen_name": "Harris_0ff", 33 | "id": 4337869213, 34 | "id_str": "4337869213", 35 | "connections": ["following", "following_requested", "followed_by"] 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /src/twitter/serializations/user.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class User 3 | include JSON::Serializable 4 | 5 | @[JSON::Field(converter: Time::Format.new("%a %b %d %T +0000 %Y"))] 6 | property created_at : Time 7 | 8 | property? default_profile : Bool 9 | 10 | property? default_profile_image : Bool 11 | 12 | property favourites_count : Int32 13 | 14 | property followers_count : Int32 15 | 16 | property friends_count : Int32 17 | 18 | property id : Int64 19 | 20 | property listed_count : Int32 21 | 22 | property location : String 23 | 24 | property name : String 25 | 26 | property needs_phone_verification : Bool? 27 | 28 | property profile_banner_url : String? 29 | 30 | property profile_image_url_https : String 31 | 32 | @[JSON::Field(key: "protected")] 33 | property? user_protected : Bool 34 | 35 | property screen_name : String 36 | 37 | property status : Status? 38 | 39 | property statuses_count : Int32 40 | 41 | property suspended : Bool? 42 | 43 | property? verified : Bool 44 | 45 | property description : String 46 | 47 | property entities : UserEntities? 48 | 49 | def_equals id 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /src/twitter/serializations/settings.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class Settings 3 | include JSON::Serializable 4 | 5 | property sleep_time : SleepTime 6 | 7 | property time_zone : TimeZone 8 | 9 | @[JSON::Field(key: "protected")] 10 | property? settings_protected : Bool 11 | 12 | property screen_name : String 13 | 14 | property? always_use_https : Bool 15 | 16 | property? use_cookie_personalization : Bool 17 | 18 | property? geo_enabled : Bool 19 | 20 | property language : String 21 | 22 | property? discoverable_by_email : Bool 23 | 24 | property? discoverable_by_mobile_phone : Bool 25 | 26 | property? display_sensitive_media : Bool 27 | 28 | property allow_contributor_request : String 29 | 30 | property allow_dms_from : String 31 | 32 | property allow_dm_groups_from : String 33 | 34 | property translator_type : String 35 | end 36 | 37 | class SleepTime 38 | include JSON::Serializable 39 | 40 | property? enabled : Bool 41 | end 42 | 43 | class TimeZone 44 | include JSON::Serializable 45 | 46 | property name : String 47 | 48 | property utc_offset : Int32 49 | 50 | property tzinfo_name : String 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /src/twitter/serializations/tweet.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | class Tweet 3 | include JSON::Serializable 4 | 5 | @[JSON::Field(converter: Time::Format.new("%a %b %d %T +0000 %Y"))] 6 | property created_at : Time 7 | 8 | property favorite_count : Int32? 9 | 10 | property favorited : Bool? 11 | 12 | property id : Int64 13 | 14 | property id_str : String 15 | 16 | property in_reply_to_screen_name : String? 17 | 18 | property in_reply_to_status_id : Float64? 19 | 20 | property in_reply_to_user_id : Int64? 21 | 22 | property lang : String? 23 | 24 | property retweet_count : Int32 25 | 26 | property? retweeted : Bool 27 | 28 | property possibly_sensitive : Bool? 29 | 30 | property source : String 31 | 32 | property text : String 33 | 34 | property? truncated : Bool 35 | 36 | property user : User 37 | 38 | property place : Place? 39 | 40 | property geo : Coordinates? 41 | 42 | property coordinates : Coordinates? 43 | 44 | property is_quote_status : Bool? 45 | 46 | property quoted_status : Tweet? 47 | 48 | property retweeted_status : Tweet? 49 | 50 | property entities : TweetEntities 51 | 52 | def_equals id 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/twitter/user_spec.cr: -------------------------------------------------------------------------------- 1 | require "../helper" 2 | 3 | describe Twitter::User do 4 | describe ".from_json" do 5 | it "parses all attributes correctly" do 6 | json = JSON.parse(File.read("./spec/fixtures/users.json"))[0].to_json 7 | user = Twitter::User.from_json(json) 8 | user.created_at.should eq(Time.parse!("Mon Jul 16 12:59:01 +0000 2007", "%a %b %d %T %z %Y")) 9 | user.default_profile?.should eq(false) 10 | user.default_profile_image?.should eq(false) 11 | user.favourites_count.should eq(14447) 12 | user.followers_count.should eq(5945) 13 | user.friends_count.should eq(881) 14 | user.id.should eq(7505382) 15 | user.listed_count.should eq(339) 16 | user.location.should eq("Berlin") 17 | user.name.should eq("Erik Michaels-Ober") 18 | user.needs_phone_verification.should eq(false) 19 | user.profile_banner_url.should eq("https://pbs.twimg.com/profile_banners/7505382/1425238640") 20 | user.profile_image_url_https.should eq("https://pbs.twimg.com/profile_images/583426225605058560/dO_rpWw1_normal.jpg") 21 | user.screen_name.should eq("sferik") 22 | user.status.should be_a(Twitter::Status) 23 | user.statuses_count.should eq(17358) 24 | user.suspended.should eq(false) 25 | user.verified?.should eq(false) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/twitter/rest/direct_messages_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../helper" 2 | 3 | describe Twitter::REST::DirectMessages do 4 | client = Mock::Client.new("CK", "CS", "AT", "AS", "UA") 5 | 6 | describe "#direct_messages_received" do 7 | it "returns Array(Twitter::DirectMessage)" do 8 | client.direct_messages_received.should be_a Array(Twitter::DirectMessage) 9 | client.direct_messages_received.first.text.should eq "booyakasha" 10 | end 11 | end 12 | 13 | describe "#direct_messages_sent" do 14 | it "returns Array(Twitter::DirectMessage)" do 15 | client.direct_messages_sent.should be_a Array(Twitter::DirectMessage) 16 | client.direct_messages_sent.first.text.should eq "Meet me behind the cafeteria after school." 17 | end 18 | end 19 | 20 | describe "#direct_message" do 21 | context "id: Int32" do 22 | it "returns Array(Twitter::DirectMessage)" do 23 | client.direct_message(1).should be_a Twitter::DirectMessage 24 | client.direct_message(1).text.should eq "booyakasha" 25 | end 26 | end 27 | 28 | context "id: Int64" do 29 | it "returns Array(Twitter::DirectMessage)" do 30 | client.direct_message(123451234512345123).should be_a Twitter::DirectMessage 31 | client.direct_message(123451234512345123).text.should eq "booyakasha" 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /src/twitter/rest/timelines.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | module REST 3 | module Timelines 4 | def home_timeline(options = {} of String => String) : Array(Twitter::Tweet) 5 | response = get("/1.1/statuses/home_timeline.json", options) 6 | Array(Twitter::Tweet).from_json(response) 7 | end 8 | 9 | def mentions_timeline(options = {} of String => String) : Array(Twitter::Tweet) 10 | response = get("/1.1/statuses/mentions_timeline.json", options) 11 | Array(Twitter::Tweet).from_json(response) 12 | end 13 | 14 | def retweets_of_me(options = {} of String => String) : Array(Twitter::Tweet) 15 | response = get("/1.1/statuses/retweets_of_me.json", options) 16 | Array(Twitter::Tweet).from_json(response) 17 | end 18 | 19 | def user_timeline(user_id : Int32 | Int64, options = {} of String => String) : Array(Twitter::Tweet) 20 | response = get("/1.1/statuses/user_timeline.json", options.merge({"user_id" => user_id.to_s})) 21 | Array(Twitter::Tweet).from_json(response) 22 | end 23 | 24 | def user_timeline(screen_name : String, options = {} of String => String) : Array(Twitter::Tweet) 25 | response = get("/1.1/statuses/user_timeline.json", options.merge({"screen_name" => screen_name})) 26 | Array(Twitter::Tweet).from_json(response) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/mock/client.cr: -------------------------------------------------------------------------------- 1 | module Mock 2 | class Client < Twitter::REST::Client 3 | def initialize(@consumer_key : String, @consumer_secret : String, @access_token : String, @access_token_secret : String, @user_agent : Nil | String = nil) 4 | @http_client = HTTP::Client.new("localhost") 5 | end 6 | 7 | def get(path : String, params = {} of String => String) 8 | File.read("spec/mock#{path}") 9 | end 10 | 11 | def post(path : String, form = {} of String => String) 12 | File.read("spec/mock#{path}") 13 | end 14 | end 15 | 16 | class DummyHttpClient < HTTP::Client 17 | def initialize(@host : String = "localhost") 18 | @port = 65535 19 | @compress = false 20 | end 21 | 22 | def get(path : String) 23 | HTTP::Client::Response.new(400, %({"errors":[{"code":400,"message":"Invalid or expired token"}]})) 24 | end 25 | 26 | def post(path : String, form = {} of String => String) 27 | HTTP::Client::Response.new(400, %({"errors":[{"code":400,"message":"Invalid or expired token"}]})) 28 | end 29 | end 30 | 31 | class ClientWithDummyHttpClient < Twitter::REST::Client 32 | def initialize(@consumer_key : String, @consumer_secret : String, @access_token : String, @access_token_secret : String, @user_agent : Nil | String = nil) 33 | @http_client = DummyHttpClient.new("localhost") 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/twitter/rest/client_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../helper" 2 | 3 | describe Twitter::REST::Client do 4 | describe "#initialize" do 5 | it "sets properties" do 6 | client = Twitter::REST::Client.new("CK", "CS", "AT", "AS", "UA") 7 | client.consumer_key.should eq("CK") 8 | client.consumer_secret.should eq("CS") 9 | client.access_token.should eq("AT") 10 | client.access_token_secret.should eq("AS") 11 | client.user_agent.should eq("UA") 12 | end 13 | it "sets default User-Agent when it is omitted" do 14 | client = Twitter::REST::Client.new("CK", "CS", "AT", "AS") 15 | client.user_agent.should eq("CrystalTwitterClient/#{Twitter::VERSION}") 16 | end 17 | end 18 | 19 | describe "#get" do 20 | it "executes an HTTP GET" do 21 | client = Mock::ClientWithDummyHttpClient.new("CK", "CS", "AT", "AS", "UA") 22 | path = "/1.1/users/show.json" 23 | options = {"screen_name" => "sferik"} 24 | expect_raises(Twitter::Errors::ClientError, "Invalid or expired token") do 25 | client.get(path, options) 26 | end 27 | end 28 | end 29 | 30 | describe "#post" do 31 | it "executes an HTTP POST" do 32 | client = Mock::ClientWithDummyHttpClient.new("CK", "CS", "AT", "AS", "UA") 33 | path = "/1.1/friendships/create.json" 34 | options = {"screen_name" => "kenta_s_dev"} 35 | expect_raises(Twitter::Errors::ClientError, "Invalid or expired token") do 36 | client.post(path, options) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /.github/workflows/crystal.yml: -------------------------------------------------------------------------------- 1 | name: Crystal CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * *' 8 | 9 | jobs: 10 | check_format: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Install Crystal 14 | uses: crystal-lang/install-crystal@v1 15 | - name: Check out repository code 16 | uses: actions/checkout@v3 17 | - name: Check format 18 | run: crystal tool format --check 19 | 20 | check_ameba: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Install Crystal 24 | uses: crystal-lang/install-crystal@v1 25 | - name: Check out repository code 26 | uses: actions/checkout@v3 27 | - name: Crystal Ameba Linter 28 | id: crystal-ameba 29 | uses: crystal-ameba/github-action@v0.7.1 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | test: 34 | strategy: 35 | fail-fast: false 36 | matrix: 37 | include: 38 | - {os: ubuntu-latest, crystal: latest} 39 | - {os: ubuntu-latest, crystal: nightly} 40 | - {os: macos-latest} 41 | runs-on: ${{ matrix.os }} 42 | steps: 43 | - name: Install Crystal 44 | uses: crystal-lang/install-crystal@v1 45 | with: 46 | crystal: ${{ matrix.crystal }} 47 | - name: Check out repository code 48 | uses: actions/checkout@v3 49 | - name: Install dependencies 50 | run: shards install 51 | - name: Run tests 52 | run: crystal spec 53 | -------------------------------------------------------------------------------- /src/twitter/streaming/statuses.cr: -------------------------------------------------------------------------------- 1 | require "../serializations/delete" 2 | require "../serializations/tweet" 3 | 4 | module Twitter 5 | module Streaming 6 | module Statuses 7 | def filter(options = {} of String => String, &) 8 | delimeted_length = options.fetch("delimited", false) 9 | 10 | post("/1.1/statuses/filter.json", options) do |response| 11 | loop do 12 | json = if delimeted_length 13 | bytes_to_read = response.gets.not_nil!.to_i 14 | response.gets(bytes_to_read).not_nil! 15 | else 16 | response.gets.not_nil! 17 | end 18 | 19 | yield parse_result(json) 20 | end 21 | end 22 | end 23 | 24 | def sample(options = {} of String => String, &) 25 | delimeted_length = options.fetch("delimited", false) 26 | 27 | get("/1.1/statuses/sample.json", options) do |response| 28 | loop do 29 | json = if delimeted_length 30 | bytes_to_read = response.gets.not_nil!.to_i 31 | response.gets(bytes_to_read).not_nil! 32 | else 33 | response.gets.not_nil! 34 | end 35 | 36 | yield parse_result(json) 37 | end 38 | end 39 | end 40 | 41 | def parse_result(json) 42 | (return Twitter::Tweet.from_json(json)) rescue nil 43 | (Twitter::Delete.from_json(json, root: "delete")) rescue nil 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/mock/1.1/account/update_profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "contributors_enabled": false, 3 | "created_at": "Thu Aug 23 19:45:07 +0000 2012", 4 | "default_profile": false, 5 | "default_profile_image": false, 6 | "description": "Keep calm and rock on.", 7 | "favourites_count": 0, 8 | "follow_request_sent": false, 9 | "followers_count": 0, 10 | "following": false, 11 | "friends_count": 10, 12 | "geo_enabled": true, 13 | "id": 776627022, 14 | "id_str": "776627022", 15 | "is_translator": false, 16 | "lang": "en", 17 | "listed_count": 0, 18 | "location": "San Francisco, CA", 19 | "name": "Sean Cook", 20 | "notifications": false, 21 | "profile_background_color": "9AE4E8", 22 | "profile_background_image_url": "http://a0.twimg.com/images/themes/theme16/bg.gif", 23 | "profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme16/bg.gif", 24 | "profile_background_tile": false, 25 | "profile_image_url": "http://a0.twimg.com/profile_images/2550256790/hv5rtkvistn50nvcuydl_normal.jpeg", 26 | "profile_image_url_https": "https://si0.twimg.com/profile_images/2550256790/hv5rtkvistn50nvcuydl_normal.jpeg", 27 | "profile_link_color": "0084B4", 28 | "profile_sidebar_border_color": "BDDCAD", 29 | "profile_sidebar_fill_color": "DDFFCC", 30 | "profile_text_color": "333333", 31 | "profile_use_background_image": true, 32 | "protected": false, 33 | "screen_name": "s0c1alm3dia", 34 | "show_all_inline_media": false, 35 | "statuses_count": 0, 36 | "time_zone": "Pacific Time (US & Canada)", 37 | "url": "http://cnn.com", 38 | "utc_offset": -28800, 39 | "verified": false 40 | } 41 | -------------------------------------------------------------------------------- /spec/mock/1.1/account/update_profile_background_image.json: -------------------------------------------------------------------------------- 1 | { 2 | "contributors_enabled": false, 3 | "created_at": "Thu Aug 23 19:45:07 +0000 2012", 4 | "default_profile": false, 5 | "default_profile_image": false, 6 | "description": "Keep calm and test", 7 | "favourites_count": 0, 8 | "follow_request_sent": false, 9 | "followers_count": 0, 10 | "following": false, 11 | "friends_count": 10, 12 | "geo_enabled": true, 13 | "id": 776627022, 14 | "id_str": "776627022", 15 | "is_translator": false, 16 | "lang": "en", 17 | "listed_count": 0, 18 | "location": "San Francisco, CA", 19 | "name": "Sean Test", 20 | "notifications": false, 21 | "profile_background_color": "9AE4E8", 22 | "profile_background_image_url": "http://a0.twimg.com/images/themes/theme16/bg.gif", 23 | "profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme16/bg.gif", 24 | "profile_background_tile": false, 25 | "profile_image_url": "http://a0.twimg.com/profile_images/2550256790/hv5rtkvistn50nvcuydl_normal.jpeg", 26 | "profile_image_url_https": "https://si0.twimg.com/profile_images/2550256790/hv5rtkvistn50nvcuydl_normal.jpeg", 27 | "profile_link_color": "0084B4", 28 | "profile_sidebar_border_color": "BDDCAD", 29 | "profile_sidebar_fill_color": "DDFFCC", 30 | "profile_text_color": "333333", 31 | "profile_use_background_image": true, 32 | "protected": false, 33 | "screen_name": "s0c1alm3dia", 34 | "show_all_inline_media": false, 35 | "statuses_count": 0, 36 | "time_zone": "Pacific Time (US & Canada)", 37 | "url": "http://cnn.com", 38 | "utc_offset": -28800, 39 | "verified": false 40 | } 41 | -------------------------------------------------------------------------------- /spec/mock/1.1/account/update_profile_image.json: -------------------------------------------------------------------------------- 1 | { 2 | "contributors_enabled": false, 3 | "created_at": "Thu Aug 23 19:45:07 +0000 2012", 4 | "default_profile": false, 5 | "default_profile_image": false, 6 | "description": "with great power comes great responsibility.", 7 | "favourites_count": 0, 8 | "follow_request_sent": false, 9 | "followers_count": 0, 10 | "following": false, 11 | "friends_count": 10, 12 | "geo_enabled": true, 13 | "id": 776627022, 14 | "id_str": "776627022", 15 | "is_translator": false, 16 | "lang": "en", 17 | "listed_count": 0, 18 | "location": "San Francisco, CA", 19 | "name": "kenta-s", 20 | "notifications": false, 21 | "profile_background_color": "000000", 22 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/644522235/cdjlccey99gy36j3em67.jpeg", 23 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/644522235/cdjlccey99gy36j3em67.jpeg", 24 | "profile_background_tile": true, 25 | "profile_image_url": "http://a0.twimg.com/profile_images/2550256790/hv5rtkvistn50nvcuydl_normal.jpeg", 26 | "profile_image_url_https": "https://si0.twimg.com/profile_images/2550256790/hv5rtkvistn50nvcuydl_normal.jpeg", 27 | "profile_link_color": "000000", 28 | "profile_sidebar_border_color": "000000", 29 | "profile_sidebar_fill_color": "000000", 30 | "profile_text_color": "000000", 31 | "profile_use_background_image": false, 32 | "protected": false, 33 | "screen_name": "kenta_s_dev", 34 | "show_all_inline_media": false, 35 | "statuses_count": 0, 36 | "time_zone": "Pacific Time (US & Canada)", 37 | "url": "http://twitter.com", 38 | "utc_offset": -28800, 39 | "verified": false 40 | } 41 | -------------------------------------------------------------------------------- /spec/mock/1.1/blocks/create.json: -------------------------------------------------------------------------------- 1 | { 2 | "contributors_enabled": true, 3 | "created_at": "Sat May 09 17:58:22 +0000 2009", 4 | "default_profile": false, 5 | "default_profile_image": false, 6 | "description": "I taught your phone that thing you like. The Mobile Partner Engineer @Twitter. ", 7 | "entities": { 8 | "description": { 9 | "urls": [] 10 | }, 11 | "url": { 12 | "urls": [] 13 | } 14 | }, 15 | "favourites_count": 586, 16 | "follow_request_sent": false, 17 | "followers_count": 10622, 18 | "following": false, 19 | "friends_count": 1181, 20 | "geo_enabled": true, 21 | "id": 38895958, 22 | "id_str": "38895958", 23 | "is_translator": false, 24 | "lang": "en", 25 | "listed_count": 190, 26 | "location": "San Francisco", 27 | "name": "Sean Cook", 28 | "notifications": false, 29 | "profile_background_color": "1A1B1F", 30 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/495742332/purty_wood.png", 31 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/495742332/purty_wood.png", 32 | "profile_background_tile": true, 33 | "profile_image_url": "http://a0.twimg.com/profile_images/1751506047/dead_sexy_normal.JPG", 34 | "profile_image_url_https": "https://si0.twimg.com/profile_images/1751506047/dead_sexy_normal.JPG", 35 | "profile_link_color": "2FC2EF", 36 | "profile_sidebar_border_color": "181A1E", 37 | "profile_sidebar_fill_color": "252429", 38 | "profile_text_color": "666666", 39 | "profile_use_background_image": true, 40 | "protected": false, 41 | "screen_name": "theSeanCook", 42 | "show_all_inline_media": true, 43 | "statuses_count": 2609, 44 | "time_zone": "Pacific Time (US & Canada)", 45 | "url": null, 46 | "utc_offset": -28800, 47 | "verified": false 48 | } 49 | -------------------------------------------------------------------------------- /spec/mock/1.1/blocks/destroy.json: -------------------------------------------------------------------------------- 1 | { 2 | "contributors_enabled": true, 3 | "created_at": "Sat May 09 17:58:22 +0000 2009", 4 | "default_profile": false, 5 | "default_profile_image": false, 6 | "description": "I taught your phone that thing you like. The Mobile Partner Engineer @Twitter. ", 7 | "entities": { 8 | "description": { 9 | "urls": [] 10 | }, 11 | "url": { 12 | "urls": [] 13 | } 14 | }, 15 | "favourites_count": 586, 16 | "follow_request_sent": false, 17 | "followers_count": 10622, 18 | "following": false, 19 | "friends_count": 1181, 20 | "geo_enabled": true, 21 | "id": 38895958, 22 | "id_str": "38895958", 23 | "is_translator": false, 24 | "lang": "en", 25 | "listed_count": 190, 26 | "location": "San Francisco", 27 | "name": "Sean Cook", 28 | "notifications": false, 29 | "profile_background_color": "1A1B1F", 30 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/495742332/purty_wood.png", 31 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/495742332/purty_wood.png", 32 | "profile_background_tile": true, 33 | "profile_image_url": "http://a0.twimg.com/profile_images/1751506047/dead_sexy_normal.JPG", 34 | "profile_image_url_https": "https://si0.twimg.com/profile_images/1751506047/dead_sexy_normal.JPG", 35 | "profile_link_color": "2FC2EF", 36 | "profile_sidebar_border_color": "181A1E", 37 | "profile_sidebar_fill_color": "252429", 38 | "profile_text_color": "666666", 39 | "profile_use_background_image": true, 40 | "protected": false, 41 | "screen_name": "theSeanCook", 42 | "show_all_inline_media": true, 43 | "statuses_count": 2609, 44 | "time_zone": "Pacific Time (US & Canada)", 45 | "url": null, 46 | "utc_offset": -28800, 47 | "verified": false 48 | } 49 | -------------------------------------------------------------------------------- /src/twitter/rest/tweets.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | module REST 3 | module Tweets # a.k.a Statuses 4 | def update(status : String, options = {} of String => String) : Twitter::Tweet 5 | response = post("/1.1/statuses/update.json", options.merge({"status" => status})) 6 | Twitter::Tweet.from_json(response) 7 | end 8 | 9 | def destroy_status(tweet_id : Int32 | Int64, options = {} of String => String) : Twitter::Tweet 10 | response = post("/1.1/statuses/destroy.json", options.merge({"id" => tweet_id.to_s})) 11 | Twitter::Tweet.from_json(response) 12 | end 13 | 14 | def destroy_status(tweet : Twitter::Tweet, options = {} of String => String) : Twitter::Tweet 15 | destroy_status(tweet.id, options) 16 | end 17 | 18 | def retweet(tweet_id : Int32 | Int64, options = {} of String => String) : Twitter::Tweet 19 | response = post("/1.1/statuses/retweet.json", options.merge({"id" => tweet_id.to_s})) 20 | Twitter::Tweet.from_json(response) 21 | end 22 | 23 | def retweet(tweet : Twitter::Tweet, options = {} of String => String) : Twitter::Tweet 24 | retweet(tweet.id) 25 | end 26 | 27 | def unretweet(tweet_id : Int32 | Int64, options = {} of String => String) : Twitter::Tweet 28 | response = post("/1.1/statuses/unretweet.json", options.merge({"id" => tweet_id.to_s})) 29 | Twitter::Tweet.from_json(response) 30 | end 31 | 32 | def unretweet(tweet : Twitter::Tweet, options = {} of String => String) : Twitter::Tweet 33 | unretweet(tweet.id) 34 | end 35 | 36 | # Fetch a particular Tweet by *id* 37 | def status(id : Int32 | Int64) : Twitter::Tweet 38 | response = get("/1.1/statuses/show.json", {"id" => id.to_s}) 39 | Twitter::Tweet.from_json(response) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/mock/1.1/blocks/list.json: -------------------------------------------------------------------------------- 1 | { 2 | "previous_cursor": 0, 3 | "previous_cursor_str": "0", 4 | "next_cursor": 0, 5 | "users": [ 6 | { 7 | "profile_sidebar_fill_color": "DDEEF6", 8 | "profile_background_tile": false, 9 | "profile_sidebar_border_color": "C0DEED", 10 | "name": "Javier Heady", 11 | "created_at": "Thu Mar 01 00:16:47 +0000 2012", 12 | "profile_image_url": "http://a0.twimg.com/sticky/default_profile_images/default_profile_4_normal.png", 13 | "location": "", 14 | "is_translator": false, 15 | "follow_request_sent": false, 16 | "profile_link_color": "0084B4", 17 | "id_str": "509466276", 18 | "entities": { 19 | "description": { 20 | "urls": [] 21 | }, 22 | "url": { 23 | "urls": [] 24 | } 25 | }, 26 | "contributors_enabled": false, 27 | "favourites_count": 0, 28 | "url": null, 29 | "default_profile": true, 30 | "utc_offset": null, 31 | "profile_image_url_https": "https://si0.twimg.com/sticky/default_profile_images/default_profile_4_normal.png", 32 | "id": 509466276, 33 | "listed_count": 0, 34 | "profile_use_background_image": true, 35 | "followers_count": 0, 36 | "protected": false, 37 | "lang": "en", 38 | "profile_text_color": "333333", 39 | "profile_background_color": "C0DEED", 40 | "notifications": false, 41 | "verified": false, 42 | "description": "", 43 | "geo_enabled": false, 44 | "time_zone": null, 45 | "profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme1/bg.png", 46 | "friends_count": 0, 47 | "default_profile_image": true, 48 | "statuses_count": 4, 49 | "profile_background_image_url": "http://a0.twimg.com/images/themes/theme1/bg.png", 50 | "following": false, 51 | "screen_name": "javierg3ong" 52 | } 53 | ], 54 | "next_cursor_str": "0" 55 | } 56 | -------------------------------------------------------------------------------- /src/twitter/rest/client.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | module REST 3 | class Client 4 | include Twitter::REST::API 5 | 6 | HOST = "api.twitter.com" 7 | 8 | property access_token : String 9 | property access_token_secret : String 10 | property consumer_key : String 11 | property consumer_secret : String 12 | property user_agent : String? 13 | 14 | def initialize(@consumer_key, @consumer_secret, @access_token, @access_token_secret, @user_agent = nil) 15 | @user_agent ||= "CrystalTwitterClient/#{Twitter::VERSION}" 16 | consumer = OAuth::Consumer.new(HOST, consumer_key, consumer_secret) 17 | access_token = OAuth::AccessToken.new(access_token, access_token_secret) 18 | @http_client = HTTP::Client.new(HOST, tls: true) 19 | consumer.authenticate(@http_client, access_token) 20 | end 21 | 22 | def get(path : String, params = {} of String => String) 23 | path += "?#{to_query_string(params)}" unless params.empty? 24 | response = @http_client.get(path) 25 | handle_response(response) 26 | end 27 | 28 | def post(path : String, form = {} of String => String) 29 | response = @http_client.post(path, form: form) 30 | handle_response(response) 31 | end 32 | 33 | private def handle_response(response : HTTP::Client::Response) 34 | case response.status_code 35 | when 200..299 36 | response.body 37 | when 400..499 38 | message = Twitter::Errors.from_json(response.body).errors.first.message 39 | raise Twitter::Errors::ClientError.new(message) 40 | when 500 41 | raise Twitter::Errors::ServerError.new("Internal Server Error") 42 | when 502 43 | raise Twitter::Errors::ServerError.new("Bad Gateway") 44 | when 503 45 | raise Twitter::Errors::ServerError.new("Service Unavailable") 46 | when 504 47 | raise Twitter::Errors::ServerError.new("Gateway Timeout") 48 | else 49 | "" 50 | end 51 | end 52 | 53 | private def to_query_string(hash : Hash) 54 | HTTP::Params.build do |form_builder| 55 | hash.each do |key, value| 56 | form_builder.add(key, value) 57 | end 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /src/twitter/streaming/client.cr: -------------------------------------------------------------------------------- 1 | module Twitter 2 | module Streaming 3 | class Client 4 | include Twitter::Streaming::API 5 | 6 | HOST = "stream.twitter.com" 7 | 8 | property access_token : String 9 | property access_token_secret : String 10 | property consumer_key : String 11 | property consumer_secret : String 12 | property user_agent : String? 13 | property http_client : HTTP::Client 14 | 15 | def initialize(@consumer_key, @consumer_secret, @access_token, @access_token_secret, @user_agent = nil, connect_timeout : Time::Span? = 30.seconds) 16 | @user_agent ||= "CrystalTwitterClient/#{Twitter::VERSION}" 17 | consumer = OAuth::Consumer.new(HOST, consumer_key, consumer_secret) 18 | access_token = OAuth::AccessToken.new(access_token, access_token_secret) 19 | @http_client = HTTP::Client.new(HOST, tls: true) 20 | @http_client.connect_timeout = connect_timeout if connect_timeout 21 | consumer.authenticate(http_client, access_token) 22 | end 23 | 24 | def get(path : String, params = {} of String => String, &) 25 | path += "?#{to_query_string(params)}" unless params.empty? 26 | http_client.get(path) do |response| 27 | yield handle_response(response) 28 | end 29 | end 30 | 31 | def post(path : String, form = {} of String => String, &) 32 | http_client.post(path, form: form) do |response| 33 | yield handle_response(response) 34 | end 35 | end 36 | 37 | private def handle_response(response : HTTP::Client::Response) 38 | case response.status_code 39 | when 200..299 40 | response.body_io 41 | when 400..499 42 | message = Twitter::Errors.from_json(response.body).errors.first.message 43 | raise Twitter::Errors::ClientError.new(message) 44 | when 502 45 | raise Twitter::Errors::ServerError.new("Bad Gateway") 46 | when 503 47 | raise Twitter::Errors::ServerError.new("Service Unavailable") 48 | when 504 49 | raise Twitter::Errors::ServerError.new("Gateway Timeout") 50 | else 51 | raise Twitter::Errors::ServerError.new("Internal Server Error") 52 | end 53 | end 54 | 55 | private def to_query_string(hash : Hash) 56 | HTTP::Params.build do |form_builder| 57 | hash.each do |key, value| 58 | form_builder.add(key, value) 59 | end 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/mock/1.1/mutes/users/create.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 54931584, 3 | "id_str": "54931584", 4 | "name": "kenta-s", 5 | "screen_name": "kenta_s_dev", 6 | "location": "", 7 | "description": "mwah-ha-haaaaa", 8 | "url": null, 9 | "entities": { 10 | "description": { 11 | "urls": [] 12 | }, 13 | "url": { 14 | "urls": [] 15 | } 16 | }, 17 | "protected": false, 18 | "followers_count": 16, 19 | "friends_count": 10, 20 | "listed_count": 0, 21 | "created_at": "Wed Jul 08 15:40:42 +0000 2009", 22 | "favourites_count": 1, 23 | "utc_offset": 3600, 24 | "time_zone": "London", 25 | "geo_enabled": false, 26 | "verified": false, 27 | "statuses_count": 71, 28 | "lang": "en", 29 | "status": { 30 | "created_at": "Tue Apr 15 00:10:23 +0000 2014", 31 | "id": 455860653731753984, 32 | "id_str": "455860653731753984", 33 | "text": "Super cool app to install http://t.co/ZiH6VOqLB3", 34 | "source": "Twitter for iPhone", 35 | "truncated": false, 36 | "in_reply_to_status_id": null, 37 | "in_reply_to_status_id_str": null, 38 | "in_reply_to_user_id": null, 39 | "in_reply_to_user_id_str": null, 40 | "in_reply_to_screen_name": null, 41 | "geo": null, 42 | "coordinates": null, 43 | "place": null, 44 | "contributors": null, 45 | "retweet_count": 0, 46 | "favorite_count": 0, 47 | "entities": { 48 | "hashtags": [], 49 | "symbols": [], 50 | "urls": [ 51 | { 52 | "url": "http://t.co/ZiH6VOqLB3", 53 | "expanded_url": "http://cards-demo.cfapps.io/owntracks.html", 54 | "display_url": "cards-demo.cfapps.io/owntracks.html", 55 | "indices": [26, 48] 56 | } 57 | ], 58 | "user_mentions": [] 59 | }, 60 | "favorited": false, 61 | "retweeted": false, 62 | "possibly_sensitive": false, 63 | "lang": "en" 64 | }, 65 | "contributors_enabled": false, 66 | "is_translator": false, 67 | "is_translation_enabled": false, 68 | "profile_background_color": "1A1B1F", 69 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", 70 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", 71 | "profile_background_tile": false, 72 | "profile_image_url": "http://pbs.twimg.com/profile_images/307611594/evil_normal.png", 73 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/307611594/evil_normal.png", 74 | "profile_link_color": "A6001E", 75 | "profile_sidebar_border_color": "181A1E", 76 | "profile_sidebar_fill_color": "949399", 77 | "profile_text_color": "120312", 78 | "profile_use_background_image": false, 79 | "default_profile": false, 80 | "default_profile_image": false, 81 | "following": true, 82 | "follow_request_sent": false, 83 | "notifications": false, 84 | "muting": true 85 | } 86 | -------------------------------------------------------------------------------- /spec/mock/1.1/mutes/users/destroy.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 54931584, 3 | "id_str": "54931584", 4 | "name": "Evil Piper", 5 | "screen_name": "evilpiper", 6 | "location": "", 7 | "description": "mwah-ha-haaaaa", 8 | "url": null, 9 | "entities": { 10 | "description": { 11 | "urls": [] 12 | }, 13 | "url": { 14 | "urls": [] 15 | } 16 | }, 17 | "protected": false, 18 | "followers_count": 16, 19 | "friends_count": 10, 20 | "listed_count": 0, 21 | "created_at": "Wed Jul 08 15:40:42 +0000 2009", 22 | "favourites_count": 1, 23 | "utc_offset": 3600, 24 | "time_zone": "London", 25 | "geo_enabled": false, 26 | "verified": false, 27 | "statuses_count": 71, 28 | "lang": "en", 29 | "status": { 30 | "created_at": "Tue Apr 15 00:10:23 +0000 2014", 31 | "id": 455860653731753984, 32 | "id_str": "455860653731753984", 33 | "text": "Super cool app to install http://t.co/ZiH6VOqLB3", 34 | "source": "Twitter for iPhone", 35 | "truncated": false, 36 | "in_reply_to_status_id": null, 37 | "in_reply_to_status_id_str": null, 38 | "in_reply_to_user_id": null, 39 | "in_reply_to_user_id_str": null, 40 | "in_reply_to_screen_name": null, 41 | "geo": null, 42 | "coordinates": null, 43 | "place": null, 44 | "contributors": null, 45 | "retweet_count": 0, 46 | "favorite_count": 0, 47 | "entities": { 48 | "hashtags": [], 49 | "symbols": [], 50 | "urls": [ 51 | { 52 | "url": "http://t.co/ZiH6VOqLB3", 53 | "expanded_url": "http://cards-demo.cfapps.io/owntracks.html", 54 | "display_url": "cards-demo.cfapps.io/owntracks.html", 55 | "indices": [26, 48] 56 | } 57 | ], 58 | "user_mentions": [] 59 | }, 60 | "favorited": false, 61 | "retweeted": false, 62 | "possibly_sensitive": false, 63 | "lang": "en" 64 | }, 65 | "contributors_enabled": false, 66 | "is_translator": false, 67 | "is_translation_enabled": false, 68 | "profile_background_color": "1A1B1F", 69 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", 70 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", 71 | "profile_background_tile": false, 72 | "profile_image_url": "http://pbs.twimg.com/profile_images/307611594/evil_normal.png", 73 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/307611594/evil_normal.png", 74 | "profile_link_color": "A6001E", 75 | "profile_sidebar_border_color": "181A1E", 76 | "profile_sidebar_fill_color": "949399", 77 | "profile_text_color": "120312", 78 | "profile_use_background_image": false, 79 | "default_profile": false, 80 | "default_profile_image": false, 81 | "following": true, 82 | "follow_request_sent": false, 83 | "notifications": false, 84 | "muting": true 85 | } 86 | -------------------------------------------------------------------------------- /spec/mock/1.1/statuses/update.json: -------------------------------------------------------------------------------- 1 | { 2 | "coordinates": null, 3 | "favorited": false, 4 | "created_at": "Wed Sep 05 00:37:15 +0000 2012", 5 | "truncated": false, 6 | "id_str": "243145735212777472", 7 | "entities": { 8 | "urls": [], 9 | "hashtags": [ 10 | { 11 | "text": "peterfalk", 12 | "indices": [35, 45] 13 | } 14 | ], 15 | "user_mentions": [], 16 | "symbols": [] 17 | }, 18 | "in_reply_to_user_id_str": null, 19 | "text": "Maybe he'll finally find his keys. #peterfalk", 20 | "contributors": null, 21 | "retweet_count": 0, 22 | "id": 243145735212777472, 23 | "in_reply_to_status_id_str": null, 24 | "geo": null, 25 | "retweeted": false, 26 | "in_reply_to_user_id": null, 27 | "place": null, 28 | "user": { 29 | "name": "Jason Costa", 30 | "profile_sidebar_border_color": "86A4A6", 31 | "profile_sidebar_fill_color": "A0C5C7", 32 | "profile_background_tile": false, 33 | "profile_image_url": "http://a0.twimg.com/profile_images/1751674923/new_york_beard_normal.jpg", 34 | "created_at": "Wed May 28 00:20:15 +0000 2008", 35 | "location": "", 36 | "is_translator": true, 37 | "follow_request_sent": false, 38 | "id_str": "14927800", 39 | "profile_link_color": "FF3300", 40 | "entities": { 41 | "url": { 42 | "urls": [ 43 | { 44 | "expanded_url": "http://www.jason-costa.blogspot.com/", 45 | "url": "http://t.co/YCA3ZKY", 46 | "indices": [0, 19], 47 | "display_url": "jason-costa.blogspot.com" 48 | } 49 | ] 50 | }, 51 | "description": { 52 | "urls": [] 53 | } 54 | }, 55 | "default_profile": false, 56 | "contributors_enabled": false, 57 | "url": "http://t.co/YCA3ZKY", 58 | "favourites_count": 883, 59 | "utc_offset": -28800, 60 | "id": 14927800, 61 | "profile_image_url_https": "https://si0.twimg.com/profile_images/1751674923/new_york_beard_normal.jpg", 62 | "profile_use_background_image": true, 63 | "listed_count": 150, 64 | "profile_text_color": "333333", 65 | "protected": false, 66 | "lang": "en", 67 | "followers_count": 8760, 68 | "time_zone": "Pacific Time (US & Canada)", 69 | "profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme6/bg.gif", 70 | "verified": false, 71 | "profile_background_color": "709397", 72 | "notifications": false, 73 | "description": "Platform at Twitter", 74 | "geo_enabled": true, 75 | "statuses_count": 5532, 76 | "default_profile_image": false, 77 | "friends_count": 166, 78 | "profile_background_image_url": "http://a0.twimg.com/images/themes/theme6/bg.gif", 79 | "show_all_inline_media": true, 80 | "screen_name": "jasoncosta", 81 | "following": false 82 | }, 83 | "source": "My Shiny App", 84 | "in_reply_to_screen_name": null, 85 | "in_reply_to_status_id": null 86 | } 87 | -------------------------------------------------------------------------------- /spec/twitter/rest/friends_and_followers_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../helper" 2 | 3 | describe Twitter::REST::FriendsAndFollowers do 4 | client = Mock::Client.new("CK", "CS", "AT", "AS", "UA") 5 | 6 | describe "#follow" do 7 | context "called with Int32" do 8 | user = client.follow(12345) 9 | it "returns Twitter::User" do 10 | user.should be_a Twitter::User 11 | user.name.should eq "Doug Williams" 12 | end 13 | end 14 | context "called with Int64" do 15 | user = client.follow(1234512345123451234) 16 | it "returns Twitter::User" do 17 | user.should be_a Twitter::User 18 | user.name.should eq "Doug Williams" 19 | end 20 | end 21 | 22 | context "called with String" do 23 | user = client.follow("dougw") 24 | it "returns Twitter::User" do 25 | user.should be_a Twitter::User 26 | user.name.should eq "Doug Williams" 27 | end 28 | end 29 | 30 | context "called with Twitter::User" do 31 | target_user = client.user("dougw") 32 | user = client.follow(target_user) 33 | it "returns Twitter::User" do 34 | user.should be_a Twitter::User 35 | user.name.should eq "Doug Williams" 36 | end 37 | end 38 | end 39 | 40 | describe "#unfollow" do 41 | context "called with Int32" do 42 | user = client.unfollow(12345) 43 | it "returns Twitter::User" do 44 | user.should be_a Twitter::User 45 | user.name.should eq "Doug Williams" 46 | end 47 | end 48 | 49 | context "called with Int64" do 50 | user = client.unfollow(1234512345123451234) 51 | it "returns Twitter::User" do 52 | user.should be_a Twitter::User 53 | user.name.should eq "Doug Williams" 54 | end 55 | end 56 | 57 | context "called with String" do 58 | user = client.unfollow("dougw") 59 | it "returns Twitter::User" do 60 | user.should be_a Twitter::User 61 | user.name.should eq "Doug Williams" 62 | end 63 | end 64 | 65 | context "called with Twitter::User" do 66 | target_user = client.user("dougw") 67 | user = client.unfollow(target_user) 68 | it "returns Twitter::User" do 69 | user.should be_a Twitter::User 70 | user.name.should eq "Doug Williams" 71 | end 72 | end 73 | end 74 | 75 | describe "#friend_ids" do 76 | context "called without any args" do 77 | ids = client.friend_ids 78 | it "returns Array(Int64)" do 79 | ids.should be_a Array(Int64) 80 | ids.size.should eq 31 81 | ids[0].should eq 657693 82 | ids[10].should eq 1249881 83 | end 84 | end 85 | end 86 | 87 | describe "#follower_ids" do 88 | context "called without any args" do 89 | ids = client.follower_ids 90 | it "returns Array(Int64)" do 91 | ids.should be_a Array(Int64) 92 | ids.size.should eq 29 93 | ids[0].should eq 788892 94 | ids[10].should eq 1927800 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/mock/1.1/statuses/destroy.json: -------------------------------------------------------------------------------- 1 | { 2 | "coordinates": null, 3 | "favorited": false, 4 | "created_at": "Wed Aug 29 16:54:38 +0000 2012", 5 | "truncated": false, 6 | "id_str": "240854986559455234", 7 | "entities": { 8 | "urls": [ 9 | { 10 | "expanded_url": "http://venturebeat.com/2012/08/29/vimeo-dropbox/#.UD5JLsYptSs.twitter", 11 | "url": "http://t.co/7UlkvZzM", 12 | "indices": [69, 89], 13 | "display_url": "venturebeat.com/2012/08/29/vim\u2026" 14 | } 15 | ], 16 | "hashtags": [], 17 | "symbols": [], 18 | "user_mentions": [] 19 | }, 20 | "in_reply_to_user_id_str": null, 21 | "text": "\"Vimeo integrates with Dropbox for easier video uploads and shares\": http://t.co/7UlkvZzM", 22 | "contributors": null, 23 | "retweet_count": 1, 24 | "id": 240854986559455234, 25 | "in_reply_to_status_id_str": null, 26 | "geo": null, 27 | "retweeted": false, 28 | "in_reply_to_user_id": null, 29 | "possibly_sensitive": false, 30 | "place": null, 31 | "user": { 32 | "name": "Jason Costa", 33 | "profile_sidebar_border_color": "86A4A6", 34 | "profile_sidebar_fill_color": "A0C5C7", 35 | "profile_background_tile": false, 36 | "profile_image_url": "http://a0.twimg.com/profile_images/1751674923/new_york_beard_normal.jpg", 37 | "created_at": "Wed May 28 00:20:15 +0000 2008", 38 | "location": "", 39 | "is_translator": true, 40 | "follow_request_sent": false, 41 | "id_str": "14927800", 42 | "profile_link_color": "FF3300", 43 | "entities": { 44 | "url": { 45 | "urls": [ 46 | { 47 | "expanded_url": "http://www.jason-costa.blogspot.com/", 48 | "url": "http://t.co/YCA3ZKY", 49 | "indices": [0, 19], 50 | "display_url": "jason-costa.blogspot.com" 51 | } 52 | ] 53 | }, 54 | "description": { 55 | "urls": [] 56 | } 57 | }, 58 | "default_profile": false, 59 | "contributors_enabled": false, 60 | "url": "http://t.co/YCA3ZKY", 61 | "favourites_count": 883, 62 | "utc_offset": -28800, 63 | "id": 14927800, 64 | "profile_image_url_https": "https://si0.twimg.com/profile_images/1751674923/new_york_beard_normal.jpg", 65 | "profile_use_background_image": true, 66 | "listed_count": 150, 67 | "profile_text_color": "333333", 68 | "protected": false, 69 | "lang": "en", 70 | "followers_count": 8760, 71 | "time_zone": "Pacific Time (US & Canada)", 72 | "profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme6/bg.gif", 73 | "verified": false, 74 | "profile_background_color": "709397", 75 | "notifications": false, 76 | "description": "Platform at Twitter", 77 | "geo_enabled": true, 78 | "statuses_count": 5531, 79 | "default_profile_image": false, 80 | "friends_count": 166, 81 | "profile_background_image_url": "http://a0.twimg.com/images/themes/theme6/bg.gif", 82 | "show_all_inline_media": true, 83 | "screen_name": "jasoncosta", 84 | "following": false 85 | }, 86 | "possibly_sensitive_editable": true, 87 | "source": "Tweet Button", 88 | "in_reply_to_screen_name": null, 89 | "in_reply_to_status_id": null 90 | } 91 | -------------------------------------------------------------------------------- /spec/twitter/rest/timelines_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../helper" 2 | 3 | describe Twitter::REST::Timelines do 4 | client = Mock::Client.new("CK", "CS", "AT", "AS", "UA") 5 | 6 | describe "#home_timeline" do 7 | context "called without any args" do 8 | timeline = client.home_timeline 9 | it "returns Array(Twitter::Tweet)" do 10 | timeline.should be_a Array(Twitter::Tweet) 11 | timeline[0].text.should eq "just another test" 12 | timeline[1].text.should eq "lecturing at the \"analyzing big data with twitter\" class at @cal with @othman http://t.co/bfj7zkDJ" 13 | end 14 | end 15 | end 16 | 17 | describe "#mentions_timeline" do 18 | context "called without any args" do 19 | timeline = client.mentions_timeline 20 | it "returns Array(Twitter::Tweet)" do 21 | timeline.should be_a Array(Twitter::Tweet) 22 | timeline[0].text.should eq "@jasoncosta @themattharris Hey! Going to be in Frisco in October. Was hoping to have a meeting to talk about @thinkwall if you're around?" 23 | timeline[1].text.should eq "Got the shirt @jasoncosta thanks man! Loving the #twitter bird on the shirt :-)" 24 | end 25 | end 26 | end 27 | 28 | describe "#retweets_of_me" do 29 | context "called without any args" do 30 | timeline = client.retweets_of_me 31 | it "returns Array(Twitter::Tweet)" do 32 | timeline.should be_a Array(Twitter::Tweet) 33 | timeline[0].text.should eq "It's bring your migraine to work day today!" 34 | end 35 | end 36 | end 37 | 38 | describe "#user_timeline" do 39 | context "called with String" do 40 | timeline = client.user_timeline("kenta_s_dev") 41 | it "returns Array(Twitter::Tweet)" do 42 | timeline.should be_a Array(Twitter::Tweet) 43 | timeline[0].text.should eq "RT @TwitterDev: 1/ Today we’re sharing our vision for the future of the Twitter API platform!\nhttps://t.co/XweGngmxlP" 44 | timeline[1].text.should eq "RT @TwitterMktg: Starting today, businesses can request and share locations when engaging with people in Direct Messages. https://t.co/rpYn…" 45 | end 46 | end 47 | 48 | context "called with Int32" do 49 | timeline = client.user_timeline(12345) 50 | it "returns Array(Twitter::Tweet)" do 51 | timeline.should be_a Array(Twitter::Tweet) 52 | timeline[0].text.should eq "RT @TwitterDev: 1/ Today we’re sharing our vision for the future of the Twitter API platform!\nhttps://t.co/XweGngmxlP" 53 | timeline[1].text.should eq "RT @TwitterMktg: Starting today, businesses can request and share locations when engaging with people in Direct Messages. https://t.co/rpYn…" 54 | end 55 | end 56 | 57 | context "called with Int64" do 58 | timeline = client.user_timeline(1234512345123451234) 59 | it "returns Array(Twitter::Tweet)" do 60 | timeline.should be_a Array(Twitter::Tweet) 61 | timeline[0].text.should eq "RT @TwitterDev: 1/ Today we’re sharing our vision for the future of the Twitter API platform!\nhttps://t.co/XweGngmxlP" 62 | timeline[1].text.should eq "RT @TwitterMktg: Starting today, businesses can request and share locations when engaging with people in Direct Messages. https://t.co/rpYn…" 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/mock/1.1/statuses/retweets_of_me.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "coordinates": null, 4 | "truncated": false, 5 | "favorited": false, 6 | "created_at": "Fri Oct 19 15:51:49 +0000 2012", 7 | "id_str": "259320959964680192", 8 | "entities": { 9 | "urls": [], 10 | "hashtags": [], 11 | "symbols": [], 12 | "user_mentions": [] 13 | }, 14 | "in_reply_to_user_id_str": null, 15 | "contributors": null, 16 | "text": "It's bring your migraine to work day today!", 17 | "in_reply_to_status_id_str": null, 18 | "id": 259320959964680192, 19 | "retweet_count": 1, 20 | "geo": null, 21 | "retweeted": false, 22 | "in_reply_to_user_id": null, 23 | "source": "YoruFukurou", 24 | "user": { 25 | "name": "Taylor Singletary", 26 | "profile_sidebar_fill_color": "FBFBFB", 27 | "profile_background_tile": false, 28 | "profile_sidebar_border_color": "000000", 29 | "location": "San Francisco, CA", 30 | "profile_image_url": "http://a0.twimg.com/profile_images/2766969649/5e1a50995a9f9bfcdcdc7503e1271422_normal.jpeg", 31 | "created_at": "Wed Mar 07 22:23:19 +0000 2007", 32 | "profile_link_color": "CC1442", 33 | "is_translator": false, 34 | "id_str": "819797", 35 | "follow_request_sent": false, 36 | "entities": { 37 | "url": { 38 | "urls": [ 39 | { 40 | "expanded_url": "http://soundcloud.com/reality-technician", 41 | "url": "http://t.co/bKlJ80Do", 42 | "indices": [0, 20], 43 | "display_url": "soundcloud.com/reality-techni…" 44 | } 45 | ] 46 | }, 47 | "description": { 48 | "urls": [] 49 | } 50 | }, 51 | "favourites_count": 17094, 52 | "url": "http://t.co/bKlJ80Do", 53 | "contributors_enabled": false, 54 | "default_profile": false, 55 | "profile_image_url_https": "https://si0.twimg.com/profile_images/2766969649/5e1a50995a9f9bfcdcdc7503e1271422_normal.jpeg", 56 | "profile_banner_url": "https://si0.twimg.com/profile_banners/819797/1351262715", 57 | "utc_offset": -28800, 58 | "id": 819797, 59 | "profile_use_background_image": false, 60 | "listed_count": 351, 61 | "followers_count": 7701, 62 | "profile_text_color": "D20909", 63 | "protected": false, 64 | "lang": "en", 65 | "geo_enabled": true, 66 | "time_zone": "Pacific Time (US & Canada)", 67 | "notifications": false, 68 | "profile_background_color": "6B0F0F", 69 | "description": "Reality Technician, Twitter API team, synth enthusiast. A most excellent adventure in timelines. Through the darkness of future past, the magician longs to see.", 70 | "verified": false, 71 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/686878932/6447abb9f83c76fb4fbd68e626c6c8c1.png", 72 | "friends_count": 5549, 73 | "default_profile_image": false, 74 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/686878932/6447abb9f83c76fb4fbd68e626c6c8c1.png", 75 | "statuses_count": 18626, 76 | "screen_name": "episod", 77 | "following": false 78 | }, 79 | "place": null, 80 | "in_reply_to_screen_name": null, 81 | "in_reply_to_status_id": null 82 | } 83 | ] 84 | -------------------------------------------------------------------------------- /spec/mock/1.1/statuses/unretweet.json: -------------------------------------------------------------------------------- 1 | { 2 | "contributors": null, 3 | "coordinates": null, 4 | "created_at": "Fri Dec 25 23:11:29 +0000 2015", 5 | "entities": { 6 | "hashtags": [], 7 | "symbols": [], 8 | "urls": [], 9 | "user_mentions": [ 10 | { 11 | "id": 2244994945, 12 | "id_str": "2244994945", 13 | "indices": [30, 41], 14 | "name": "TwitterDev", 15 | "screen_name": "TwitterDev" 16 | } 17 | ] 18 | }, 19 | "favorite_count": 78, 20 | "favorited": false, 21 | "geo": null, 22 | "id": 680526305473867776, 23 | "id_str": "680526305473867776", 24 | "in_reply_to_screen_name": null, 25 | "in_reply_to_status_id": null, 26 | "in_reply_to_status_id_str": null, 27 | "in_reply_to_user_id": null, 28 | "in_reply_to_user_id_str": null, 29 | "is_quote_status": false, 30 | "lang": "en", 31 | "place": null, 32 | "retweet_count": 25, 33 | "retweeted": true, 34 | "source": "Twitter for iPhone", 35 | "text": "Happy holidays from all of us @twitterdev! Here's to an exciting 2016!", 36 | "truncated": false, 37 | "user": { 38 | "contributors_enabled": false, 39 | "created_at": "Sat Dec 14 04:35:55 +0000 2013", 40 | "default_profile": false, 41 | "default_profile_image": false, 42 | "description": "Developer and Platform Relations @Twitter. We are developer advocates. We can't answer all your questions, but we listen to all of them!", 43 | "entities": { 44 | "description": { 45 | "urls": [] 46 | }, 47 | "url": { 48 | "urls": [ 49 | { 50 | "display_url": "dev.twitter.com", 51 | "expanded_url": "https://dev.twitter.com/", 52 | "indices": [0, 23], 53 | "url": "https://t.co/66w26cua1O" 54 | } 55 | ] 56 | } 57 | }, 58 | "favourites_count": 876, 59 | "follow_request_sent": false, 60 | "followers_count": 336055, 61 | "following": true, 62 | "friends_count": 1499, 63 | "geo_enabled": true, 64 | "has_extended_profile": false, 65 | "id": 2244994945, 66 | "id_str": "2244994945", 67 | "is_translation_enabled": false, 68 | "is_translator": false, 69 | "lang": "en", 70 | "listed_count": 728, 71 | "location": "Internet", 72 | "name": "TwitterDev", 73 | "notifications": false, 74 | "profile_background_color": "FFFFFF", 75 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", 76 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", 77 | "profile_background_tile": false, 78 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/2244994945/1396995246", 79 | "profile_image_url": "http://pbs.twimg.com/profile_images/530814764687949824/npQQVkq8_normal.png", 80 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/530814764687949824/npQQVkq8_normal.png", 81 | "profile_link_color": "0084B4", 82 | "profile_sidebar_border_color": "FFFFFF", 83 | "profile_sidebar_fill_color": "DDEEF6", 84 | "profile_text_color": "333333", 85 | "profile_use_background_image": false, 86 | "protected": false, 87 | "screen_name": "TwitterDev", 88 | "statuses_count": 1663, 89 | "time_zone": "Pacific Time (US & Canada)", 90 | "url": "https://t.co/66w26cua1O", 91 | "utc_offset": -28800, 92 | "verified": true 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /spec/mock/1.1/friendships/create.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Doug Williams", 3 | "profile_sidebar_border_color": "87BC44", 4 | "profile_sidebar_fill_color": "F2F8FC", 5 | "profile_background_tile": false, 6 | "created_at": "Sun Mar 18 06:42:26 +0000 2007", 7 | "profile_image_url": "http://a0.twimg.com/profile_images/1947332700/dougw_avatar_can2_normal.jpg", 8 | "location": "San Francisco, CA", 9 | "profile_link_color": "0000FF", 10 | "follow_request_sent": false, 11 | "is_translator": false, 12 | "id_str": "1401881", 13 | "entities": { 14 | "description": { 15 | "urls": [] 16 | }, 17 | "url": { 18 | "urls": [] 19 | } 20 | }, 21 | "default_profile": false, 22 | "url": null, 23 | "favourites_count": 2620, 24 | "contributors_enabled": true, 25 | "utc_offset": -28800, 26 | "id": 1401881, 27 | "profile_image_url_https": "https://si0.twimg.com/profile_images/1947332700/dougw_avatar_can2_normal.jpg", 28 | "listed_count": 890, 29 | "profile_use_background_image": true, 30 | "profile_text_color": "000000", 31 | "protected": false, 32 | "lang": "en", 33 | "followers_count": 306557, 34 | "geo_enabled": true, 35 | "time_zone": "Pacific Time (US & Canada)", 36 | "notifications": false, 37 | "verified": false, 38 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/2752608/twitter_bg_grass.jpg", 39 | "profile_background_color": "9AE4E8", 40 | "description": "Partnerships at @twitter", 41 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/2752608/twitter_bg_grass.jpg", 42 | "friends_count": 1284, 43 | "statuses_count": 11873, 44 | "default_profile_image": false, 45 | "status": { 46 | "coordinates": null, 47 | "truncated": false, 48 | "created_at": "Sun Aug 26 21:58:35 +0000 2012", 49 | "favorited": false, 50 | "id_str": "239844310982459392", 51 | "entities": { 52 | "urls": [], 53 | "media": [ 54 | { 55 | "media_url_https": "https://p.twimg.com/A1QZIU3CAAAnCkS.jpg", 56 | "expanded_url": "http://twitter.com/dougw/status/239844310982459392/photo/1", 57 | "sizes": { 58 | "large": { 59 | "w": 816, 60 | "resize": "fit", 61 | "h": 612 62 | }, 63 | "medium": { 64 | "w": 600, 65 | "resize": "fit", 66 | "h": 450 67 | }, 68 | "small": { 69 | "w": 340, 70 | "resize": "fit", 71 | "h": 255 72 | }, 73 | "thumb": { 74 | "w": 150, 75 | "resize": "crop", 76 | "h": 150 77 | } 78 | }, 79 | "id_str": "239844310986653696", 80 | "url": "http://t.co/1uG4mhaB", 81 | "id": 239844310986653696, 82 | "type": "photo", 83 | "indices": [45, 65], 84 | "media_url": "http://p.twimg.com/A1QZIU3CAAAnCkS.jpg", 85 | "display_url": "pic.twitter.com/1uG4mhaB" 86 | } 87 | ], 88 | "hashtags": [], 89 | "user_mentions": [] 90 | }, 91 | "in_reply_to_user_id_str": null, 92 | "text": "Warm day by the Bay. Watching boats sail by. http://t.co/1uG4mhaB", 93 | "contributors": null, 94 | "id": 239844310982459392, 95 | "retweet_count": 0, 96 | "in_reply_to_status_id_str": null, 97 | "geo": null, 98 | "retweeted": false, 99 | "in_reply_to_user_id": null, 100 | "possibly_sensitive": false, 101 | "place": null, 102 | "possibly_sensitive_editable": true, 103 | "source": "Camera on iOS", 104 | "in_reply_to_screen_name": null, 105 | "in_reply_to_status_id": null 106 | }, 107 | "show_all_inline_media": true, 108 | "screen_name": "dougw", 109 | "following": true 110 | } 111 | -------------------------------------------------------------------------------- /spec/mock/1.1/friendships/destroy.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Doug Williams", 3 | "profile_sidebar_border_color": "87BC44", 4 | "profile_sidebar_fill_color": "F2F8FC", 5 | "profile_background_tile": false, 6 | "created_at": "Sun Mar 18 06:42:26 +0000 2007", 7 | "profile_image_url": "http://a0.twimg.com/profile_images/1947332700/dougw_avatar_can2_normal.jpg", 8 | "location": "San Francisco, CA", 9 | "profile_link_color": "0000FF", 10 | "follow_request_sent": false, 11 | "is_translator": false, 12 | "id_str": "1401881", 13 | "entities": { 14 | "description": { 15 | "urls": [] 16 | }, 17 | "url": { 18 | "urls": [] 19 | } 20 | }, 21 | "default_profile": false, 22 | "url": null, 23 | "favourites_count": 2620, 24 | "contributors_enabled": true, 25 | "utc_offset": -28800, 26 | "id": 1401881, 27 | "profile_image_url_https": "https://si0.twimg.com/profile_images/1947332700/dougw_avatar_can2_normal.jpg", 28 | "listed_count": 890, 29 | "profile_use_background_image": true, 30 | "profile_text_color": "000000", 31 | "protected": false, 32 | "lang": "en", 33 | "followers_count": 306556, 34 | "geo_enabled": true, 35 | "time_zone": "Pacific Time (US & Canada)", 36 | "notifications": false, 37 | "verified": false, 38 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/2752608/twitter_bg_grass.jpg", 39 | "profile_background_color": "9AE4E8", 40 | "description": "Partnerships at @twitter", 41 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/2752608/twitter_bg_grass.jpg", 42 | "friends_count": 1284, 43 | "statuses_count": 11873, 44 | "default_profile_image": false, 45 | "status": { 46 | "coordinates": null, 47 | "truncated": false, 48 | "created_at": "Sun Aug 26 21:58:35 +0000 2012", 49 | "favorited": false, 50 | "id_str": "239844310982459392", 51 | "entities": { 52 | "urls": [], 53 | "media": [ 54 | { 55 | "media_url_https": "https://p.twimg.com/A1QZIU3CAAAnCkS.jpg", 56 | "expanded_url": "http://twitter.com/dougw/status/239844310982459392/photo/1", 57 | "sizes": { 58 | "large": { 59 | "w": 816, 60 | "resize": "fit", 61 | "h": 612 62 | }, 63 | "medium": { 64 | "w": 600, 65 | "resize": "fit", 66 | "h": 450 67 | }, 68 | "small": { 69 | "w": 340, 70 | "resize": "fit", 71 | "h": 255 72 | }, 73 | "thumb": { 74 | "w": 150, 75 | "resize": "crop", 76 | "h": 150 77 | } 78 | }, 79 | "id_str": "239844310986653696", 80 | "url": "http://t.co/1uG4mhaB", 81 | "id": 239844310986653696, 82 | "type": "photo", 83 | "indices": [45, 65], 84 | "media_url": "http://p.twimg.com/A1QZIU3CAAAnCkS.jpg", 85 | "display_url": "pic.twitter.com/1uG4mhaB" 86 | } 87 | ], 88 | "hashtags": [], 89 | "user_mentions": [] 90 | }, 91 | "in_reply_to_user_id_str": null, 92 | "text": "Warm day by the Bay. Watching boats sail by. http://t.co/1uG4mhaB", 93 | "contributors": null, 94 | "id": 239844310982459392, 95 | "retweet_count": 0, 96 | "in_reply_to_status_id_str": null, 97 | "geo": null, 98 | "retweeted": false, 99 | "in_reply_to_user_id": null, 100 | "possibly_sensitive": false, 101 | "place": null, 102 | "possibly_sensitive_editable": true, 103 | "source": "Camera on iOS", 104 | "in_reply_to_screen_name": null, 105 | "in_reply_to_status_id": null 106 | }, 107 | "show_all_inline_media": true, 108 | "screen_name": "dougw", 109 | "following": false 110 | } 111 | -------------------------------------------------------------------------------- /spec/mock/1.1/statuses/show.json: -------------------------------------------------------------------------------- 1 | { 2 | "coordinates": null, 3 | "favorited": false, 4 | "created_at": "Wed Aug 29 16:54:38 +0000 2012", 5 | "truncated": false, 6 | "id_str": "240854986559455234", 7 | "entities": { 8 | "urls": [ 9 | { 10 | "expanded_url": "http://venturebeat.com/2012/08/29/vimeo-dropbox/#.UD5JLsYptSs.twitter", 11 | "url": "http://t.co/7UlkvZzM", 12 | "indices": [69, 89], 13 | "display_url": "venturebeat.com/2012/08/29/vim\u2026" 14 | } 15 | ], 16 | "hashtags": [], 17 | "symbols": [], 18 | "user_mentions": [] 19 | }, 20 | "in_reply_to_user_id_str": null, 21 | "text": "\"Vimeo integrates with Dropbox for easier video uploads and shares\": http://t.co/7UlkvZzM", 22 | "contributors": null, 23 | "retweet_count": 1, 24 | "id": 240854986559455234, 25 | "in_reply_to_status_id_str": null, 26 | "geo": null, 27 | "retweeted": false, 28 | "in_reply_to_user_id": null, 29 | "possibly_sensitive": false, 30 | "place": { 31 | "id": "07d9db48bc083000", 32 | "url": "https://api.twitter.com/1.1/geo/id/07d9db48bc083000.json", 33 | "place_type": "poi", 34 | "name": "McIntosh Lake", 35 | "full_name": "McIntosh Lake", 36 | "country_code": "US", 37 | "country": "United States", 38 | "bounding_box": { 39 | "type": "Polygon", 40 | "coordinates": [ 41 | [ 42 | [-105.14544, 40.192138], 43 | [-105.14544, 40.192138], 44 | [-105.14544, 40.192138], 45 | [-105.14544, 40.192138] 46 | ] 47 | ] 48 | }, 49 | "attributes": {} 50 | }, 51 | 52 | "user": { 53 | "name": "Jason Costa", 54 | "profile_sidebar_border_color": "86A4A6", 55 | "profile_sidebar_fill_color": "A0C5C7", 56 | "profile_background_tile": false, 57 | "profile_image_url": "http://a0.twimg.com/profile_images/1751674923/new_york_beard_normal.jpg", 58 | "created_at": "Wed May 28 00:20:15 +0000 2008", 59 | "location": "", 60 | "is_translator": true, 61 | "follow_request_sent": false, 62 | "id_str": "14927800", 63 | "profile_link_color": "FF3300", 64 | "entities": { 65 | "url": { 66 | "urls": [ 67 | { 68 | "expanded_url": "http://www.jason-costa.blogspot.com/", 69 | "url": "http://t.co/YCA3ZKY", 70 | "indices": [0, 19], 71 | "display_url": "jason-costa.blogspot.com" 72 | } 73 | ] 74 | }, 75 | "description": { 76 | "urls": [] 77 | } 78 | }, 79 | "default_profile": false, 80 | "contributors_enabled": false, 81 | "url": "http://t.co/YCA3ZKY", 82 | "favourites_count": 883, 83 | "utc_offset": -28800, 84 | "id": 14927800, 85 | "profile_image_url_https": "https://si0.twimg.com/profile_images/1751674923/new_york_beard_normal.jpg", 86 | "profile_use_background_image": true, 87 | "listed_count": 150, 88 | "profile_text_color": "333333", 89 | "protected": false, 90 | "lang": "en", 91 | "followers_count": 8760, 92 | "time_zone": "Pacific Time (US & Canada)", 93 | "profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme6/bg.gif", 94 | "verified": false, 95 | "profile_background_color": "709397", 96 | "notifications": false, 97 | "description": "Platform at Twitter", 98 | "geo_enabled": true, 99 | "statuses_count": 5531, 100 | "default_profile_image": false, 101 | "friends_count": 166, 102 | "profile_background_image_url": "http://a0.twimg.com/images/themes/theme6/bg.gif", 103 | "show_all_inline_media": true, 104 | "screen_name": "jasoncosta", 105 | "following": false 106 | }, 107 | "possibly_sensitive_editable": true, 108 | "source": "Tweet Button", 109 | "in_reply_to_screen_name": null, 110 | "in_reply_to_status_id": null 111 | } 112 | -------------------------------------------------------------------------------- /spec/mock/1.1/direct_messages/show.json: -------------------------------------------------------------------------------- 1 | { 2 | "created_at": "Mon Aug 27 17:21:03 +0000 2012", 3 | "entities": { 4 | "hashtags": [], 5 | "symbols": [], 6 | "urls": [], 7 | "user_mentions": [] 8 | }, 9 | "id": 240136858829479936, 10 | "id_str": "240136858829479936", 11 | "recipient": { 12 | "contributors_enabled": false, 13 | "created_at": "Thu Aug 23 19:45:07 +0000 2012", 14 | "default_profile": false, 15 | "default_profile_image": false, 16 | "description": "Keep calm and test", 17 | "favourites_count": 0, 18 | "follow_request_sent": false, 19 | "followers_count": 0, 20 | "following": false, 21 | "friends_count": 10, 22 | "geo_enabled": true, 23 | "id": 776627022, 24 | "id_str": "776627022", 25 | "is_translator": false, 26 | "lang": "en", 27 | "listed_count": 0, 28 | "location": "San Francisco, CA", 29 | "name": "Mick Jagger", 30 | "notifications": false, 31 | "profile_background_color": "000000", 32 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/644522235/cdjlccey99gy36j3em67.jpeg", 33 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/644522235/cdjlccey99gy36j3em67.jpeg", 34 | "profile_background_tile": true, 35 | "profile_image_url": "http://a0.twimg.com/profile_images/2550226257/y0ef5abcx5yrba8du0sk_normal.jpeg", 36 | "profile_image_url_https": "https://si0.twimg.com/profile_images/2550226257/y0ef5abcx5yrba8du0sk_normal.jpeg", 37 | "profile_link_color": "000000", 38 | "profile_sidebar_border_color": "000000", 39 | "profile_sidebar_fill_color": "000000", 40 | "profile_text_color": "000000", 41 | "profile_use_background_image": false, 42 | "protected": false, 43 | "screen_name": "s0c1alm3dia", 44 | "show_all_inline_media": false, 45 | "statuses_count": 0, 46 | "time_zone": "Pacific Time (US & Canada)", 47 | "url": "http://cnn.com", 48 | "utc_offset": -28800, 49 | "verified": false 50 | }, 51 | "recipient_id": 776627022, 52 | "recipient_screen_name": "s0c1alm3dia", 53 | "sender": { 54 | "contributors_enabled": true, 55 | "created_at": "Sat May 09 17:58:22 +0000 2009", 56 | "default_profile": false, 57 | "default_profile_image": false, 58 | "description": "I taught your phone that thing you like. The Mobile Partner Engineer @Twitter. ", 59 | "favourites_count": 584, 60 | "follow_request_sent": false, 61 | "followers_count": 10621, 62 | "following": false, 63 | "friends_count": 1181, 64 | "geo_enabled": true, 65 | "id": 38895958, 66 | "id_str": "38895958", 67 | "is_translator": false, 68 | "lang": "en", 69 | "listed_count": 190, 70 | "location": "San Francisco", 71 | "name": "Sean Cook", 72 | "notifications": false, 73 | "profile_background_color": "1A1B1F", 74 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/495742332/purty_wood.png", 75 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/495742332/purty_wood.png", 76 | "profile_background_tile": true, 77 | "profile_image_url": "http://a0.twimg.com/profile_images/1751506047/dead_sexy_normal.JPG", 78 | "profile_image_url_https": "https://si0.twimg.com/profile_images/1751506047/dead_sexy_normal.JPG", 79 | "profile_link_color": "2FC2EF", 80 | "profile_sidebar_border_color": "181A1E", 81 | "profile_sidebar_fill_color": "252429", 82 | "profile_text_color": "666666", 83 | "profile_use_background_image": true, 84 | "protected": false, 85 | "screen_name": "theSeanCook", 86 | "show_all_inline_media": true, 87 | "statuses_count": 2608, 88 | "time_zone": "Pacific Time (US & Canada)", 89 | "url": null, 90 | "utc_offset": -28800, 91 | "verified": false 92 | }, 93 | "sender_id": 38895958, 94 | "sender_screen_name": "theSeanCook", 95 | "text": "booyakasha" 96 | } 97 | -------------------------------------------------------------------------------- /spec/mock/1.1/direct_messages/sent.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "created_at": "Tue Aug 28 00:40:56 +0000 2012", 4 | "entities": { 5 | "hashtags": [], 6 | "symbols": [], 7 | "urls": [], 8 | "user_mentions": [] 9 | }, 10 | "id": 240247560269340673, 11 | "id_str": "240247560269340673", 12 | "recipient": { 13 | "contributors_enabled": true, 14 | "created_at": "Sat May 09 17:58:22 +0000 2009", 15 | "default_profile": false, 16 | "default_profile_image": false, 17 | "description": "I taught your phone that thing you like. The Mobile Partner Engineer @Twitter. ", 18 | "favourites_count": 584, 19 | "follow_request_sent": false, 20 | "followers_count": 10622, 21 | "following": true, 22 | "friends_count": 1181, 23 | "geo_enabled": true, 24 | "id": 38895958, 25 | "id_str": "38895958", 26 | "is_translator": false, 27 | "lang": "en", 28 | "listed_count": 190, 29 | "location": "San Francisco", 30 | "name": "Sean Cook", 31 | "notifications": false, 32 | "profile_background_color": "1A1B1F", 33 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/495742332/purty_wood.png", 34 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/495742332/purty_wood.png", 35 | "profile_background_tile": true, 36 | "profile_image_url": "http://a0.twimg.com/profile_images/1751506047/dead_sexy_normal.JPG", 37 | "profile_image_url_https": "https://si0.twimg.com/profile_images/1751506047/dead_sexy_normal.JPG", 38 | "profile_link_color": "2FC2EF", 39 | "profile_sidebar_border_color": "181A1E", 40 | "profile_sidebar_fill_color": "252429", 41 | "profile_text_color": "666666", 42 | "profile_use_background_image": true, 43 | "protected": false, 44 | "screen_name": "theSeanCook", 45 | "show_all_inline_media": true, 46 | "statuses_count": 2608, 47 | "time_zone": "Pacific Time (US & Canada)", 48 | "url": null, 49 | "utc_offset": -28800, 50 | "verified": false 51 | }, 52 | "recipient_id": 38895958, 53 | "recipient_screen_name": "theSeanCook", 54 | "sender": { 55 | "contributors_enabled": false, 56 | "created_at": "Thu Aug 23 19:45:07 +0000 2012", 57 | "default_profile": false, 58 | "default_profile_image": false, 59 | "description": "Keep calm and test", 60 | "favourites_count": 0, 61 | "follow_request_sent": false, 62 | "followers_count": 0, 63 | "following": false, 64 | "friends_count": 11, 65 | "geo_enabled": true, 66 | "id": 776627022, 67 | "id_str": "776627022", 68 | "is_translator": false, 69 | "lang": "en", 70 | "listed_count": 0, 71 | "location": "San Francisco, CA", 72 | "name": "Mick Jagger", 73 | "notifications": false, 74 | "profile_background_color": "000000", 75 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/644522235/cdjlccey99gy36j3em67.jpeg", 76 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/644522235/cdjlccey99gy36j3em67.jpeg", 77 | "profile_background_tile": true, 78 | "profile_image_url": "http://a0.twimg.com/profile_images/2550256790/hv5rtkvistn50nvcuydl_normal.jpeg", 79 | "profile_image_url_https": "https://si0.twimg.com/profile_images/2550256790/hv5rtkvistn50nvcuydl_normal.jpeg", 80 | "profile_link_color": "000000", 81 | "profile_sidebar_border_color": "000000", 82 | "profile_sidebar_fill_color": "000000", 83 | "profile_text_color": "000000", 84 | "profile_use_background_image": false, 85 | "protected": false, 86 | "screen_name": "s0c1alm3dia", 87 | "show_all_inline_media": false, 88 | "statuses_count": 0, 89 | "time_zone": "Pacific Time (US & Canada)", 90 | "url": "http://cnn.com", 91 | "utc_offset": -28800, 92 | "verified": false 93 | }, 94 | "sender_id": 776627022, 95 | "sender_screen_name": "s0c1alm3dia", 96 | "text": "Meet me behind the cafeteria after school." 97 | } 98 | ] 99 | -------------------------------------------------------------------------------- /spec/mock/1.1/users/show.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 7505382, 3 | "id_str": "7505382", 4 | "name": "Erik Michaels-Ober", 5 | "screen_name": "sferik", 6 | "location": "Berlin", 7 | "profile_location": null, 8 | "description": "This is fine.", 9 | "url": "https://t.co/L2xIBazeZH", 10 | "entities": { 11 | "url": { 12 | "urls": [ 13 | { 14 | "url": "https://t.co/L2xIBazeZH", 15 | "expanded_url": "https://github.com/sferik", 16 | "display_url": "github.com/sferik", 17 | "indices": [0, 23] 18 | } 19 | ] 20 | }, 21 | "description": { 22 | "urls": [] 23 | } 24 | }, 25 | "protected": false, 26 | "followers_count": 5945, 27 | "friends_count": 881, 28 | "listed_count": 339, 29 | "created_at": "Mon Jul 16 12:59:01 +0000 2007", 30 | "favourites_count": 14447, 31 | "utc_offset": 7200, 32 | "time_zone": "Berlin", 33 | "geo_enabled": true, 34 | "verified": false, 35 | "statuses_count": 17358, 36 | "lang": "en", 37 | "status": { 38 | "created_at": "Sat Aug 29 13:36:37 +0000 2015", 39 | "id": 637619867223478272, 40 | "id_str": "637619867223478272", 41 | "text": "@megerman Is purchasing power zero-sum?", 42 | "source": "\u003ca href=\"http://twitter.com\" rel=\"nofollow\"\u003eTwitter Web Client\u003c/a\u003e", 43 | "truncated": false, 44 | "in_reply_to_status_id": 637619436560752640, 45 | "in_reply_to_status_id_str": "637619436560752640", 46 | "in_reply_to_user_id": 831831750, 47 | "in_reply_to_user_id_str": "831831750", 48 | "in_reply_to_screen_name": "megerman", 49 | "geo": null, 50 | "coordinates": null, 51 | "place": { 52 | "id": "3078869807f9dd36", 53 | "url": "https://api.twitter.com/1.1/geo/id/3078869807f9dd36.json", 54 | "place_type": "city", 55 | "name": "Berlin", 56 | "full_name": "Berlin, Germany", 57 | "country_code": "DE", 58 | "country": "Deutschland", 59 | "contained_within": [], 60 | "bounding_box": { 61 | "type": "Polygon", 62 | "coordinates": [ 63 | [ 64 | [13.088304, 52.338079], 65 | [13.760909, 52.338079], 66 | [13.760909, 52.675323], 67 | [13.088304, 52.675323] 68 | ] 69 | ] 70 | }, 71 | "attributes": {} 72 | }, 73 | "contributors": null, 74 | "retweet_count": 0, 75 | "favorite_count": 0, 76 | "entities": { 77 | "hashtags": [], 78 | "symbols": [], 79 | "user_mentions": [ 80 | { 81 | "screen_name": "megerman", 82 | "name": "Mark Egerman", 83 | "id": 831831750, 84 | "id_str": "831831750", 85 | "indices": [0, 9] 86 | } 87 | ], 88 | "urls": [] 89 | }, 90 | "favorited": false, 91 | "retweeted": false, 92 | "lang": "en" 93 | }, 94 | "contributors_enabled": false, 95 | "is_translator": false, 96 | "is_translation_enabled": false, 97 | "profile_background_color": "000000", 98 | "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/677717672/bb0b3653dcf0644e344823e0a2eb3382.png", 99 | "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/677717672/bb0b3653dcf0644e344823e0a2eb3382.png", 100 | "profile_background_tile": false, 101 | "profile_image_url": "http://pbs.twimg.com/profile_images/583426225605058560/dO_rpWw1_normal.jpg", 102 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/583426225605058560/dO_rpWw1_normal.jpg", 103 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/7505382/1425238640", 104 | "profile_link_color": "393740", 105 | "profile_sidebar_border_color": "000000", 106 | "profile_sidebar_fill_color": "DDEEF6", 107 | "profile_text_color": "333333", 108 | "profile_use_background_image": true, 109 | "has_extended_profile": true, 110 | "default_profile": false, 111 | "default_profile_image": false, 112 | "following": false, 113 | "follow_request_sent": false, 114 | "notifications": false, 115 | "suspended": false, 116 | "needs_phone_verification": false 117 | } 118 | -------------------------------------------------------------------------------- /spec/mock/1.1/account/verify_credentials.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 7505382, 3 | "id_str": "7505382", 4 | "name": "Erik Michaels-Ober", 5 | "screen_name": "sferik", 6 | "location": "Berlin", 7 | "profile_location": null, 8 | "description": "This is fine.", 9 | "url": "https://t.co/L2xIBazeZH", 10 | "entities": { 11 | "url": { 12 | "urls": [ 13 | { 14 | "url": "https://t.co/L2xIBazeZH", 15 | "expanded_url": "https://github.com/sferik", 16 | "display_url": "github.com/sferik", 17 | "indices": [0, 23] 18 | } 19 | ] 20 | }, 21 | "description": { 22 | "urls": [] 23 | } 24 | }, 25 | "protected": false, 26 | "followers_count": 5945, 27 | "friends_count": 881, 28 | "listed_count": 339, 29 | "created_at": "Mon Jul 16 12:59:01 +0000 2007", 30 | "favourites_count": 14447, 31 | "utc_offset": 7200, 32 | "time_zone": "Berlin", 33 | "geo_enabled": true, 34 | "verified": false, 35 | "statuses_count": 17358, 36 | "lang": "en", 37 | "status": { 38 | "created_at": "Sat Aug 29 13:36:37 +0000 2015", 39 | "id": 637619867223478272, 40 | "id_str": "637619867223478272", 41 | "text": "@megerman Is purchasing power zero-sum?", 42 | "source": "\u003ca href=\"http://twitter.com\" rel=\"nofollow\"\u003eTwitter Web Client\u003c/a\u003e", 43 | "truncated": false, 44 | "in_reply_to_status_id": 637619436560752640, 45 | "in_reply_to_status_id_str": "637619436560752640", 46 | "in_reply_to_user_id": 831831750, 47 | "in_reply_to_user_id_str": "831831750", 48 | "in_reply_to_screen_name": "megerman", 49 | "geo": null, 50 | "coordinates": null, 51 | "place": { 52 | "id": "3078869807f9dd36", 53 | "url": "https://api.twitter.com/1.1/geo/id/3078869807f9dd36.json", 54 | "place_type": "city", 55 | "name": "Berlin", 56 | "full_name": "Berlin, Germany", 57 | "country_code": "DE", 58 | "country": "Deutschland", 59 | "contained_within": [], 60 | "bounding_box": { 61 | "type": "Polygon", 62 | "coordinates": [ 63 | [ 64 | [13.088304, 52.338079], 65 | [13.760909, 52.338079], 66 | [13.760909, 52.675323], 67 | [13.088304, 52.675323] 68 | ] 69 | ] 70 | }, 71 | "attributes": {} 72 | }, 73 | "contributors": null, 74 | "retweet_count": 0, 75 | "favorite_count": 0, 76 | "entities": { 77 | "hashtags": [], 78 | "symbols": [], 79 | "user_mentions": [ 80 | { 81 | "screen_name": "megerman", 82 | "name": "Mark Egerman", 83 | "id": 831831750, 84 | "id_str": "831831750", 85 | "indices": [0, 9] 86 | } 87 | ], 88 | "urls": [] 89 | }, 90 | "favorited": false, 91 | "retweeted": false, 92 | "lang": "en" 93 | }, 94 | "contributors_enabled": false, 95 | "is_translator": false, 96 | "is_translation_enabled": false, 97 | "profile_background_color": "000000", 98 | "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/677717672/bb0b3653dcf0644e344823e0a2eb3382.png", 99 | "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/677717672/bb0b3653dcf0644e344823e0a2eb3382.png", 100 | "profile_background_tile": false, 101 | "profile_image_url": "http://pbs.twimg.com/profile_images/583426225605058560/dO_rpWw1_normal.jpg", 102 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/583426225605058560/dO_rpWw1_normal.jpg", 103 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/7505382/1425238640", 104 | "profile_link_color": "393740", 105 | "profile_sidebar_border_color": "000000", 106 | "profile_sidebar_fill_color": "DDEEF6", 107 | "profile_text_color": "333333", 108 | "profile_use_background_image": true, 109 | "has_extended_profile": true, 110 | "default_profile": false, 111 | "default_profile_image": false, 112 | "following": false, 113 | "follow_request_sent": false, 114 | "notifications": false, 115 | "suspended": false, 116 | "needs_phone_verification": false 117 | } 118 | -------------------------------------------------------------------------------- /spec/mock/1.1/direct_messages.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "created_at": "Mon Aug 27 17:21:03 +0000 2012", 4 | "entities": { 5 | "hashtags": [], 6 | "symbols": [], 7 | "urls": [], 8 | "user_mentions": [], 9 | "media": [], 10 | "polls": [] 11 | }, 12 | "id": 240136858829479936, 13 | "id_str": "240136858829479936", 14 | "recipient": { 15 | "contributors_enabled": false, 16 | "created_at": "Thu Aug 23 19:45:07 +0000 2012", 17 | "default_profile": false, 18 | "default_profile_image": false, 19 | "description": "Keep calm and test", 20 | "favourites_count": 0, 21 | "follow_request_sent": false, 22 | "followers_count": 0, 23 | "following": false, 24 | "friends_count": 10, 25 | "geo_enabled": true, 26 | "id": 776627022, 27 | "id_str": "776627022", 28 | "is_translator": false, 29 | "lang": "en", 30 | "listed_count": 0, 31 | "location": "San Francisco, CA", 32 | "name": "Mick Jagger", 33 | "notifications": false, 34 | "profile_background_color": "000000", 35 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/644522235/cdjlccey99gy36j3em67.jpeg", 36 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/644522235/cdjlccey99gy36j3em67.jpeg", 37 | "profile_background_tile": true, 38 | "profile_image_url": "http://a0.twimg.com/profile_images/2550226257/y0ef5abcx5yrba8du0sk_normal.jpeg", 39 | "profile_image_url_https": "https://si0.twimg.com/profile_images/2550226257/y0ef5abcx5yrba8du0sk_normal.jpeg", 40 | "profile_link_color": "000000", 41 | "profile_sidebar_border_color": "000000", 42 | "profile_sidebar_fill_color": "000000", 43 | "profile_text_color": "000000", 44 | "profile_use_background_image": false, 45 | "protected": false, 46 | "screen_name": "s0c1alm3dia", 47 | "show_all_inline_media": false, 48 | "statuses_count": 0, 49 | "time_zone": "Pacific Time (US & Canada)", 50 | "url": "http://cnn.com", 51 | "utc_offset": -28800, 52 | "verified": false 53 | }, 54 | "recipient_id": 776627022, 55 | "recipient_screen_name": "s0c1alm3dia", 56 | "sender": { 57 | "contributors_enabled": true, 58 | "created_at": "Sat May 09 17:58:22 +0000 2009", 59 | "default_profile": false, 60 | "default_profile_image": false, 61 | "description": "I taught your phone that thing you like. The Mobile Partner Engineer @Twitter. ", 62 | "favourites_count": 584, 63 | "follow_request_sent": false, 64 | "followers_count": 10621, 65 | "following": false, 66 | "friends_count": 1181, 67 | "geo_enabled": true, 68 | "id": 38895958, 69 | "id_str": "38895958", 70 | "is_translator": false, 71 | "lang": "en", 72 | "listed_count": 190, 73 | "location": "San Francisco", 74 | "name": "Sean Cook", 75 | "notifications": false, 76 | "profile_background_color": "1A1B1F", 77 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/495742332/purty_wood.png", 78 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/495742332/purty_wood.png", 79 | "profile_background_tile": true, 80 | "profile_image_url": "http://a0.twimg.com/profile_images/1751506047/dead_sexy_normal.JPG", 81 | "profile_image_url_https": "https://si0.twimg.com/profile_images/1751506047/dead_sexy_normal.JPG", 82 | "profile_link_color": "2FC2EF", 83 | "profile_sidebar_border_color": "181A1E", 84 | "profile_sidebar_fill_color": "252429", 85 | "profile_text_color": "666666", 86 | "profile_use_background_image": true, 87 | "protected": false, 88 | "screen_name": "theSeanCook", 89 | "show_all_inline_media": true, 90 | "statuses_count": 2608, 91 | "time_zone": "Pacific Time (US & Canada)", 92 | "url": null, 93 | "utc_offset": -28800, 94 | "verified": false 95 | }, 96 | "sender_id": 38895958, 97 | "sender_screen_name": "theSeanCook", 98 | "text": "booyakasha" 99 | } 100 | ] 101 | -------------------------------------------------------------------------------- /src/twitter/rest/friends_and_followers.cr: -------------------------------------------------------------------------------- 1 | require "../serializations/user" 2 | 3 | module Twitter 4 | module REST 5 | module FriendsAndFollowers 6 | def follow(user_id : Int32 | Int64, options = {} of String => String) : Twitter::User 7 | response = post("/1.1/friendships/create.json", options.merge({"user_id" => user_id.to_s})) 8 | Twitter::User.from_json(response) 9 | end 10 | 11 | def follow(screen_name : String, options = {} of String => String) : Twitter::User 12 | response = post("/1.1/friendships/create.json", options.merge({"screen_name" => screen_name})) 13 | Twitter::User.from_json(response) 14 | end 15 | 16 | def follow(user : Twitter::User, options = {} of String => String) : Twitter::User 17 | follow(user.id, options) 18 | end 19 | 20 | def unfollow(user_id : Int32 | Int64, options = {} of String => String) : Twitter::User 21 | response = post("/1.1/friendships/destroy.json", options.merge({"user_id" => user_id.to_s})) 22 | Twitter::User.from_json(response) 23 | end 24 | 25 | def unfollow(screen_name : String, options = {} of String => String) : Twitter::User 26 | response = post("/1.1/friendships/destroy.json", options.merge({"screen_name" => screen_name})) 27 | Twitter::User.from_json(response) 28 | end 29 | 30 | def unfollow(user : Twitter::User, options = {} of String => String) : Twitter::User 31 | unfollow(user.id, options) 32 | end 33 | 34 | def friend_ids(options = {} of String => String) : Array(Int64) 35 | response = get("/1.1/friends/ids.json", options) 36 | JSON.parse(response)["ids"].as_a.map(&.as_i64) 37 | end 38 | 39 | def friend_ids(screen_name : String, options = {} of String => String) : Array(Int64) 40 | friend_ids(options.merge({"screen_name" => screen_name})) 41 | end 42 | 43 | def friend_ids(user_id : Int32 | Int64, options = {} of String => String) : Array(Int64) 44 | friend_ids(options.merge({"user_id" => user_id.to_s})) 45 | end 46 | 47 | def friend_ids(user : Twitter::User, options = {} of String => String) : Array(Int64) 48 | friend_ids(user.id, options) 49 | end 50 | 51 | def friends(options = {} of String => String) : Array(Twitter::User) 52 | response = get("/1.1/friends/list.json", options) 53 | Array(Twitter::User).from_json(JSON.parse(response)["users"].to_json) 54 | end 55 | 56 | def friends(screen_name : String, options = {} of String => String) : Array(Twitter::User) 57 | friends(options.merge({"screen_name" => screen_name})) 58 | end 59 | 60 | def friends(user_id : Int32 | Int64, options = {} of String => String) : Array(Twitter::User) 61 | friends(options.merge({"user_id" => user_id.to_s})) 62 | end 63 | 64 | def friends(user : Twitter::User, options = {} of String => String) : Array(Twitter::User) 65 | friends(user.id, options) 66 | end 67 | 68 | def follower_ids(options = {} of String => String) : Array(Int64) 69 | response = get("/1.1/followers/ids.json", options) 70 | JSON.parse(response)["ids"].as_a.map(&.as_i64) 71 | end 72 | 73 | def follower_ids(screen_name : String, options = {} of String => String) : Array(Int64) 74 | follower_ids(options.merge({"screen_name" => screen_name})) 75 | end 76 | 77 | def follower_ids(user_id : Int32 | Int64, options = {} of String => String) : Array(Int64) 78 | follower_ids(options.merge({"user_id" => user_id.to_s})) 79 | end 80 | 81 | def follower_ids(user : Twitter::User, options = {} of String => String) : Array(Int64) 82 | follower_ids(user.id, options) 83 | end 84 | 85 | def followers(options = {} of String => String) : Array(Twitter::User) 86 | response = get("/1.1/followers/list.json", options) 87 | Array(Twitter::User).from_json(JSON.parse(response)["users"].to_json) 88 | end 89 | 90 | def followers(screen_name : String, options = {} of String => String) : Array(Twitter::User) 91 | followers(options.merge({"screen_name" => screen_name})) 92 | end 93 | 94 | def followers(user_id : Int32 | Int64, options = {} of String => String) : Array(Twitter::User) 95 | followers(options.merge({"user_id" => user_id.to_s})) 96 | end 97 | 98 | def followers(user : Twitter::User, options = {} of String => String) : Array(Twitter::User) 99 | followers(user.id, options) 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /spec/twitter/rest/tweets_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../helper" 2 | 3 | describe Twitter::REST::Tweets do 4 | client = Mock::Client.new("CK", "CS", "AT", "AS", "UA") 5 | 6 | describe "#update" do 7 | context "called with String" do 8 | tweet = client.update("Maybe he'll finally find his keys. #peterfalk") 9 | it "returns Twitter::Tweet" do 10 | tweet.should be_a Twitter::Tweet 11 | tweet.id.should eq 243145735212777472 12 | end 13 | end 14 | end 15 | 16 | describe "#status" do 17 | context "called with Int32" do 18 | tweet = client.status(12345) 19 | it "returns Twitter::Tweet" do 20 | tweet.should be_a Twitter::Tweet 21 | tweet.text.should eq "\"Vimeo integrates with Dropbox for easier video uploads and shares\": http://t.co/7UlkvZzM" 22 | end 23 | end 24 | 25 | context "called with Int64" do 26 | tweet = client.status(240854986559455234) 27 | it "returns Twitter::Tweet" do 28 | tweet.should be_a Twitter::Tweet 29 | tweet.text.should eq "\"Vimeo integrates with Dropbox for easier video uploads and shares\": http://t.co/7UlkvZzM" 30 | end 31 | end 32 | 33 | context "Tweet with Twitter Place" do 34 | tweet = client.status(240854986559455234) 35 | it "returns Twitter::Tweet with Twitter::Place" do 36 | tweet.should be_a Twitter::Tweet 37 | tweet.place.should be_a Twitter::Place? 38 | 39 | place = tweet.place.not_nil! 40 | place.name.should eq "McIntosh Lake" 41 | place.country.should eq "United States" 42 | end 43 | end 44 | end 45 | 46 | describe "#destroy_status" do 47 | context "called with Int32" do 48 | tweet = client.destroy_status(12345) 49 | it "returns Twitter::Tweet" do 50 | tweet.should be_a Twitter::Tweet 51 | tweet.text.should eq "\"Vimeo integrates with Dropbox for easier video uploads and shares\": http://t.co/7UlkvZzM" 52 | end 53 | end 54 | 55 | context "called with Int64" do 56 | tweet = client.destroy_status(240854986559455234) 57 | it "returns Twitter::Tweet" do 58 | tweet.should be_a Twitter::Tweet 59 | tweet.text.should eq "\"Vimeo integrates with Dropbox for easier video uploads and shares\": http://t.co/7UlkvZzM" 60 | end 61 | end 62 | 63 | context "called with Twitter::Tweet" do 64 | tweet = client.destroy_status(client.update("hello")) 65 | it "returns Twitter::Tweet" do 66 | tweet.should be_a Twitter::Tweet 67 | tweet.id.should eq 240854986559455234 68 | end 69 | end 70 | end 71 | 72 | describe "#retweet" do 73 | context "called with Int32" do 74 | tweet = client.retweet(12345) 75 | it "returns Twitter::Tweet" do 76 | tweet.should be_a Twitter::Tweet 77 | tweet.text.should eq "RT @kurrik: tcptrace and imagemagick - two command line tools TOTALLY worth learning" 78 | end 79 | end 80 | 81 | context "called with Int64" do 82 | tweet = client.retweet(243149503589400576) 83 | it "returns Twitter::Tweet" do 84 | tweet.should be_a Twitter::Tweet 85 | tweet.text.should eq "RT @kurrik: tcptrace and imagemagick - two command line tools TOTALLY worth learning" 86 | end 87 | end 88 | 89 | context "called with Twitter::Tweet" do 90 | tweet = client.retweet(client.update("hello")) 91 | it "returns Twitter::Tweet" do 92 | tweet.should be_a Twitter::Tweet 93 | 94 | # always returns the same text because Twitter::REST::Client#post is stubbed 95 | tweet.text.should eq "RT @kurrik: tcptrace and imagemagick - two command line tools TOTALLY worth learning" 96 | end 97 | end 98 | end 99 | 100 | describe "#unretweet" do 101 | context "called with Int32" do 102 | tweet = client.unretweet(12345) 103 | it "returns Twitter::Tweet" do 104 | tweet.should be_a Twitter::Tweet 105 | tweet.text.should eq "Happy holidays from all of us @twitterdev! Here's to an exciting 2016!" 106 | end 107 | end 108 | 109 | context "called with Int64" do 110 | tweet = client.unretweet(243149503589400576) 111 | it "returns Twitter::Tweet" do 112 | tweet.should be_a Twitter::Tweet 113 | tweet.text.should eq "Happy holidays from all of us @twitterdev! Here's to an exciting 2016!" 114 | end 115 | end 116 | 117 | context "called with Twitter::Tweet" do 118 | tweet = client.unretweet(client.update("hello")) 119 | it "returns Twitter::Tweet" do 120 | tweet.should be_a Twitter::Tweet 121 | 122 | # always returns the same text because Twitter::REST::Client#post is stubbed 123 | tweet.text.should eq "Happy holidays from all of us @twitterdev! Here's to an exciting 2016!" 124 | end 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # twitter-crystal 3 | 4 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/8f698870aeeb4e5a9cb53549725ae653)](https://app.codacy.com/app/mamantoha/twitter-crystal?utm_source=github.com&utm_medium=referral&utm_content=mamantoha/twitter-crystal&utm_campaign=Badge_Grade_Dashboard) 5 | ![Crystal CI](https://github.com/mamantoha/twitter-crystal/workflows/Crystal%20CI/badge.svg?branch=master) 6 | [![GitHub release](https://img.shields.io/github/release/mamantoha/twitter-crystal.svg)](https://github.com/mamantoha/twitter-crystal/releases) 7 | [![Docs](https://img.shields.io/badge/docs-available-brightgreen.svg)](https://mamantoha.github.io/twitter-crystal/) 8 | [![License](https://img.shields.io/github/license/mamantoha/twitter-crystal.svg)](https://github.com/mamantoha/twitter-crystal/blob/master/LICENSE.md) 9 | 10 | A library to access the Twitter API using Crystal 11 | 12 | > This is a fork of [twitter-crystal](https://github.com/sferik/twitter-crystal) which was originally written by Erik Michaels-Ober. 13 | > 14 | > The original repository is no longer maintained and does not work with the latest Crystal version. 15 | 16 | ## Installation 17 | 18 | Add this to your application's `shard.yml`: 19 | 20 | ```yaml 21 | dependencies: 22 | twitter-crystal: 23 | github: mamantoha/twitter-crystal 24 | ``` 25 | 26 | In your terminal run: 27 | 28 | ```console 29 | shards 30 | ``` 31 | 32 | This will get the latest code from this github repository and copy it to a `lib` directory. All that's left is to require it: 33 | 34 | ```crystal 35 | require "twitter-crystal" 36 | ``` 37 | 38 | ## Usage 39 | 40 | After the installation, you can use twitter-crystal by creating a client object: 41 | 42 | ```crystal 43 | require "twitter-crystal" 44 | 45 | consumer_key = "your consumer key" 46 | consumer_secret = "your consumer secret" 47 | access_token = "your access token" 48 | access_token_secret = "your access token secret " 49 | 50 | client = Twitter::REST::Client.new(consumer_key, consumer_secret, access_token, access_token_secret) 51 | ``` 52 | 53 | All the necessary keys can be generated by [creating a Twitter application](https://dev.twitter.com/oauth/overview/application-owner-access-tokens). 54 | 55 | After configuring a `client`, you can do the following things. 56 | 57 | ### Post/Delete a tweet 58 | 59 | ```crystal 60 | # post a tweet 61 | client.update("Good morning") 62 | 63 | # delete a tweet 64 | client.destroy_status(897099923128172545) 65 | ``` 66 | 67 | ### Fetch a particular Tweet by ID 68 | 69 | ```crystal 70 | client.status(950491199454044162) 71 | ``` 72 | 73 | ### Follow a user(by screen name or user_id) 74 | 75 | ```crystal 76 | client.follow("kenta_s_dev") 77 | client.follow(776284343173906432) 78 | ``` 79 | 80 | ### Unfollow a user(by screen name or user_id) 81 | 82 | ```crystal 83 | client.unfollow("kenta_s_dev") 84 | client.unfollow(776284343173906432) 85 | ``` 86 | 87 | ### Fetch a list of followers with profile details (by screen name or user ID, or by implicit authenticated user) 88 | 89 | ```crystal 90 | client.followers("crystallanguage") 91 | client.followers(15) 92 | client.followers 93 | ``` 94 | 95 | ### Fetch a list of friends with profile details (by screen name or user ID, or by implicit authenticated user) 96 | 97 | ```crystal 98 | client.friends("crystallanguage") 99 | client.friends(15) 100 | client.friends 101 | ``` 102 | 103 | ### Search users 104 | 105 | ```crystal 106 | client.user_search("Crystal lang") # returns maximum of 20 users 107 | ``` 108 | 109 | ### Fetch users by user_id/screen_name 110 | 111 | ```crystal 112 | client.users("kenta_s_dev") 113 | 114 | # fetch multiple users(maximum is 100) 115 | client.users("sferik", "yukihiro_matz", "dhh") 116 | ``` 117 | 118 | ### Fetch followers' IDs 119 | 120 | ```crystal 121 | client.follower_ids 122 | ``` 123 | 124 | ### Fetch followees' IDs 125 | 126 | ```crystal 127 | # In Twitter API documents, followees are called 'friends'. 128 | client.friend_ids 129 | ``` 130 | 131 | ### You can also call Twitter's API directly using the `get` or `post` method 132 | 133 | ```crystal 134 | client.get("/1.1/users/show.json", { "screen_name" => "sferik" }) 135 | client.post("/1.1/statuses/update.json", { "status" => "The world is your oyster." }) 136 | ``` 137 | 138 | ## Streaming 139 | 140 | ### Configuration works just like `Twitter::REST::Client` 141 | 142 | ```crystal 143 | consumer_key = "your consumer key" 144 | consumer_secret = "your consumer secret" 145 | access_token = "your access token" 146 | access_token_secret = "your access token secret " 147 | 148 | client = Twitter::Streaming::Client.new(consumer_key, consumer_secret, access_token, access_token_secret) 149 | ``` 150 | 151 | ### Stream a random sample of all tweets 152 | 153 | ```crystal 154 | client.sample do |object| 155 | puts object.text if object.is_a?(Twitter::Tweet) 156 | end 157 | ``` 158 | 159 | ### Stream mentions of coffee or tea 160 | 161 | ```crystal 162 | topics = ["coffee", "tea"] 163 | client.filter({"track" => topics.join(",")}) do |object| 164 | puts object.text if object.is_a?(Twitter::Tweet) 165 | end 166 | ``` 167 | 168 | If you want to call the API directly, refer to the [API reference](https://dev.twitter.com/rest/reference). 169 | 170 | ## Contributing 171 | 172 | 1. Fork it () 173 | 2. Create your feature branch (git checkout -b my-new-feature) 174 | 3. Commit your changes (git commit -am 'Add some feature') 175 | 4. Push to the branch (git push origin my-new-feature) 176 | 5. Create a new Pull Request 177 | 178 | ### Pull Requests are very welcome 179 | 180 | The goal of the project is to implement all methods to call [Twitter REST API](https://dev.twitter.com/rest/public). There are a lot of things need to be done. Pull Requests are welcome :) 181 | 182 | ## Contributors 183 | 184 | - [sferik](https://github.com/sferik) Erik Michaels-Ober - creator 185 | - [kenta-s](https://github.com/kenta-s) Kenta Shirai - maintainer 186 | - [mamantoha](https://github.com/mamantoha) Anton Maminov - maintainer 187 | - [vladfaust](https://github.com/vladfaust) Vlad Faust - Streaming API support 188 | -------------------------------------------------------------------------------- /src/twitter/rest/users.cr: -------------------------------------------------------------------------------- 1 | require "../serializations/cursor" 2 | 3 | module Twitter 4 | module REST 5 | module Users 6 | def settings(options = {} of String => String) 7 | response = if options.size.zero? 8 | get("/1.1/account/settings.json") 9 | else 10 | post("/1.1/account/settings.json", options) 11 | end 12 | Twitter::Settings.from_json(response) 13 | end 14 | 15 | def user(options = {} of String => String) : Twitter::User 16 | response = get("/1.1/account/verify_credentials.json", options) 17 | Twitter::User.from_json(response) 18 | end 19 | 20 | def user(user_id : Int32 | Int64, options = {} of String => String) : Twitter::User 21 | response = get("/1.1/users/show.json", options.merge({"user_id" => user_id.to_s})) 22 | Twitter::User.from_json(response) 23 | end 24 | 25 | def user(screen_name : String, options = {} of String => String) : Twitter::User 26 | response = get("/1.1/users/show.json", options.merge({"screen_name" => screen_name})) 27 | Twitter::User.from_json(response) 28 | end 29 | 30 | def user_search(query : String, options = {} of String => String) : Array(Twitter::User) 31 | response = get("/1.1/users/search.json", options.merge({"q" => query})) 32 | Array(Twitter::User).from_json(response) 33 | end 34 | 35 | def users(*user_ids : Int32 | Int64, options = {} of String => String) : Array(Twitter::User) 36 | response = post("/1.1/users/lookup.json", options.merge({"user_id" => user_ids.join(',')})) 37 | Array(Twitter::User).from_json(response) 38 | end 39 | 40 | def users(*screen_names : String, options = {} of String => String) : Array(Twitter::User) 41 | response = post("/1.1/users/lookup.json", options.merge({"screen_name" => screen_names.join(',')})) 42 | Array(Twitter::User).from_json(response) 43 | end 44 | 45 | def friendships(*user_ids : Int32 | Int64) : Array(Twitter::Relationship) 46 | response = get("/1.1/friendships/lookup.json", {"screen_name" => user_ids.join(',')}) 47 | Array(Twitter::Relationship).from_json(response) 48 | end 49 | 50 | def friendships(*screen_names : String) : Array(Twitter::Relationship) 51 | response = get("/1.1/friendships/lookup.json", {"screen_name" => screen_names.join(',')}) 52 | Array(Twitter::Relationship).from_json(response) 53 | end 54 | 55 | def blocked(options = {} of String => String) : Array(Twitter::User) 56 | response = get("/1.1/blocks/list.json", options) 57 | Array(Twitter::User).from_json(JSON.parse(response)["users"].to_json) 58 | end 59 | 60 | def blocked_ids(options = {} of String => String) : Array(Int64) 61 | response = get("/1.1/blocks/ids.json", options) 62 | JSON.parse(response)["ids"].as_a.map(&.as_i64) 63 | end 64 | 65 | def block?(user_id : Int32 | Int64, options = {} of String => String) : Bool 66 | blocked_ids(options).includes?(user_id) 67 | end 68 | 69 | def block?(user : Twitter::User, options = {} of String => String) : Bool 70 | blocked_ids(options).includes?(user.id) 71 | end 72 | 73 | def block?(screen_name : String, options = {} of String => String) : Bool 74 | block?(user(screen_name)) 75 | end 76 | 77 | def block(screen_name : String, options = {} of String => String) : Twitter::User 78 | response = post("/1.1/blocks/create.json", options.merge({"screen_name" => screen_name})) 79 | Twitter::User.from_json(response) 80 | end 81 | 82 | def block(user_id : Int32 | Int64, options = {} of String => String) : Twitter::User 83 | response = post("/1.1/blocks/create.json", options.merge({"user_id" => user_id.to_s})) 84 | Twitter::User.from_json(response) 85 | end 86 | 87 | def block(user : Twitter::User, options = {} of String => String) : Twitter::User 88 | block(user.id) 89 | end 90 | 91 | def unblock(screen_name : String, options = {} of String => String) : Twitter::User 92 | response = post("/1.1/blocks/destroy.json", options.merge({"screen_name" => screen_name})) 93 | Twitter::User.from_json(response) 94 | end 95 | 96 | def unblock(user_id : Int32 | Int64, options = {} of String => String) : Twitter::User 97 | response = post("/1.1/blocks/destroy.json", options.merge({"user_id" => user_id.to_s})) 98 | Twitter::User.from_json(response) 99 | end 100 | 101 | def unblock(user : Twitter::User, options = {} of String => String) : Twitter::User 102 | unblock(user.id) 103 | end 104 | 105 | def update_profile(options = {} of String => String) : Twitter::User 106 | response = post("/1.1/account/update_profile.json", options) 107 | Twitter::User.from_json(response) 108 | end 109 | 110 | def update_profile_background_image(options = {} of String => String) : Twitter::User 111 | response = post("/1.1/account/update_profile_background_image.json", options) 112 | Twitter::User.from_json(response) 113 | end 114 | 115 | def update_profile_image(base64_string : String, options = {} of String => String) : Twitter::User 116 | response = post("/1.1/account/update_profile_image.json", options.merge({"image" => base64_string})) 117 | Twitter::User.from_json(response) 118 | end 119 | 120 | def update_profile_banner(base64_string : String, options = {} of String => String) : Void 121 | post("/1.1/account/update_profile_banner.json", options.merge({"banner" => base64_string})) 122 | Void # this API returns an empty body, so this method returns nothing 123 | end 124 | 125 | def mute(options = {} of String => String) : Twitter::User 126 | response = post("/1.1/mutes/users/create.json", options) 127 | Twitter::User.from_json(response) 128 | end 129 | 130 | def unmute(options = {} of String => String) : Twitter::User 131 | response = post("/1.1/mutes/users/destroy.json", options) 132 | Twitter::User.from_json(response) 133 | end 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /spec/mock/1.1/statuses/retweet.json: -------------------------------------------------------------------------------- 1 | { 2 | "truncated": false, 3 | "retweeted": false, 4 | "id_str": "243149503589400576", 5 | "coordinates": null, 6 | "in_reply_to_screen_name": null, 7 | "in_reply_to_status_id_str": null, 8 | "geo": null, 9 | "in_reply_to_status_id": null, 10 | "contributors": null, 11 | "source": "My Shiny App", 12 | "in_reply_to_user_id_str": null, 13 | "created_at": "Wed Sep 05 00:52:13 +0000 2012", 14 | "favorited": false, 15 | "entities": { 16 | "user_mentions": [ 17 | { 18 | "indices": [3, 10], 19 | "id_str": "7588892", 20 | "screen_name": "kurrik", 21 | "name": "Arne Roomann-Kurrik", 22 | "id": 7588892 23 | } 24 | ], 25 | "hashtags": [], 26 | "symbols": [], 27 | "urls": [] 28 | }, 29 | "user": { 30 | "following": false, 31 | "geo_enabled": true, 32 | "profile_background_image_url": "http://a0.twimg.com/images/themes/theme6/bg.gif", 33 | "description": "Platform at Twitter", 34 | "notifications": false, 35 | "friends_count": 166, 36 | "profile_link_color": "FF3300", 37 | "location": "", 38 | "id_str": "14927800", 39 | "default_profile_image": false, 40 | "profile_image_url_https": "https://si0.twimg.com/profile_images/1751674923/new_york_beard_normal.jpg", 41 | "favourites_count": 883, 42 | "profile_background_color": "709397", 43 | "url": "http://t.co/YCA3ZKY", 44 | "screen_name": "jasoncosta", 45 | "profile_background_tile": false, 46 | "contributors_enabled": false, 47 | "verified": false, 48 | "created_at": "Wed May 28 00:20:15 +0000 2008", 49 | "profile_sidebar_fill_color": "A0C5C7", 50 | "followers_count": 8761, 51 | "lang": "en", 52 | "listed_count": 150, 53 | "profile_sidebar_border_color": "86A4A6", 54 | "protected": false, 55 | "entities": { 56 | "description": { 57 | "urls": [] 58 | }, 59 | "url": { 60 | "urls": [ 61 | { 62 | "indices": [0, 19], 63 | "url": "http://t.co/YCA3ZKY", 64 | "display_url": "jason-costa.blogspot.com", 65 | "expanded_url": "http://www.jason-costa.blogspot.com/" 66 | } 67 | ] 68 | } 69 | }, 70 | "show_all_inline_media": true, 71 | "follow_request_sent": false, 72 | "statuses_count": 5533, 73 | "profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme6/bg.gif", 74 | "name": "Jason Costa", 75 | "default_profile": false, 76 | "profile_use_background_image": true, 77 | "profile_image_url": "http://a0.twimg.com/profile_images/1751674923/new_york_beard_normal.jpg", 78 | "id": 14927800, 79 | "is_translator": true, 80 | "time_zone": "Pacific Time (US & Canada)", 81 | "utc_offset": -28800, 82 | "profile_text_color": "333333" 83 | }, 84 | "place": null, 85 | "id": 243149503589400580, 86 | "retweeted_status": { 87 | "truncated": false, 88 | "retweeted": false, 89 | "id_str": "241259202004267009", 90 | "coordinates": null, 91 | "in_reply_to_screen_name": null, 92 | "in_reply_to_status_id_str": null, 93 | "geo": null, 94 | "in_reply_to_status_id": null, 95 | "contributors": null, 96 | "source": "web", 97 | "in_reply_to_user_id_str": null, 98 | "created_at": "Thu Aug 30 19:40:50 +0000 2012", 99 | "favorited": false, 100 | "entities": { 101 | "user_mentions": [], 102 | "hashtags": [], 103 | "symbols": [], 104 | "urls": [] 105 | }, 106 | "user": { 107 | "following": true, 108 | "geo_enabled": true, 109 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/342542280/background7.png", 110 | "description": "Developer Advocate at Twitter, practitioner of dark sandwich arts. ", 111 | "notifications": false, 112 | "friends_count": 500, 113 | "profile_link_color": "0084B4", 114 | "location": "Scan Francesco", 115 | "id_str": "7588892", 116 | "default_profile_image": false, 117 | "profile_image_url_https": "https://si0.twimg.com/profile_images/24229162/arne001_normal.jpg", 118 | "favourites_count": 624, 119 | "profile_background_color": "8FC1FF", 120 | "url": "http://t.co/bGmVjSox", 121 | "screen_name": "kurrik", 122 | "profile_background_tile": true, 123 | "contributors_enabled": false, 124 | "verified": false, 125 | "created_at": "Thu Jul 19 15:58:07 +0000 2007", 126 | "profile_sidebar_fill_color": "C7E0FF", 127 | "followers_count": 3514, 128 | "lang": "en", 129 | "listed_count": 165, 130 | "profile_sidebar_border_color": "6BAEFF", 131 | "protected": false, 132 | "entities": { 133 | "description": { 134 | "urls": [] 135 | }, 136 | "url": { 137 | "urls": [ 138 | { 139 | "indices": [0, 20], 140 | "url": "http://t.co/bGmVjSox", 141 | "display_url": "start.roomanna.com", 142 | "expanded_url": "http://start.roomanna.com/" 143 | } 144 | ] 145 | } 146 | }, 147 | "show_all_inline_media": false, 148 | "follow_request_sent": false, 149 | "statuses_count": 2963, 150 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/342542280/background7.png", 151 | "name": "Arne Roomann-Kurrik", 152 | "default_profile": false, 153 | "profile_use_background_image": true, 154 | "profile_image_url": "http://a0.twimg.com/profile_images/24229162/arne001_normal.jpg", 155 | "id": 7588892, 156 | "is_translator": false, 157 | "time_zone": "Pacific Time (US & Canada)", 158 | "utc_offset": -28800, 159 | "profile_text_color": "000000" 160 | }, 161 | "place": null, 162 | "id": 241259202004267000, 163 | "retweet_count": 1, 164 | "in_reply_to_user_id": null, 165 | "text": "tcptrace and imagemagick - two command line tools TOTALLY worth learning" 166 | }, 167 | "retweet_count": 1, 168 | "in_reply_to_user_id": null, 169 | "text": "RT @kurrik: tcptrace and imagemagick - two command line tools TOTALLY worth learning" 170 | } 171 | -------------------------------------------------------------------------------- /spec/mock/1.1/users/lookup.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 7505382, 4 | "id_str": "7505382", 5 | "name": "Erik Michaels-Ober", 6 | "screen_name": "sferik", 7 | "location": "Berlin", 8 | "profile_location": null, 9 | "description": "This is fine.", 10 | "url": "https://t.co/L2xIBazeZH", 11 | "entities": { 12 | "url": { 13 | "urls": [ 14 | { 15 | "url": "https://t.co/L2xIBazeZH", 16 | "expanded_url": "https://github.com/sferik", 17 | "display_url": "github.com/sferik", 18 | "indices": [0, 23] 19 | } 20 | ] 21 | }, 22 | "description": { 23 | "urls": [] 24 | } 25 | }, 26 | "protected": false, 27 | "followers_count": 5945, 28 | "friends_count": 881, 29 | "listed_count": 339, 30 | "created_at": "Mon Jul 16 12:59:01 +0000 2007", 31 | "favourites_count": 14447, 32 | "utc_offset": 7200, 33 | "time_zone": "Berlin", 34 | "geo_enabled": true, 35 | "verified": false, 36 | "statuses_count": 17358, 37 | "lang": "en", 38 | "status": { 39 | "created_at": "Sat Aug 29 13:36:37 +0000 2015", 40 | "id": 637619867223478272, 41 | "id_str": "637619867223478272", 42 | "text": "@megerman Is purchasing power zero-sum?", 43 | "source": "\u003ca href=\"http://twitter.com\" rel=\"nofollow\"\u003eTwitter Web Client\u003c/a\u003e", 44 | "truncated": false, 45 | "in_reply_to_status_id": 637619436560752640, 46 | "in_reply_to_status_id_str": "637619436560752640", 47 | "in_reply_to_user_id": 831831750, 48 | "in_reply_to_user_id_str": "831831750", 49 | "in_reply_to_screen_name": "megerman", 50 | "geo": null, 51 | "coordinates": null, 52 | "place": { 53 | "id": "3078869807f9dd36", 54 | "url": "https://api.twitter.com/1.1/geo/id/3078869807f9dd36.json", 55 | "place_type": "city", 56 | "name": "Berlin", 57 | "full_name": "Berlin, Germany", 58 | "country_code": "DE", 59 | "country": "Deutschland", 60 | "contained_within": [], 61 | "bounding_box": { 62 | "type": "Polygon", 63 | "coordinates": [ 64 | [ 65 | [13.088304, 52.338079], 66 | [13.760909, 52.338079], 67 | [13.760909, 52.675323], 68 | [13.088304, 52.675323] 69 | ] 70 | ] 71 | }, 72 | "attributes": {} 73 | }, 74 | "contributors": null, 75 | "retweet_count": 0, 76 | "favorite_count": 0, 77 | "entities": { 78 | "hashtags": [], 79 | "symbols": [], 80 | "user_mentions": [ 81 | { 82 | "screen_name": "megerman", 83 | "name": "Mark Egerman", 84 | "id": 831831750, 85 | "id_str": "831831750", 86 | "indices": [0, 9] 87 | } 88 | ], 89 | "urls": [] 90 | }, 91 | "favorited": false, 92 | "retweeted": false, 93 | "lang": "en" 94 | }, 95 | "contributors_enabled": false, 96 | "is_translator": false, 97 | "is_translation_enabled": false, 98 | "profile_background_color": "000000", 99 | "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/677717672/bb0b3653dcf0644e344823e0a2eb3382.png", 100 | "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/677717672/bb0b3653dcf0644e344823e0a2eb3382.png", 101 | "profile_background_tile": false, 102 | "profile_image_url": "http://pbs.twimg.com/profile_images/583426225605058560/dO_rpWw1_normal.jpg", 103 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/583426225605058560/dO_rpWw1_normal.jpg", 104 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/7505382/1425238640", 105 | "profile_link_color": "393740", 106 | "profile_sidebar_border_color": "000000", 107 | "profile_sidebar_fill_color": "DDEEF6", 108 | "profile_text_color": "333333", 109 | "profile_use_background_image": true, 110 | "has_extended_profile": true, 111 | "default_profile": false, 112 | "default_profile_image": false, 113 | "following": false, 114 | "follow_request_sent": false, 115 | "notifications": false, 116 | "suspended": false, 117 | "needs_phone_verification": false 118 | }, 119 | { 120 | "id": 776284343173906432, 121 | "id_str": "776284343173906432", 122 | "name": "kenta-s", 123 | "screen_name": "kenta_s_dev", 124 | "location": "\u65e5\u672c \u6771\u4eac", 125 | "profile_location": null, 126 | "description": "Segmentation fault (\u30b3\u30a2\u30c0\u30f3\u30d7)", 127 | "url": null, 128 | "entities": { 129 | "description": { 130 | "urls": [] 131 | }, 132 | "url": { 133 | "urls": [] 134 | } 135 | }, 136 | "protected": false, 137 | "followers_count": 49, 138 | "friends_count": 62, 139 | "listed_count": 3, 140 | "created_at": "Thu Sep 15 04:59:44 +0000 2016", 141 | "favourites_count": 2066, 142 | "utc_offset": null, 143 | "time_zone": null, 144 | "geo_enabled": false, 145 | "verified": false, 146 | "statuses_count": 2489, 147 | "lang": "ja", 148 | "status": { 149 | "created_at": "Fri Aug 18 00:19:07 +0000 2017", 150 | "id": 898338431394131968, 151 | "id_str": "898338431394131968", 152 | "text": "Hello", 153 | "truncated": false, 154 | "entities": { 155 | "hashtags": [], 156 | "symbols": [], 157 | "user_mentions": [], 158 | "urls": [] 159 | }, 160 | "source": "\u003ca href=\"http://twitter.com\" rel=\"nofollow\"\u003eTwitter Web Client\u003c/a\u003e", 161 | "in_reply_to_status_id": null, 162 | "in_reply_to_status_id_str": null, 163 | "in_reply_to_user_id": null, 164 | "in_reply_to_user_id_str": null, 165 | "in_reply_to_screen_name": null, 166 | "geo": null, 167 | "coordinates": null, 168 | "place": null, 169 | "contributors": null, 170 | "is_quote_status": false, 171 | "retweet_count": 0, 172 | "favorite_count": 2, 173 | "favorited": false, 174 | "retweeted": false, 175 | "lang": "ja" 176 | }, 177 | "contributors_enabled": false, 178 | "is_translator": false, 179 | "is_translation_enabled": false, 180 | "profile_background_color": "000000", 181 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", 182 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", 183 | "profile_background_tile": false, 184 | "profile_image_url": "http://pbs.twimg.com/profile_images/776285474369392640/SCjsxi14_normal.jpg", 185 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/776285474369392640/SCjsxi14_normal.jpg", 186 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/776284343173906432/1483188050", 187 | "profile_link_color": "4E28A5", 188 | "profile_sidebar_border_color": "000000", 189 | "profile_sidebar_fill_color": "000000", 190 | "profile_text_color": "000000", 191 | "profile_use_background_image": false, 192 | "has_extended_profile": true, 193 | "default_profile": false, 194 | "default_profile_image": false, 195 | "following": false, 196 | "follow_request_sent": false, 197 | "notifications": false, 198 | "translator_type": "none" 199 | } 200 | ] 201 | -------------------------------------------------------------------------------- /spec/mock/1.1/users/search.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 7505382, 4 | "id_str": "7505382", 5 | "name": "Erik Michaels-Ober", 6 | "screen_name": "sferik", 7 | "location": "Berlin", 8 | "profile_location": null, 9 | "description": "This is fine.", 10 | "url": "https://t.co/L2xIBazeZH", 11 | "entities": { 12 | "url": { 13 | "urls": [ 14 | { 15 | "url": "https://t.co/L2xIBazeZH", 16 | "expanded_url": "https://github.com/sferik", 17 | "display_url": "github.com/sferik", 18 | "indices": [0, 23] 19 | } 20 | ] 21 | }, 22 | "description": { 23 | "urls": [] 24 | } 25 | }, 26 | "protected": false, 27 | "followers_count": 5945, 28 | "friends_count": 881, 29 | "listed_count": 339, 30 | "created_at": "Mon Jul 16 12:59:01 +0000 2007", 31 | "favourites_count": 14447, 32 | "utc_offset": 7200, 33 | "time_zone": "Berlin", 34 | "geo_enabled": true, 35 | "verified": false, 36 | "statuses_count": 17358, 37 | "lang": "en", 38 | "status": { 39 | "created_at": "Sat Aug 29 13:36:37 +0000 2015", 40 | "id": 637619867223478272, 41 | "id_str": "637619867223478272", 42 | "text": "@megerman Is purchasing power zero-sum?", 43 | "source": "\u003ca href=\"http://twitter.com\" rel=\"nofollow\"\u003eTwitter Web Client\u003c/a\u003e", 44 | "truncated": false, 45 | "in_reply_to_status_id": 637619436560752640, 46 | "in_reply_to_status_id_str": "637619436560752640", 47 | "in_reply_to_user_id": 831831750, 48 | "in_reply_to_user_id_str": "831831750", 49 | "in_reply_to_screen_name": "megerman", 50 | "geo": null, 51 | "coordinates": null, 52 | "place": { 53 | "id": "3078869807f9dd36", 54 | "url": "https://api.twitter.com/1.1/geo/id/3078869807f9dd36.json", 55 | "place_type": "city", 56 | "name": "Berlin", 57 | "full_name": "Berlin, Germany", 58 | "country_code": "DE", 59 | "country": "Deutschland", 60 | "contained_within": [], 61 | "bounding_box": { 62 | "type": "Polygon", 63 | "coordinates": [ 64 | [ 65 | [13.088304, 52.338079], 66 | [13.760909, 52.338079], 67 | [13.760909, 52.675323], 68 | [13.088304, 52.675323] 69 | ] 70 | ] 71 | }, 72 | "attributes": {} 73 | }, 74 | "contributors": null, 75 | "retweet_count": 0, 76 | "favorite_count": 0, 77 | "entities": { 78 | "hashtags": [], 79 | "symbols": [], 80 | "user_mentions": [ 81 | { 82 | "screen_name": "megerman", 83 | "name": "Mark Egerman", 84 | "id": 831831750, 85 | "id_str": "831831750", 86 | "indices": [0, 9] 87 | } 88 | ], 89 | "urls": [] 90 | }, 91 | "favorited": false, 92 | "retweeted": false, 93 | "lang": "en" 94 | }, 95 | "contributors_enabled": false, 96 | "is_translator": false, 97 | "is_translation_enabled": false, 98 | "profile_background_color": "000000", 99 | "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/677717672/bb0b3653dcf0644e344823e0a2eb3382.png", 100 | "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/677717672/bb0b3653dcf0644e344823e0a2eb3382.png", 101 | "profile_background_tile": false, 102 | "profile_image_url": "http://pbs.twimg.com/profile_images/583426225605058560/dO_rpWw1_normal.jpg", 103 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/583426225605058560/dO_rpWw1_normal.jpg", 104 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/7505382/1425238640", 105 | "profile_link_color": "393740", 106 | "profile_sidebar_border_color": "000000", 107 | "profile_sidebar_fill_color": "DDEEF6", 108 | "profile_text_color": "333333", 109 | "profile_use_background_image": true, 110 | "has_extended_profile": true, 111 | "default_profile": false, 112 | "default_profile_image": false, 113 | "following": false, 114 | "follow_request_sent": false, 115 | "notifications": false, 116 | "suspended": false, 117 | "needs_phone_verification": false 118 | }, 119 | { 120 | "id": 776284343173906432, 121 | "id_str": "776284343173906432", 122 | "name": "kenta-s", 123 | "screen_name": "kenta_s_dev", 124 | "location": "\u65e5\u672c \u6771\u4eac", 125 | "profile_location": null, 126 | "description": "Segmentation fault (\u30b3\u30a2\u30c0\u30f3\u30d7)", 127 | "url": null, 128 | "entities": { 129 | "description": { 130 | "urls": [] 131 | }, 132 | "url": { 133 | "urls": [] 134 | } 135 | }, 136 | "protected": false, 137 | "followers_count": 49, 138 | "friends_count": 62, 139 | "listed_count": 3, 140 | "created_at": "Thu Sep 15 04:59:44 +0000 2016", 141 | "favourites_count": 2066, 142 | "utc_offset": null, 143 | "time_zone": null, 144 | "geo_enabled": false, 145 | "verified": false, 146 | "statuses_count": 2489, 147 | "lang": "ja", 148 | "status": { 149 | "created_at": "Fri Aug 18 00:19:07 +0000 2017", 150 | "id": 898338431394131968, 151 | "id_str": "898338431394131968", 152 | "text": "Hello", 153 | "truncated": false, 154 | "entities": { 155 | "hashtags": [], 156 | "urls": [], 157 | "user_mentions": [], 158 | "media": [], 159 | "symbols": [], 160 | "polls": [] 161 | }, 162 | "source": "\u003ca href=\"http://twitter.com\" rel=\"nofollow\"\u003eTwitter Web Client\u003c/a\u003e", 163 | "in_reply_to_status_id": null, 164 | "in_reply_to_status_id_str": null, 165 | "in_reply_to_user_id": null, 166 | "in_reply_to_user_id_str": null, 167 | "in_reply_to_screen_name": null, 168 | "geo": null, 169 | "coordinates": null, 170 | "place": null, 171 | "contributors": null, 172 | "is_quote_status": false, 173 | "retweet_count": 0, 174 | "favorite_count": 2, 175 | "favorited": false, 176 | "retweeted": false, 177 | "lang": "ja" 178 | }, 179 | "contributors_enabled": false, 180 | "is_translator": false, 181 | "is_translation_enabled": false, 182 | "profile_background_color": "000000", 183 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", 184 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", 185 | "profile_background_tile": false, 186 | "profile_image_url": "http://pbs.twimg.com/profile_images/776285474369392640/SCjsxi14_normal.jpg", 187 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/776285474369392640/SCjsxi14_normal.jpg", 188 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/776284343173906432/1483188050", 189 | "profile_link_color": "4E28A5", 190 | "profile_sidebar_border_color": "000000", 191 | "profile_sidebar_fill_color": "000000", 192 | "profile_text_color": "000000", 193 | "profile_use_background_image": false, 194 | "has_extended_profile": true, 195 | "default_profile": false, 196 | "default_profile_image": false, 197 | "following": false, 198 | "follow_request_sent": false, 199 | "notifications": false, 200 | "translator_type": "none" 201 | } 202 | ] 203 | -------------------------------------------------------------------------------- /spec/mock/1.1/statuses/mentions_timeline.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "coordinates": null, 4 | "favorited": false, 5 | "truncated": false, 6 | "created_at": "Mon Sep 03 13:24:14 +0000 2012", 7 | "id_str": "242613977966850048", 8 | "entities": { 9 | "urls": [], 10 | "hashtags": [], 11 | "symbols": [], 12 | "user_mentions": [ 13 | { 14 | "name": "Jason Costa", 15 | "id_str": "14927800", 16 | "id": 14927800, 17 | "indices": [0, 11], 18 | "screen_name": "jasoncosta" 19 | }, 20 | { 21 | "name": "Matt Harris", 22 | "id_str": "777925", 23 | "id": 777925, 24 | "indices": [12, 26], 25 | "screen_name": "themattharris" 26 | }, 27 | { 28 | "name": "ThinkWall", 29 | "id_str": "117426578", 30 | "id": 117426578, 31 | "indices": [109, 119], 32 | "screen_name": "thinkwall" 33 | } 34 | ] 35 | }, 36 | "in_reply_to_user_id_str": "14927800", 37 | "contributors": null, 38 | "text": "@jasoncosta @themattharris Hey! Going to be in Frisco in October. Was hoping to have a meeting to talk about @thinkwall if you're around?", 39 | "retweet_count": 0, 40 | "in_reply_to_status_id_str": null, 41 | "id": 242613977966850048, 42 | "geo": null, 43 | "retweeted": false, 44 | "in_reply_to_user_id": 14927800, 45 | "place": null, 46 | "user": { 47 | "profile_sidebar_fill_color": "EEEEEE", 48 | "profile_sidebar_border_color": "000000", 49 | "profile_background_tile": false, 50 | "name": "Andrew Spode Miller", 51 | "profile_image_url": "http://a0.twimg.com/profile_images/1227466231/spode-balloon-medium_normal.jpg", 52 | "created_at": "Mon Sep 22 13:12:01 +0000 2008", 53 | "location": "London via Gravesend", 54 | "follow_request_sent": false, 55 | "profile_link_color": "F31B52", 56 | "is_translator": false, 57 | "id_str": "16402947", 58 | "entities": { 59 | "url": { 60 | "urls": [ 61 | { 62 | "expanded_url": "http://www.linkedin.com/in/spode", 63 | "display_url": "http://www.linkedin.com/in/spode", 64 | "url": "http://www.linkedin.com/in/spode", 65 | "indices": [0, 32] 66 | } 67 | ] 68 | }, 69 | "description": { 70 | "urls": [] 71 | } 72 | }, 73 | "default_profile": false, 74 | "contributors_enabled": false, 75 | "favourites_count": 16, 76 | "url": "http://www.linkedin.com/in/spode", 77 | "profile_image_url_https": "https://si0.twimg.com/profile_images/1227466231/spode-balloon-medium_normal.jpg", 78 | "utc_offset": 0, 79 | "id": 16402947, 80 | "profile_use_background_image": false, 81 | "listed_count": 129, 82 | "profile_text_color": "262626", 83 | "lang": "en", 84 | "followers_count": 2013, 85 | "protected": false, 86 | "notifications": null, 87 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/16420220/twitter-background-final.png", 88 | "profile_background_color": "FFFFFF", 89 | "verified": false, 90 | "geo_enabled": true, 91 | "time_zone": "London", 92 | "description": "Co-Founder/Dev (PHP/jQuery) @justFDI. Run @thinkbikes and @thinkwall for events. Ex tech journo, helps run @uktjpr. Passion for Linux and customises everything.", 93 | "default_profile_image": false, 94 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/16420220/twitter-background-final.png", 95 | "statuses_count": 11550, 96 | "friends_count": 770, 97 | "following": null, 98 | "show_all_inline_media": true, 99 | "screen_name": "spode" 100 | }, 101 | "in_reply_to_screen_name": "jasoncosta", 102 | "source": "JournoTwit", 103 | "in_reply_to_status_id": null 104 | }, 105 | { 106 | "coordinates": { 107 | "coordinates": [121.0132101, 14.5191613], 108 | "type": "Point" 109 | }, 110 | "favorited": false, 111 | "truncated": false, 112 | "created_at": "Mon Sep 03 08:08:02 +0000 2012", 113 | "id_str": "242534402280783873", 114 | "entities": { 115 | "urls": [], 116 | "hashtags": [ 117 | { 118 | "text": "twitter", 119 | "indices": [49, 57] 120 | } 121 | ], 122 | "symbols": [], 123 | "user_mentions": [ 124 | { 125 | "name": "Jason Costa", 126 | "id_str": "14927800", 127 | "id": 14927800, 128 | "indices": [14, 25], 129 | "screen_name": "jasoncosta" 130 | } 131 | ] 132 | }, 133 | "in_reply_to_user_id_str": null, 134 | "contributors": null, 135 | "text": "Got the shirt @jasoncosta thanks man! Loving the #twitter bird on the shirt :-)", 136 | "retweet_count": 0, 137 | "in_reply_to_status_id_str": null, 138 | "id": 242534402280783873, 139 | "geo": { 140 | "coordinates": [14.5191613, 121.0132101], 141 | "type": "Point" 142 | }, 143 | "retweeted": false, 144 | "in_reply_to_user_id": null, 145 | "place": null, 146 | "user": { 147 | "profile_sidebar_fill_color": "EFEFEF", 148 | "profile_sidebar_border_color": "EEEEEE", 149 | "profile_background_tile": true, 150 | "name": "Mikey", 151 | "profile_image_url": "http://a0.twimg.com/profile_images/1305509670/chatMikeTwitter_normal.png", 152 | "created_at": "Fri Jun 20 15:57:08 +0000 2008", 153 | "location": "Singapore", 154 | "follow_request_sent": false, 155 | "profile_link_color": "009999", 156 | "is_translator": false, 157 | "id_str": "15181205", 158 | "entities": { 159 | "url": { 160 | "urls": [ 161 | { 162 | "expanded_url": "http://about.me/michaelangelo", 163 | "display_url": "http://about.me/michaelangelo", 164 | "url": "http://about.me/michaelangelo", 165 | "indices": [0, 29] 166 | } 167 | ] 168 | }, 169 | "description": { 170 | "urls": [] 171 | } 172 | }, 173 | "default_profile": false, 174 | "contributors_enabled": false, 175 | "favourites_count": 11, 176 | "url": "http://about.me/michaelangelo", 177 | "profile_image_url_https": "https://si0.twimg.com/profile_images/1305509670/chatMikeTwitter_normal.png", 178 | "utc_offset": 28800, 179 | "id": 15181205, 180 | "profile_use_background_image": true, 181 | "listed_count": 61, 182 | "profile_text_color": "333333", 183 | "lang": "en", 184 | "followers_count": 577, 185 | "protected": false, 186 | "notifications": null, 187 | "profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme14/bg.gif", 188 | "profile_background_color": "131516", 189 | "verified": false, 190 | "geo_enabled": true, 191 | "time_zone": "Hong Kong", 192 | "description": "Android Applications Developer, Studying Martial Arts, Plays MTG, Food and movie junkie", 193 | "default_profile_image": false, 194 | "profile_background_image_url": "http://a0.twimg.com/images/themes/theme14/bg.gif", 195 | "statuses_count": 11327, 196 | "friends_count": 138, 197 | "following": null, 198 | "show_all_inline_media": true, 199 | "screen_name": "mikedroid" 200 | }, 201 | "in_reply_to_screen_name": null, 202 | "source": "Twitter for Android", 203 | "in_reply_to_status_id": null 204 | } 205 | ] 206 | -------------------------------------------------------------------------------- /spec/fixtures/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id":7505382, 4 | "id_str":"7505382", 5 | "name":"Erik Michaels-Ober", 6 | "screen_name":"sferik", 7 | "location":"Berlin", 8 | "profile_location":null, 9 | "description":"This is fine.", 10 | "url":"https:\/\/t.co\/L2xIBazeZH", 11 | "entities":{ 12 | "url":{ 13 | "urls":[ 14 | { 15 | "url":"https:\/\/t.co\/L2xIBazeZH", 16 | "expanded_url":"https:\/\/github.com\/sferik", 17 | "display_url":"github.com\/sferik", 18 | "indices":[ 19 | 0, 20 | 23 21 | ] 22 | } 23 | ] 24 | }, 25 | "description":{ 26 | "urls":[ 27 | 28 | ] 29 | } 30 | }, 31 | "protected":false, 32 | "followers_count":5945, 33 | "friends_count":881, 34 | "listed_count":339, 35 | "created_at":"Mon Jul 16 12:59:01 +0000 2007", 36 | "favourites_count":14447, 37 | "utc_offset":7200, 38 | "time_zone":"Berlin", 39 | "geo_enabled":true, 40 | "verified":false, 41 | "statuses_count":17358, 42 | "lang":"en", 43 | "status":{ 44 | "created_at":"Sat Aug 29 13:36:37 +0000 2015", 45 | "id":637619867223478272, 46 | "id_str":"637619867223478272", 47 | "text":"@megerman Is purchasing power zero-sum?", 48 | "source":"\u003ca href=\"http:\/\/twitter.com\" rel=\"nofollow\"\u003eTwitter Web Client\u003c\/a\u003e", 49 | "truncated":false, 50 | "in_reply_to_status_id":637619436560752640, 51 | "in_reply_to_status_id_str":"637619436560752640", 52 | "in_reply_to_user_id":831831750, 53 | "in_reply_to_user_id_str":"831831750", 54 | "in_reply_to_screen_name":"megerman", 55 | "geo":null, 56 | "coordinates":null, 57 | "place":{ 58 | "id":"3078869807f9dd36", 59 | "url":"https:\/\/api.twitter.com\/1.1\/geo\/id\/3078869807f9dd36.json", 60 | "place_type":"city", 61 | "name":"Berlin", 62 | "full_name":"Berlin, Germany", 63 | "country_code":"DE", 64 | "country":"Deutschland", 65 | "contained_within":[ 66 | 67 | ], 68 | "bounding_box":{ 69 | "type":"Polygon", 70 | "coordinates":[ 71 | [ 72 | [ 73 | 13.088304, 74 | 52.338079 75 | ], 76 | [ 77 | 13.760909, 78 | 52.338079 79 | ], 80 | [ 81 | 13.760909, 82 | 52.675323 83 | ], 84 | [ 85 | 13.088304, 86 | 52.675323 87 | ] 88 | ] 89 | ] 90 | }, 91 | "attributes":{ 92 | 93 | } 94 | }, 95 | "contributors":null, 96 | "retweet_count":0, 97 | "favorite_count":0, 98 | "entities":{ 99 | "hashtags":[ 100 | 101 | ], 102 | "symbols":[ 103 | 104 | ], 105 | "user_mentions":[ 106 | { 107 | "screen_name":"megerman", 108 | "name":"Mark Egerman", 109 | "id":831831750, 110 | "id_str":"831831750", 111 | "indices":[ 112 | 0, 113 | 9 114 | ] 115 | } 116 | ], 117 | "urls":[ 118 | 119 | ] 120 | }, 121 | "favorited":false, 122 | "retweeted":false, 123 | "lang":"en" 124 | }, 125 | "contributors_enabled":false, 126 | "is_translator":false, 127 | "is_translation_enabled":false, 128 | "profile_background_color":"000000", 129 | "profile_background_image_url":"http:\/\/pbs.twimg.com\/profile_background_images\/677717672\/bb0b3653dcf0644e344823e0a2eb3382.png", 130 | "profile_background_image_url_https":"https:\/\/pbs.twimg.com\/profile_background_images\/677717672\/bb0b3653dcf0644e344823e0a2eb3382.png", 131 | "profile_background_tile":false, 132 | "profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/583426225605058560\/dO_rpWw1_normal.jpg", 133 | "profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/583426225605058560\/dO_rpWw1_normal.jpg", 134 | "profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/7505382\/1425238640", 135 | "profile_link_color":"393740", 136 | "profile_sidebar_border_color":"000000", 137 | "profile_sidebar_fill_color":"DDEEF6", 138 | "profile_text_color":"333333", 139 | "profile_use_background_image":true, 140 | "has_extended_profile":true, 141 | "default_profile":false, 142 | "default_profile_image":false, 143 | "following":false, 144 | "follow_request_sent":false, 145 | "notifications":false, 146 | "suspended":false, 147 | "needs_phone_verification":false 148 | }, 149 | { 150 | "id":776284343173906432, 151 | "id_str":"776284343173906432", 152 | "name":"kenta-s", 153 | "screen_name":"kenta_s_dev", 154 | "location":"\u65e5\u672c \u6771\u4eac", 155 | "profile_location":null, 156 | "description":"Segmentation fault (\u30b3\u30a2\u30c0\u30f3\u30d7)", 157 | "url":null, 158 | "entities":{ 159 | "description":{ 160 | "urls":[ 161 | 162 | ] 163 | }, 164 | "url":{ 165 | "urls":[ 166 | 167 | ] 168 | } 169 | }, 170 | "protected":false, 171 | "followers_count":49, 172 | "friends_count":62, 173 | "listed_count":3, 174 | "created_at":"Thu Sep 15 04:59:44 +0000 2016", 175 | "favourites_count":2066, 176 | "utc_offset":null, 177 | "time_zone":null, 178 | "geo_enabled":false, 179 | "verified":false, 180 | "statuses_count":2489, 181 | "lang":"ja", 182 | "status":{ 183 | "created_at":"Fri Aug 18 00:19:07 +0000 2017", 184 | "id":898338431394131968, 185 | "id_str":"898338431394131968", 186 | "text":"Hello", 187 | "truncated":false, 188 | "entities":{ 189 | "hashtags":[ 190 | 191 | ], 192 | "symbols":[ 193 | 194 | ], 195 | "user_mentions":[ 196 | 197 | ], 198 | "urls":[ 199 | 200 | ] 201 | }, 202 | "source":"\u003ca href=\"http:\/\/twitter.com\" rel=\"nofollow\"\u003eTwitter Web Client\u003c\/a\u003e", 203 | "in_reply_to_status_id":null, 204 | "in_reply_to_status_id_str":null, 205 | "in_reply_to_user_id":null, 206 | "in_reply_to_user_id_str":null, 207 | "in_reply_to_screen_name":null, 208 | "geo":null, 209 | "coordinates":null, 210 | "place":null, 211 | "contributors":null, 212 | "is_quote_status":false, 213 | "retweet_count":0, 214 | "favorite_count":2, 215 | "favorited":false, 216 | "retweeted":false, 217 | "lang":"ja" 218 | }, 219 | "contributors_enabled":false, 220 | "is_translator":false, 221 | "is_translation_enabled":false, 222 | "profile_background_color":"000000", 223 | "profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png", 224 | "profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png", 225 | "profile_background_tile":false, 226 | "profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/776285474369392640\/SCjsxi14_normal.jpg", 227 | "profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/776285474369392640\/SCjsxi14_normal.jpg", 228 | "profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/776284343173906432\/1483188050", 229 | "profile_link_color":"4E28A5", 230 | "profile_sidebar_border_color":"000000", 231 | "profile_sidebar_fill_color":"000000", 232 | "profile_text_color":"000000", 233 | "profile_use_background_image":false, 234 | "has_extended_profile":true, 235 | "default_profile":false, 236 | "default_profile_image":false, 237 | "following":false, 238 | "follow_request_sent":false, 239 | "notifications":false, 240 | "translator_type":"none" 241 | } 242 | ] 243 | -------------------------------------------------------------------------------- /spec/twitter/rest/users_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../helper" 2 | 3 | describe Twitter::REST::Users do 4 | client = Mock::Client.new("CK", "CS", "AT", "AS", "UA") 5 | 6 | describe "#settings" do 7 | settings = client.settings 8 | context "called with no args" do 9 | it "returns Twitter::Settings" do 10 | settings.should be_a Twitter::Settings 11 | settings.screen_name.should eq "theSeanCook" 12 | end 13 | end 14 | 15 | context "called with no args" do 16 | options = {"time_zone" => "Pacific Time (US & Canada)"} 17 | settings = client.settings(options) 18 | it "returns Twitter::Settings" do 19 | settings.should be_a Twitter::Settings 20 | settings.screen_name.should eq "theSeanCook" 21 | settings.time_zone.name.should eq "Pacific Time (US & Canada)" 22 | end 23 | end 24 | end 25 | 26 | describe "#user" do 27 | context "without any args" do 28 | it "returns user: 7505382" do 29 | client.user.id.should eq 7505382 30 | end 31 | end 32 | 33 | context "with 7505382" do 34 | it "returns user: 7505382" do 35 | client.user.id.should eq 7505382 36 | end 37 | end 38 | 39 | context "with 'sferik'" do 40 | it "returns 7505382" do 41 | client.user("sferik").id.should eq 7505382 42 | end 43 | end 44 | end 45 | 46 | describe "#users" do 47 | context "with String" do 48 | users = client.users("hello") 49 | it "returns Array(Twitter::User)" do 50 | users.should be_a Array(Twitter::User) 51 | users[0].screen_name.should eq "sferik" 52 | users[1].screen_name.should eq "kenta_s_dev" 53 | end 54 | end 55 | 56 | context "with Int64" do 57 | users = client.users(123451234512345123) 58 | it "returns Array(Twitter::User)" do 59 | users.should be_a Array(Twitter::User) 60 | users[0].screen_name.should eq "sferik" 61 | users[1].screen_name.should eq "kenta_s_dev" 62 | end 63 | end 64 | 65 | context "with Int32" do 66 | users = client.users(12345) 67 | it "returns Array(Twitter::User)" do 68 | users.should be_a Array(Twitter::User) 69 | users[0].screen_name.should eq "sferik" 70 | users[1].screen_name.should eq "kenta_s_dev" 71 | end 72 | end 73 | end 74 | 75 | describe "#user_search" do 76 | context "with String" do 77 | users = client.user_search("Hello World") 78 | it "returns Array(Twitter::User)" do 79 | users.should be_a Array(Twitter::User) 80 | users[0].screen_name.should eq "sferik" 81 | users[1].screen_name.should eq "kenta_s_dev" 82 | end 83 | end 84 | end 85 | 86 | describe "#blocked" do 87 | blocked = client.blocked 88 | it "returns Array(Twitter::User)" do 89 | blocked.should be_a Array(Twitter::User) 90 | blocked[0].name.should eq "Javier Heady" 91 | end 92 | end 93 | 94 | describe "#blocked_ids" do 95 | blocked = client.blocked_ids 96 | it "returns Array(Int64)" do 97 | blocked.should be_a Array(Int64) 98 | blocked[0].should eq 123 99 | end 100 | end 101 | 102 | describe "#block?" do 103 | it { client.block?(123).should be_truthy } 104 | it { client.block?(776284343173906432).should be_truthy } 105 | it { client.block?(1234).should be_falsey } 106 | it { client.block?("any_user").should be_truthy } 107 | end 108 | 109 | describe "#block" do 110 | context "got String" do 111 | user = client.block("theSeanCook") 112 | it "returns Twitter::User" do 113 | user.should be_a Twitter::User 114 | user.id.should eq 38895958 115 | end 116 | end 117 | 118 | context "got Int32" do 119 | user = client.block(38895958) 120 | it "returns Twitter::User" do 121 | user.should be_a Twitter::User 122 | user.id.should eq 38895958 123 | end 124 | end 125 | 126 | context "got Int64" do 127 | user = client.block(776284343173906432) 128 | it "returns Twitter::User" do 129 | user.should be_a Twitter::User 130 | user.id.should eq 38895958 # always returns the same id because `Twitter::REST::Client#get` is stubbed 131 | end 132 | end 133 | 134 | context "got Twitter::User" do 135 | user = client.block(client.user) 136 | it "returns Twitter::User" do 137 | user.should be_a Twitter::User 138 | user.id.should eq 38895958 139 | end 140 | end 141 | end 142 | 143 | describe "#unblock" do 144 | context "got String" do 145 | user = client.unblock("theSeanCook") 146 | it "returns Twitter::User" do 147 | user.should be_a Twitter::User 148 | user.id.should eq 38895958 149 | end 150 | end 151 | 152 | context "got Int32" do 153 | user = client.unblock(38895958) 154 | it "returns Twitter::User" do 155 | user.should be_a Twitter::User 156 | user.id.should eq 38895958 157 | end 158 | end 159 | 160 | context "got Int64" do 161 | user = client.unblock(776284343173906432) 162 | it "returns Twitter::User" do 163 | user.should be_a Twitter::User 164 | user.id.should eq 38895958 # always returns the same id because `Twitter::REST::Client#get` is stubbed 165 | end 166 | end 167 | 168 | context "got Twitter::User" do 169 | user = client.unblock(client.user) 170 | it "returns Twitter::User" do 171 | user.should be_a Twitter::User 172 | user.id.should eq 38895958 173 | end 174 | end 175 | end 176 | 177 | describe "#update_profile" do 178 | user = client.update_profile({"name" => "Sean Cook", "description" => "Keep calm and rock on."}) 179 | it { user.should be_a Twitter::User } 180 | it { user.name.should eq "Sean Cook" } 181 | it { user.description.should eq "Keep calm and rock on." } 182 | end 183 | 184 | describe "#update_profile_image" do 185 | user = client.update_profile_image("base64encodedstringbase64encodedstringbase64encodedstring") 186 | it { user.should be_a Twitter::User } 187 | it { user.name.should eq "kenta-s" } 188 | it { user.description.should eq "with great power comes great responsibility." } 189 | end 190 | 191 | describe "#update_profile_background_image" do 192 | user = client.update_profile_background_image({"image" => "base64encodedstringbase64encodedstringbase64encodedstring"}) 193 | it { user.should be_a Twitter::User } 194 | it { user.name.should eq "Sean Test" } 195 | it { user.description.should eq "Keep calm and test" } 196 | end 197 | 198 | describe "#update_profile_banner" do 199 | response = client.update_profile_banner("base64encodedstringbase64encodedstringbase64encodedstring") 200 | it { response.should be_a Nil } 201 | end 202 | 203 | describe "#mute" do 204 | user = client.mute({"screen_name" => "kenta_s_dev"}) 205 | it "returns a Twitter::User" do 206 | user.should be_a Twitter::User 207 | end 208 | 209 | it "returns a Twitter::User whose name is kenta-s" do 210 | user.name.should eq "kenta-s" 211 | end 212 | end 213 | 214 | describe "#unmute" do 215 | user = client.unmute({"screen_name" => "evilpiper"}) 216 | it "returns a Twitter::User" do 217 | user.should be_a Twitter::User 218 | end 219 | 220 | it "returns a Twitter::User whose name is Evil Piper" do 221 | user.name.should eq "Evil Piper" 222 | end 223 | end 224 | 225 | describe "#friendships" do 226 | context "with String" do 227 | users = client.friendships("hello") 228 | it "returns Array(Twitter::Relationship)" do 229 | users.should be_a Array(Twitter::Relationship) 230 | users[0].screen_name.should eq "andypiper" 231 | users[0].connections.should eq ["following"] 232 | end 233 | end 234 | 235 | context "with Int64" do 236 | users = client.friendships(123451234512345123) 237 | it "returns Array(Twitter::Relationship)" do 238 | users.should be_a Array(Twitter::Relationship) 239 | users[0].screen_name.should eq "andypiper" 240 | users[0].connections.should eq ["following"] 241 | end 242 | end 243 | end 244 | end 245 | -------------------------------------------------------------------------------- /spec/mock/1.1/statuses/home_timeline.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "coordinates": null, 4 | "truncated": false, 5 | "created_at": "Tue Aug 28 21:16:23 +0000 2012", 6 | "favorited": false, 7 | "id_str": "240558470661799936", 8 | "in_reply_to_user_id_str": null, 9 | "entities": { 10 | "urls": [], 11 | "hashtags": [], 12 | "symbols": [], 13 | "user_mentions": [] 14 | }, 15 | "text": "just another test", 16 | "contributors": null, 17 | "id": 240558470661799936, 18 | "retweet_count": 0, 19 | "in_reply_to_status_id_str": null, 20 | "geo": null, 21 | "retweeted": false, 22 | "in_reply_to_user_id": null, 23 | "place": null, 24 | "source": "OAuth Dancer Reborn", 25 | "user": { 26 | "name": "OAuth Dancer", 27 | "profile_sidebar_fill_color": "DDEEF6", 28 | "profile_background_tile": true, 29 | "profile_sidebar_border_color": "C0DEED", 30 | "profile_image_url": "http://a0.twimg.com/profile_images/730275945/oauth-dancer_normal.jpg", 31 | "created_at": "Wed Mar 03 19:37:35 +0000 2010", 32 | "location": "San Francisco, CA", 33 | "follow_request_sent": false, 34 | "id_str": "119476949", 35 | "is_translator": false, 36 | "profile_link_color": "0084B4", 37 | "entities": { 38 | "url": { 39 | "urls": [ 40 | { 41 | "expanded_url": "http://bit.ly/oauth-dancer", 42 | "display_url": "http://bit.ly/oauth-dancer", 43 | "url": "http://bit.ly/oauth-dancer", 44 | "indices": [0, 26] 45 | } 46 | ] 47 | }, 48 | "description": { 49 | "urls": [] 50 | } 51 | }, 52 | "default_profile": false, 53 | "url": "http://bit.ly/oauth-dancer", 54 | "contributors_enabled": false, 55 | "favourites_count": 7, 56 | "utc_offset": null, 57 | "profile_image_url_https": "https://si0.twimg.com/profile_images/730275945/oauth-dancer_normal.jpg", 58 | "id": 119476949, 59 | "listed_count": 1, 60 | "profile_use_background_image": true, 61 | "profile_text_color": "333333", 62 | "followers_count": 28, 63 | "lang": "en", 64 | "protected": false, 65 | "geo_enabled": true, 66 | "notifications": false, 67 | "description": "", 68 | "profile_background_color": "C0DEED", 69 | "verified": false, 70 | "time_zone": null, 71 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/80151733/oauth-dance.png", 72 | "statuses_count": 166, 73 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/80151733/oauth-dance.png", 74 | "default_profile_image": false, 75 | "friends_count": 14, 76 | "following": false, 77 | "show_all_inline_media": false, 78 | "screen_name": "oauth_dancer" 79 | }, 80 | "in_reply_to_screen_name": null, 81 | "in_reply_to_status_id": null 82 | }, 83 | { 84 | "coordinates": { 85 | "coordinates": [-122.25831, 37.871609], 86 | "type": "Point" 87 | }, 88 | "truncated": false, 89 | "created_at": "Tue Aug 28 21:08:15 +0000 2012", 90 | "favorited": false, 91 | "id_str": "240556426106372096", 92 | "in_reply_to_user_id_str": null, 93 | "entities": { 94 | "urls": [ 95 | { 96 | "expanded_url": "http://blogs.ischool.berkeley.edu/i290-abdt-s12/", 97 | "url": "http://t.co/bfj7zkDJ", 98 | "indices": [79, 99], 99 | "display_url": "blogs.ischool.berkeley.edu/i290-abdt-s12/" 100 | } 101 | ], 102 | "hashtags": [], 103 | "symbols": [], 104 | "user_mentions": [ 105 | { 106 | "name": "Cal", 107 | "id_str": "17445752", 108 | "id": 17445752, 109 | "indices": [60, 64], 110 | "screen_name": "Cal" 111 | }, 112 | { 113 | "name": "Othman Laraki", 114 | "id_str": "20495814", 115 | "id": 20495814, 116 | "indices": [70, 77], 117 | "screen_name": "othman" 118 | } 119 | ] 120 | }, 121 | "text": "lecturing at the \"analyzing big data with twitter\" class at @cal with @othman http://t.co/bfj7zkDJ", 122 | "contributors": null, 123 | "id": 240556426106372096, 124 | "retweet_count": 3, 125 | "in_reply_to_status_id_str": null, 126 | "geo": { 127 | "coordinates": [37.871609, -122.25831], 128 | "type": "Point" 129 | }, 130 | "retweeted": false, 131 | "possibly_sensitive": false, 132 | "in_reply_to_user_id": null, 133 | "place": { 134 | "name": "Berkeley", 135 | "country_code": "US", 136 | "country": "United States", 137 | "attributes": {}, 138 | "url": "http://api.twitter.com/1/geo/id/5ef5b7f391e30aff.json", 139 | "id": "5ef5b7f391e30aff", 140 | "bounding_box": { 141 | "coordinates": [ 142 | [ 143 | [-122.367781, 37.835727], 144 | [-122.234185, 37.835727], 145 | [-122.234185, 37.905824], 146 | [-122.367781, 37.905824] 147 | ] 148 | ], 149 | "type": "Polygon" 150 | }, 151 | "full_name": "Berkeley, CA", 152 | "place_type": "city" 153 | }, 154 | "source": "Safari on iOS", 155 | "user": { 156 | "name": "Raffi Krikorian", 157 | "profile_sidebar_fill_color": "DDEEF6", 158 | "profile_background_tile": false, 159 | "profile_sidebar_border_color": "C0DEED", 160 | "profile_image_url": "http://a0.twimg.com/profile_images/1270234259/raffi-headshot-casual_normal.png", 161 | "created_at": "Sun Aug 19 14:24:06 +0000 2007", 162 | "location": "San Francisco, California", 163 | "follow_request_sent": false, 164 | "id_str": "8285392", 165 | "is_translator": false, 166 | "profile_link_color": "0084B4", 167 | "entities": { 168 | "url": { 169 | "urls": [ 170 | { 171 | "expanded_url": "http://about.me/raffi.krikorian", 172 | "url": "http://t.co/eNmnM6q", 173 | "indices": [0, 19], 174 | "display_url": "about.me/raffi.krikorian" 175 | } 176 | ] 177 | }, 178 | "description": { 179 | "urls": [] 180 | } 181 | }, 182 | "default_profile": true, 183 | "url": "http://t.co/eNmnM6q", 184 | "contributors_enabled": false, 185 | "favourites_count": 724, 186 | "utc_offset": -28800, 187 | "profile_image_url_https": "https://si0.twimg.com/profile_images/1270234259/raffi-headshot-casual_normal.png", 188 | "id": 8285392, 189 | "listed_count": 619, 190 | "profile_use_background_image": true, 191 | "profile_text_color": "333333", 192 | "followers_count": 18752, 193 | "lang": "en", 194 | "protected": false, 195 | "geo_enabled": true, 196 | "notifications": false, 197 | "description": "Director of @twittereng's Platform Services. I break things.", 198 | "profile_background_color": "C0DEED", 199 | "verified": false, 200 | "time_zone": "Pacific Time (US & Canada)", 201 | "profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme1/bg.png", 202 | "statuses_count": 5007, 203 | "profile_background_image_url": "http://a0.twimg.com/images/themes/theme1/bg.png", 204 | "default_profile_image": false, 205 | "friends_count": 701, 206 | "following": true, 207 | "show_all_inline_media": true, 208 | "screen_name": "raffi" 209 | }, 210 | "in_reply_to_screen_name": null, 211 | "in_reply_to_status_id": null 212 | }, 213 | { 214 | "coordinates": null, 215 | "truncated": false, 216 | "created_at": "Tue Aug 28 19:59:34 +0000 2012", 217 | "favorited": false, 218 | "id_str": "240539141056638977", 219 | "in_reply_to_user_id_str": null, 220 | "entities": { 221 | "urls": [], 222 | "hashtags": [], 223 | "symbols": [], 224 | "user_mentions": [] 225 | }, 226 | "text": "You'd be right more often if you thought you were wrong.", 227 | "contributors": null, 228 | "id": 240539141056638977, 229 | "retweet_count": 1, 230 | "in_reply_to_status_id_str": null, 231 | "geo": null, 232 | "retweeted": false, 233 | "in_reply_to_user_id": null, 234 | "place": null, 235 | "source": "web", 236 | "user": { 237 | "name": "Taylor Singletary", 238 | "profile_sidebar_fill_color": "FBFBFB", 239 | "profile_background_tile": true, 240 | "profile_sidebar_border_color": "000000", 241 | "profile_image_url": "http://a0.twimg.com/profile_images/2546730059/f6a8zq58mg1hn0ha8vie_normal.jpeg", 242 | "created_at": "Wed Mar 07 22:23:19 +0000 2007", 243 | "location": "San Francisco, CA", 244 | "follow_request_sent": false, 245 | "id_str": "819797", 246 | "is_translator": false, 247 | "profile_link_color": "c71818", 248 | "entities": { 249 | "url": { 250 | "urls": [ 251 | { 252 | "expanded_url": "http://www.rebelmouse.com/episod/", 253 | "url": "http://t.co/Lxw7upbN", 254 | "indices": [0, 20], 255 | "display_url": "rebelmouse.com/episod/" 256 | } 257 | ] 258 | }, 259 | "description": { 260 | "urls": [] 261 | } 262 | }, 263 | "default_profile": false, 264 | "url": "http://t.co/Lxw7upbN", 265 | "contributors_enabled": false, 266 | "favourites_count": 15990, 267 | "utc_offset": -28800, 268 | "profile_image_url_https": "https://si0.twimg.com/profile_images/2546730059/f6a8zq58mg1hn0ha8vie_normal.jpeg", 269 | "id": 819797, 270 | "listed_count": 340, 271 | "profile_use_background_image": true, 272 | "profile_text_color": "D20909", 273 | "followers_count": 7126, 274 | "lang": "en", 275 | "protected": false, 276 | "geo_enabled": true, 277 | "notifications": false, 278 | "description": "Reality Technician, Twitter API team, synthesizer enthusiast; a most excellent adventure in timelines. I know it's hard to believe in something you can't see.", 279 | "profile_background_color": "000000", 280 | "verified": false, 281 | "time_zone": "Pacific Time (US & Canada)", 282 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/643655842/hzfv12wini4q60zzrthg.png", 283 | "statuses_count": 18076, 284 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/643655842/hzfv12wini4q60zzrthg.png", 285 | "default_profile_image": false, 286 | "friends_count": 5444, 287 | "following": true, 288 | "show_all_inline_media": true, 289 | "screen_name": "episod" 290 | }, 291 | "in_reply_to_screen_name": null, 292 | "in_reply_to_status_id": null 293 | } 294 | ] 295 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2014-2017 Erik Michaels-Ober 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /spec/mock/1.1/search/tweets.json: -------------------------------------------------------------------------------- 1 | { 2 | "statuses": [ 3 | { 4 | "coordinates": null, 5 | "favorited": false, 6 | "truncated": false, 7 | "created_at": "Mon Sep 24 03:35:21 +0000 2012", 8 | "id_str": "250075927172759552", 9 | "entities": { 10 | "urls": [], 11 | "hashtags": [ 12 | { 13 | "text": "freebandnames", 14 | "indices": [20, 34] 15 | } 16 | ], 17 | "symbols": [], 18 | "user_mentions": [] 19 | }, 20 | "in_reply_to_user_id_str": null, 21 | "contributors": null, 22 | "text": "Aggressive Ponytail #freebandnames", 23 | "metadata": { 24 | "iso_language_code": "en", 25 | "result_type": "recent" 26 | }, 27 | "retweet_count": 0, 28 | "in_reply_to_status_id_str": null, 29 | "id": 250075927172759552, 30 | "geo": null, 31 | "retweeted": false, 32 | "in_reply_to_user_id": null, 33 | "place": null, 34 | "user": { 35 | "profile_sidebar_fill_color": "DDEEF6", 36 | "profile_sidebar_border_color": "C0DEED", 37 | "profile_background_tile": false, 38 | "name": "Sean Cummings", 39 | "profile_image_url": "http://a0.twimg.com/profile_images/2359746665/1v6zfgqo8g0d3mk7ii5s_normal.jpeg", 40 | "created_at": "Mon Apr 26 06:01:55 +0000 2010", 41 | "location": "LA, CA", 42 | "follow_request_sent": null, 43 | "profile_link_color": "0084B4", 44 | "is_translator": false, 45 | "id_str": "137238150", 46 | "entities": { 47 | "url": { 48 | "urls": [ 49 | { 50 | "expanded_url": "", 51 | "url": "", 52 | "display_url": "", 53 | "indices": [0, 0] 54 | } 55 | ] 56 | }, 57 | "description": { 58 | "urls": [] 59 | } 60 | }, 61 | "default_profile": true, 62 | "contributors_enabled": false, 63 | "favourites_count": 0, 64 | "url": null, 65 | "profile_image_url_https": "https://si0.twimg.com/profile_images/2359746665/1v6zfgqo8g0d3mk7ii5s_normal.jpeg", 66 | "utc_offset": -28800, 67 | "id": 137238150, 68 | "profile_use_background_image": true, 69 | "listed_count": 2, 70 | "profile_text_color": "333333", 71 | "lang": "en", 72 | "followers_count": 70, 73 | "protected": false, 74 | "notifications": null, 75 | "profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme1/bg.png", 76 | "profile_background_color": "C0DEED", 77 | "verified": false, 78 | "geo_enabled": true, 79 | "time_zone": "Pacific Time (US & Canada)", 80 | "description": "Born 330 Live 310", 81 | "default_profile_image": false, 82 | "profile_background_image_url": "http://a0.twimg.com/images/themes/theme1/bg.png", 83 | "statuses_count": 579, 84 | "friends_count": 110, 85 | "following": null, 86 | "show_all_inline_media": false, 87 | "screen_name": "sean_cummings" 88 | }, 89 | "in_reply_to_screen_name": null, 90 | "source": "Twitter for Mac", 91 | "in_reply_to_status_id": null 92 | }, 93 | { 94 | "coordinates": null, 95 | "favorited": false, 96 | "truncated": false, 97 | "created_at": "Fri Sep 21 23:40:54 +0000 2012", 98 | "id_str": "249292149810667520", 99 | "entities": { 100 | "urls": [], 101 | "hashtags": [ 102 | { 103 | "text": "FreeBandNames", 104 | "indices": [20, 34] 105 | } 106 | ], 107 | "symbols": [], 108 | "user_mentions": [] 109 | }, 110 | "in_reply_to_user_id_str": null, 111 | "contributors": null, 112 | "text": "Thee Namaste Nerdz. #FreeBandNames", 113 | "metadata": { 114 | "iso_language_code": "pl", 115 | "result_type": "recent" 116 | }, 117 | "retweet_count": 0, 118 | "in_reply_to_status_id_str": null, 119 | "id": 249292149810667520, 120 | "geo": null, 121 | "retweeted": false, 122 | "in_reply_to_user_id": null, 123 | "place": null, 124 | "user": { 125 | "profile_sidebar_fill_color": "DDFFCC", 126 | "profile_sidebar_border_color": "BDDCAD", 127 | "profile_background_tile": true, 128 | "name": "Chaz Martenstein", 129 | "profile_image_url": "http://a0.twimg.com/profile_images/447958234/Lichtenstein_normal.jpg", 130 | "created_at": "Tue Apr 07 19:05:07 +0000 2009", 131 | "location": "Durham, NC", 132 | "follow_request_sent": null, 133 | "profile_link_color": "0084B4", 134 | "is_translator": false, 135 | "id_str": "29516238", 136 | "entities": { 137 | "url": { 138 | "urls": [ 139 | { 140 | "expanded_url": "http://bullcityrecords.com/wnng/", 141 | "url": "http://bullcityrecords.com/wnng/", 142 | "display_url": "http://bullcityrecords.com/wnng/", 143 | "indices": [0, 32] 144 | } 145 | ] 146 | }, 147 | "description": { 148 | "urls": [] 149 | } 150 | }, 151 | "default_profile": false, 152 | "contributors_enabled": false, 153 | "favourites_count": 8, 154 | "url": "http://bullcityrecords.com/wnng/", 155 | "profile_image_url_https": "https://si0.twimg.com/profile_images/447958234/Lichtenstein_normal.jpg", 156 | "utc_offset": -18000, 157 | "id": 29516238, 158 | "profile_use_background_image": true, 159 | "listed_count": 118, 160 | "profile_text_color": "333333", 161 | "lang": "en", 162 | "followers_count": 2052, 163 | "protected": false, 164 | "notifications": null, 165 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/9423277/background_tile.bmp", 166 | "profile_background_color": "9AE4E8", 167 | "verified": false, 168 | "geo_enabled": false, 169 | "time_zone": "Eastern Time (US & Canada)", 170 | "description": "You will come to Durham, North Carolina. I will sell you some records then, here in Durham, North Carolina. Fun will happen.", 171 | "default_profile_image": false, 172 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/9423277/background_tile.bmp", 173 | "statuses_count": 7579, 174 | "friends_count": 348, 175 | "following": null, 176 | "show_all_inline_media": true, 177 | "screen_name": "bullcityrecords" 178 | }, 179 | "in_reply_to_screen_name": null, 180 | "source": "web", 181 | "in_reply_to_status_id": null 182 | }, 183 | { 184 | "coordinates": null, 185 | "favorited": false, 186 | "truncated": false, 187 | "created_at": "Fri Sep 21 23:30:20 +0000 2012", 188 | "id_str": "249289491129438208", 189 | "entities": { 190 | "urls": [], 191 | "hashtags": [ 192 | { 193 | "text": "freebandnames", 194 | "indices": [29, 43] 195 | } 196 | ], 197 | "symbols": [], 198 | "user_mentions": [] 199 | }, 200 | "in_reply_to_user_id_str": null, 201 | "contributors": null, 202 | "text": "Mexican Heaven, Mexican Hell #freebandnames", 203 | "metadata": { 204 | "iso_language_code": "en", 205 | "result_type": "recent" 206 | }, 207 | "retweet_count": 0, 208 | "in_reply_to_status_id_str": null, 209 | "id": 249289491129438208, 210 | "geo": null, 211 | "retweeted": false, 212 | "in_reply_to_user_id": null, 213 | "place": null, 214 | "user": { 215 | "profile_sidebar_fill_color": "99CC33", 216 | "profile_sidebar_border_color": "829D5E", 217 | "profile_background_tile": false, 218 | "name": "Thomas John Wakeman", 219 | "profile_image_url": "http://a0.twimg.com/profile_images/2219333930/Froggystyle_normal.png", 220 | "created_at": "Tue Sep 01 21:21:35 +0000 2009", 221 | "location": "Kingston New York", 222 | "follow_request_sent": null, 223 | "profile_link_color": "D02B55", 224 | "is_translator": false, 225 | "id_str": "70789458", 226 | "entities": { 227 | "url": { 228 | "urls": [ 229 | { 230 | "expanded_url": "", 231 | "display_url": "", 232 | "url": "", 233 | "indices": [0, 0] 234 | } 235 | ] 236 | }, 237 | "description": { 238 | "urls": [] 239 | } 240 | }, 241 | "default_profile": false, 242 | "contributors_enabled": false, 243 | "favourites_count": 19, 244 | "url": null, 245 | "profile_image_url_https": "https://si0.twimg.com/profile_images/2219333930/Froggystyle_normal.png", 246 | "utc_offset": -18000, 247 | "id": 70789458, 248 | "profile_use_background_image": true, 249 | "listed_count": 1, 250 | "profile_text_color": "3E4415", 251 | "lang": "en", 252 | "followers_count": 63, 253 | "protected": false, 254 | "notifications": null, 255 | "profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme5/bg.gif", 256 | "profile_background_color": "352726", 257 | "verified": false, 258 | "geo_enabled": false, 259 | "time_zone": "Eastern Time (US & Canada)", 260 | "description": "Science Fiction Writer, sort of. Likes Superheroes, Mole People, Alt. Timelines.", 261 | "default_profile_image": false, 262 | "profile_background_image_url": "http://a0.twimg.com/images/themes/theme5/bg.gif", 263 | "statuses_count": 1048, 264 | "friends_count": 63, 265 | "following": null, 266 | "show_all_inline_media": false, 267 | "screen_name": "MonkiesFist" 268 | }, 269 | "in_reply_to_screen_name": null, 270 | "source": "web", 271 | "in_reply_to_status_id": null 272 | }, 273 | { 274 | "coordinates": null, 275 | "favorited": false, 276 | "truncated": false, 277 | "created_at": "Fri Sep 21 22:51:18 +0000 2012", 278 | "id_str": "249279667666817024", 279 | "entities": { 280 | "urls": [], 281 | "hashtags": [ 282 | { 283 | "text": "freebandnames", 284 | "indices": [20, 34] 285 | } 286 | ], 287 | "symbols": [], 288 | "user_mentions": [] 289 | }, 290 | "in_reply_to_user_id_str": null, 291 | "contributors": null, 292 | "text": "The Foolish Mortals #freebandnames", 293 | "metadata": { 294 | "iso_language_code": "en", 295 | "result_type": "recent" 296 | }, 297 | "retweet_count": 0, 298 | "in_reply_to_status_id_str": null, 299 | "id": 249279667666817024, 300 | "geo": null, 301 | "retweeted": false, 302 | "in_reply_to_user_id": null, 303 | "place": null, 304 | "user": { 305 | "profile_sidebar_fill_color": "BFAC83", 306 | "profile_sidebar_border_color": "615A44", 307 | "profile_background_tile": true, 308 | "name": "Marty Elmer", 309 | "profile_image_url": "http://a0.twimg.com/profile_images/1629790393/shrinker_2000_trans_normal.png", 310 | "created_at": "Mon May 04 00:05:00 +0000 2009", 311 | "location": "Wisconsin, USA", 312 | "follow_request_sent": null, 313 | "profile_link_color": "3B2A26", 314 | "is_translator": false, 315 | "id_str": "37539828", 316 | "entities": { 317 | "url": { 318 | "urls": [ 319 | { 320 | "expanded_url": "http://www.omnitarian.me", 321 | "display_url": "http://www.omnitarian.me", 322 | "url": "http://www.omnitarian.me", 323 | "indices": [0, 24] 324 | } 325 | ] 326 | }, 327 | "description": { 328 | "urls": [] 329 | } 330 | }, 331 | "default_profile": false, 332 | "contributors_enabled": false, 333 | "favourites_count": 647, 334 | "url": "http://www.omnitarian.me", 335 | "profile_image_url_https": "https://si0.twimg.com/profile_images/1629790393/shrinker_2000_trans_normal.png", 336 | "utc_offset": -21600, 337 | "id": 37539828, 338 | "profile_use_background_image": true, 339 | "listed_count": 52, 340 | "profile_text_color": "000000", 341 | "lang": "en", 342 | "followers_count": 608, 343 | "protected": false, 344 | "notifications": null, 345 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/106455659/rect6056-9.png", 346 | "profile_background_color": "EEE3C4", 347 | "verified": false, 348 | "geo_enabled": false, 349 | "time_zone": "Central Time (US & Canada)", 350 | "description": "Cartoonist, Illustrator, and T-Shirt connoisseur", 351 | "default_profile_image": false, 352 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/106455659/rect6056-9.png", 353 | "statuses_count": 3575, 354 | "friends_count": 249, 355 | "following": null, 356 | "show_all_inline_media": true, 357 | "screen_name": "Omnitarian" 358 | }, 359 | "in_reply_to_screen_name": null, 360 | "source": "Twitter for iPhone", 361 | "in_reply_to_status_id": null 362 | } 363 | ], 364 | "search_metadata": { 365 | "max_id": 250126199840518145, 366 | "since_id": 24012619984051000, 367 | "refresh_url": "?since_id=250126199840518145&q=%23freebandnames&result_type=mixed&include_entities=1", 368 | "next_results": "?max_id=249279667666817023&q=%23freebandnames&count=4&include_entities=1&result_type=mixed", 369 | "count": 4, 370 | "completed_in": 0.035, 371 | "since_id_str": "24012619984051000", 372 | "query": "%23freebandnames", 373 | "max_id_str": "250126199840518145" 374 | } 375 | } 376 | --------------------------------------------------------------------------------