Repository: faker-ruby/faker (Ruby)

Issue:
Update Faker::Twitter: rename to Faker::X and update deprecated attributes
Although we cannot keep track of all the external APIs changes, we do have the opportunity to update `Faker::Twitter` after its rename:

- deprecate `Faker::Twitter` in favour of `Faker::X`
- update `user` to follow the API's updates. Some fields have been deprecated (see https://docs.x.com/x-api/enterprise-gnip-2.0/fundamentals/data-dictionary#no-longer-supported-deprecated-attributes)
- include a note in the docs
- when it's releases, include a note in the CHANGELOG about the breaking changes:

This way, users of the current version can continue using `Faker::Twitter`, while being notified of the upcoming changes in the next version. When they are ready to update their faker version, they can use `Faker::X` with the updated attributes.

### Example of user with deprecated attributes

Extracted from the docs: https://docs.x.com/x-api/enterprise-gnip-2.0/fundamentals/data-dictionary#no-longer-supported-deprecated-attributes

```JSON
{
	"id": 6253282,
	"id_str": "6253282",
	"name": "Twitter API",
	"screen_name": "TwitterAPI",
	"location": "San Francisco, CA",
	"profile_location": null,
	"url": "https:\/\/t.co\/8IkCzCDr19",
	"description": "The Real Twitter API. Tweets about API changes, service issues and our Developer Platform. Don't get an answer? It's on my website.",
        "protected": false,
	"entities": {
		"url": {
			"urls": [{
				"url": "https:\/\/t.co\/8IkCzCDr19",
				"expanded_url": "https:\/\/developer.twitter.com",
				"display_url": "developer.twitter.com",
				"indices": [
					0,
					23
				]
			}]
		},
		"description": {
			"urls": []
		}
	},
	"followers_count": 6133636,
	"friends_count": 12,
	"listed_count": 12936,
	"created_at": "Wed May 23 06:01:13 +0000 2007",
	"favourites_count": 31,
	"verified": true,
	"statuses_count": 3656,
	"profile_image_url_https": "https:\/\/pbs.twimg.com\/profile_images\/942858479592554497\/BbazLO9L_normal.jpg",
	"profile_banner_url": "https:\/\/pbs.twimg.com\/profile_images\/942858479592554497\/BbazLO9L_normal.jpg",
	"default_profile": false,
	"default_profile_image": false,
}
```






Fix (patch):
diff --git a/README.md b/README.md
index cd57430b07..929ca184cd 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,7 @@ and it was the original impetus for the creation of this gem.
 
 - [Faker](#faker)
   - [Quick links](#quick-links)
+  - [In the media](#in-the-media)
   - [Table of Contents](#table-of-contents)
     - [Notes](#notes)
   - [Getting Started](#getting-started)
@@ -291,13 +292,13 @@ gem 'faker', :git => 'https://github.com/faker-ruby/faker.git', :branch => 'main
   - [Faker::Team](doc/default/team.md)
   - [Faker::Theater](doc/default/theater.md)
   - [Faker::Time](doc/default/time.md)
-  - [Faker::Twitter](doc/default/twitter.md)
   - [Faker::Types](doc/default/types.md)
   - [Faker::University](doc/default/university.md)
   - [Faker::Vehicle](doc/default/vehicle.md)
   - [Faker::Verbs](doc/default/verbs.md)
   - [Faker::VulnerabilityIdentifier](doc/default/vulnerability_identifier.md)
   - [Faker::WorldCup](doc/default/world_cup.md)
+  - [Faker::X](doc/default/x.md)
 </details>
 
 <details>
diff --git a/doc/default/twitter.md b/doc/default/twitter.md
deleted file mode 100644
index 4531daf142..0000000000
--- a/doc/default/twitter.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Faker::Twitter
-
-Available since version 1.7.3.
-
-Generate realistic Twitter user and status objects similar to what you would get back from the API.
-
-```json
-{
-  "created_at": "Mon Dec 10 00:00:00 +0000 2012",
-  "id": 8821452687517076614,
-  "id_str": "8821452687517076614",
-  "text": "Ea et laboriosam vel non.",
-  // ...
-}
-```
-
-```ruby
-# Keyword arguments: include_status, include_email
-Faker::Twitter.user #=>  {:id=>8821452687517076614, :name=>"Lincoln Paucek", :screen_name=>"cody"...
-Faker::Twitter.user(include_status: false) # Just get a user object with no embed status
-Faker::Twitter.user(include_email: true) # Simulate an authenticated user with the email permission
-
-# Keyword arguments: include_user, include_photo
-Faker::Twitter.status #=> {:id=>8821452687517076614, :text=>"Ea et laboriosam vel non."...
-Faker::Twitter.status(include_user: false) # Just get a status object with no embed user
-Faker::Twitter.status(include_photo: true) # Includes entities for an attached image
-
-Faker::Twitter.screen_name #=> "audreanne_hackett"
-```
diff --git a/doc/default/x.md b/doc/default/x.md
new file mode 100644
index 0000000000..7ba3aae4d0
--- /dev/null
+++ b/doc/default/x.md
@@ -0,0 +1,228 @@
+`Faker::Twitter` has been deprecated in favor of `Faker::X`. While it's still maintained, its docs can be found at [Faker::Twitter](#fakertwitter).
+
+# Faker::X
+
+Generates approximate X (previously Twitter) user and tweet objects, based on X's API v2 responses.
+
+The generators are not a comprehensive match of the API. However, they are enough to create a demo app using X’s user or tweet data, for example.
+
+## Faker::X.user
+
+Produces a random X user based on X's v2 API:
+
+```ruby
+Faker::X.user =>
+{
+  data: [
+    {
+      author_id: "5688134805624042468",
+      id: "2007502004337242257",
+      text: "Qui sint magni vel."
+    }
+  ],
+  includes: {
+    users: [
+      {
+        public_metrics: {
+          followers_count: 1000,
+          following_count: 77,
+          tweet_count: 4642,
+          listed_count: 704
+        },
+        username: "lilian",
+        pinned_tweet_id: "1702549793917523469",
+        entities: {
+          url: {
+            urls: [ { url: "https://t.co/0iz5wx1ysh", expanded_url: "http://example.com/stuart", display_url: "example.com/stuart" }]
+          },
+          description: { hashtags: [{tag: "Adipisci"}] }
+        },
+        description: "Est est laborum dolores.",
+        name: "Kimberli Ullrich Jr.",
+        verified: false,
+        location: "104.82.135.3",
+        id: "5688134805624042468",
+        protected: false,
+        url: "https://t.co/lqsqf67cx5",
+        profile_image_url: "https://robohash.org/990174365255127568.png?size=48x48&set=set1",
+        created_at: "2018-07-11T00:00:00+00:00"
+      }
+    ]
+  }
+}
+```
+
+## Faker::X.tweet
+
+Produces a random X tweet with default attributes. Available extensions can be returned with `include_media` and `include_user`:
+
+```ruby
+# Keyword arguments: include_user, include_media
+Faker::Twitter.tweet #=> { data: [{:id=>"8821452687517076614", :text=>"Ea et laboriosam vel non.",...
+Faker::Twitter.tweet(include_user: true) # Includes user attributes
+Faker::Twitter.tweet(include_media: true) # Includes media (photo) attributes
+```
+
+Example outputs:
+
+```ruby
+Faker::X.tweet =>
+{
+  data: [{
+    id: "5530076569335337477",
+    text: "Omnis facere ullam velit.",
+    lang: "ja",
+    conversation_id: "5530076569335337477",
+    created_at: "2009-02-21T07:00:00.000Z",
+    author_id: "2788144046134446176",
+    public_metrics: {
+      retweet_count: 95,
+      reply_count: 3,
+      like_count: 10,
+      quote_count: 3
+    },
+    possibly_sensitive: false,
+    entities: {
+      urls: [{
+        start: 0,
+        end: 5,
+        url: "https://t.co/t6o3lav9z1",
+        expanded_url: "http://example.com/errol.upton",
+        display_url: "example.com/errol.upton"
+      }],
+      hashtags: [{
+        start: 0,
+        end: 5,
+        tag: "Odit"
+      }]
+    }
+  }]
+}
+```
+
+With additional fields:
+
+```ruby
+Faker::X.tweet(include_media: true, include_user: true) =>
+{
+  data: [{
+    id: "5340086698567112794",
+    text: "Esse nulla minus qui.",
+    lang: "en",
+    conversation_id: "5340086698567112794",
+    created_at: "2009-07-04T06:00:00.000Z",
+    author_id: "5156189524741091965",
+    public_metrics: {
+      retweet_count: 56,
+      reply_count: 2,
+      like_count: 23,
+      quote_count: 1
+    },
+    possibly_sensitive: false,
+    entities: {
+      urls: [{
+        start: 0,
+        end: 5,
+        url: "https://t.co/mqplf9rhpn",
+        expanded_url: "http://example.com/mohamed_koelpin",
+        display_url: "example.com/mohamed_koelpin"
+      }],
+      hashtags: [{
+        start: 0,
+        end: 5,
+        tag: "Atque"
+      }]
+    },
+    attachments: {
+      media_keys: ["6992225089295851582"]
+    }
+  }],
+  includes: {
+    media: [{
+      height: 526,
+      media_key: "6992225089295851582",
+      type: "photo",
+      preview_image_url: "https://loremflickr.com/1064/600",
+      width: 1571,
+      alt_text: "Qui ratione magnam et."
+    }],
+    users: [{
+      public_metrics: {
+        followers_count: 467,
+        following_count: 3,
+        tweet_count: 9006,
+        listed_count: 984
+      },
+      username: "gayle",
+      pinned_tweet_id: "2282479924658708548",
+      entities: {
+        url: {
+          urls: [{
+            start: 0,
+            end: 5,
+            url: "https://t.co/69eytnuwwu",
+            expanded_url: "http://example.com/werner",
+            display_url: "example.com/werner"
+          }]
+        },
+        description: {
+          hashtags: [{
+            start: 0,
+            end: 5,
+            tag: "Soluta"
+          }]
+        }
+      },
+      description: "Esse harum voluptatem voluptate.",
+      name: "Elva Spinka",
+      verified: false,
+      location: "34.230.131.77",
+      id: "2365736908578621112",
+      protected: false,
+      url: "https://t.co/pyuqky3gdl",
+      profile_image_url: "https://robohash.org/2204799175591912732.png?size=48x48&set=set1",
+      created_at: "2025-01-30T07:00:00.000Z"
+    }]
+  }
+}
+```
+
+## Faker::X.screen_name
+
+Produces a random screen_name:
+
+```ruby
+Faker::X.screen_name #=> "audreanne_hackett"
+```
+
+# Faker::Twitter
+
+This generator has been deprecated. Please use `Faker::X` instead. Note that some attributes have been deprecated by [X's API](https://docs.x.com/x-api/migrate/data-format-migration).
+
+Available since version 1.7.3.
+
+Generate realistic Twitter user and status objects similar to what you would get back from the API.
+
+```json
+{
+  "created_at": "Mon Dec 10 00:00:00 +0000 2012",
+  "id": 8821452687517076614,
+  "id_str": "8821452687517076614",
+  "text": "Ea et laboriosam vel non.",
+  // ...
+}
+```
+
+```ruby
+# Keyword arguments: include_status, include_email
+Faker::Twitter.user #=>  {:id=>8821452687517076614, :name=>"Lincoln Paucek", :screen_name=>"cody"...
+Faker::Twitter.user(include_status: false) # Just get a user object with no embed status
+Faker::Twitter.user(include_email: true) # Simulate an authenticated user with the email permission
+
+# Keyword arguments: include_user, include_photo
+Faker::Twitter.status #=> {:id=>8821452687517076614, :text=>"Ea et laboriosam vel non."...
+Faker::Twitter.status(include_user: false) # Just get a status object with no embed user
+Faker::Twitter.status(include_photo: true) # Includes entities for an attached image
+
+Faker::Twitter.screen_name #=> "audreanne_hackett"
+```
diff --git a/lib/faker/default/twitter.rb b/lib/faker/default/twitter.rb
index 7ea67b1fad..02c5d3f0f6 100644
--- a/lib/faker/default/twitter.rb
+++ b/lib/faker/default/twitter.rb
@@ -17,6 +17,9 @@ class << self
       #
       # @faker.version 1.7.3
       def user(include_status: true, include_email: false)
+        warn('DEPRECATION WARNING: Faker::Twitter is deprecated. Use Faker::X instead. Some return attributes \
+          will be removed, check the docs for more details.')
+
         user_id = id
         background_image_url = Faker::LoremFlickr.image(size: '600x400')
         profile_image_url = Faker::Avatar.image(slug: user_id, size: '48x48')
@@ -81,6 +84,9 @@ def user(include_status: true, include_email: false)
       #
       # @faker.version 1.7.3
       def status(include_user: true, include_photo: false)
+        warn('DEPRECATION WARNING: Faker::Twitter is deprecated. Use Faker::X instead. Some return attributes \
+          will be removed, check the docs for more details.')
+
         status_id = id
         status = {
           id: status_id,
@@ -123,6 +129,8 @@ def status(include_user: true, include_photo: false)
       #
       # @faker.version 1.7.3
       def screen_name
+        warn('DEPRECATION WARNING: Faker::Twitter is deprecated. Use Faker::X instead.')
+
         Faker::Internet.username(specifier: nil, separators: ['_'])[0...20]
       end
 
@@ -204,4 +212,190 @@ def photo_entity
       end
     end
   end
+
+  class X < Base
+    class << self
+      ##
+      # Produces a random X user based on X's v2 API.
+      #
+      # @return [Hash]
+      #
+      # @example
+      #   Faker::X.user #=>  { data: [{:id=>"8821452687517076614", :name=>"Lincoln Paucek", :screen_name=>"cody"...
+      #
+      # @faker.version 1.7.3
+      def user
+        author_id = Faker::Number.between(from: 1, to: 9_223_372_036_854_775_807)
+
+        {
+          data: [
+            {
+              author_id: author_id.to_s,
+              id: id.to_s,
+              text: Faker::Lorem.sentence
+            }
+          ],
+          includes: {
+            users: [
+              {
+                public_metrics: {
+                  followers_count: Faker::Number.between(to: 1, from: 1_000),
+                  following_count: Faker::Number.between(to: 1, from: 200),
+                  tweet_count: Faker::Number.between(to: 1, from: 10_000),
+                  listed_count: Faker::Number.between(to: 1, from: 1_000)
+                },
+                username: screen_name,
+                pinned_tweet_id: Faker::Number.between(from: 1, to: 9_223_372_036_854_775_807).to_s,
+                entities: user_entities,
+                description: Faker::Lorem.sentence,
+                name: Faker::Name.name,
+                verified: Faker::Boolean.boolean(true_ratio: 0.1),
+                location: Faker::Internet.public_ip_v4_address,
+                id: author_id.to_s,
+                protected: Faker::Boolean.boolean(true_ratio: 0.1),
+                url: url,
+                profile_image_url: Faker::Avatar.image(slug: id, size: '48x48'),
+                created_at: created_at
+              }
+            ]
+          }
+        }
+      end
+
+      ##
+      # Produces a random X tweet with default attributes.
+      #
+      # @param include_user [Boolean] Include user details
+      # @param include_media [Boolean] Include user media (photo) details
+      # @return [Hash]
+      #
+      # @example
+      #   Faker::X.tweet #=> { data: [{:id=>"8821452687517076614", :text=>"Ea et laboriosam vel non."...
+      #   Faker::X.tweet(include_media: true) # Get a tweet object with a photo media attachment
+      #   Faker::X.tweet(include_user: true) # Get a tweet object with an user details
+      #
+      # @faker.version 1.7.3
+      def tweet(include_media: false, include_user: false)
+        tweet = {}
+        tweet_object = tweet_item
+        includes = {}
+
+        if include_media
+          media_key = Faker::Number.between(from: 1, to: 9_223_372_036_854_775_807).to_s
+
+          includes[:media] = media(media_key)
+          tweet_object[:attachments] = { media_keys: [media_key] }
+        end
+
+        includes[:users] = user[:includes][:users] if include_user
+
+        tweet[:data] = [
+          tweet_object
+        ]
+
+        unless includes.empty?
+          tweet[:includes] = includes
+        end
+
+        tweet
+      end
+
+      ##
+      # Produces a random screen_name.
+      #
+      # @return [String]
+      #
+      # @example
+      #   Faker::X.screen_name #=> "audreanne_hackett"
+      #
+      # @faker.version 1.7.3
+      def screen_name
+        Faker::Internet.username(specifier: nil, separators: ['_'])[0...20]
+      end
+
+      private
+
+      def id
+        Faker::Number.between(from: 1, to: 9_223_372_036_854_775_807)
+      end
+
+      def created_at
+        Faker::Date.between(from: '2006-03-21', to: ::Date.today).to_time.utc.iso8601(3)
+      end
+
+      def url
+        "https://t.co/#{Faker::Lorem.characters(number: 10)}"
+      end
+
+      def user_entities
+        entities = tweet_entities
+
+        {
+          url: {
+            urls: entities[:urls]
+          },
+          description: {
+            hashtags: entities[:hashtags]
+          }
+        }
+      end
+
+      def tweet_entities
+        display_url = Faker::Internet.url(host: 'example.com')
+
+        {
+          urls: [
+            {
+              start: 0,
+              end: 5,
+              url: url,
+              expanded_url: display_url,
+              display_url: display_url.sub('http://', '')
+            }
+          ],
+          hashtags: [
+            {
+              start: 0,
+              end: 5,
+              tag: Faker::Lorem.word.capitalize
+            }
+          ]
+        }
+      end
+
+      def media(media_key)
+        expanded_url = Faker::LoremFlickr.image(size: '1064x600')
+
+        [{
+          height: Faker::Number.between(to: 1, from: 1000),
+          media_key: media_key,
+          type: 'photo',
+          preview_image_url: expanded_url,
+          width: Faker::Number.between(to: 1, from: 2000),
+          alt_text: Faker::Lorem.sentence
+        }]
+      end
+
+      def tweet_item
+        conversation_id = id.to_s
+
+        {
+          id: conversation_id,
+          text: Faker::Lorem.sentence,
+          lang: sample(%w[en fr es pt it ja]),
+          conversation_id: conversation_id,
+          created_at: created_at,
+          author_id: Faker::Number.between(from: 1, to: 9_223_372_036_854_775_807).to_s,
+          public_metrics: {
+            retweet_count: Faker::Number.between(to: 1, from: 100),
+            reply_count: Faker::Number.between(to: 1, from: 20),
+            like_count: Faker::Number.between(to: 1, from: 25),
+            quote_count: Faker::Number.between(to: 1, from: 10)
+          },
+          possibly_sensitive: Faker::Boolean.boolean(true_ratio: 0.1),
+          entities: tweet_entities
+        }
+      end
+    end
+  end
 end