diff --git a/lib/x/client.rb b/lib/x/client.rb index 7a2c917..453112c 100644 --- a/lib/x/client.rb +++ b/lib/x/client.rb @@ -8,6 +8,7 @@ require_relative "redirect_handler" require_relative "request_builder" require_relative "response_parser" +require_relative "users" module X # A client for interacting with the X API @@ -15,6 +16,7 @@ module X class Client extend Forwardable include ClientCredentials + include Users # Default base URL for the X API DEFAULT_BASE_URL = "https://api.twitter.com/2/".freeze diff --git a/lib/x/users.rb b/lib/x/users.rb new file mode 100644 index 0000000..6da0cf8 --- /dev/null +++ b/lib/x/users.rb @@ -0,0 +1,110 @@ +module X + # Methods for the Users Lookup API + # @see https://docs.x.com/x-api/users/lookup/introduction + # @api public + module Users + # Retrieve a single user by ID + # + # @param id [String, Integer] the user ID + # @param user_fields [Array, String, nil] list of user fields to return + # @param expansions [Array, String, nil] list of expansions to include + # @param tweet_fields [Array, String, nil] list of tweet fields to return (requires pinned_tweet_id expansion) + # @return [Hash] the parsed response body + # @see https://docs.x.com/x-api/users/lookup/api-reference/get-users-id + # @example Retrieve a user with default fields + # client.user("123456") + # @example Retrieve a user with additional fields + # client.user("123456", user_fields: ["created_at", "description", "public_metrics"]) + def user(id, user_fields: nil, expansions: nil, tweet_fields: nil) + get(build_users_endpoint("users/#{id}", user_fields:, expansions:, tweet_fields:)) + end + + # Retrieve multiple users by their IDs + # + # @param ids [Array, Array] list of user IDs (up to 100) + # @param user_fields [Array, String, nil] list of user fields to return + # @param expansions [Array, String, nil] list of expansions to include + # @param tweet_fields [Array, String, nil] list of tweet fields to return (requires pinned_tweet_id expansion) + # @return [Hash] the parsed response body + # @see https://docs.x.com/x-api/users/lookup/api-reference/get-users + # @example Retrieve multiple users + # client.users(ids: ["123456", "789012"]) + # @example Retrieve multiple users with fields + # client.users(ids: ["123456", "789012"], user_fields: ["created_at", "public_metrics"]) + def users(ids:, user_fields: nil, expansions: nil, tweet_fields: nil) + params = assemble_users_params(user_fields:, expansions:, tweet_fields:) + params["ids"] = Array(ids).join(",") + get("users?#{URI.encode_www_form(params)}") + end + + # Retrieve a single user by username + # + # @param username [String] the username (without @ prefix) + # @param user_fields [Array, String, nil] list of user fields to return + # @param expansions [Array, String, nil] list of expansions to include + # @param tweet_fields [Array, String, nil] list of tweet fields to return (requires pinned_tweet_id expansion) + # @return [Hash] the parsed response body + # @see https://docs.x.com/x-api/users/lookup/api-reference/get-users-by-username-username + # @example Retrieve a user by username + # client.user_by_username("sferik") + # @example Retrieve a user by username with fields + # client.user_by_username("sferik", user_fields: ["created_at", "description"]) + def user_by_username(username, user_fields: nil, expansions: nil, tweet_fields: nil) + get(build_users_endpoint("users/by/username/#{username}", user_fields:, expansions:, tweet_fields:)) + end + + # Retrieve multiple users by their usernames + # + # @param usernames [Array] list of usernames (up to 100, without @ prefix) + # @param user_fields [Array, String, nil] list of user fields to return + # @param expansions [Array, String, nil] list of expansions to include + # @param tweet_fields [Array, String, nil] list of tweet fields to return (requires pinned_tweet_id expansion) + # @return [Hash] the parsed response body + # @see https://docs.x.com/x-api/users/lookup/api-reference/get-users-by + # @example Retrieve users by usernames + # client.users_by_usernames(usernames: ["sferik", "xdevelopers"]) + # @example Retrieve users by usernames with fields + # client.users_by_usernames(usernames: ["sferik", "xdevelopers"], user_fields: ["created_at"]) + def users_by_usernames(usernames:, user_fields: nil, expansions: nil, tweet_fields: nil) + params = assemble_users_params(user_fields:, expansions:, tweet_fields:) + params["usernames"] = Array(usernames).join(",") + get("users/by?#{URI.encode_www_form(params)}") + end + + # Retrieve the authenticated user + # + # @param user_fields [Array, String, nil] list of user fields to return + # @param expansions [Array, String, nil] list of expansions to include + # @param tweet_fields [Array, String, nil] list of tweet fields to return (requires pinned_tweet_id expansion) + # @return [Hash] the parsed response body + # @see https://docs.x.com/x-api/users/lookup/api-reference/get-users-me + # @example Retrieve the authenticated user + # client.me + # @example Retrieve the authenticated user with fields + # client.me(user_fields: ["created_at", "description", "public_metrics"]) + def me(user_fields: nil, expansions: nil, tweet_fields: nil) + get(build_users_endpoint("users/me", user_fields:, expansions:, tweet_fields:)) + end + + private + + # Build an endpoint path with optional query parameters + # @api private + def build_users_endpoint(path, user_fields: nil, expansions: nil, tweet_fields: nil) + params = assemble_users_params(user_fields:, expansions:, tweet_fields:) + return path if params.empty? + + "#{path}?#{URI.encode_www_form(params)}" + end + + # Assemble query parameters hash from field options + # @api private + def assemble_users_params(user_fields: nil, expansions: nil, tweet_fields: nil) + params = {} + params["user.fields"] = Array(user_fields).join(",") if user_fields + params["expansions"] = Array(expansions).join(",") if expansions + params["tweet.fields"] = Array(tweet_fields).join(",") if tweet_fields + params + end + end +end diff --git a/test/x/users_test.rb b/test/x/users_test.rb new file mode 100644 index 0000000..bdd2cec --- /dev/null +++ b/test/x/users_test.rb @@ -0,0 +1,171 @@ +require_relative "../test_helper" + +module X + class UsersTest < Minitest::Test + cover Users + + BASE_URL = "https://api.twitter.com/2/" + + def setup + @client = Client.new(bearer_token: TEST_BEARER_TOKEN) + @user_data = {"data" => {"id" => "123", "name" => "Test", "username" => "test"}}.freeze + @users_data = {"data" => [{"id" => "123", "name" => "Test", "username" => "test"}]}.freeze + @headers = {"Content-Type" => "application/json"} + end + + def test_user + stub_request(:get, "#{BASE_URL}users/123") + .to_return(body: @user_data.to_json, headers: @headers) + + result = @client.user("123") + + assert_equal @user_data, result + end + + def test_user_with_user_fields + stub_request(:get, "#{BASE_URL}users/123?user.fields=created_at,description") + .to_return(body: @user_data.to_json, headers: @headers) + + result = @client.user("123", user_fields: %w[created_at description]) + + assert_equal @user_data, result + end + + def test_user_with_expansions + stub_request(:get, "#{BASE_URL}users/123?expansions=pinned_tweet_id") + .to_return(body: @user_data.to_json, headers: @headers) + + result = @client.user("123", expansions: ["pinned_tweet_id"]) + + assert_equal @user_data, result + end + + def test_user_with_tweet_fields + stub_request(:get, "#{BASE_URL}users/123?tweet.fields=created_at,text") + .to_return(body: @user_data.to_json, headers: @headers) + + result = @client.user("123", tweet_fields: %w[created_at text]) + + assert_equal @user_data, result + end + + def test_user_with_all_fields + stub_request(:get, "#{BASE_URL}users/123?user.fields=created_at&expansions=pinned_tweet_id&tweet.fields=text") + .to_return(body: @user_data.to_json, headers: @headers) + + result = @client.user("123", user_fields: ["created_at"], expansions: ["pinned_tweet_id"], + tweet_fields: ["text"]) + + assert_equal @user_data, result + end + + def test_user_with_string_fields + stub_request(:get, "#{BASE_URL}users/123?user.fields=created_at") + .to_return(body: @user_data.to_json, headers: @headers) + + result = @client.user("123", user_fields: "created_at") + + assert_equal @user_data, result + end + + def test_users + stub_request(:get, "#{BASE_URL}users?ids=123,456") + .to_return(body: @users_data.to_json, headers: @headers) + + result = @client.users(ids: %w[123 456]) + + assert_equal @users_data, result + end + + def test_users_with_user_fields + stub_request(:get, "#{BASE_URL}users?user.fields=created_at,description&ids=123,456") + .to_return(body: @users_data.to_json, headers: @headers) + + result = @client.users(ids: %w[123 456], user_fields: %w[created_at description]) + + assert_equal @users_data, result + end + + def test_users_with_single_id + stub_request(:get, "#{BASE_URL}users?ids=123") + .to_return(body: @users_data.to_json, headers: @headers) + + result = @client.users(ids: "123") + + assert_equal @users_data, result + end + + def test_user_by_username + stub_request(:get, "#{BASE_URL}users/by/username/sferik") + .to_return(body: @user_data.to_json, headers: @headers) + + result = @client.user_by_username("sferik") + + assert_equal @user_data, result + end + + def test_user_by_username_with_user_fields + stub_request(:get, "#{BASE_URL}users/by/username/sferik?user.fields=created_at,public_metrics") + .to_return(body: @user_data.to_json, headers: @headers) + + result = @client.user_by_username("sferik", user_fields: %w[created_at public_metrics]) + + assert_equal @user_data, result + end + + def test_users_by_usernames + stub_request(:get, "#{BASE_URL}users/by?usernames=sferik,xdevelopers") + .to_return(body: @users_data.to_json, headers: @headers) + + result = @client.users_by_usernames(usernames: %w[sferik xdevelopers]) + + assert_equal @users_data, result + end + + def test_users_by_usernames_with_user_fields + stub_request(:get, "#{BASE_URL}users/by?user.fields=created_at&usernames=sferik,xdevelopers") + .to_return(body: @users_data.to_json, headers: @headers) + + result = @client.users_by_usernames(usernames: %w[sferik xdevelopers], user_fields: ["created_at"]) + + assert_equal @users_data, result + end + + def test_users_by_usernames_with_single_username + stub_request(:get, "#{BASE_URL}users/by?usernames=sferik") + .to_return(body: @users_data.to_json, headers: @headers) + + result = @client.users_by_usernames(usernames: "sferik") + + assert_equal @users_data, result + end + + def test_me + stub_request(:get, "#{BASE_URL}users/me") + .to_return(body: @user_data.to_json, headers: @headers) + + result = @client.me + + assert_equal @user_data, result + end + + def test_me_with_user_fields + stub_request(:get, "#{BASE_URL}users/me?user.fields=created_at,description,public_metrics") + .to_return(body: @user_data.to_json, headers: @headers) + + result = @client.me(user_fields: %w[created_at description public_metrics]) + + assert_equal @user_data, result + end + + def test_me_with_all_fields + stub_request(:get, "#{BASE_URL}users/me?user.fields=created_at&expansions=pinned_tweet_id&tweet.fields=text") + .to_return(body: @user_data.to_json, headers: @headers) + + result = @client.me(user_fields: ["created_at"], expansions: ["pinned_tweet_id"], + tweet_fields: ["text"]) + + assert_equal @user_data, result + end + end +end