diff --git a/.eslintrc.js b/.eslintrc.js index a8d43c6f..871593f3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -22,38 +22,43 @@ module.exports = { '@typescript-eslint/naming-convention': [ 'warn', { - selector: ['class'], + selector: ['class', 'enum'], format: ['PascalCase'], }, { - selector: 'interface', + selector: ['interface'], format: ['PascalCase'], prefix: ['I'], }, { - selector: 'enum', + selector: ['typeAlias'], format: ['PascalCase'], - prefix: ['E'], }, { - selector: ['variableLike', 'memberLike'], + selector: ['memberLike', 'variableLike'], format: ['camelCase'], + leadingUnderscore: 'allow', }, { - selector: ['variableLike', 'property'], + selector: ['memberLike'], modifiers: ['private'], format: ['camelCase'], leadingUnderscore: 'require', }, { - selector: ['variableLike', 'memberLike'], + selector: ['memberLike'], modifiers: ['static', 'readonly'], format: ['UPPER_CASE'], }, { - selector: 'enumMember', + selector: ['enumMember'], format: ['UPPER_CASE'], }, + { + selector: ['variable'], + modifiers: ['global'], + format: ['PascalCase'], + }, ], '@typescript-eslint/explicit-function-return-type': 'error', '@typescript-eslint/explicit-module-boundary-types': 'error', diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 00000000..7e806edd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,57 @@ +name: '๐Ÿ› Bug report' +description: Report a bug to help us improve Rettiwt-API. +labels: ['triage', 'bug'] +body: + - type: markdown + attributes: + value: | + Before reporting a bug, please make sure you have read through our [documentation](https://rishikant181.github.io/Rettiwt-API/) and checked existing [issues](https://github.com/Rishikant181/Rettiwt-API/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc). + - type: textarea + id: env + attributes: + label: Environment + description: Please provide your environment details. You can use `node -v` and `npm list rettiwt-api` to fill this section. + placeholder: | + - Operating System: `Windows/Linux/macOS` + - Node Version: `v22.x.x` (Rettiwt-API requires NodeJS 20+) + - Rettiwt-API Version: `x.x.x` + - Package Manager: `npm/yarn/pnpm` + - CLI or Dependency: `CLI` or `Dependency` + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Reproduction + description: Please provide a minimal code snippet or CLI command that reproduces the issue. If possible, include the API_KEY usage and any relevant configuration. If the report is vague and has no reproduction, it may be closed automatically. + placeholder: | + ```ts + import { Rettiwt } from 'rettiwt-api'; + const rettiwt = new Rettiwt({ apiKey: '' }); + // ... + ``` + or + ```sh + rettiwt -k + ``` + validations: + required: true + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, mention it here. + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional context + description: If applicable, add any other context, configuration, or screenshots here. + - type: textarea + id: logs + attributes: + label: Logs + description: | + Please copy-paste any error logs or stack traces here. Avoid screenshots if possible. + render: shell-script diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 00000000..9e79917a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,20 @@ +name: '๐Ÿš€ Feature request (Rettiwt-API)' +description: Suggest an idea or enhancement for Rettiwt-API. +labels: ['triage', 'enhancement', 'feature-request'] +body: + - type: markdown + attributes: + value: | + Before requesting a feature, please make sure you have read through our [documentation](https://rishikant181.github.io/Rettiwt-API/) and checked existing [issues](https://github.com/Rishikant181/Rettiwt-API/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc). + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of the feature or enhancement. Include possible use cases, alternatives, and links to any prototype or related module. + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional context + description: If applicable, add any other context, configuration, or screenshots here. diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 00000000..f050283f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,15 @@ +name: '๐Ÿ’ฌ Question (Rettiwt-API)' +description: Ask a question about Rettiwt-API. +labels: ['question'] +body: + - type: markdown + attributes: + value: | + Before asking a question, please make sure you have read through our [documentation](https://rishikant181.github.io/Rettiwt-API/) and checked existing [issues](https://github.com/Rishikant181/Rettiwt-API/issues?q=is%3Aissue%20is%3Aopen%20sort%3Aupdated-desc). + - type: textarea + id: description + attributes: + label: Description + description: Please provide a clear and concise question. Include any relevant context, code snippets, or links to documentation. + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..2c2d621f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,32 @@ +## ๐Ÿ”— Related Issue + + + +## โ“ Type of Change + + + +- [ ] ๐Ÿ“– Documentation (docs, README, or comments) +- [ ] ๐Ÿž Bug fix (non-breaking fix for an issue) +- [ ] ๐Ÿ‘Œ Enhancement (improvement to existing functionality) +- [ ] โœจ New feature (adds new functionality) +- [ ] ๐Ÿงน Chore (build, tooling, dependencies) +- [ ] โš ๏ธ Breaking change (affects existing usage) + +## ๐Ÿ“š Description + + + +## ๐Ÿ“ Checklist + +- [ ] Issue/discussion linked above. +- [ ] Documentation updated (if needed). +- [ ] Code follows project conventions and ESLint rules. +- [ ] No sensitive data or credentials are included. + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..e9c8dc05 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: ci + +on: + push: + branches: + - dev + pull_request: + branches: + - dev +jobs: + ci: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest] + node: [22] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install node + uses: actions/setup-node@v4 + + - name: Install dependencies + run: npm install + + - name: Run Format check + run: npm run format:check + + - name: Run Lint check + run: npm run lint:check diff --git a/.gitignore b/.gitignore index 4340f8a9..086bf41a 100644 --- a/.gitignore +++ b/.gitignore @@ -121,6 +121,7 @@ dist # VSCode configs .vscode +.devcontainer # Stores VSCode versions used for testing VSCode extensions .vscode-test @@ -138,4 +139,7 @@ test # Documentation docs -.idea \ No newline at end of file +.idea + +# Debugging +src/debug.ts \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..f5b3ef39 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.21.0 \ No newline at end of file diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index b8867a43..00000000 --- a/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -nodejs 22.13.1 diff --git a/README.md b/README.md index 8124646a..7e1191f2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A CLI tool and an API for fetching data from Twitter for free! ## Prerequisites -- NodeJS 20 +- NodeJS 22 - A working Twitter account (optional) ## Installation @@ -29,9 +29,16 @@ Rettiwt-API can be used with or without logging in to Twitter. As such, the two - 'User' authentication (logging in) grants access to the following resources/actions: + - Direct Message Inbox + - Direct Message Conversations + - Direct Message Delete Conversation + - List Add Member + - List Details - List Members + - List Remove Member - List Tweets - Tweet Details - Single and Bulk + - Tweet Bookmark - Tweet Like - Tweet Likers - Tweet Media Upload @@ -42,12 +49,16 @@ Rettiwt-API can be used with or without logging in to Twitter. As such, the two - Tweet Schedule - Tweet Search - Tweet Stream + - Tweet Unbookmark - Tweet Unlike - Tweet Unpost - Tweet Unretweet - Tweet Unschedule - User Affiliates + - User Analytics (Only for Premium accounts) - User Bookmarks + - User Bookmark Folders + - User Bookmark Folder Tweets - User Details - Single (by ID and Username) and Bulk (by ID only) - User Follow - User Followed Feed @@ -55,13 +66,16 @@ Rettiwt-API can be used with or without logging in to Twitter. As such, the two - User Following - User Highlights - User Likes + - User Lists - User Media - User Notification - User Recommended Feed - User Replies Timeline + - User Search - User Subscriptions - User Timeline - User Unfollow + - User Profile Update By default, Rettiwt-API uses 'guest' authentication. If however, access to the full set of resources is required, 'user' authentication can be used. This is done by using the cookies associated with your Twitter/X account, and encoding them into an `API_KEY` for convenience. The said `API_KEY` can be obtained by using a browser extension, as follows: @@ -124,9 +138,10 @@ A new Rettiwt instance can be initialized using the following code snippets: - `const rettiwt = new Rettiwt()` (for 'guest' authentication) - `const rettiwt = new Rettiwt({ apiKey: API_KEY })` (for 'user' authentication) -The Rettiwt class has three members: +The Rettiwt class has four members: -- `list` memeber, for accessing resources related to lists. +- `dm` member, for accessing resources related to direct messages. +- `list` member, for accessing resources related to lists. - `tweet` member, for accessing resources related to tweets. - `user` member, for accessing resources related to users. @@ -143,7 +158,8 @@ When initializing a new Rettiwt instance, it can be configures using various par - `errorHandler` (interface) - The custom error handler to use. - `tidProvider` (interface) - The custom TID provider to use for generating transaction token. - `headers` (object) - Custom HTTP headers to append to the default headers. -- `delay` (number/function) - The delay to use between concurrent requests, can either be a number in milliseconds, or a function that returns the number. +- `delay` (number/function) - The delay to use between concurrent requests, can either be a number in milliseconds, or a function that returns the number. Default is 0 (no delay). +- `maxRetries` (number) - The maximum number of retries to use in case when a random error 404 is encountered. Default is 0 (no retries). Of these parameters, the following are hot-swappable, using their respective setters: @@ -209,7 +225,7 @@ const rettiwt = new Rettiwt({ apiKey: API_KEY }); */ rettiwt.tweet.search({ fromUsers: [''], - words: ['', ''] + includeWords: ['', ''] }) .then(data => { ... @@ -242,7 +258,7 @@ const rettiwt = new Rettiwt({ apiKey: API_KEY }); */ rettiwt.tweet.search({ fromUsers: [''], - words: ['', ''] + includeWords: ['', ''] }, count, data.next.value) .then(data => { ... @@ -279,6 +295,34 @@ Where, - `` is the username associated with the Twitter account. - `` is the password to the Twitter account. +## Using a custom error handler + +Out of the box, `Rettiwt`'s error handling is bare-minimum, only able to parse basic error messages. For advanced scenarios, where full error response might be required, in order to diagnose error reason, it's recommended to use a custom error handler, by implementing the `IErrorHandler` interface, as follows: + +```ts +import { Rettiwt, IErrorHandler } from 'rettiwt-api'; + +// Implementing an error handler +class CustomErrorHandler implements IErrorHandler { + /** + * This is where you handle the error yourself. + */ + public handler(error: unknown): void { + // The 'error' variable has the full, raw error response returned from Twitter. + /** + * You custom error handling logic goes here + */ + + console.log(`Raw Twitter Error: ${JSON.stringify(error)}`); + } +} + +// Now we'll use the implemented error handler while initializing Rettiwt +const rettiwt = new Rettiwt({ apiKey: '', errorHandler: CustomErrorHandler }); +``` + +You can then use the created `rettiwt` instance and your custom error handler will handler all the error responses, bypassing `Rettiwt`'s error handling logic. + ## Using a proxy For masking of IP address using a proxy server, use the following code snippet for instantiation of Rettiwt: @@ -398,7 +442,7 @@ rettiwt.user.details('') However, if further control over the raw response is required, Rettiwt-API provides the [`FetcherService`](https://rishikant181.github.io/Rettiwt-API/classes/FetcherService.html) class which provides direct access to the raw response, but keep in mind, this delegates the task of parsing and filtering the results to the consumer of the library. The following example demonstrates using the `FetcherService` class: ```ts -import { RettiwtConfig, FetcherService, EResourceType, IUserDetailsResponse } from 'rettiwt-api'; +import { RettiwtConfig, FetcherService, ResourceType, IUserDetailsResponse } from 'rettiwt-api'; // Creating the configuration for Rettiwt const config = new RettiwtConfig({ apiKey: '' }); @@ -408,7 +452,7 @@ const fetcher = new FetcherService(config); // Fetching the details of the given user fetcher - .request(EResourceType.USER_DETAILS_BY_USERNAME, { id: 'user1' }) + .request(ResourceType.USER_DETAILS_BY_USERNAME, { id: 'user1' }) .then((res) => { console.log(res); }) @@ -417,23 +461,39 @@ fetcher }); ``` -As demonstrated by the example, the raw data can be accessed by using the `request` method of the `FetcherService` class, which takes two parameters. The first parameter is the name of the requested resource, while the second is an object specifying the associated arguments required for the given resource. The complete list of resource type can be checked [here](https://rishikant181.github.io/Rettiwt-API/enums/AuthService.html#EResourceType). As for the resource specific argurments, they are the same as that of the methods of `Rettiwt` class' methods for the respective resources, but structured as an object. Notice how the `FetcherService` class takes the same arguments as the `Rettiwt` class, and the arguments have the same effects as they have in case of `Rettiwt` class. +As demonstrated by the example, the raw data can be accessed by using the `request` method of the `FetcherService` class, which takes two parameters. The first parameter is the name of the requested resource, while the second is an object specifying the associated arguments required for the given resource. The complete list of resource type can be checked [here](https://rishikant181.github.io/Rettiwt-API/enums/AuthService.html#ResourceType). As for the resource specific argurments, they are the same as that of the methods of `Rettiwt` class' methods for the respective resources, but structured as an object. Notice how the `FetcherService` class takes the same arguments as the `Rettiwt` class, and the arguments have the same effects as they have in case of `Rettiwt` class. #### Notes: - For for hot-swapping in case of using `FetcherService`, the setters are accessed from the `config` object as `config.apiKey = ...`, `config.proxyUrl = ...`, etc. +## Data serialization + +The data returned by all functions of `Rettiwt` are complex objects, containing non-serialized fields like `raw`. In order to get JSON-serializable data, all data objects returned by `Rettiwt` provide a function `toJSON()` which converts the data into a serializable JSON, whose type is described by their respective interfaces i.e, `ITweet` for `Tweet`, `IUser` for `User` and so on. + +For handling and processing of data returned by the functions, it's always advisable to serialize them using the `toJSON()` function. + ## Features So far, the following operations are supported: +### Direct Messages + +- [Getting the DM inbox](https://rishikant181.github.io/Rettiwt-API/classes/DirectMessageService.html#inbox) +- [Getting a specific conversation with full message history](https://rishikant181.github.io/Rettiwt-API/classes/DirectMessageService.html#conversation) +- [Deleting a conversation](https://rishikant181.github.io/Rettiwt-API/classes/DirectMessageService.html#deleteConversation) + ### List +- [Adding a member to a given Twitter list](https://rishikant181.github.io/Rettiwt-API/classes/ListService.html#addMember) +- [Getting the details of a given Twitter list](https://rishikant181.github.io/Rettiwt-API/classes/ListService.html#details) - [Getting the members of a given Twitter list](https://rishikant181.github.io/Rettiwt-API/classes/ListService.html#members) +- [Removing a member from a given Twitter list](https://rishikant181.github.io/Rettiwt-API/classes/ListService.html#removeMember) - [Getting the list of tweets from a given Twitter list](https://rishikant181.github.io/Rettiwt-API/classes/ListService.html#tweets) ### Tweets +- [Bookmarking a tweet](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#bookmark) - [Getting the details of a tweet/multiple tweets](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#details) - [Liking a tweet](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#like) - [Getting the list of users who liked your tweet](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#likers) @@ -444,6 +504,7 @@ So far, the following operations are supported: - [Scheduling a new tweet](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#schedule) - [Searching for the list of tweets that match a given filter](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#search) - [Streaming filtered tweets in pseudo-realtime](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#stream) +- [Unbookmarking a tweet](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#unbookmark) - [Unliking a tweet](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#unlike) - [Unposting a tweet](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#unpost) - [Unretweeting a tweet](https://rishikant181.github.io/Rettiwt-API/classes/TweetService.html#unretweet) @@ -453,7 +514,10 @@ So far, the following operations are supported: ### Users - [Getting the list of users affiliated with the given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#affiliates) +- [Getting the analytics of the logged-in user (premium accounts only)](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#analytics) - [Getting the list of tweets bookmarked by the logged-in user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#bookmarks) +- [Getting the list of bookmark folders of the logged-in user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#bookmarkFolders) +- [Getting the list of tweets in a specific bookmark folder](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#bookmarkFolderTweets) - [Getting the details of a user/multiple users](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#details) - [Following a given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#follow) - [Getting the followed feed of the logged-in user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#followed) @@ -461,12 +525,15 @@ So far, the following operations are supported: - [Getting the list of users who are followed by the given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#following) - [Getting the list of highlighted tweets of the given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#highlights) - [Getting the list of tweets liked by the logged-in user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#likes) +- [Getting the lists of the logged-in user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#lists) - [Getting the media timeline of the given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#media) - [Streaming notifications of the logged-in user in pseudo-realtime](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#notifications) - [Getting the recommended feed of the logged-in user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#recommended) - [Getting the replies timeline of the given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#replies) +- [Searching for a username](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#search) - [Getting the tweet timeline of the given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#timeline) - [Unfollowing a given user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#unfollow) +- [Updating the profile of the logged-in user](https://rishikant181.github.io/Rettiwt-API/classes/UserService.html#updateProfile) ## CLI Usage diff --git a/eslint.config.mjs b/eslint.config.mjs index a4a2c836..60d7d728 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -11,7 +11,7 @@ const compat = new FlatCompat({ export default [ { - ignores: ['dist/', 'node_modules/', 'docs/'], + ignores: ['dist/', 'node_modules/', 'docs/', 'playground/'], }, ...compat.extends('.eslintrc.js'), ]; diff --git a/package-lock.json b/package-lock.json index 066ef671..58ff6cb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,45 +1,215 @@ { "name": "rettiwt-api", - "version": "5.1.0-alpha.0", + "version": "6.3.0-alpha.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rettiwt-api", - "version": "5.1.0-alpha.0", + "version": "6.3.0-alpha.0", "license": "ISC", "dependencies": { - "axios": "1.8.4", - "chalk": "5.4.1", - "commander": "11.1.0", - "cookiejar": "2.1.4", - "https-proxy-agent": "7.0.6", - "node-html-parser": "7.0.1" + "axios": "^1.8.4", + "chalk": "^5.4.1", + "commander": "^11.1.0", + "cookiejar": "^2.1.4", + "https-proxy-agent": "^7.0.6", + "jsdom": "^27.2.0", + "node-html-parser": "^7.0.1", + "x-client-transaction-id": "^0.1.9" }, "bin": { "rettiwt": "dist/cli.js" }, "devDependencies": { - "@types/cookiejar": "2.1.5", - "@types/node": "22.13.1", - "@typescript-eslint/eslint-plugin": "8.24.0", - "@typescript-eslint/parser": "8.24.0", - "eslint": "9.20.1", - "eslint-plugin-import": "2.31.0", - "eslint-plugin-tsdoc": "0.4.0", - "nodemon": "3.1.9", - "prettier": "3.5.1", - "typedoc": "0.27.7", - "typescript": "5.7.3" + "@types/cookiejar": "^2.1.5", + "@types/jsdom": "^27.0.0", + "@types/node": "^22.13.1", + "@typescript-eslint/eslint-plugin": "^8.24.0", + "@typescript-eslint/parser": "^8.24.0", + "eslint": "^9.20.1", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-tsdoc": "^0.4.0", + "nodemon": "^3.1.9", + "prettier": "^3.5.1", + "typedoc": "^0.27.7", + "typescript": "^5.7.3" + }, + "engines": { + "node": "^22.21.0" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.23", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.23.tgz", + "integrity": "sha512-2kJ1HxBKzPLbmhZpxBiTZggjtgCwKg1ma5RHShxvd6zgqhDEdEkzpiwe7jLkI2p2BrZvFCXIihdoMkl1H39VnA==", + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz", + "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==", + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.1" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.4.tgz", + "integrity": "sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.2" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "license": "MIT" + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" }, "engines": { - "node": "^22.13.1" + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.16.tgz", + "integrity": "sha512-2SpS4/UaWQaGpBINyG5ZuCHnUDeVByOhvbkARwfmnfxDvTaj80yOI1cD8Tw93ICV5Fx4fnyDKWQZI1CDtcWyUg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { @@ -66,9 +236,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -81,9 +251,9 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -104,10 +274,20 @@ "node": "*" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/core": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", - "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -118,9 +298,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -142,9 +322,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -152,6 +332,16 @@ "concat-map": "0.0.1" } }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -166,13 +356,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", - "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", + "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -186,13 +379,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.10.0", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { @@ -200,9 +393,9 @@ } }, "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -277,9 +470,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -416,9 +609,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -432,6 +625,44 @@ "@types/unist": "*" } }, + "node_modules/@types/jsdom": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-27.0.0.tgz", + "integrity": "sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/jsdom/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@types/jsdom/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -447,15 +678,22 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz", - "integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==", + "version": "22.15.31", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.31.tgz", + "integrity": "sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -464,21 +702,21 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz", - "integrity": "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.0.tgz", + "integrity": "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/type-utils": "8.24.0", - "@typescript-eslint/utils": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/type-utils": "8.34.0", + "@typescript-eslint/utils": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -488,22 +726,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.34.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.0.tgz", - "integrity": "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.0.tgz", + "integrity": "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/typescript-estree": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/typescript-estree": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4" }, "engines": { @@ -515,18 +753,40 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.0.tgz", + "integrity": "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.34.0", + "@typescript-eslint/types": "^8.34.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz", - "integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.0.tgz", + "integrity": "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0" + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -536,17 +796,34 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz", + "integrity": "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.0.tgz", - "integrity": "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.0.tgz", + "integrity": "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.24.0", - "@typescript-eslint/utils": "8.24.0", + "@typescript-eslint/typescript-estree": "8.34.0", + "@typescript-eslint/utils": "8.34.0", "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -557,13 +834,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz", - "integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.0.tgz", + "integrity": "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==", "dev": true, "license": "MIT", "engines": { @@ -575,20 +852,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz", - "integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.0.tgz", + "integrity": "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", + "@typescript-eslint/project-service": "8.34.0", + "@typescript-eslint/tsconfig-utils": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -598,20 +877,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.0.tgz", - "integrity": "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.0.tgz", + "integrity": "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/typescript-estree": "8.24.0" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/typescript-estree": "8.34.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -622,17 +901,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz", - "integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.0.tgz", + "integrity": "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/types": "8.34.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -644,9 +923,9 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -657,9 +936,9 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -760,18 +1039,20 @@ } }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -781,18 +1062,19 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -894,13 +1176,13 @@ } }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz", + "integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -911,6 +1193,15 @@ "dev": true, "license": "MIT" }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -931,9 +1222,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -986,14 +1277,14 @@ } }, "node_modules/call-bound": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "get-intrinsic": "^1.2.6" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -1147,6 +1438,19 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -1159,6 +1463,39 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.3.tgz", + "integrity": "sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.0.3", + "@csstools/css-syntax-patches-for-csstree": "^1.0.14", + "css-tree": "^3.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -1214,9 +1551,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1230,6 +1567,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1377,9 +1720,9 @@ } }, "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "dev": true, "license": "MIT", "dependencies": { @@ -1387,18 +1730,18 @@ "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -1410,21 +1753,24 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", + "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", + "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -1433,7 +1779,7 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -1532,22 +1878,23 @@ } }, "node_modules/eslint": { - "version": "9.20.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", - "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz", + "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.11.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.20.0", - "@eslint/plugin-kit": "^0.2.5", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.28.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -1555,7 +1902,7 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", + "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", @@ -1676,9 +2023,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -1731,9 +2078,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1761,9 +2108,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -1789,9 +2136,9 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1811,6 +2158,16 @@ "node": ">=8" } }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1838,15 +2195,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1856,9 +2213,9 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1966,9 +2323,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", - "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, "license": "ISC", "dependencies": { @@ -2033,9 +2390,9 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, @@ -2076,14 +2433,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -2146,17 +2504,17 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "get-proto": "^1.0.0", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -2362,6 +2720,68 @@ "he": "bin/he" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -2375,10 +2795,22 @@ "node": ">= 14" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -2653,6 +3085,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2680,6 +3125,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -2847,9 +3298,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -2859,6 +3310,45 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "27.2.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.2.0.tgz", + "integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==", + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.23", + "@asamuzakjp/dom-selector": "^6.7.4", + "cssstyle": "^5.3.3", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2917,6 +3407,19 @@ "node": ">= 0.8.0" } }, + "node_modules/linkedom": { + "version": "0.18.11", + "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.11.tgz", + "integrity": "sha512-K03GU3FUlnhBAP0jPb7tN7YJl7LbjZx30Z8h6wgLXusnKF7+BEZvfEbdkN/lO9LfFzxN3S0ZAriDuJ/13dIsLA==", + "license": "ISC", + "dependencies": { + "css-select": "^5.1.0", + "cssom": "^0.5.0", + "html-escaper": "^3.0.3", + "htmlparser2": "^10.0.0", + "uhyphen": "^0.2.0" + } + }, "node_modules/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", @@ -2950,6 +3453,15 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/lunr": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", @@ -2984,6 +3496,12 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "license": "CC0-1.0" + }, "node_modules/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", @@ -3086,9 +3604,9 @@ } }, "node_modules/nodemon": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", - "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", "dev": true, "license": "MIT", "dependencies": { @@ -3115,9 +3633,9 @@ } }, "node_modules/nodemon/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -3338,6 +3856,30 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3399,9 +3941,9 @@ } }, "node_modules/prettier": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz", - "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", "bin": { @@ -3431,7 +3973,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3529,7 +4070,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3567,9 +4107,9 @@ } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -3656,10 +4196,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -3830,6 +4388,29 @@ "node": ">=10" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", @@ -3938,6 +4519,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, + "node_modules/tldts": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz", + "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.17" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", + "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3961,10 +4566,34 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ts-api-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", - "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { @@ -4079,9 +4708,9 @@ } }, "node_modules/typedoc": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.27.7.tgz", - "integrity": "sha512-K/JaUPX18+61W3VXek1cWC5gwmuLvYTOXJzBvD9W7jFvbPnefRnCHQCEPw7MSNrP/Hj7JJrhZtDDLKdcYm6ucg==", + "version": "0.27.9", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.27.9.tgz", + "integrity": "sha512-/z585740YHURLl9DN2jCWe6OW7zKYm6VoQ93H0sxZ1cwHQEQrUn5BJrEnkWhfzUdyO+BLGjnKUZ9iz9hKloFDw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4098,13 +4727,13 @@ "node": ">= 18" }, "peerDependencies": { - "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x" + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" } }, "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4122,6 +4751,12 @@ "dev": true, "license": "MIT" }, + "node_modules/uhyphen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", + "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==", + "license": "ISC" + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -4149,9 +4784,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, @@ -4165,6 +4800,61 @@ "punycode": "^2.1.0" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4249,16 +4939,17 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", - "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "for-each": "^0.3.3", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, @@ -4279,17 +4970,62 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/x-client-transaction-id": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/x-client-transaction-id/-/x-client-transaction-id-0.1.9.tgz", + "integrity": "sha512-CES4zgkJ0wbfFWm0qgdKphthyb+L7lVHymgOY15v6ivcWSx5p9lp5kzAed+BuqJSP7bS0GbQyJ16ONkRthgsUw==", + "license": "MIT", + "dependencies": { + "linkedom": "^0.18.9" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, "node_modules/yaml": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } }, "node_modules/yocto-queue": { @@ -4304,6 +5040,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "playground": { + "name": "rettiwt-playground", + "version": "1.0.0", + "extraneous": true, + "dependencies": { + "dotenv": "^17.2.0", + "rettiwt-api": "file:../" + } } } } diff --git a/package.json b/package.json index 6bfecbd4..869a47fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rettiwt-api", - "version": "5.1.0-alpha.0", + "version": "6.3.0-alpha.0", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "An API for fetching data from TwitterAPI, without any rate limits!", @@ -11,9 +11,10 @@ "build": "tsc", "prepare": "tsc", "format": "prettier --write .", + "format:check": "prettier --check .", "lint": "eslint --max-warnings 0 --fix .", - "docs": "typedoc --excludePrivate --excludeProtected --excludeInternal src/index.ts", - "debug": "nodemon ./dist/index.js --inspect=0.0.0.0:9229" + "lint:check": "eslint --max-warnings 0 .", + "docs": "typedoc --excludePrivate --excludeProtected --excludeInternal src/index.ts" }, "repository": { "type": "git", @@ -29,27 +30,30 @@ }, "homepage": "https://rishikant181.github.io/Rettiwt-API/", "engines": { - "node": "^22.13.1" + "node": "^22.21.0" }, "devDependencies": { - "@types/cookiejar": "2.1.5", - "@types/node": "22.13.1", - "@typescript-eslint/eslint-plugin": "8.24.0", - "@typescript-eslint/parser": "8.24.0", - "eslint": "9.20.1", - "eslint-plugin-import": "2.31.0", - "eslint-plugin-tsdoc": "0.4.0", - "nodemon": "3.1.9", - "prettier": "3.5.1", - "typedoc": "0.27.7", - "typescript": "5.7.3" + "@types/cookiejar": "^2.1.5", + "@types/jsdom": "^27.0.0", + "@types/node": "^22.13.1", + "@typescript-eslint/eslint-plugin": "^8.24.0", + "@typescript-eslint/parser": "^8.24.0", + "eslint": "^9.20.1", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-tsdoc": "^0.4.0", + "nodemon": "^3.1.9", + "prettier": "^3.5.1", + "typedoc": "^0.27.7", + "typescript": "^5.7.3" }, "dependencies": { - "axios": "1.8.4", - "chalk": "5.4.1", - "commander": "11.1.0", - "cookiejar": "2.1.4", - "https-proxy-agent": "7.0.6", - "node-html-parser": "7.0.1" + "axios": "^1.8.4", + "chalk": "^5.4.1", + "commander": "^11.1.0", + "cookiejar": "^2.1.4", + "https-proxy-agent": "^7.0.6", + "jsdom": "^27.2.0", + "node-html-parser": "^7.0.1", + "x-client-transaction-id": "^0.1.9" } } diff --git a/playground/.env.example b/playground/.env.example new file mode 100644 index 00000000..f5e6c1a3 --- /dev/null +++ b/playground/.env.example @@ -0,0 +1 @@ +API_KEY="" \ No newline at end of file diff --git a/playground/README.md b/playground/README.md new file mode 100644 index 00000000..2c132cc5 --- /dev/null +++ b/playground/README.md @@ -0,0 +1,53 @@ +# Rettiwt Playground + +This playground is intended for developers to test and experiment with features from the Rettiwt-API package in a local development environment. + +## Getting Started + +### Prerequisites + +- Node.js (v22 or higher recommended) +- npm (v7+ recommended for workspace support) + +### Setup + +1. **Install dependencies** + From the root of the monorepo, run: + + ```sh + npm install + ``` + + This will install dependencies for all workspaces, including `playground` and `src`. + +2. **Environment Variables** + Create a `.env` file in the `playground` directory with your API credentials: + ```env + API_KEY=your_api_key_here + ``` + +### Usage + +- The main entry point is [`index.js`](./index.js), which demonstrates usage of the Rettiwt-API. +- To run the playground: + ```sh + npm start --workspace=playground + ``` + or from the `playground` directory: + ```sh + npm start + ``` + +### Modifying Playground Code + +- Edit `index.js` to try different API features or test new functionality. +- The `rettiwt-api` dependency is linked via npm workspaces, so changes in `src` are immediately available in the playground after rebuilding if necessary. + +## Notes + +- This playground is for development and testing only. Do not use production credentials. +- For more advanced usage, add scripts or files as needed. + +--- + +For questions or issues, see the main project README or open an issue. diff --git a/playground/index.js b/playground/index.js new file mode 100644 index 00000000..2f51031f --- /dev/null +++ b/playground/index.js @@ -0,0 +1,15 @@ +import { Rettiwt } from 'rettiwt-api'; +import 'dotenv/config'; + +const rettiwt = new Rettiwt({ apiKey: process.env.API_KEY }); + +async function userDetails() { + try { + const user = await rettiwt.user.details(); + console.log(user); + } catch (error) { + console.error('Error fetching user details:', error); + } +} + +await userDetails(); diff --git a/playground/package.json b/playground/package.json new file mode 100644 index 00000000..1d7a00b4 --- /dev/null +++ b/playground/package.json @@ -0,0 +1,15 @@ +{ + "name": "rettiwt-playground", + "version": "1.0.0", + "description": "A playground for testing Rettiwt-API features and functionalities.", + "main": "index.js", + "type": "module", + "scripts": { + "start": "node index.js", + "test": "echo \"No tests specified\" && exit 0" + }, + "dependencies": { + "dotenv": "^17.2.0", + "rettiwt-api": "file:../" + } +} diff --git a/src/Rettiwt.ts b/src/Rettiwt.ts index 9e4c8548..1bb93bfa 100644 --- a/src/Rettiwt.ts +++ b/src/Rettiwt.ts @@ -1,4 +1,5 @@ import { RettiwtConfig } from './models/RettiwtConfig'; +import { DirectMessageService } from './services/public/DirectMessageService'; import { ListService } from './services/public/ListService'; import { TweetService } from './services/public/TweetService'; import { UserService } from './services/public/UserService'; @@ -49,6 +50,9 @@ export class Rettiwt { /** The configuration for Rettiwt. */ private _config: RettiwtConfig; + /** The instance used to fetch data related to direct messages. */ + public dm: DirectMessageService; + /** The instance used to fetch data related to lists. */ public list: ListService; @@ -65,11 +69,17 @@ export class Rettiwt { */ public constructor(config?: IRettiwtConfig) { this._config = new RettiwtConfig(config); + this.dm = new DirectMessageService(this._config); this.list = new ListService(this._config); this.tweet = new TweetService(this._config); this.user = new UserService(this._config); } + /** Get the current API key associated with this instance. */ + public get apiKey(): string | undefined { + return this._config.apiKey; + } + /** Set the API key for the current instance. */ public set apiKey(apiKey: string | undefined) { this._config.apiKey = apiKey; diff --git a/src/cli.ts b/src/cli.ts index 381843d4..a096cbaf 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,39 +2,47 @@ import { createCommand } from 'commander'; +import dm from './commands/DirectMessage'; import list from './commands/List'; import tweet from './commands/Tweet'; import user from './commands/User'; import { Rettiwt } from './Rettiwt'; // Creating a new commandline program -const program = createCommand('rettiwt') +const Program = createCommand('rettiwt') .description('A CLI tool for accessing the Twitter API for free!') .passThroughOptions() .enablePositionalOptions(); // Adding options -program - .option('-k, --key ', 'The API key to use for authentication') +Program.option('-k, --key ', 'The API key to use for authentication') .option('-l, --log', 'Enable logging to console') .option('-p, --proxy ', 'The URL to the proxy to use') - .option('-t, --timeout ', 'The timout (in milli-seconds) to use for requests'); + .option('-t, --timeout ', 'The timout (in milli-seconds) to use for requests') + .option( + '-r, --retries ', + 'The maximum number of retries to use, a value of 5 combined with a delay of 1000 is recommended', + ) + .option('-d, --delay ', 'The delay in milliseconds to use in-between successive requests'); // Parsing the program to get supplied options -program.parse(); +Program.parse(); // Initializing Rettiwt instance using the given options -const rettiwt: Rettiwt = new Rettiwt({ - apiKey: process.env.API_KEY ?? (program.opts().key as string), - logging: program.opts().log ? true : false, - proxyUrl: program.opts().proxy as URL, - timeout: program.opts().timeout ? Number(program.opts().timeout) : undefined, +const RettiwtInstance = new Rettiwt({ + apiKey: process.env.API_KEY ?? (Program.opts().key as string), + logging: Program.opts().log ? true : false, + proxyUrl: Program.opts().proxy as URL, + timeout: Program.opts().timeout ? Number(Program.opts().timeout) : undefined, + maxRetries: Program.opts().retries as number, + delay: Program.opts().delay as number, }); // Adding sub-commands -program.addCommand(list(rettiwt)); -program.addCommand(tweet(rettiwt)); -program.addCommand(user(rettiwt)); +Program.addCommand(dm(RettiwtInstance)); +Program.addCommand(list(RettiwtInstance)); +Program.addCommand(tweet(RettiwtInstance)); +Program.addCommand(user(RettiwtInstance)); // Finalizing the CLI -program.parse(); +Program.parse(); diff --git a/src/collections/Extractors.ts b/src/collections/Extractors.ts index b4e0fd32..b18b21f3 100644 --- a/src/collections/Extractors.ts +++ b/src/collections/Extractors.ts @@ -1,11 +1,23 @@ -import { EBaseType } from '../enums/Data'; +import { BaseType } from '../enums/Data'; +import { Analytics } from '../models/data/Analytics'; +import { BookmarkFolder } from '../models/data/BookmarkFolder'; +import { Conversation } from '../models/data/Conversation'; import { CursoredData } from '../models/data/CursoredData'; +import { Inbox } from '../models/data/Inbox'; +import { List } from '../models/data/List'; import { Notification } from '../models/data/Notification'; import { Tweet } from '../models/data/Tweet'; import { User } from '../models/data/User'; +import { IConversationTimelineResponse } from '../types/raw/dm/Conversation'; +import { IInboxInitialResponse } from '../types/raw/dm/InboxInitial'; +import { IInboxTimelineResponse } from '../types/raw/dm/InboxTimeline'; +import { IListMemberAddResponse } from '../types/raw/list/AddMember'; +import { IListDetailsResponse } from '../types/raw/list/Details'; import { IListMembersResponse } from '../types/raw/list/Members'; +import { IListMemberRemoveResponse } from '../types/raw/list/RemoveMember'; import { IListTweetsResponse } from '../types/raw/list/Tweets'; import { IMediaInitializeUploadResponse } from '../types/raw/media/InitalizeUpload'; +import { ITweetBookmarkResponse } from '../types/raw/tweet/Bookmark'; import { ITweetDetailsResponse } from '../types/raw/tweet/Details'; import { ITweetDetailsBulkResponse } from '../types/raw/tweet/DetailsBulk'; import { ITweetLikeResponse } from '../types/raw/tweet/Like'; @@ -16,11 +28,15 @@ import { ITweetRetweetResponse } from '../types/raw/tweet/Retweet'; import { ITweetRetweetersResponse } from '../types/raw/tweet/Retweeters'; import { ITweetScheduleResponse } from '../types/raw/tweet/Schedule'; import { ITweetSearchResponse } from '../types/raw/tweet/Search'; +import { ITweetUnbookmarkResponse } from '../types/raw/tweet/Unbookmark'; import { ITweetUnlikeResponse } from '../types/raw/tweet/Unlike'; import { ITweetUnpostResponse } from '../types/raw/tweet/Unpost'; import { ITweetUnretweetResponse } from '../types/raw/tweet/Unretweet'; import { ITweetUnscheduleResponse } from '../types/raw/tweet/Unschedule'; import { IUserAffiliatesResponse } from '../types/raw/user/Affiliates'; +import { IUserAnalyticsResponse } from '../types/raw/user/Analytics'; +import { IUserBookmarkFoldersResponse } from '../types/raw/user/BookmarkFolders'; +import { IUserBookmarkFolderTweetsResponse } from '../types/raw/user/BookmarkFolderTweets'; import { IUserBookmarksResponse } from '../types/raw/user/Bookmarks'; import { IUserDetailsResponse } from '../types/raw/user/Details'; import { IUserDetailsBulkResponse } from '../types/raw/user/DetailsBulk'; @@ -30,9 +46,12 @@ import { IUserFollowersResponse } from '../types/raw/user/Followers'; import { IUserFollowingResponse } from '../types/raw/user/Following'; import { IUserHighlightsResponse } from '../types/raw/user/Highlights'; import { IUserLikesResponse } from '../types/raw/user/Likes'; +import { IUserListsResponse } from '../types/raw/user/Lists'; import { IUserMediaResponse } from '../types/raw/user/Media'; import { IUserNotificationsResponse } from '../types/raw/user/Notifications'; +import { IUserProfileUpdateResponse } from '../types/raw/user/ProfileUpdate'; import { IUserRecommendedResponse } from '../types/raw/user/Recommended'; +import { IUserSearchResponse } from '../types/raw/user/Search'; import { IUserSubscriptionsResponse } from '../types/raw/user/Subscriptions'; import { IUserTweetsResponse } from '../types/raw/user/Tweets'; import { IUserTweetsAndRepliesResponse } from '../types/raw/user/TweetsAndReplies'; @@ -43,35 +62,47 @@ import { IUserUnfollowResponse } from '../types/raw/user/Unfollow'; * * @internal */ -export const extractors = { +export const Extractors = { /* eslint-disable @typescript-eslint/naming-convention */ + LIST_DETAILS: (response: IListDetailsResponse, id: string): List | undefined => List.single(response, id), LIST_MEMBERS: (response: IListMembersResponse): CursoredData => - new CursoredData(response, EBaseType.USER), + new CursoredData(response, BaseType.USER), + LIST_MEMBER_ADD: (response: IListMemberAddResponse): number | undefined => + response.data?.list?.member_count ?? undefined, + LIST_MEMBER_REMOVE: (response: IListMemberRemoveResponse): number | undefined => + response.data?.list?.member_count ?? undefined, LIST_TWEETS: (response: IListTweetsResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), MEDIA_UPLOAD_APPEND: (): void => undefined, MEDIA_UPLOAD_FINALIZE: (): void => undefined, MEDIA_UPLOAD_INITIALIZE: (response: IMediaInitializeUploadResponse): string => response.media_id_string ?? undefined, + DM_CONVERSATION: (response: IConversationTimelineResponse): Conversation | undefined => + Conversation.fromConversationTimeline(response), + DM_INBOX_INITIAL_STATE: (response: IInboxInitialResponse): Inbox => new Inbox(response), + DM_INBOX_TIMELINE: (response: IInboxTimelineResponse): Inbox => new Inbox(response), + + TWEET_BOOKMARK: (response: ITweetBookmarkResponse): boolean => response?.data?.tweet_bookmark_put === 'Done', TWEET_DETAILS: (response: ITweetDetailsResponse, id: string): Tweet | undefined => Tweet.single(response, id), TWEET_DETAILS_ALT: (response: ITweetRepliesResponse, id: string): Tweet | undefined => Tweet.single(response, id), TWEET_DETAILS_BULK: (response: ITweetDetailsBulkResponse, ids: string[]): Tweet[] => Tweet.multiple(response, ids), TWEET_LIKE: (response: ITweetLikeResponse): boolean => (response?.data?.favorite_tweet ? true : false), TWEET_LIKERS: (response: ITweetLikersResponse): CursoredData => - new CursoredData(response, EBaseType.USER), + new CursoredData(response, BaseType.USER), TWEET_POST: (response: ITweetPostResponse): string => response?.data?.create_tweet?.tweet_results?.result?.rest_id ?? undefined, TWEET_REPLIES: (response: ITweetDetailsResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), TWEET_RETWEET: (response: ITweetRetweetResponse): boolean => (response?.data?.create_retweet ? true : false), TWEET_RETWEETERS: (response: ITweetRetweetersResponse): CursoredData => - new CursoredData(response, EBaseType.USER), + new CursoredData(response, BaseType.USER), TWEET_SCHEDULE: (response: ITweetScheduleResponse): string => response?.data?.tweet?.rest_id ?? undefined, TWEET_SEARCH: (response: ITweetSearchResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), + TWEET_UNBOOKMARK: (response: ITweetUnbookmarkResponse): boolean => response?.data?.tweet_bookmark_delete === 'Done', TWEET_UNLIKE: (response: ITweetUnlikeResponse): boolean => (response?.data?.unfavorite_tweet ? true : false), TWEET_UNPOST: (response: ITweetUnpostResponse): boolean => (response?.data?.delete_tweet ? true : false), TWEET_UNRETWEET: (response: ITweetUnretweetResponse): boolean => @@ -79,37 +110,46 @@ export const extractors = { TWEET_UNSCHEDULE: (response: ITweetUnscheduleResponse): boolean => response?.data?.scheduledtweet_delete == 'Done', USER_AFFILIATES: (response: IUserAffiliatesResponse): CursoredData => - new CursoredData(response, EBaseType.USER), + new CursoredData(response, BaseType.USER), + USER_ANALYTICS: (response: IUserAnalyticsResponse): Analytics => + new Analytics(response.data.viewer_v2.user_results.result), USER_BOOKMARKS: (response: IUserBookmarksResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), + USER_BOOKMARK_FOLDERS: (response: IUserBookmarkFoldersResponse): CursoredData => + new CursoredData(response, BaseType.BOOKMARK_FOLDER), + USER_BOOKMARK_FOLDER_TWEETS: (response: IUserBookmarkFolderTweetsResponse): CursoredData => + new CursoredData(response, BaseType.TWEET), USER_DETAILS_BY_USERNAME: (response: IUserDetailsResponse): User | undefined => User.single(response), USER_DETAILS_BY_ID: (response: IUserDetailsResponse): User | undefined => User.single(response), USER_DETAILS_BY_IDS_BULK: (response: IUserDetailsBulkResponse, ids: string[]): User[] => User.multiple(response, ids), USER_FEED_FOLLOWED: (response: IUserFollowedResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), USER_FEED_RECOMMENDED: (response: IUserRecommendedResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), USER_FOLLOW: (response: IUserFollowResponse): boolean => (response?.id ? true : false), USER_FOLLOWING: (response: IUserFollowingResponse): CursoredData => - new CursoredData(response, EBaseType.USER), + new CursoredData(response, BaseType.USER), USER_FOLLOWERS: (response: IUserFollowersResponse): CursoredData => - new CursoredData(response, EBaseType.USER), + new CursoredData(response, BaseType.USER), USER_HIGHLIGHTS: (response: IUserHighlightsResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), + USER_LISTS: (response: IUserListsResponse): CursoredData => new CursoredData(response, BaseType.LIST), USER_LIKES: (response: IUserLikesResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), USER_MEDIA: (response: IUserMediaResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), USER_NOTIFICATIONS: (response: IUserNotificationsResponse): CursoredData => - new CursoredData(response, EBaseType.NOTIFICATION), + new CursoredData(response, BaseType.NOTIFICATION), + USER_SEARCH: (response: IUserSearchResponse): CursoredData => new CursoredData(response, BaseType.USER), USER_SUBSCRIPTIONS: (response: IUserSubscriptionsResponse): CursoredData => - new CursoredData(response, EBaseType.USER), + new CursoredData(response, BaseType.USER), USER_TIMELINE: (response: IUserTweetsResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), USER_TIMELINE_AND_REPLIES: (response: IUserTweetsAndRepliesResponse): CursoredData => - new CursoredData(response, EBaseType.TWEET), + new CursoredData(response, BaseType.TWEET), USER_UNFOLLOW: (response: IUserUnfollowResponse): boolean => (response?.id ? true : false), + USER_PROFILE_UPDATE: (response: IUserProfileUpdateResponse): boolean => (response?.name ? true : false), /* eslint-enable @typescript-eslint/naming-convention */ }; diff --git a/src/collections/Groups.ts b/src/collections/Groups.ts index d7962064..6cf0229d 100644 --- a/src/collections/Groups.ts +++ b/src/collections/Groups.ts @@ -1,14 +1,14 @@ -import { EResourceType } from '../enums/Resource'; +import { ResourceType } from '../enums/Resource'; /** * Collection of resources that allow guest authentication. * * @internal */ -export const allowGuestAuthentication = [ - EResourceType.TWEET_DETAILS, - EResourceType.USER_DETAILS_BY_USERNAME, - EResourceType.USER_TIMELINE, +export const AllowGuestAuthenticationGroup = [ + ResourceType.TWEET_DETAILS, + ResourceType.USER_DETAILS_BY_USERNAME, + ResourceType.USER_TIMELINE, ]; /** @@ -16,32 +16,41 @@ export const allowGuestAuthentication = [ * * @internal */ -export const fetchResources = [ - EResourceType.LIST_MEMBERS, - EResourceType.LIST_TWEETS, - EResourceType.TWEET_DETAILS, - EResourceType.TWEET_DETAILS_ALT, - EResourceType.TWEET_DETAILS_BULK, - EResourceType.TWEET_LIKERS, - EResourceType.TWEET_REPLIES, - EResourceType.TWEET_RETWEETERS, - EResourceType.TWEET_SEARCH, - EResourceType.USER_AFFILIATES, - EResourceType.USER_BOOKMARKS, - EResourceType.USER_DETAILS_BY_USERNAME, - EResourceType.USER_DETAILS_BY_ID, - EResourceType.USER_DETAILS_BY_IDS_BULK, - EResourceType.USER_FEED_FOLLOWED, - EResourceType.USER_FEED_RECOMMENDED, - EResourceType.USER_FOLLOWING, - EResourceType.USER_FOLLOWERS, - EResourceType.USER_HIGHLIGHTS, - EResourceType.USER_LIKES, - EResourceType.USER_MEDIA, - EResourceType.USER_NOTIFICATIONS, - EResourceType.USER_SUBSCRIPTIONS, - EResourceType.USER_TIMELINE, - EResourceType.USER_TIMELINE_AND_REPLIES, +export const FetchResourcesGroup = [ + ResourceType.LIST_DETAILS, + ResourceType.LIST_MEMBERS, + ResourceType.LIST_TWEETS, + ResourceType.DM_CONVERSATION, + ResourceType.DM_INBOX_INITIAL_STATE, + ResourceType.DM_INBOX_TIMELINE, + ResourceType.TWEET_DETAILS, + ResourceType.TWEET_DETAILS_ALT, + ResourceType.TWEET_DETAILS_BULK, + ResourceType.TWEET_LIKERS, + ResourceType.TWEET_REPLIES, + ResourceType.TWEET_RETWEETERS, + ResourceType.TWEET_SEARCH, + ResourceType.USER_AFFILIATES, + ResourceType.USER_ANALYTICS, + ResourceType.USER_BOOKMARKS, + ResourceType.USER_BOOKMARK_FOLDERS, + ResourceType.USER_BOOKMARK_FOLDER_TWEETS, + ResourceType.USER_DETAILS_BY_USERNAME, + ResourceType.USER_DETAILS_BY_ID, + ResourceType.USER_DETAILS_BY_IDS_BULK, + ResourceType.USER_FEED_FOLLOWED, + ResourceType.USER_FEED_RECOMMENDED, + ResourceType.USER_FOLLOWING, + ResourceType.USER_FOLLOWERS, + ResourceType.USER_HIGHLIGHTS, + ResourceType.USER_LIKES, + ResourceType.USER_LISTS, + ResourceType.USER_MEDIA, + ResourceType.USER_NOTIFICATIONS, + ResourceType.USER_SEARCH, + ResourceType.USER_SUBSCRIPTIONS, + ResourceType.USER_TIMELINE, + ResourceType.USER_TIMELINE_AND_REPLIES, ]; /** @@ -49,18 +58,24 @@ export const fetchResources = [ * * @internal */ -export const postResources = [ - EResourceType.MEDIA_UPLOAD_APPEND, - EResourceType.MEDIA_UPLOAD_FINALIZE, - EResourceType.MEDIA_UPLOAD_INITIALIZE, - EResourceType.TWEET_LIKE, - EResourceType.TWEET_POST, - EResourceType.TWEET_RETWEET, - EResourceType.TWEET_SCHEDULE, - EResourceType.TWEET_UNLIKE, - EResourceType.TWEET_UNPOST, - EResourceType.TWEET_UNRETWEET, - EResourceType.TWEET_UNSCHEDULE, - EResourceType.USER_FOLLOW, - EResourceType.USER_UNFOLLOW, +export const PostResourcesGroup = [ + ResourceType.LIST_MEMBER_ADD, + ResourceType.LIST_MEMBER_REMOVE, + ResourceType.MEDIA_UPLOAD_APPEND, + ResourceType.MEDIA_UPLOAD_FINALIZE, + ResourceType.MEDIA_UPLOAD_INITIALIZE, + ResourceType.DM_DELETE_CONVERSATION, + ResourceType.TWEET_BOOKMARK, + ResourceType.TWEET_LIKE, + ResourceType.TWEET_POST, + ResourceType.TWEET_RETWEET, + ResourceType.TWEET_SCHEDULE, + ResourceType.TWEET_UNBOOKMARK, + ResourceType.TWEET_UNLIKE, + ResourceType.TWEET_UNPOST, + ResourceType.TWEET_UNRETWEET, + ResourceType.TWEET_UNSCHEDULE, + ResourceType.USER_FOLLOW, + ResourceType.USER_UNFOLLOW, + ResourceType.USER_PROFILE_UPDATE, ]; diff --git a/src/collections/Requests.ts b/src/collections/Requests.ts index 0f7e1879..cdd7b969 100644 --- a/src/collections/Requests.ts +++ b/src/collections/Requests.ts @@ -1,6 +1,7 @@ import { AxiosRequestConfig } from 'axios'; -import { EResourceType } from '../enums/Resource'; +import { ResourceType } from '../enums/Resource'; +import { DMRequests } from '../requests/DirectMessage'; import { ListRequests } from '../requests/List'; import { MediaRequests } from '../requests/Media'; import { TweetRequests } from '../requests/Tweet'; @@ -8,23 +9,32 @@ import { UserRequests } from '../requests/User'; import { IFetchArgs } from '../types/args/FetchArgs'; import { IPostArgs } from '../types/args/PostArgs'; -import { rawTweetRepliesSortType } from './Tweet'; +import { TweetRepliesSortTypeMap } from './Tweet'; /** * Collection of requests to various resources. * * @internal */ -export const requests: { [key in keyof typeof EResourceType]: (args: IFetchArgs | IPostArgs) => AxiosRequestConfig } = { +export const Requests: { [key in keyof typeof ResourceType]: (args: IFetchArgs | IPostArgs) => AxiosRequestConfig } = { /* eslint-disable @typescript-eslint/naming-convention */ + LIST_DETAILS: (args: IFetchArgs) => ListRequests.details(args.id!), LIST_MEMBERS: (args: IFetchArgs) => ListRequests.members(args.id!, args.count, args.cursor), + LIST_MEMBER_ADD: (args: IPostArgs) => ListRequests.addMember(args.id!, args.userId!), + LIST_MEMBER_REMOVE: (args: IPostArgs) => ListRequests.removeMember(args.id!, args.userId!), LIST_TWEETS: (args: IFetchArgs) => ListRequests.tweets(args.id!, args.count, args.cursor), MEDIA_UPLOAD_APPEND: (args: IPostArgs) => MediaRequests.appendUpload(args.upload!.id!, args.upload!.media!), MEDIA_UPLOAD_FINALIZE: (args: IPostArgs) => MediaRequests.finalizeUpload(args.upload!.id!), MEDIA_UPLOAD_INITIALIZE: (args: IPostArgs) => MediaRequests.initializeUpload(args.upload!.size!), + DM_CONVERSATION: (args: IFetchArgs) => DMRequests.conversation(args.conversationId!, args.maxId), + DM_INBOX_INITIAL_STATE: () => DMRequests.inboxInitial(), + DM_INBOX_TIMELINE: (args: IFetchArgs) => DMRequests.inboxTimeline(args.maxId), + DM_DELETE_CONVERSATION: (args: IPostArgs) => DMRequests.deleteConversation(args.conversationId!), + + TWEET_BOOKMARK: (args: IPostArgs) => TweetRequests.bookmark(args.id!), TWEET_DETAILS: (args: IFetchArgs) => TweetRequests.details(args.id!), TWEET_DETAILS_ALT: (args: IFetchArgs) => TweetRequests.replies(args.id!), TWEET_DETAILS_BULK: (args: IFetchArgs) => TweetRequests.bulkDetails(args.ids!), @@ -32,18 +42,30 @@ export const requests: { [key in keyof typeof EResourceType]: (args: IFetchArgs TWEET_LIKERS: (args: IFetchArgs) => TweetRequests.likers(args.id!, args.count, args.cursor), TWEET_POST: (args: IPostArgs) => TweetRequests.post(args.tweet!), TWEET_REPLIES: (args: IFetchArgs) => - TweetRequests.replies(args.id!, args.cursor, args.sortBy ? rawTweetRepliesSortType[args.sortBy] : undefined), + TweetRequests.replies(args.id!, args.cursor, args.sortBy ? TweetRepliesSortTypeMap[args.sortBy] : undefined), TWEET_RETWEET: (args: IPostArgs) => TweetRequests.retweet(args.id!), TWEET_RETWEETERS: (args: IFetchArgs) => TweetRequests.retweeters(args.id!, args.count, args.cursor), TWEET_SCHEDULE: (args: IPostArgs) => TweetRequests.schedule(args.tweet!), TWEET_SEARCH: (args: IFetchArgs) => TweetRequests.search(args.filter!, args.count, args.cursor), + TWEET_UNBOOKMARK: (args: IPostArgs) => TweetRequests.unbookmark(args.id!), TWEET_UNLIKE: (args: IPostArgs) => TweetRequests.unlike(args.id!), TWEET_UNPOST: (args: IPostArgs) => TweetRequests.unpost(args.id!), TWEET_UNRETWEET: (args: IPostArgs) => TweetRequests.unretweet(args.id!), TWEET_UNSCHEDULE: (args: IPostArgs) => TweetRequests.unschedule(args.id!), USER_AFFILIATES: (args: IFetchArgs) => UserRequests.affiliates(args.id!, args.count, args.cursor), + USER_ANALYTICS: (args: IFetchArgs) => + UserRequests.analytics( + args.fromTime!, + args.toTime!, + args.granularity!, + args.metrics!, + args.showVerifiedFollowers!, + ), USER_BOOKMARKS: (args: IFetchArgs) => UserRequests.bookmarks(args.count, args.cursor), + USER_BOOKMARK_FOLDERS: (args: IFetchArgs) => UserRequests.bookmarkFolders(args.cursor), + USER_BOOKMARK_FOLDER_TWEETS: (args: IFetchArgs) => + UserRequests.bookmarkFolderTweets(args.id!, args.count, args.cursor), USER_DETAILS_BY_USERNAME: (args: IFetchArgs) => UserRequests.detailsByUsername(args.id!), USER_DETAILS_BY_ID: (args: IFetchArgs) => UserRequests.detailsById(args.id!), USER_DETAILS_BY_IDS_BULK: (args: IFetchArgs) => UserRequests.bulkDetailsByIds(args.ids!), @@ -54,12 +76,15 @@ export const requests: { [key in keyof typeof EResourceType]: (args: IFetchArgs USER_FOLLOWERS: (args: IFetchArgs) => UserRequests.followers(args.id!, args.count, args.cursor), USER_HIGHLIGHTS: (args: IFetchArgs) => UserRequests.highlights(args.id!, args.count, args.cursor), USER_LIKES: (args: IFetchArgs) => UserRequests.likes(args.id!, args.count, args.cursor), + USER_LISTS: (args: IFetchArgs) => UserRequests.lists(args.id!, args.count, args.cursor), USER_MEDIA: (args: IFetchArgs) => UserRequests.media(args.id!, args.count, args.cursor), USER_NOTIFICATIONS: (args: IFetchArgs) => UserRequests.notifications(args.count, args.cursor), + USER_SEARCH: (args: IFetchArgs) => UserRequests.search(args.id!, args.count, args.cursor), USER_SUBSCRIPTIONS: (args: IFetchArgs) => UserRequests.subscriptions(args.id!, args.count, args.cursor), USER_TIMELINE: (args: IFetchArgs) => UserRequests.tweets(args.id!, args.count, args.cursor), USER_TIMELINE_AND_REPLIES: (args: IFetchArgs) => UserRequests.tweetsAndReplies(args.id!, args.count, args.cursor), USER_UNFOLLOW: (args: IPostArgs) => UserRequests.unfollow(args.id!), + USER_PROFILE_UPDATE: (args: IPostArgs) => UserRequests.updateProfile(args.profileOptions!), /* eslint-enable @typescript-eslint/naming-convention */ }; diff --git a/src/collections/Tweet.ts b/src/collections/Tweet.ts index 3f24bce8..35e9e4c8 100644 --- a/src/collections/Tweet.ts +++ b/src/collections/Tweet.ts @@ -1,17 +1,17 @@ -import { ERawTweetRepliesSortType } from '../enums/raw/Tweet'; -import { ETweetRepliesSortType } from '../enums/Tweet'; +import { RawTweetRepliesSortType } from '../enums/raw/Tweet'; +import { TweetRepliesSortType } from '../enums/Tweet'; /** * Collection of mapping from parsed reply sort type to raw reply sort type. * * @internal */ -export const rawTweetRepliesSortType: { [key in keyof typeof ETweetRepliesSortType]: ERawTweetRepliesSortType } = { +export const TweetRepliesSortTypeMap: { [key in keyof typeof TweetRepliesSortType]: RawTweetRepliesSortType } = { /* eslint-disable @typescript-eslint/naming-convention */ - LATEST: ERawTweetRepliesSortType.LATEST, - LIKES: ERawTweetRepliesSortType.LIKES, - RELEVANCE: ERawTweetRepliesSortType.RELEVACE, + LATEST: RawTweetRepliesSortType.LATEST, + LIKES: RawTweetRepliesSortType.LIKES, + RELEVANCE: RawTweetRepliesSortType.RELEVACE, /* eslint-enable @typescript-eslint/naming-convention */ }; diff --git a/src/commands/DirectMessage.ts b/src/commands/DirectMessage.ts new file mode 100644 index 00000000..f152c725 --- /dev/null +++ b/src/commands/DirectMessage.ts @@ -0,0 +1,62 @@ +import { Command, createCommand } from 'commander'; + +import { output } from '../helper/CliUtils'; +import { Rettiwt } from '../Rettiwt'; + +/** + * Creates a new 'dm' command which uses the given Rettiwt instance. + * + * @param rettiwt - The Rettiwt instance to use. + * @returns The created 'dm' command. + */ +function createDirectMessageCommand(rettiwt: Rettiwt): Command { + // Creating the 'dm' command + const dm = createCommand('dm').description('Access resources related to direct messages'); + + // Conversation + dm.command('conversation') + .description('Get the full conversation history for a specific conversation') + .argument('', 'The ID of the conversation (e.g., "394028042-1712730991884689408")') + .argument('[cursor]', 'The cursor for pagination (maxId from previous response)') + .action(async (conversationId: string, cursor?: string) => { + try { + const conversation = await rettiwt.dm.conversation(conversationId, cursor); + output(conversation); + } catch (error) { + output(error); + } + }); + + // Delete conversation + dm.command('delete-conversation') + .description('Delete a conversation (you will leave the conversation and it will be removed from your inbox)') + .argument('', 'The ID of the conversation to delete') + .action(async (conversationId: string) => { + try { + await rettiwt.dm.deleteConversation(conversationId); + output({ success: true, message: 'Conversation deleted successfully' }); + } catch (error) { + output(error); + } + }); + + // Inbox + dm.command('inbox') + .description('Get your DM inbox') + .argument( + '[cursor]', + 'The cursor to the batch of conversations to fetch. If not provided, initial inbox is fetched', + ) + .action(async (cursor?: string) => { + try { + const inbox = await rettiwt.dm.inbox(cursor); + output(inbox); + } catch (error) { + output(error); + } + }); + + return dm; +} + +export default createDirectMessageCommand; diff --git a/src/commands/List.ts b/src/commands/List.ts index 52c59813..e074bc25 100644 --- a/src/commands/List.ts +++ b/src/commands/List.ts @@ -11,12 +11,39 @@ import { Rettiwt } from '../Rettiwt'; */ function createListCommand(rettiwt: Rettiwt): Command { // Creating the 'list' command - const list = createCommand('list').description('Access resources releated to lists'); + const list = createCommand('list').description('Access resources related to lists'); + + // Add member + list.command('add-member') + .description('Add a new member to a list') + .argument('', 'The ID of the tweet list') + .argument('', 'The ID of the user to add') + .action(async (listId: string, userId: string) => { + try { + const memberCount = await rettiwt.list.addMember(listId, userId); + output(memberCount); + } catch (error) { + output(error); + } + }); + + // Details + list.command('details') + .description('Fetch the details of a list') + .argument('', 'The ID of the tweet list') + .action(async (id: string) => { + try { + const details = await rettiwt.list.details(id); + output(details); + } catch (error) { + output(error); + } + }); // Members list.command('members') .description('Fetch the list of members of the given tweet list') - .argument('', 'The id of the tweet list') + .argument('', 'The ID of the tweet list') .argument('[count]', 'The number of members to fetch') .argument('[cursor]', 'The cursor to the batch of members to fetch') .action(async (id: string, count?: string, cursor?: string) => { @@ -28,10 +55,24 @@ function createListCommand(rettiwt: Rettiwt): Command { } }); + // Remove member + list.command('remove-member') + .description('Remove a new member from a list') + .argument('', 'The ID of the tweet list') + .argument('', 'The ID of the user to remove') + .action(async (listId: string, userId: string) => { + try { + const memberCount = await rettiwt.list.removeMember(listId, userId); + output(memberCount); + } catch (error) { + output(error); + } + }); + // Tweets list.command('tweets') .description('Fetch the list of tweets in the tweet list with the given id') - .argument('', 'The id of the tweet list') + .argument('', 'The ID of the tweet list') .argument('[count]', 'The number of tweets to fetch') .argument('[cursor]', 'The cursor to the batch of tweets to fetch') .action(async (id: string, count?: string, cursor?: string) => { diff --git a/src/commands/Tweet.ts b/src/commands/Tweet.ts index 907c35e7..bce6d4fe 100644 --- a/src/commands/Tweet.ts +++ b/src/commands/Tweet.ts @@ -1,5 +1,6 @@ import { Command, createCommand } from 'commander'; +import { TweetRepliesSortType } from '../enums/Tweet'; import { output } from '../helper/CliUtils'; import { TweetFilter } from '../models/args/FetchArgs'; import { Rettiwt } from '../Rettiwt'; @@ -13,7 +14,21 @@ import { ITweetFilter } from '../types/args/FetchArgs'; */ function createTweetCommand(rettiwt: Rettiwt): Command { // Creating the 'tweet' command - const tweet = createCommand('tweet').description('Access resources releated to tweets'); + const tweet = createCommand('tweet').description('Access resources related to tweets'); + + // Bookmark + tweet + .command('bookmark') + .description('Bookmark a tweet') + .argument('', 'The tweet to bookmark') + .action(async (id: string) => { + try { + const result = await rettiwt.tweet.bookmark(id); + output(result); + } catch (error) { + output(error); + } + }); // Details tweet @@ -63,8 +78,8 @@ function createTweetCommand(rettiwt: Rettiwt): Command { .argument('[cursor]', 'The cursor to the batch of likers to fetch') .action(async (id: string, count?: string, cursor?: string) => { try { - const tweets = await rettiwt.tweet.likers(id, count ? parseInt(count) : undefined, cursor); - output(tweets); + const users = await rettiwt.tweet.likers(id, count ? parseInt(count) : undefined, cursor); + output(users); } catch (error) { output(error); } @@ -95,6 +110,34 @@ function createTweetCommand(rettiwt: Rettiwt): Command { } }); + // Replies + tweet + .command('replies') + .description( + 'Fetch the list of replies to a tweet, with the first batch containing the whole thread, if the tweet is/part of a thread', + ) + .argument('', 'The id of the tweet') + .argument('[cursor]', 'The cursor to the batch of replies to fetch') + .option('-s, --sort-by ', 'Sort the tweets by likes, latest or relevance, default is latest') + .action(async (id: string, cursor?: string, options?: { sortBy: string }) => { + try { + // Determining the sort type + let sortType: TweetRepliesSortType | undefined = undefined; + if (options?.sortBy === 'likes') { + sortType = TweetRepliesSortType.LIKES; + } else if (options?.sortBy === 'latest') { + sortType = TweetRepliesSortType.LATEST; + } else if (options?.sortBy === 'relevance') { + sortType = TweetRepliesSortType.RELEVANCE; + } + + const tweets = await rettiwt.tweet.replies(id, cursor, sortType); + output(tweets); + } catch (error) { + output(error); + } + }); + // Retweet tweet .command('retweet') @@ -118,8 +161,8 @@ function createTweetCommand(rettiwt: Rettiwt): Command { .argument('[cursor]', 'The cursor to the batch of retweeters to fetch') .action(async (id: string, count?: string, cursor?: string) => { try { - const tweets = await rettiwt.tweet.retweeters(id, count ? parseInt(count) : undefined, cursor); - output(tweets); + const users = await rettiwt.tweet.retweeters(id, count ? parseInt(count) : undefined, cursor); + output(users); } catch (error) { output(error); } @@ -214,6 +257,20 @@ function createTweetCommand(rettiwt: Rettiwt): Command { } }); + // Unbookmark + tweet + .command('unbookmark') + .description('Unbookmark a tweet') + .argument('', 'The id of the tweet') + .action(async (id: string) => { + try { + const result = await rettiwt.tweet.unbookmark(id); + output(result); + } catch (error) { + output(error); + } + }); + // Unlike tweet .command('unlike') diff --git a/src/commands/User.ts b/src/commands/User.ts index 4a57c6e6..91c1ef5a 100644 --- a/src/commands/User.ts +++ b/src/commands/User.ts @@ -1,5 +1,6 @@ import { Command, createCommand } from 'commander'; +import { RawAnalyticsGranularity, RawAnalyticsMetric } from '../enums/raw/Analytics'; import { output } from '../helper/CliUtils'; import { Rettiwt } from '../Rettiwt'; @@ -11,7 +12,7 @@ import { Rettiwt } from '../Rettiwt'; */ function createUserCommand(rettiwt: Rettiwt): Command { // Creating the 'user' command - const user = createCommand('user').description('Access resources releated to users'); + const user = createCommand('user').description('Access resources related to users'); // Affiliates user.command('affiliates') @@ -28,6 +29,44 @@ function createUserCommand(rettiwt: Rettiwt): Command { } }); + // Analytics + user.command('analytics') + .description('Fetch the analytics of the logged-in user (premium accounts only)') + .option('-f, --from-time ', 'The start time for fetching analytics') + .option('-t, --to-time ', 'The end time for fetching analytics') + .option( + '-g, --granularity ', + 'The granularity of the analytics data. Defaults to daily. Check https://rishikant181.github.io/Rettiwt-API/enums/RawAnalyticsGranularity.html for granularity options', + ) + .option( + '-m, --metrics ', + 'Comma-separated list of metrics required. Check https://rishikant181.github.io/Rettiwt-API/enums/RawAnalyticsMetric.html for available metrics', + ) + .option( + '-v, --verified-followers', + 'Whether to include verified follower count and relationship counts in the response. Defaults to true', + ) + .action(async (options?: UserAnalyticsOptions) => { + try { + const analytics = await rettiwt.user.analytics( + options?.fromTime ? new Date(options.fromTime) : undefined, + options?.toTime ? new Date(options.toTime) : undefined, + options?.granularity + ? RawAnalyticsGranularity[options.granularity as keyof typeof RawAnalyticsGranularity] + : undefined, + options?.metrics + ? options.metrics + .split(',') + .map((item) => RawAnalyticsMetric[item as keyof typeof RawAnalyticsMetric]) + : undefined, + options?.verifiedFollowers, + ); + output(analytics); + } catch (error) { + output(error); + } + }); + user.command('bookmarks') .description('Fetch your list of bookmarks') .argument('[count]', 'The number of bookmarks to fetch') @@ -41,6 +80,36 @@ function createUserCommand(rettiwt: Rettiwt): Command { } }); + user.command('bookmark-folders') + .description('Fetch your list of bookmark folders') + .argument('[cursor]', 'The cursor to the batch of bookmark folders to fetch') + .action(async (cursor?: string) => { + try { + const folders = await rettiwt.user.bookmarkFolders(cursor); + output(folders); + } catch (error) { + output(error); + } + }); + + user.command('bookmark-folder-tweets') + .description('Fetch tweets from a specific bookmark folder') + .argument('', 'The ID of the bookmark folder') + .argument('[count]', 'The number of tweets to fetch') + .argument('[cursor]', 'The cursor to the batch of tweets to fetch') + .action(async (folderId: string, count?: string, cursor?: string) => { + try { + const tweets = await rettiwt.user.bookmarkFolderTweets( + folderId, + count ? parseInt(count) : undefined, + cursor, + ); + output(tweets); + } catch (error) { + output(error); + } + }); + // Details user.command('details') .description('Fetch the details of the user with the given id/username') @@ -150,6 +219,20 @@ function createUserCommand(rettiwt: Rettiwt): Command { } }); + // Lists + user.command('lists') + .description('Fetch your lists') + .argument('[count]', 'The number of lists to fetch') + .argument('[cursor]', 'The cursor to the batch of lists to fetch') + .action(async (count?: string, cursor?: string) => { + try { + const lists = await rettiwt.user.lists(count ? parseInt(count) : undefined, cursor); + output(lists); + } catch (error) { + output(error); + } + }); + // Media user.command('media') .description('Fetch the media timeline the given user') @@ -193,6 +276,21 @@ function createUserCommand(rettiwt: Rettiwt): Command { } }); + // Replies + user.command('search') + .description('Search for a username') + .argument('', 'The username to search for') + .argument('[count]', 'The number of results to fetch') + .argument('[cursor]', 'The cursor to the batch of results to fetch') + .action(async (userName: string, count?: string, cursor?: string) => { + try { + const replies = await rettiwt.user.search(userName, count ? parseInt(count) : undefined, cursor); + output(replies); + } catch (error) { + output(error); + } + }); + // Timeline user.command('timeline') .description('Fetch the tweets timeline the given user') @@ -221,7 +319,49 @@ function createUserCommand(rettiwt: Rettiwt): Command { } }); + // Update Profile + user.command('update-profile') + .description('Update your profile information') + .option('-n, --name ', 'Display name (max 50 characters)') + .option('-u, --url ', 'Profile URL') + .option('-l, --location ', 'Location (max 30 characters)') + .option('-d, --description ', 'Description/bio (max 160 characters)') + .action(async (options?: UserProfileUpdateOptions) => { + try { + const result = await rettiwt.user.updateProfile({ + name: options?.name, + url: options?.url, + location: options?.location, + description: options?.description, + }); + output(result); + } catch (error) { + output(error); + } + }); + return user; } +/** + * The options for fetching user analytics. + */ +type UserAnalyticsOptions = { + fromTime?: string; + toTime?: string; + granularity?: string; + metrics?: string; + verifiedFollowers?: boolean; +}; + +/** + * The options for updating user profile. + */ +type UserProfileUpdateOptions = { + name?: string; + url?: string; + location?: string; + description?: string; +}; + export default createUserCommand; diff --git a/src/enums/Api.ts b/src/enums/Api.ts index 1dcc56c2..94003c0e 100644 --- a/src/enums/Api.ts +++ b/src/enums/Api.ts @@ -3,7 +3,7 @@ * * @public */ -export enum EApiErrors { +export enum ApiErrors { COULD_NOT_AUTHENTICATE = 'Failed to authenticate', BAD_AUTHENTICATION = 'Invalid authentication data', RESOURCE_NOT_ALLOWED = 'Not authorized to access requested resource', diff --git a/src/enums/Authentication.ts b/src/enums/Authentication.ts index b091a8f9..402ce2b2 100644 --- a/src/enums/Authentication.ts +++ b/src/enums/Authentication.ts @@ -3,7 +3,7 @@ * * @public */ -export enum EAuthenticationType { +export enum AuthenticationType { GUEST = 'GUEST', USER = 'USER', LOGIN = 'LOGIN', diff --git a/src/enums/Data.ts b/src/enums/Data.ts index 2718a33f..dba20539 100644 --- a/src/enums/Data.ts +++ b/src/enums/Data.ts @@ -3,8 +3,11 @@ * * @internal */ -export enum EBaseType { +export enum BaseType { + DIRECT_MESSAGE = 'DIRECT_MESSAGE', NOTIFICATION = 'NOTIFICATION', TWEET = 'TWEET', USER = 'USER', + LIST = 'LIST', + BOOKMARK_FOLDER = 'BOOKMARK_FOLDER', } diff --git a/src/enums/Logging.ts b/src/enums/Logging.ts index 34746fb8..cf537f7c 100644 --- a/src/enums/Logging.ts +++ b/src/enums/Logging.ts @@ -3,7 +3,7 @@ * * @internal */ -export enum ELogActions { +export enum LogActions { AUTHORIZATION = 'AUTHORIZATION', DESERIALIZE = 'DESERIALIZE', EXTRACT = 'EXTRACT', diff --git a/src/enums/Media.ts b/src/enums/Media.ts index 982e7192..38198855 100644 --- a/src/enums/Media.ts +++ b/src/enums/Media.ts @@ -3,7 +3,7 @@ * * @public */ -export enum EMediaType { +export enum MediaType { PHOTO = 'PHOTO', VIDEO = 'VIDEO', GIF = 'GIF', diff --git a/src/enums/Notification.ts b/src/enums/Notification.ts index ca59ea41..754fc63a 100644 --- a/src/enums/Notification.ts +++ b/src/enums/Notification.ts @@ -3,7 +3,7 @@ * * @public */ -export enum ENotificationType { +export enum NotificationType { RECOMMENDATION = 'RECOMMENDATION', INFORMATION = 'INFORMATION', LIVE = 'LIVE', diff --git a/src/enums/Resource.ts b/src/enums/Resource.ts index 7b7c75c3..24b36aad 100644 --- a/src/enums/Resource.ts +++ b/src/enums/Resource.ts @@ -3,8 +3,11 @@ * * @public */ -export enum EResourceType { +export enum ResourceType { // LIST + LIST_DETAILS = 'LIST_DETAILS', + LIST_MEMBER_ADD = 'LIST_MEMBER_ADD', + LIST_MEMBER_REMOVE = 'LIST_MEMBER_REMOVE', LIST_MEMBERS = 'LIST_MEMBERS', LIST_TWEETS = 'LIST_TWEETS', @@ -13,7 +16,14 @@ export enum EResourceType { MEDIA_UPLOAD_FINALIZE = 'MEDIA_UPLOAD_FINALIZE', MEDIA_UPLOAD_INITIALIZE = 'MEDIA_UPLOAD_INITIALIZE', + // DM + DM_CONVERSATION = 'DM_CONVERSATION', + DM_INBOX_INITIAL_STATE = 'DM_INBOX_INITIAL_STATE', + DM_INBOX_TIMELINE = 'DM_INBOX_TIMELINE', + DM_DELETE_CONVERSATION = 'DM_DELETE_CONVERSATION', + // TWEET + TWEET_BOOKMARK = 'TWEET_BOOKMARK', TWEET_DETAILS = 'TWEET_DETAILS', TWEET_DETAILS_ALT = 'TWEET_DETAILS_ALT', TWEET_DETAILS_BULK = 'TWEET_DETAILS_BULK', @@ -25,6 +35,7 @@ export enum EResourceType { TWEET_RETWEETERS = 'TWEET_RETWEETERS', TWEET_SCHEDULE = 'TWEET_SCHEDULE', TWEET_SEARCH = 'TWEET_SEARCH', + TWEET_UNBOOKMARK = 'TWEET_UNBOOKMARK', TWEET_UNLIKE = 'TWEET_UNLIKE', TWEET_UNPOST = 'TWEET_UNPOST', TWEET_UNRETWEET = 'TWEET_UNRETWEET', @@ -32,7 +43,10 @@ export enum EResourceType { // USER USER_AFFILIATES = 'USER_AFFILIATES', + USER_ANALYTICS = 'USER_ANALYTICS', USER_BOOKMARKS = 'USER_BOOKMARKS', + USER_BOOKMARK_FOLDERS = 'USER_BOOKMARK_FOLDERS', + USER_BOOKMARK_FOLDER_TWEETS = 'USER_BOOKMARK_FOLDER_TWEETS', USER_DETAILS_BY_USERNAME = 'USER_DETAILS_BY_USERNAME', USER_DETAILS_BY_ID = 'USER_DETAILS_BY_ID', USER_DETAILS_BY_IDS_BULK = 'USER_DETAILS_BY_IDS_BULK', @@ -43,10 +57,13 @@ export enum EResourceType { USER_FOLLOWERS = 'USER_FOLLOWERS', USER_HIGHLIGHTS = 'USER_HIGHLIGHTS', USER_LIKES = 'USER_LIKES', + USER_LISTS = 'USER_LISTS', USER_MEDIA = 'USER_MEDIA', USER_NOTIFICATIONS = 'USER_NOTIFICATIONS', + USER_SEARCH = 'USER_SEARCH', USER_SUBSCRIPTIONS = 'USER_SUBSCRIPTIONS', USER_TIMELINE = 'USER_TIMELINE', USER_TIMELINE_AND_REPLIES = 'USER_TIMELINE_AND_REPLIES', USER_UNFOLLOW = 'USER_UNFOLLOW', + USER_PROFILE_UPDATE = 'USER_PROFILE_UPDATE', } diff --git a/src/enums/Tweet.ts b/src/enums/Tweet.ts index 195dd0b2..7ebf0353 100644 --- a/src/enums/Tweet.ts +++ b/src/enums/Tweet.ts @@ -1,7 +1,7 @@ /** * The different types of sorting options when fetching replies to tweets. */ -export enum ETweetRepliesSortType { +export enum TweetRepliesSortType { LIKES = 'LIKES', LATEST = 'LATEST', RELEVANCE = 'RELEVANCE', diff --git a/src/enums/raw/Analytics.ts b/src/enums/raw/Analytics.ts index a21fee26..3c70d51c 100644 --- a/src/enums/raw/Analytics.ts +++ b/src/enums/raw/Analytics.ts @@ -3,7 +3,7 @@ * * @public */ -export enum ERawAnalyticsGranularity { +export enum RawAnalyticsGranularity { DAILY = 'Daily', WEEKLY = 'Weekly', MONTHLY = 'Monthly', @@ -14,16 +14,19 @@ export enum ERawAnalyticsGranularity { * * @public */ -export enum ERawAnalyticsMetric { +export enum RawAnalyticsMetric { ENGAGEMENTS = 'Engagements', IMPRESSIONS = 'Impressions', PROFILE_VISITS = 'ProfileVisits', FOLLOWS = 'Follows', - VIDEO_VIEWS = 'VideoViews', REPLIES = 'Replies', LIKES = 'Likes', RETWEETS = 'Retweets', - MEDIA_VIEWS = 'MediaViews', BOOKMARK = 'Bookmark', SHARE = 'Share', + URL_CLICKS = 'UrlClicks', + CREATE_TWEET = 'CreateTweet', + CREATE_QUOTE = 'CreateQuote', + CREATE_REPLY = 'CreateReply', + UNFOLLOWS = 'Unfollows', } diff --git a/src/enums/raw/Media.ts b/src/enums/raw/Media.ts index 65cf0417..0abb4393 100644 --- a/src/enums/raw/Media.ts +++ b/src/enums/raw/Media.ts @@ -3,7 +3,7 @@ * * @public */ -export enum ERawMediaType { +export enum RawMediaType { PHOTO = 'photo', VIDEO = 'video', GIF = 'animated_gif', diff --git a/src/enums/raw/Notification.ts b/src/enums/raw/Notification.ts index 0723419f..47973fcd 100644 --- a/src/enums/raw/Notification.ts +++ b/src/enums/raw/Notification.ts @@ -3,7 +3,7 @@ * * @public */ -export enum ERawNotificationType { +export enum RawNotificationType { RECOMMENDATION = 'recommendation_icon', INFORMATION = 'bird_icon', LIVE = 'live_icon', diff --git a/src/enums/raw/Tweet.ts b/src/enums/raw/Tweet.ts index 13a6dcbe..9aa7a06b 100644 --- a/src/enums/raw/Tweet.ts +++ b/src/enums/raw/Tweet.ts @@ -3,7 +3,7 @@ * * @public */ -export enum ERawTweetSearchResultType { +export enum RawTweetSearchResultType { LATEST = 'Latest', TOP = 'Top', } @@ -13,7 +13,7 @@ export enum ERawTweetSearchResultType { * * @public */ -export enum ERawTweetRepliesSortType { +export enum RawTweetRepliesSortType { RELEVACE = 'Relevance', LATEST = 'Recency', LIKES = 'Likes', diff --git a/src/helper/TidUtils.ts b/src/helper/TidUtils.ts deleted file mode 100644 index 15d3213d..00000000 --- a/src/helper/TidUtils.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { createHash } from 'node:crypto'; - -import { ITidParams } from '../types/auth/TidParams'; - -export function getUnixTime(): number { - const [seconds, nanoseconds] = process.hrtime(); - return Date.now() / 1000 + seconds + nanoseconds / 1e9; -} - -export function getNanosecondPrecisionTime(): number { - return Number(((BigInt(Date.now()) * 1000000n + process.hrtime.bigint()) * 1000n) / BigInt(1000000)) / 1000000; -} - -export function calculateClientTransactionIdHeader(args: ITidParams): string { - const time = Math.floor(((args.time || getUnixTime()) * 1000 - 1682924400 * 1000) / 1000); - const timeBuffer = new Uint8Array(new Uint32Array([time]).buffer); - - const keyBytes = Array.from(Buffer.from(args.verificationKey, 'base64')); - const animationKey = args.animationKey || getAnimationKey(keyBytes, args.frames, args.indices); - - const value = [args.method, args.path, time].join('!') + args.keyword + animationKey; - const valueEncoded = new TextEncoder().encode(value); - - const hash = createHash('sha-256').update(valueEncoded).digest(); - const hashBytes = Array.from(new Uint8Array(hash)); - - const xorByte = args.xorByte || Math.floor(Math.random() * 256); - - const bytes = new Uint8Array(keyBytes.concat(Array.from(timeBuffer), hashBytes.slice(0, 16), [args.extraByte])); - return encode(xor(xorByte, bytes)); -} - -function getAnimationKey(keyBytes: number[], frames: number[][][], indices: number[]): string { - const totalTime = 4096; - const rowIndex = keyBytes[indices[0]] % 16; - const frameTime = indices - .slice(1) - .map((idx) => keyBytes[idx] % 16) - .reduce((a, b) => a * b, 1); - const targetTime = frameTime / totalTime; - const frameRow = frames[keyBytes[5] % 4][rowIndex]; - return animate(frameRow, targetTime); -} - -function animate(frameRow: number[], targetTime: number): string { - const curves = frameRow.slice(7).map((v, i) => Number(a(v, b(i), 1).toFixed(2))); - const cubicValue = getCubicCurveValue(curves, targetTime); - - const fromColor = [...frameRow.slice(0, 3), 1]; - const toColor = [...frameRow.slice(3, 6), 1]; - const color = interpolate(fromColor, toColor, cubicValue); - - const fromRotation = [0]; - const toRotation = [Math.floor(a(frameRow[6], 60, 360))]; - const rotation = interpolate(fromRotation, toRotation, cubicValue); - const matrix = convertRotationToMatrix(rotation[0]); - - const strArray: string[] = []; - for (let i = 0; i < color.length - 1; i++) { - strArray.push(Math.round(color[i]).toString(16)); - } - - for (let i = 0; i < matrix.length; i++) { - let rounded = Number(matrix[i].toFixed(2)); - if (rounded < 0) { - rounded = -rounded; - } - const hexValue = floatToHex(rounded); - if (hexValue.startsWith('.')) { - strArray.push('0' + hexValue); - } else if (hexValue) { - strArray.push(hexValue); - } else { - strArray.push('0'); - } - } - - strArray.push('0', '0'); - return strArray.join('').replace(/[.-]/g, '').toLowerCase(); -} - -function a(b: number, c: number, d: number): number { - return (b * (d - c)) / 255 + c; -} - -function b(a: number): number { - return a % 2 === 1 ? -1 : 0; -} - -function getCubicCurveValue(curves: number[], time: number): number { - let startGradient = 0; - let endGradient = 0; - - if (time <= 0) { - if (curves[0] > 0) { - startGradient = curves[1] / curves[0]; - } else if (curves[1] === 0 && curves[2] > 0) { - startGradient = curves[3] / curves[2]; - } - return startGradient * time; - } - - if (time >= 1) { - if (curves[2] < 1) { - endGradient = (curves[3] - 1) / (curves[2] - 1); - } else if (curves[2] === 1 && curves[0] < 1) { - endGradient = (curves[1] - 1) / (curves[0] - 1); - } - return 1 + endGradient * (time - 1); - } - - let start = 0; - let end = 1; - let mid = 0; - - while (start < end) { - mid = (start + end) / 2; - const xEst = calculateBezier(curves[0], curves[2], mid); - if (Math.abs(time - xEst) < 0.00001) { - return calculateBezier(curves[1], curves[3], mid); - } - if (xEst < time) { - start = mid; - } else { - end = mid; - } - } - - return calculateBezier(curves[1], curves[3], mid); -} - -function calculateBezier(a: number, b: number, m: number): number { - return 3 * a * (1 - m) * (1 - m) * m + 3 * b * (1 - m) * m * m + m * m * m; -} - -function interpolate(from: number[], to: number[], f: number): number[] { - const out: number[] = []; - for (let i = 0; i < from.length; i++) { - out.push(from[i] * (1 - f) + to[i] * f); - } - return out; -} - -function convertRotationToMatrix(degrees: number): number[] { - const radians = (degrees * Math.PI) / 180; - const c = Math.cos(radians); - const s = Math.sin(radians); - return [c, -s, s, c]; -} - -function floatToHex(x: number): string { - const result: string[] = []; - let quotient = Math.floor(x); - let fraction = x - quotient; - - while (quotient > 0) { - const remainder = quotient % 16; - quotient = Math.floor(quotient / 16); - - if (remainder > 9) { - result.unshift(String.fromCharCode(remainder + 55)); - } else { - result.unshift(remainder.toString()); - } - } - - if (fraction === 0) { - return result.join(''); - } - - result.push('.'); - - while (fraction > 0) { - fraction *= 16; - const integer = Math.floor(fraction); - fraction -= integer; - - if (integer > 9) { - result.push(String.fromCharCode(integer + 55)); - } else { - result.push(integer.toString()); - } - } - - return result.join(''); -} - -function xor(xorByte: number, data: Uint8Array): Uint8Array { - return new Uint8Array([xorByte, ...data.map((v) => v ^ xorByte)]); -} - -function encode(data: Uint8Array): string { - return btoa( - Array.from(data) - .map((v) => String.fromCharCode(v)) - .join(''), - ).replaceAll('=', ''); -} diff --git a/src/index.ts b/src/index.ts index d1efbac2..2e5f4b74 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,12 @@ export * from './enums/Tweet'; // MODELS export * from './models/args/FetchArgs'; export * from './models/args/PostArgs'; +export * from './models/args/ProfileArgs'; +export * from './models/data/BookmarkFolder'; +export * from './models/data/Conversation'; export * from './models/data/CursoredData'; +export * from './models/data/DirectMessage'; +export * from './models/data/Inbox'; export * from './models/data/List'; export * from './models/data/Notification'; export * from './models/data/Tweet'; @@ -26,12 +31,14 @@ export * from './models/data/User'; export * from './models/errors/TwitterError'; // REQUESTS +export * from './requests/DirectMessage'; export * from './requests/List'; export * from './requests/Media'; export * from './requests/Tweet'; export * from './requests/User'; // SERVICES +export * from './services/public/DirectMessageService'; export * from './services/public/FetcherService'; export * from './services/public/ListService'; export * from './services/public/TweetService'; @@ -40,8 +47,12 @@ export * from './services/public/UserService'; // TYPES export * from './types/args/FetchArgs'; export * from './types/args/PostArgs'; -export * from './types/auth/TidProvider'; +export * from './types/args/ProfileArgs'; +export * from './types/data/BookmarkFolder'; +export * from './types/data/Conversation'; export * from './types/data/CursoredData'; +export * from './types/data/DirectMessage'; +export * from './types/data/Inbox'; export * from './types/data/List'; export * from './types/data/Notification'; export * from './types/data/Tweet'; @@ -49,11 +60,13 @@ export * from './types/data/User'; export * from './types/errors/TwitterError'; export * from './types/params/Variables'; export { IAnalytics as IRawAnalytics } from './types/raw/base/Analytic'; +export { IBookmarkFolder as IRawBookmarkFolder } from './types/raw/base/BookmarkFolder'; export { ICursor as IRawCursor } from './types/raw/base/Cursor'; export { IErrorData as IRawErrorData, IErrorDetails as IRawErrorDetails } from './types/raw/base/Error'; export { ILimitedVisibilityTweet as IRawLimitedVisibilityTweet } from './types/raw/base/LimitedVisibilityTweet'; export { IList as IRawList } from './types/raw/base/List'; export { IMedia as IRawMedia } from './types/raw/base/Media'; +export { IMessage as IRawMessage } from './types/raw/base/Message'; export { INotification as IRawNotification } from './types/raw/base/Notification'; export { ISpace as IRawSpace } from './types/raw/base/Space'; export { ITweet as IRawTweet } from './types/raw/base/Tweet'; @@ -62,6 +75,8 @@ export { IDataResult as IRawDataResult } from './types/raw/composite/DataResult' export { ITimelineTweet as IRawTimelineTweet } from './types/raw/composite/TimelineTweet'; export { ITimelineUser as IRawTimelineUser } from './types/raw/composite/TimelineUser'; export { IResponse as IRawResponse } from './types/raw/generic/Response'; +export { IListMemberAddResponse as IRawListMemberAddResponse } from './types/raw/list/AddMember'; +export { IListMemberRemoveResponse as IRawListMemberRemoveResponse } from './types/raw/list/RemoveMember'; export { IListDetailsResponse as IRawListDetailsResponse } from './types/raw/list/Details'; export { IListMembersResponse as IRawListMembersResponse } from './types/raw/list/Members'; export { IListTweetsResponse as IRawListTweetsResponse } from './types/raw/list/Tweets'; @@ -84,6 +99,8 @@ export { ITweetUnretweetResponse as IRawTweetUnretweetResponse } from './types/r export { ITweetUnscheduleResponse as ITRawTweetUnscheduleResponse } from './types/raw/tweet/Unschedule'; export { IUserAffiliatesResponse as IRawUserAffiliatesResponse } from './types/raw/user/Affiliates'; export { IUserAnalyticsResponse as IRawUserAnalyticsResponse } from './types/raw/user/Analytics'; +export { IUserBookmarkFoldersResponse as IRawUserBookmarkFoldersResponse } from './types/raw/user/BookmarkFolders'; +export { IUserBookmarkFolderTweetsResponse as IRawUserBookmarkFolderTweetsResponse } from './types/raw/user/BookmarkFolderTweets'; export { IUserBookmarksResponse as IRawUserBookmarksResponse } from './types/raw/user/Bookmarks'; export { IUserDetailsResponse as IRawUserDetailsResponse } from './types/raw/user/Details'; export { IUserDetailsBulkResponse as IRawUserDetailsBulkResponse } from './types/raw/user/DetailsBulk'; @@ -96,10 +113,16 @@ export { IUserLikesResponse as IRawUserLikesResponse } from './types/raw/user/Li export { IUserMediaResponse as IRawUserMediaResponse } from './types/raw/user/Media'; export { IUserNotificationsResponse as IRawUserNotificationsResponse } from './types/raw/user/Notifications'; export { IUserRecommendedResponse as IRawUserRecommendedResponse } from './types/raw/user/Recommended'; +export { IUserSearchResponse as IRawUserSearchResponse } from './types/raw/user/Search'; export { IUserScheduledResponse as IRawUserScheduledResponse } from './types/raw/user/Scheduled'; export { IUserSubscriptionsResponse as IRawUserSubscriptionsResponse } from './types/raw/user/Subscriptions'; export { IUserTweetsResponse as IRawUserTweetsResponse } from './types/raw/user/Tweets'; export { IUserTweetsAndRepliesResponse as IRawUserTweetsAndRepliesResponse } from './types/raw/user/TweetsAndReplies'; export { IUserUnfollowResponse as IRawUserUnfollowResponse } from './types/raw/user/Unfollow'; +export { IUserProfileUpdateResponse as IRawUserProfileUpdateResponse } from './types/raw/user/ProfileUpdate'; export * from './types/ErrorHandler'; export * from './types/RettiwtConfig'; +export { IConversationTimelineResponse as IRawConversationTimelineResponse } from './types/raw/dm/Conversation'; +export { IInboxInitialResponse as IRawInboxInitialResponse } from './types/raw/dm/InboxInitial'; +export { IInboxTimelineResponse as IRawInboxTimelineResponse } from './types/raw/dm/InboxTimeline'; +export { IUserUpdatesResponse as IRawUserUpdatesResponse } from './types/raw/dm/UserUpdates'; diff --git a/src/models/RettiwtConfig.ts b/src/models/RettiwtConfig.ts index e8e982d0..dee949c1 100644 --- a/src/models/RettiwtConfig.ts +++ b/src/models/RettiwtConfig.ts @@ -3,7 +3,6 @@ import { Agent } from 'https'; import { HttpsProxyAgent } from 'https-proxy-agent'; import { AuthService } from '../services/internal/AuthService'; -import { ITidProvider } from '../types/auth/TidProvider'; import { IErrorHandler } from '../types/ErrorHandler'; import { IRettiwtConfig } from '../types/RettiwtConfig'; @@ -12,15 +11,14 @@ import { IRettiwtConfig } from '../types/RettiwtConfig'; * * @public */ -const defaultHeaders = { +const DefaultHeaders = { /* eslint-disable @typescript-eslint/naming-convention */ Authority: 'x.com', 'Accept-Language': 'en-US,en;q=0.9', 'Cache-Control': 'no-cache', Referer: 'https://x.com', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36', + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0', 'X-Twitter-Active-User': 'yes', 'X-Twitter-Client-Language': 'en', @@ -43,7 +41,7 @@ export class RettiwtConfig implements IRettiwtConfig { public readonly delay?: number | (() => number | Promise); public readonly errorHandler?: IErrorHandler; public readonly logging?: boolean; - public readonly tidProvider?: ITidProvider; + public readonly maxRetries: number; public readonly timeout?: number; /** @@ -53,14 +51,14 @@ export class RettiwtConfig implements IRettiwtConfig { this._apiKey = config?.apiKey; this._httpsAgent = config?.proxyUrl ? new HttpsProxyAgent(config?.proxyUrl) : new Agent(); this._userId = config?.apiKey ? AuthService.getUserId(config?.apiKey) : undefined; - this.delay = config?.delay; + this.delay = config?.delay ?? 0; + this.maxRetries = config?.maxRetries ?? 0; this.errorHandler = config?.errorHandler; this.logging = config?.logging; - this.tidProvider = config?.tidProvider; this.timeout = config?.timeout; this.apiKey = config?.apiKey; this._headers = { - ...defaultHeaders, + ...DefaultHeaders, ...config?.headers, }; } @@ -90,7 +88,7 @@ export class RettiwtConfig implements IRettiwtConfig { public set headers(headers: { [key: string]: string } | undefined) { this._headers = { - ...defaultHeaders, + ...DefaultHeaders, ...headers, }; } @@ -100,4 +98,4 @@ export class RettiwtConfig implements IRettiwtConfig { } } -export { defaultHeaders as DefaultRettiwtHeaders }; +export { DefaultHeaders as DefaultRettiwtHeaders }; diff --git a/src/models/args/FetchArgs.ts b/src/models/args/FetchArgs.ts index 741d2234..d0f8ac8c 100644 --- a/src/models/args/FetchArgs.ts +++ b/src/models/args/FetchArgs.ts @@ -1,18 +1,27 @@ -import { ETweetRepliesSortType } from '../../enums/Tweet'; +import { TweetRepliesSortType } from '../../enums/Tweet'; import { IFetchArgs, ITweetFilter } from '../../types/args/FetchArgs'; +import type { RawAnalyticsGranularity, RawAnalyticsMetric } from '../../enums/raw/Analytics'; /** * Options specifying the data that is to be fetched. * * @public */ export class FetchArgs implements IFetchArgs { + public activeConversationId?: string; + public conversationId?: string; public count?: number; public cursor?: string; public filter?: TweetFilter; + public fromTime?: Date; + public granularity?: RawAnalyticsGranularity; public id?: string; public ids?: string[]; - public sortBy?: ETweetRepliesSortType; + public maxId?: string; + public metrics?: RawAnalyticsMetric[]; + public showVerifiedFollowers?: boolean; + public sortBy?: TweetRepliesSortType; + public toTime?: Date; /** * @param args - Additional user-defined arguments for fetching the resource. @@ -24,6 +33,14 @@ export class FetchArgs implements IFetchArgs { this.cursor = args.cursor; this.filter = args.filter ? new TweetFilter(args.filter) : undefined; this.sortBy = args.sortBy; + this.fromTime = args.fromTime; + this.toTime = args.toTime; + this.granularity = args.granularity; + this.metrics = args.metrics; + this.showVerifiedFollowers = args.showVerifiedFollowers; + this.activeConversationId = args.activeConversationId; + this.conversationId = args.conversationId; + this.maxId = args.maxId; } } @@ -93,7 +110,7 @@ export class TweetFilter implements ITweetFilter { * @param date - The date object to convert. * @returns The Twitter string representation of the date. */ - private static dateToTwitterString(date: Date): string { + private static _dateToTwitterString(date: Date): string { // Converting localized date to UTC date const utc = new Date( Date.UTC( @@ -135,8 +152,8 @@ export class TweetFilter implements ITweetFilter { this.minLikes ? `min_faves:${this.minLikes}` : '', this.minRetweets ? `min_retweets:${this.minRetweets}` : '', this.language ? `lang:${this.language}` : '', - this.startDate ? `since:${TweetFilter.dateToTwitterString(this.startDate)}` : '', - this.endDate ? `until:${TweetFilter.dateToTwitterString(this.endDate)}` : '', + this.startDate ? `since:${TweetFilter._dateToTwitterString(this.startDate)}` : '', + this.endDate ? `until:${TweetFilter._dateToTwitterString(this.endDate)}` : '', this.sinceId ? `since_id:${this.sinceId}` : '', this.maxId ? `max_id:${this.maxId}` : '', this.quoted ? `quoted_tweet_id:${this.quoted}` : '', diff --git a/src/models/args/PostArgs.ts b/src/models/args/PostArgs.ts index ff30779c..76fbc8e3 100644 --- a/src/models/args/PostArgs.ts +++ b/src/models/args/PostArgs.ts @@ -1,14 +1,19 @@ import { INewTweet, INewTweetMedia, IPostArgs, IUploadArgs } from '../../types/args/PostArgs'; +import { ProfileUpdateOptions } from './ProfileArgs'; + /** * Options specifying the data that is to be posted. * * @public */ export class PostArgs implements IPostArgs { + public conversationId?: string; public id?: string; + public profileOptions?: ProfileUpdateOptions; public tweet?: NewTweet; public upload?: UploadArgs; + public userId?: string; /** * @param resource - The resource to be posted. @@ -18,6 +23,9 @@ export class PostArgs implements IPostArgs { this.id = args.id; this.tweet = args.tweet ? new NewTweet(args.tweet) : undefined; this.upload = args.upload ? new UploadArgs(args.upload) : undefined; + this.userId = args.userId; + this.conversationId = args.conversationId; + this.profileOptions = args.profileOptions ? new ProfileUpdateOptions(args.profileOptions) : undefined; } } diff --git a/src/models/args/ProfileArgs.ts b/src/models/args/ProfileArgs.ts new file mode 100644 index 00000000..0b9520ae --- /dev/null +++ b/src/models/args/ProfileArgs.ts @@ -0,0 +1,68 @@ +import { IProfileUpdateOptions } from '../../types/args/ProfileArgs'; + +/** + * Configuration for profile update. + * + * @public + */ +export class ProfileUpdateOptions implements IProfileUpdateOptions { + public description?: string; + public location?: string; + public name?: string; + public url?: string; + + /** + * @param options - The options specifying the profile fields to update. + */ + public constructor(options: IProfileUpdateOptions) { + this.description = options.description; + this.location = options.location; + this.name = options.name; + this.url = options.url; + + // At least one field must be provided + if ( + this.name === undefined && + this.url === undefined && + this.location === undefined && + this.description === undefined + ) { + throw new Error('At least one profile field must be provided'); + } + + // Name validation + if (this.name !== undefined) { + if (this.name.trim().length === 0) { + throw new Error('Name cannot be empty'); + } + if (this.name.length > 50) { + throw new Error('Name cannot exceed 50 characters'); + } + } + + // URL validation (minimal - just check if not empty when provided) + if (this.url !== undefined && this.url.trim().length === 0) { + throw new Error('URL cannot be empty'); + } + + // Location validation + if (this.location !== undefined) { + if (this.location.trim().length === 0) { + throw new Error('Location cannot be empty'); + } + if (this.location.length > 30) { + throw new Error('Location cannot exceed 30 characters'); + } + } + + // Description validation + if (this.description !== undefined) { + if (this.description.trim().length === 0) { + throw new Error('Description cannot be empty'); + } + if (this.description.length > 160) { + throw new Error('Description cannot exceed 160 characters'); + } + } + } +} diff --git a/src/models/auth/AuthCredential.ts b/src/models/auth/AuthCredential.ts index 4fd28f37..9b45185f 100644 --- a/src/models/auth/AuthCredential.ts +++ b/src/models/auth/AuthCredential.ts @@ -2,7 +2,7 @@ import { AxiosHeaders, AxiosRequestHeaders } from 'axios'; import { Cookie } from 'cookiejar'; -import { EAuthenticationType } from '../../enums/Authentication'; +import { AuthenticationType } from '../../enums/Authentication'; import { IAuthCredential } from '../../types/auth/AuthCredential'; import { AuthCookie } from './AuthCookie'; @@ -19,7 +19,7 @@ import { AuthCookie } from './AuthCookie'; */ export class AuthCredential implements IAuthCredential { public authToken?: string; - public authenticationType?: EAuthenticationType; + public authenticationType?: AuthenticationType; public cookies?: string; public csrfToken?: string; public guestToken?: string; @@ -34,7 +34,7 @@ export class AuthCredential implements IAuthCredential { // If guest credentials given if (!cookies && guestToken) { this.guestToken = guestToken; - this.authenticationType = EAuthenticationType.GUEST; + this.authenticationType = AuthenticationType.GUEST; } // If login credentials given else if (cookies && guestToken) { @@ -43,7 +43,7 @@ export class AuthCredential implements IAuthCredential { this.cookies = parsedCookie.toString(); this.guestToken = guestToken; - this.authenticationType = EAuthenticationType.LOGIN; + this.authenticationType = AuthenticationType.LOGIN; } // If user credentials given else if (cookies && !guestToken) { @@ -52,7 +52,7 @@ export class AuthCredential implements IAuthCredential { this.cookies = parsedCookie.toString(); this.csrfToken = parsedCookie.ct0; - this.authenticationType = EAuthenticationType.USER; + this.authenticationType = AuthenticationType.USER; } } diff --git a/src/models/data/Analytics.ts b/src/models/data/Analytics.ts new file mode 100644 index 00000000..1a3c5714 --- /dev/null +++ b/src/models/data/Analytics.ts @@ -0,0 +1,97 @@ +import { RawAnalyticsMetric } from '../../enums/raw/Analytics'; +import { IAnalytics as IRawAnalytics } from '../../types/raw/base/Analytic'; + +import type { IAnalytics } from '../../types/data/Analytics'; +import type { IAnalyticsMetric } from '../../types/raw/base/Analytic'; + +/** + * The details of the analytic result of the connected User. + * + * @public + */ +export class Analytics implements IAnalytics { + /** The raw analytic details. */ + private readonly _raw: IRawAnalytics; + + public bookmarks: number; + public createQuote: number; + public createReply: number; + public createTweets: number; + public createdAt: string; + public engagements: number; + public followers: number; + public follows: number; + public impressions: number; + public likes: number; + public organicMetricsTimeSeries: IAnalyticsMetric[]; + public profileVisits: number; + public replies: number; + public retweets: number; + public shares: number; + public unfollows: number; + public verifiedFollowers: number; + + public constructor(analytics: IRawAnalytics) { + this._raw = { ...analytics }; + this.organicMetricsTimeSeries = analytics.organic_metrics_time_series; + this.createdAt = new Date().toISOString(); + this.followers = analytics.relationship_counts.followers; + this.verifiedFollowers = parseInt(analytics.verified_follower_count, 10); + this.impressions = this._reduceMetrics(RawAnalyticsMetric.IMPRESSIONS); + this.profileVisits = this._reduceMetrics(RawAnalyticsMetric.PROFILE_VISITS); + this.engagements = this._reduceMetrics(RawAnalyticsMetric.ENGAGEMENTS); + this.follows = this._reduceMetrics(RawAnalyticsMetric.FOLLOWS); + this.replies = this._reduceMetrics(RawAnalyticsMetric.REPLIES); + this.likes = this._reduceMetrics(RawAnalyticsMetric.LIKES); + this.retweets = this._reduceMetrics(RawAnalyticsMetric.RETWEETS); + this.bookmarks = this._reduceMetrics(RawAnalyticsMetric.BOOKMARK); + this.shares = this._reduceMetrics(RawAnalyticsMetric.SHARE); + this.createTweets = this._reduceMetrics(RawAnalyticsMetric.CREATE_TWEET); + this.createQuote = this._reduceMetrics(RawAnalyticsMetric.CREATE_QUOTE); + this.createReply = this._reduceMetrics(RawAnalyticsMetric.CREATE_REPLY); + this.unfollows = this._reduceMetrics(RawAnalyticsMetric.UNFOLLOWS); + } + + /** The raw analytic details. */ + public get raw(): IRawAnalytics { + return { ...this._raw }; + } + + /** + * Reduces the organic metrics time series to a total value for a specific metric type. + * + * @param metricType - metricType The type of metric to reduce. + * @returns the total value of the specified metric type across all time series. + */ + private _reduceMetrics(metricType: RawAnalyticsMetric): number { + return this.organicMetricsTimeSeries.reduce((acc, metric) => { + const metricValue = metric.metric_values.find((m) => m.metric_type === (metricType as string)); + return acc + (metricValue ? metricValue.metric_value : 0); + }, 0); + } + + /** + * @returns A serializable JSON representation of `this` object. + */ + public toJSON(): IAnalytics { + return { + createdAt: this.createdAt, + followers: this.followers, + verifiedFollowers: this.verifiedFollowers, + impressions: this.impressions, + profileVisits: this.profileVisits, + engagements: this.engagements, + follows: this.follows, + replies: this.replies, + likes: this.likes, + retweets: this.retweets, + bookmarks: this.bookmarks, + shares: this.shares, + createTweets: this.createTweets, + createQuote: this.createQuote, + unfollows: this.unfollows, + createReply: this.createReply, + organicMetricsTimeSeries: this.organicMetricsTimeSeries, + }; + } +} diff --git a/src/models/data/BookmarkFolder.ts b/src/models/data/BookmarkFolder.ts new file mode 100644 index 00000000..bff19bfd --- /dev/null +++ b/src/models/data/BookmarkFolder.ts @@ -0,0 +1,73 @@ +import { LogActions } from '../../enums/Logging'; +import { LogService } from '../../services/internal/LogService'; +import { IBookmarkFolder } from '../../types/data/BookmarkFolder'; +import { IBookmarkFolder as IRawBookmarkFolder } from '../../types/raw/base/BookmarkFolder'; +import { IUserBookmarkFoldersResponse } from '../../types/raw/user/BookmarkFolders'; + +/** + * The details of a single Bookmark Folder. + * + * @public + */ +export class BookmarkFolder implements IBookmarkFolder { + /** The raw bookmark folder details. */ + private readonly _raw: IRawBookmarkFolder; + + public id: string; + public name: string; + + /** + * @param folder - The raw bookmark folder details. + */ + public constructor(folder: IRawBookmarkFolder) { + this._raw = { ...folder }; + this.id = folder.id; + this.name = folder.name; + } + + /** The raw bookmark folder details. */ + public get raw(): IRawBookmarkFolder { + return { ...this._raw }; + } + + /** + * Extracts and deserializes bookmark folders from the given raw response data. + * + * @param response - The raw response data. + * + * @returns The deserialized list of bookmark folders. + */ + public static list(response: NonNullable): BookmarkFolder[] { + const folders: BookmarkFolder[] = []; + + // Extract items from the response structure + const items = (response as IUserBookmarkFoldersResponse)?.data?.viewer?.user_results?.result + ?.bookmark_collections_slice?.items; + + if (!items || !Array.isArray(items)) { + return folders; + } + + // Deserialize valid folders + for (const item of items) { + if (item && item.id) { + // Logging + LogService.log(LogActions.DESERIALIZE, { id: item.id }); + + folders.push(new BookmarkFolder(item)); + } + } + + return folders; + } + + /** + * @returns A serializable JSON representation of `this` object. + */ + public toJSON(): IBookmarkFolder { + return { + id: this.id, + name: this.name, + }; + } +} diff --git a/src/models/data/Conversation.ts b/src/models/data/Conversation.ts new file mode 100644 index 00000000..ebe5fca2 --- /dev/null +++ b/src/models/data/Conversation.ts @@ -0,0 +1,344 @@ +import { IConversation } from '../../types/data/Conversation'; +import { IConversationTimelineResponse } from '../../types/raw/dm/Conversation'; +import { + IInboxInitialResponse, + Conversation as RawConversation, + Conversations as RawConversations, +} from '../../types/raw/dm/InboxInitial'; +import { IInboxTimelineResponse } from '../../types/raw/dm/InboxTimeline'; + +import { DirectMessage } from './DirectMessage'; + +/** + * Type guard to check if the response is an IConversationTimelineResponse + */ +function isConversationTimelineResponse( + response: IConversationTimelineResponse | IInboxInitialResponse | IInboxTimelineResponse, +): response is IConversationTimelineResponse { + return 'conversation_timeline' in response; +} + +/** + * Type guard to check if the response is an IInboxInitialResponse + */ +function isInboxInitialResponse( + response: IConversationTimelineResponse | IInboxInitialResponse | IInboxTimelineResponse, +): response is IInboxInitialResponse { + return 'inbox_initial_state' in response; +} + +/** + * Type guard to check if the response is an IInboxTimelineResponse + */ +function isInboxTimelineResponse( + response: IConversationTimelineResponse | IInboxInitialResponse | IInboxTimelineResponse, +): response is IInboxTimelineResponse { + return 'inbox_timeline' in response; +} + +/** + * Extract typed conversation data from raw conversations object + */ +function extractConversationData(rawConversations: RawConversations): Array<[string, RawConversation]> { + return Object.entries(rawConversations); +} + +/** + * The details of a single conversation. + * + * @public + */ +export class Conversation implements IConversation { + /** The raw conversation details. */ + private readonly _raw: RawConversation; + + public avatarUrl?: string; + public hasMore: boolean; + public id: string; + public lastActivityAt: string; + public lastMessageId?: string; + public messages: DirectMessage[]; + public muted: boolean; + public name?: string; + public notificationsDisabled: boolean; + public participants: string[]; + public trusted: boolean; + public type: 'ONE_TO_ONE' | 'GROUP_DM'; + + /** + * @param conversation - The raw conversation details from the API response. + * @param messages - Array of messages in this conversation. + */ + public constructor(conversation: unknown, messages: DirectMessage[] = []) { + this._raw = conversation as RawConversation; + + const conv = conversation as Record; + + this.id = conv.conversation_id && typeof conv.conversation_id === 'string' ? conv.conversation_id : ''; + this.type = this._parseConversationType(conv.type); + this.participants = this._parseParticipants(conv.participants); + this.name = conv.name && typeof conv.name === 'string' ? conv.name : undefined; + this.avatarUrl = this._parseAvatarUrl(conv); + this.trusted = Boolean(conv.trusted); + this.muted = Boolean(conv.muted); + this.notificationsDisabled = Boolean(conv.notifications_disabled); + this.lastActivityAt = this._parseTimestamp(conv.sort_timestamp); + this.lastMessageId = + conv.sort_event_id && typeof conv.sort_event_id === 'string' ? conv.sort_event_id : undefined; + this.hasMore = conv.status === 'HAS_MORE'; + this.messages = messages; + } + + /** The raw conversation details. */ + public get raw(): RawConversation { + return this._raw; + } + + /** + * Parse avatar URL from conversation data + */ + private _parseAvatarUrl(conv: Record): string | undefined { + // Try avatar_image_https first + if (conv.avatar_image_https && typeof conv.avatar_image_https === 'string') { + return conv.avatar_image_https; + } + + // Try nested avatar.image.original_info.url + const avatar = conv.avatar as Record | undefined; + const image = avatar?.image as Record | undefined; + const originalInfo = image?.original_info as Record | undefined; + + if (originalInfo?.url && typeof originalInfo.url === 'string') { + return originalInfo.url; + } + + return undefined; + } + + /** + * Parse conversation type with proper fallback + */ + private _parseConversationType(type: unknown): 'ONE_TO_ONE' | 'GROUP_DM' { + if (type === 'ONE_TO_ONE' || type === 'GROUP_DM') { + return type; + } + return 'ONE_TO_ONE'; + } + + /** + * Parse participants array with type safety + */ + private _parseParticipants(participants: unknown): string[] { + if (!Array.isArray(participants)) { + return []; + } + + return participants + .map((p) => { + if (p && typeof p === 'object' && 'user_id' in p) { + // eslint-disable-next-line @typescript-eslint/naming-convention + const participantObj = p as { user_id: unknown }; + if (typeof participantObj.user_id === 'string') { + return participantObj.user_id; + } + } + return ''; + }) + .filter(Boolean); + } + + /** + * Parse timestamp with proper fallback + */ + private _parseTimestamp(timestamp: unknown): string { + if (timestamp && (typeof timestamp === 'string' || typeof timestamp === 'number')) { + const date = new Date(Number(timestamp)); + if (!isNaN(date.getTime())) { + return date.toISOString(); + } + } + return new Date().toISOString(); + } + + /** + * Extracts a single conversation from conversation timeline response. + * + * @param response - The raw response data. + * + * @returns The deserialized conversation with full message history. + */ + public static fromConversationTimeline(response: IConversationTimelineResponse): Conversation | undefined { + if (!response.conversation_timeline?.conversations) { + return undefined; + } + + const rawConversations = response.conversation_timeline.conversations; + const entries = response.conversation_timeline.entries ?? []; + + // Extract messages from entries + const messages: DirectMessage[] = []; + for (const entry of entries) { + if ('message' in entry && entry.message) { + messages.push(new DirectMessage(entry.message)); + } + } + + // Get the first (and typically only) conversation + const conversationEntries = extractConversationData(rawConversations); + const firstEntry = conversationEntries[0]; + + if (firstEntry) { + const [, conversationData] = firstEntry; + return new Conversation(conversationData, messages); + } + + return undefined; + } + + /** + * Extracts conversations from inbox initial state response. + * + * @param response - The raw response data. + * + * @returns The deserialized list of conversations with their preview messages. + */ + public static listFromInboxInitial(response: IInboxInitialResponse): Conversation[] { + const conversations: Conversation[] = []; + + if (!response.inbox_initial_state?.conversations) { + return conversations; + } + + const rawConversations = response.inbox_initial_state.conversations; + const entries = response.inbox_initial_state.entries ?? []; + + // Group messages by conversation ID + const messagesByConversation = new Map(); + for (const entry of entries) { + if ('message' in entry && entry.message) { + const message = new DirectMessage(entry.message); + const convId = message.conversationId; + if (convId) { + if (!messagesByConversation.has(convId)) { + messagesByConversation.set(convId, []); + } + messagesByConversation.get(convId)!.push(message); + } + } + } + + // Create conversations with their messages + const conversationEntries = extractConversationData(rawConversations); + for (const [, conversation] of conversationEntries) { + const convId = (conversation as unknown as Record).conversation_id as string; + const messages = messagesByConversation.get(convId) ?? []; + conversations.push(new Conversation(conversation, messages)); + } + + return conversations; + } + + /** + * Extracts conversations from inbox timeline response. + * + * @param response - The raw response data. + * + * @returns The deserialized list of conversations with their messages. + */ + public static listFromInboxTimeline(response: IInboxTimelineResponse): Conversation[] { + const conversations: Conversation[] = []; + + if (!response.inbox_timeline?.conversations) { + return conversations; + } + + const rawConversations = response.inbox_timeline.conversations; + const entries = response.inbox_timeline.entries ?? []; + + // Group messages by conversation ID + const messagesByConversation = new Map(); + for (const entry of entries) { + if ('message' in entry && entry.message) { + const message = new DirectMessage(entry.message); + const convId = message.conversationId; + if (convId) { + if (!messagesByConversation.has(convId)) { + messagesByConversation.set(convId, []); + } + messagesByConversation.get(convId)!.push(message); + } + } + } + + // Create conversations with their messages + const conversationEntries = extractConversationData(rawConversations); + for (const [, conversation] of conversationEntries) { + const convId = (conversation as unknown as Record).conversation_id as string; + const messages = messagesByConversation.get(convId) ?? []; + conversations.push(new Conversation(conversation, messages)); + } + + return conversations; + } + + /** + * Generic method to extract conversations from any supported response type + */ + public static listFromResponse( + response: IConversationTimelineResponse | IInboxInitialResponse | IInboxTimelineResponse, + ): Conversation[] { + if (isConversationTimelineResponse(response)) { + const conversation = Conversation.fromConversationTimeline(response); + return conversation ? [conversation] : []; + } else if (isInboxInitialResponse(response)) { + return Conversation.listFromInboxInitial(response); + } else if (isInboxTimelineResponse(response)) { + return Conversation.listFromInboxTimeline(response); + } + return []; + } + + /** + * Get the other participant's ID (only for one-to-one conversations) + */ + public getOtherParticipant(currentUserId: string): string | undefined { + if (!this.isOneToOne() || this.participants.length !== 2) { + return undefined; + } + return this.participants.find((id) => id !== currentUserId); + } + + /** + * Check if this conversation is a group DM + */ + public isGroupDM(): boolean { + return this.type === 'GROUP_DM'; + } + + /** + * Check if this conversation is one-to-one + */ + public isOneToOne(): boolean { + return this.type === 'ONE_TO_ONE'; + } + + /** + * @returns A serializable JSON representation of `this` object. + */ + public toJSON(): IConversation { + return { + avatarUrl: this.avatarUrl, + hasMore: this.hasMore, + id: this.id, + lastActivityAt: this.lastActivityAt, + lastMessageId: this.lastMessageId, + messages: this.messages.map((msg) => msg.toJSON()), + muted: this.muted, + name: this.name, + notificationsDisabled: this.notificationsDisabled, + participants: this.participants, + trusted: this.trusted, + type: this.type, + }; + } +} diff --git a/src/models/data/CursoredData.ts b/src/models/data/CursoredData.ts index 391c605e..f19dc221 100644 --- a/src/models/data/CursoredData.ts +++ b/src/models/data/CursoredData.ts @@ -1,10 +1,13 @@ -import { EBaseType } from '../../enums/Data'; +import { BaseType } from '../../enums/Data'; import { findByFilter } from '../../helper/JsonUtils'; import { ICursoredData } from '../../types/data/CursoredData'; import { ICursor as IRawCursor } from '../../types/raw/base/Cursor'; +import { IUserBookmarkFoldersResponse } from '../../types/raw/user/BookmarkFolders'; +import { BookmarkFolder } from './BookmarkFolder'; +import { List } from './List'; import { Notification } from './Notification'; import { Tweet } from './Tweet'; import { User } from './User'; @@ -16,7 +19,7 @@ import { User } from './User'; * * @public */ -export class CursoredData implements ICursoredData { +export class CursoredData implements ICursoredData { public list: T[]; public next: string; @@ -24,20 +27,28 @@ export class CursoredData implements ICur * @param response - The raw response. * @param type - The base type of the data included in the batch. */ - public constructor(response: NonNullable, type: EBaseType) { + public constructor(response: NonNullable, type: BaseType) { // Initializing defaults this.list = []; this.next = ''; - if (type == EBaseType.TWEET) { + if (type == BaseType.TWEET) { this.list = Tweet.timeline(response) as T[]; this.next = findByFilter(response, 'cursorType', 'Bottom')[0]?.value ?? ''; - } else if (type == EBaseType.USER) { + } else if (type == BaseType.USER) { this.list = User.timeline(response) as T[]; this.next = findByFilter(response, 'cursorType', 'Bottom')[0]?.value ?? ''; - } else if (type == EBaseType.NOTIFICATION) { + } else if (type == BaseType.LIST) { + this.list = List.timeline(response) as T[]; + this.next = findByFilter(response, 'cursorType', 'Bottom')[0]?.value ?? ''; + } else if (type == BaseType.NOTIFICATION) { this.list = Notification.list(response) as T[]; - this.next = findByFilter(response, 'cursorType', 'Top')[0]?.value ?? ''; + this.next = findByFilter(response, 'cursorType', 'Bottom')[0]?.value ?? ''; + } else if (type == BaseType.BOOKMARK_FOLDER) { + this.list = BookmarkFolder.list(response) as T[]; + const sliceInfo = (response as IUserBookmarkFoldersResponse)?.data?.viewer?.user_results?.result + ?.bookmark_collections_slice?.slice_info; + this.next = sliceInfo?.next_cursor ?? ''; } } diff --git a/src/models/data/DirectMessage.ts b/src/models/data/DirectMessage.ts new file mode 100644 index 00000000..7a6dc76e --- /dev/null +++ b/src/models/data/DirectMessage.ts @@ -0,0 +1,335 @@ +import { IDirectMessage } from '../../types/data/DirectMessage'; +import { IMessage as IRawMessage } from '../../types/raw/base/Message'; +import { IConversationTimelineResponse } from '../../types/raw/dm/Conversation'; +import { IInboxInitialResponse } from '../../types/raw/dm/InboxInitial'; +import { IInboxTimelineResponse } from '../../types/raw/dm/InboxTimeline'; + +/** + * Type guard to check if the response is an IInboxInitialResponse + */ +function isInboxInitialResponse( + response: IInboxInitialResponse | IConversationTimelineResponse | IInboxTimelineResponse, +): response is IInboxInitialResponse { + return 'inbox_initial_state' in response; +} + +/** + * Type guard to check if the response is an IConversationTimelineResponse + */ +function isConversationTimelineResponse( + response: IInboxInitialResponse | IConversationTimelineResponse | IInboxTimelineResponse, +): response is IConversationTimelineResponse { + return 'conversation_timeline' in response; +} + +/** + * Type guard to check if the response is an IInboxTimelineResponse + */ +function isInboxTimelineResponse( + response: IInboxInitialResponse | IConversationTimelineResponse | IInboxTimelineResponse, +): response is IInboxTimelineResponse { + return 'inbox_timeline' in response; +} + +/** + * The details of a single direct message. + * + * @public + */ +export class DirectMessage implements IDirectMessage { + /** The raw message details. */ + private readonly _raw: IRawMessage; + + public conversationId: string; + public createdAt: string; + public editCount?: number; + public id: string; + public mediaUrls?: string[]; + public read?: boolean; + public recipientId?: string; + public senderId: string; + public text: string; + + /** + * @param message - The raw message details from the API response. + */ + public constructor(message: unknown) { + this._raw = message as IRawMessage; + + const parsedData = this._parseMessageData(message); + + this.id = parsedData.id; + this.conversationId = parsedData.conversationId; + this.senderId = parsedData.senderId; + this.recipientId = parsedData.recipientId; + this.text = parsedData.text; + this.createdAt = parsedData.createdAt; + this.editCount = parsedData.editCount ?? 0; + this.mediaUrls = this._extractMediaUrls(message); + this.read = true; // Default to true, can be enhanced later + } + + /** The raw message details. */ + public get raw(): IRawMessage { + return this._raw; + } + + /** + * Extract messages from conversation timeline response + */ + private static _extractFromConversationTimeline(response: IConversationTimelineResponse): DirectMessage[] { + const messages: DirectMessage[] = []; + const entries = response.conversation_timeline?.entries ?? []; + + for (const entry of entries) { + if ('message' in entry && entry.message) { + messages.push(new DirectMessage(entry.message)); + } + } + + return messages; + } + + /** + * Extract messages from inbox initial response + */ + private static _extractFromInboxInitial(response: IInboxInitialResponse): DirectMessage[] { + const messages: DirectMessage[] = []; + const entries = response.inbox_initial_state?.entries ?? []; + + for (const entry of entries) { + if ('message' in entry && entry.message) { + messages.push(new DirectMessage(entry.message)); + } + } + + return messages; + } + + /** + * Extract messages from inbox timeline response + */ + private static _extractFromInboxTimeline(response: IInboxTimelineResponse): DirectMessage[] { + const messages: DirectMessage[] = []; + const entries = response.inbox_timeline?.entries ?? []; + + for (const entry of entries) { + if ('message' in entry && entry.message) { + messages.push(new DirectMessage(entry.message)); + } + } + + return messages; + } + + /** + * Extract media URLs from message attachment data with proper type safety. + */ + private _extractMediaUrls(message: unknown): string[] | undefined { + const urls: string[] = []; + const msg = message as Record; + const messageData = msg.message_data as Record | undefined; + + // Check for card attachments with images + const attachment = messageData?.attachment as Record | undefined; + const card = attachment?.card as Record | undefined; + const bindingValues = card?.binding_values as Record | undefined; + + if (bindingValues) { + // Extract URLs from various image binding values + const imageBindings = [ + 'thumbnail_image', + 'photo_image_full_size', + 'summary_photo_image', + 'thumbnail_image_original', + 'summary_photo_image_original', + 'photo_image_full_size_original', + ]; + + for (const bindingKey of imageBindings) { + const imageBinding = bindingValues[bindingKey] as Record | undefined; + const imageValue = imageBinding?.image_value as Record | undefined; + + if (imageValue?.url && typeof imageValue.url === 'string') { + urls.push(imageValue.url); + } + } + } + + // Check for tweet attachments + const tweet = attachment?.tweet as Record | undefined; + if (tweet?.expanded_url && typeof tweet.expanded_url === 'string') { + urls.push(tweet.expanded_url); + } + + return urls.length > 0 ? [...new Set(urls)] : undefined; // Remove duplicates + } + + /** + * Safely extract number value + */ + private _extractNumberValue(value: unknown): number | undefined { + if (typeof value === 'number') { + return value; + } + if (typeof value === 'string') { + const parsed = Number(value); + return isNaN(parsed) ? undefined : parsed; + } + return undefined; + } + + /** + * Safely extract string value with fallback + */ + private _extractStringValue(...values: unknown[]): string | undefined { + for (const value of values) { + if (typeof value === 'string' && value.length > 0) { + return value; + } + } + return undefined; + } + + /** + * Parse message data with proper type safety + */ + private _parseMessageData(message: unknown): IDirectMessage { + const msg = message as Record; + const messageData = msg.message_data as Record | undefined; + + const id = this._extractStringValue(messageData?.id, msg.id) ?? ''; + const conversationId = this._extractStringValue(msg.conversation_id, messageData?.conversation_id) ?? ''; + const senderId = this._extractStringValue(messageData?.sender_id, msg.sender_id) ?? ''; + const recipientId = this._extractStringValue(messageData?.recipient_id, msg.recipient_id); + const text = this._extractStringValue(messageData?.text, msg.text) ?? ''; + const createdAt = this._parseTimestamp(this._extractStringValue(messageData?.time, msg.time) ?? ''); + const editCount = this._extractNumberValue(messageData?.edit_count); + + return { + id, + conversationId, + senderId, + recipientId, + createdAt, + text, + editCount, + }; + } + + /** + * Parse timestamp with proper validation + */ + private _parseTimestamp(timestamp: string): string { + const numericTimestamp = Number(timestamp); + if (!isNaN(numericTimestamp)) { + const date = new Date(numericTimestamp); + if (!isNaN(date.getTime())) { + return date.toISOString(); + } + } + return new Date().toISOString(); + } + + /** + * Filter messages by conversation ID + */ + public static filterByConversation(messages: DirectMessage[], conversationId: string): DirectMessage[] { + return messages.filter((message) => message.conversationId === conversationId); + } + + /** + * Filter messages by sender ID + */ + public static filterBySender(messages: DirectMessage[], senderId: string): DirectMessage[] { + return messages.filter((message) => message.isFromSender(senderId)); + } + + /** + * Extracts and deserializes the list of direct messages from the given raw response data. + * + * @param response - The raw response data. + * + * @returns The deserialized list of direct messages. + */ + public static list( + response: IInboxInitialResponse | IConversationTimelineResponse | IInboxTimelineResponse, + ): DirectMessage[] { + const messages: DirectMessage[] = []; + + if (isInboxInitialResponse(response)) { + return DirectMessage._extractFromInboxInitial(response); + } else if (isConversationTimelineResponse(response)) { + return DirectMessage._extractFromConversationTimeline(response); + } else if (isInboxTimelineResponse(response)) { + return DirectMessage._extractFromInboxTimeline(response); + } + + return messages; + } + + /** + * Generic method to extract messages from any supported response type + */ + public static listFromResponse( + response: IInboxInitialResponse | IConversationTimelineResponse | IInboxTimelineResponse, + ): DirectMessage[] { + return DirectMessage.list(response); + } + + /** + * Sort messages by creation time (oldest to newest) + */ + public static sortByTime(messages: DirectMessage[], ascending = true): DirectMessage[] { + return [...messages].sort((a, b) => { + const timeA = new Date(a.createdAt).getTime(); + const timeB = new Date(b.createdAt).getTime(); + return ascending ? timeA - timeB : timeB - timeA; + }); + } + + /** + * Get the age of this message in milliseconds + */ + public getAgeInMs(): number { + return Date.now() - new Date(this.createdAt).getTime(); + } + + /** + * Check if this message has media attachments + */ + public hasMedia(): boolean { + return Boolean(this.mediaUrls && this.mediaUrls.length > 0); + } + + /** + * Check if this message is from a specific sender + */ + public isFromSender(senderId: string): boolean { + return this.senderId === senderId; + } + + /** + * @returns A serializable JSON representation of `this` object. + */ + public toJSON(): IDirectMessage { + return { + conversationId: this.conversationId, + createdAt: this.createdAt, + editCount: this.editCount, + id: this.id, + mediaUrls: this.mediaUrls, + read: this.read, + recipientId: this.recipientId, + senderId: this.senderId, + text: this.text, + }; + } + + /** + * Check if this message was edited + */ + public wasEdited(): boolean { + return Boolean(this.editCount && this.editCount > 0); + } +} diff --git a/src/models/data/Inbox.ts b/src/models/data/Inbox.ts new file mode 100644 index 00000000..1a1a7cf2 --- /dev/null +++ b/src/models/data/Inbox.ts @@ -0,0 +1,124 @@ +import { IInbox } from '../../types/data/Inbox'; +import { IInboxInitialResponse } from '../../types/raw/dm/InboxInitial'; +import { IInboxTimelineResponse } from '../../types/raw/dm/InboxTimeline'; + +import { Conversation } from './Conversation'; + +/** + * Type guard to check if the response is an IInboxInitialResponse + */ +function isInboxInitialResponse( + response: IInboxInitialResponse | IInboxTimelineResponse, +): response is IInboxInitialResponse { + return 'inbox_initial_state' in response; +} + +/** + * Type guard to check if the response is an IInboxTimelineResponse + */ +function isInboxTimelineResponse( + response: IInboxInitialResponse | IInboxTimelineResponse, +): response is IInboxTimelineResponse { + return 'inbox_timeline' in response; +} + +/** + * The details of a DM inbox containing conversations and metadata. + * + * @public + */ +export class Inbox implements IInbox { + /** The raw inbox details. */ + private readonly _raw: IInboxInitialResponse | IInboxTimelineResponse; + + public conversations: Conversation[]; + public cursor: string; + public lastSeenEventId: string; + public trustedLastSeenEventId: string; + public untrustedLastSeenEventId: string; + + /** + * @param response - The raw inbox response from the API. + */ + public constructor(response: IInboxInitialResponse | IInboxTimelineResponse) { + this._raw = response; + + // Handle inbox initial state response + if (isInboxInitialResponse(response)) { + const inboxState = response.inbox_initial_state; + + this.cursor = inboxState.cursor ?? ''; + this.lastSeenEventId = inboxState.last_seen_event_id ?? ''; + this.trustedLastSeenEventId = inboxState.trusted_last_seen_event_id ?? ''; + this.untrustedLastSeenEventId = inboxState.untrusted_last_seen_event_id ?? ''; + + // Parse conversations from inbox initial state + this.conversations = Conversation.listFromInboxInitial(response); + } + // Handle inbox timeline response + else if (isInboxTimelineResponse(response)) { + const inboxTimeline = response.inbox_timeline; + + this.cursor = inboxTimeline.min_entry_id ?? ''; + this.lastSeenEventId = ''; + this.trustedLastSeenEventId = ''; + this.untrustedLastSeenEventId = ''; + + // Parse conversations from inbox timeline + this.conversations = Conversation.listFromInboxTimeline(response); + } else { + // Fallback defaults (this should never happen with proper typing) + this.cursor = ''; + this.lastSeenEventId = ''; + this.trustedLastSeenEventId = ''; + this.untrustedLastSeenEventId = ''; + this.conversations = []; + } + } + + /** The raw inbox details. */ + public get raw(): IInboxInitialResponse | IInboxTimelineResponse { + return this._raw; + } + + /** + * Get the raw inbox initial state if this inbox was created from one + */ + public getInitialState(): IInboxInitialResponse | undefined { + return this.isInitialState() ? (this._raw as IInboxInitialResponse) : undefined; + } + + /** + * Get the raw inbox timeline if this inbox was created from one + */ + public getTimeline(): IInboxTimelineResponse | undefined { + return this.isTimeline() ? (this._raw as IInboxTimelineResponse) : undefined; + } + + /** + * Check if this inbox was created from an initial state response + */ + public isInitialState(): boolean { + return isInboxInitialResponse(this._raw); + } + + /** + * Check if this inbox was created from a timeline response + */ + public isTimeline(): boolean { + return isInboxTimelineResponse(this._raw); + } + + /** + * @returns A serializable JSON representation of `this` object. + */ + public toJSON(): IInbox { + return { + conversations: this.conversations.map((conv) => conv.toJSON()), + cursor: this.cursor, + lastSeenEventId: this.lastSeenEventId, + trustedLastSeenEventId: this.trustedLastSeenEventId, + untrustedLastSeenEventId: this.untrustedLastSeenEventId, + }; + } +} diff --git a/src/models/data/List.ts b/src/models/data/List.ts index 076bcce8..11a1924b 100644 --- a/src/models/data/List.ts +++ b/src/models/data/List.ts @@ -1,5 +1,10 @@ +import { LogActions } from '../../enums/Logging'; +import { findByFilter } from '../../helper/JsonUtils'; +import { LogService } from '../../services/internal/LogService'; import { IList } from '../../types/data/List'; import { IList as IRawList } from '../../types/raw/base/List'; +import { ITimelineList } from '../../types/raw/composite/TimelineList'; +import { IListDetailsResponse } from '../../types/raw/list/Details'; /** * The details of a single Twitter List. @@ -14,6 +19,8 @@ export class List implements IList { public createdBy: string; public description?: string; public id: string; + public isFollowing: boolean; + public isMember: boolean; public memberCount: number; public name: string; public subscriberCount: number; @@ -27,9 +34,11 @@ export class List implements IList { this.name = list.name; this.createdAt = new Date(list.created_at).toISOString(); this.description = list.description.length ? list.description : undefined; + this.isFollowing = list.following; + this.isMember = list.is_member; this.memberCount = list.member_count; this.subscriberCount = list.subscriber_count; - this.createdBy = list.user_results.result.id; + this.createdBy = list.user_results.result.rest_id; } /** The raw list details. */ @@ -37,6 +46,54 @@ export class List implements IList { return { ...this._raw }; } + /** + * Extracts and deserializes a single target list from the given raw response data. + * + * @param response - The raw response data. + * @param id - The id of the target list. + * + * @returns The target deserialized list. + */ + public static single(response: IListDetailsResponse, id: string): List | undefined { + // If list found + if (response.data.list.id_str === id) { + return new List(response.data.list as unknown as IRawList); + } + // If not found + else { + return undefined; + } + } + + /** + * Extracts and deserializes the timeline of lists followed from the given raw response data. + * + * @param response - The raw response data. + * + * @returns The deserialized timeline of lists. + */ + public static timeline(response: NonNullable): List[] { + const lists: List[] = []; + + // Extracting the matching data + const extract = findByFilter(response, '__typename', 'TimelineTwitterList').map( + (item) => item.list, + ); + + // Deserializing valid data + for (const item of extract) { + // If valid list + if (item !== undefined && item.id !== undefined && item.following === true) { + // Logging + LogService.log(LogActions.DESERIALIZE, { id: item.id }); + + lists.push(new List(item)); + } + } + + return lists; + } + /** * @returns A serializable JSON representation of `this` object. */ @@ -46,6 +103,8 @@ export class List implements IList { createdBy: this.createdBy, description: this.description, id: this.id, + isFollowing: this.isFollowing, + isMember: this.isMember, memberCount: this.memberCount, name: this.name, subscriberCount: this.subscriberCount, diff --git a/src/models/data/Notification.ts b/src/models/data/Notification.ts index d685f53e..fade68d3 100644 --- a/src/models/data/Notification.ts +++ b/src/models/data/Notification.ts @@ -1,9 +1,7 @@ -import { ENotificationType } from '../../enums/Notification'; -import { ERawNotificationType } from '../../enums/raw/Notification'; -import { findKeyByValue } from '../../helper/JsonUtils'; +import { NotificationType } from '../../enums/Notification'; +import { findByFilter } from '../../helper/JsonUtils'; import { INotification } from '../../types/data/Notification'; import { INotification as IRawNotification } from '../../types/raw/base/Notification'; -import { IUserNotificationsResponse } from '../../types/raw/user/Notifications'; /** * The details of a single notification. @@ -19,7 +17,7 @@ export class Notification implements INotification { public message: string; public receivedAt: string; public target: string[]; - public type?: ENotificationType; + public type?: NotificationType; /** * @param notification - The raw notification details. @@ -28,20 +26,20 @@ export class Notification implements INotification { this._raw = { ...notification }; // Getting the original notification type - const notificationType: string | undefined = findKeyByValue(ERawNotificationType, notification.icon.id); + const notificationType = notification.notification_icon.toString(); - this.from = notification.template?.aggregateUserActionsV1?.fromUsers - ? notification.template.aggregateUserActionsV1.fromUsers.map((item) => item.user.id) + this.from = notification.template.from_users + ? notification.template.from_users.map((item) => item.user_results.result.rest_id) : []; this.id = notification.id; - this.message = notification.message.text; - this.receivedAt = new Date(Number(notification.timestampMs)).toISOString(); - this.target = notification.template?.aggregateUserActionsV1?.targetObjects - ? notification.template.aggregateUserActionsV1.targetObjects.map((item) => item.tweet.id) + this.message = notification.rich_message.text; + this.receivedAt = new Date(notification.timestamp_ms).toISOString(); + this.target = notification.template.target_objects + ? notification.template.target_objects.map((item) => item.tweet_results.result.rest_id) : []; this.type = notificationType - ? ENotificationType[notificationType as keyof typeof ENotificationType] - : ENotificationType.UNDEFINED; + ? NotificationType[notificationType as keyof typeof NotificationType] + : NotificationType.UNDEFINED; } /** The raw notification details. */ @@ -59,14 +57,12 @@ export class Notification implements INotification { public static list(response: NonNullable): Notification[] { const notifications: Notification[] = []; - // Extracting notifications - if ((response as IUserNotificationsResponse).globalObjects.notifications) { - // Iterating over the raw list of notifications - for (const [, value] of Object.entries( - (response as IUserNotificationsResponse).globalObjects.notifications, - )) { - notifications.push(new Notification(value as IRawNotification)); - } + // Extracting the matching data + const extract = findByFilter(response, '__typename', 'TimelineNotification'); + + // Deserializing valid data + for (const item of extract) { + notifications.push(new Notification(item)); } return notifications; diff --git a/src/models/data/Tweet.ts b/src/models/data/Tweet.ts index 9b5eb2e8..92c86dd6 100644 --- a/src/models/data/Tweet.ts +++ b/src/models/data/Tweet.ts @@ -1,6 +1,6 @@ -import { ELogActions } from '../../enums/Logging'; -import { EMediaType } from '../../enums/Media'; -import { ERawMediaType } from '../../enums/raw/Media'; +import { LogActions } from '../../enums/Logging'; +import { MediaType } from '../../enums/Media'; +import { RawMediaType } from '../../enums/raw/Media'; import { findByFilter } from '../../helper/JsonUtils'; import { LogService } from '../../services/internal/LogService'; @@ -22,24 +22,24 @@ export class Tweet implements ITweet { /** The raw tweet details. */ private readonly _raw: IRawTweet; - public bookmarkCount: number; + public bookmarkCount?: number; public conversationId: string; public createdAt: string; public entities: TweetEntities; public fullText: string; public id: string; public lang: string; - public likeCount: number; + public likeCount?: number; public media?: TweetMedia[]; - public quoteCount: number; + public quoteCount?: number; public quoted?: Tweet; - public replyCount: number; + public replyCount?: number; public replyTo?: string; - public retweetCount: number; + public retweetCount?: number; public retweetedTweet?: Tweet; public tweetBy: User; public url: string; - public viewCount: number; + public viewCount?: number; /** * @param tweet - The raw tweet details. @@ -52,17 +52,19 @@ export class Tweet implements ITweet { this.tweetBy = new User(tweet.core.user_results.result); this.entities = new TweetEntities(tweet.legacy.entities); this.media = tweet.legacy.extended_entities?.media?.map((media) => new TweetMedia(media)); - this.quoted = this.getQuotedTweet(tweet); - this.fullText = tweet.note_tweet ? tweet.note_tweet.note_tweet_results.result.text : tweet.legacy.full_text; + this.quoted = this._getQuotedTweet(tweet); + this.fullText = tweet.note_tweet?.note_tweet_results?.result?.text + ? tweet.note_tweet.note_tweet_results.result.text + : tweet.legacy.full_text; this.replyTo = tweet.legacy.in_reply_to_status_id_str; this.lang = tweet.legacy.lang; this.quoteCount = tweet.legacy.quote_count; this.replyCount = tweet.legacy.reply_count; this.retweetCount = tweet.legacy.retweet_count; this.likeCount = tweet.legacy.favorite_count; - this.viewCount = tweet.views.count ? parseInt(tweet.views.count) : 0; + this.viewCount = tweet.views?.count ? parseInt(tweet.views.count) : undefined; this.bookmarkCount = tweet.legacy.bookmark_count; - this.retweetedTweet = this.getRetweetedTweet(tweet); + this.retweetedTweet = this._getRetweetedTweet(tweet); this.url = `https://x.com/${this.tweetBy.userName}/status/${this.id}`; } @@ -78,7 +80,7 @@ export class Tweet implements ITweet { * * @returns - The deserialized original quoted tweet. */ - private getQuotedTweet(tweet: IRawTweet): Tweet | undefined { + private _getQuotedTweet(tweet: IRawTweet): Tweet | undefined { // If tweet with limited visibility if ( tweet.quoted_status_result && @@ -104,7 +106,7 @@ export class Tweet implements ITweet { * * @returns - The deserialized original retweeted tweet. */ - private getRetweetedTweet(tweet: IRawTweet): Tweet | undefined { + private _getRetweetedTweet(tweet: IRawTweet): Tweet | undefined { // If retweet with limited visibility if ( tweet.legacy?.retweeted_status_result && @@ -141,13 +143,13 @@ export class Tweet implements ITweet { for (const item of extract) { if (item.legacy) { // Logging - LogService.log(ELogActions.DESERIALIZE, { id: item.rest_id }); + LogService.log(LogActions.DESERIALIZE, { id: item.rest_id }); tweets.push(new Tweet(item)); } else { // Logging - LogService.log(ELogActions.WARNING, { - action: ELogActions.DESERIALIZE, + LogService.log(LogActions.WARNING, { + action: LogActions.DESERIALIZE, message: `Tweet not found, skipping`, }); } @@ -179,13 +181,13 @@ export class Tweet implements ITweet { for (const item of extract) { if (item.legacy) { // Logging - LogService.log(ELogActions.DESERIALIZE, { id: item.rest_id }); + LogService.log(LogActions.DESERIALIZE, { id: item.rest_id }); tweets.push(new Tweet(item)); } else { // Logging - LogService.log(ELogActions.WARNING, { - action: ELogActions.DESERIALIZE, + LogService.log(LogActions.WARNING, { + action: LogActions.DESERIALIZE, message: `Tweet not found, skipping`, }); } @@ -221,15 +223,15 @@ export class Tweet implements ITweet { // If normal tweet else if ((item.tweet_results?.result as IRawTweet)?.legacy) { // Logging - LogService.log(ELogActions.DESERIALIZE, { id: (item.tweet_results.result as IRawTweet).rest_id }); + LogService.log(LogActions.DESERIALIZE, { id: (item.tweet_results.result as IRawTweet).rest_id }); tweets.push(new Tweet(item.tweet_results.result as IRawTweet)); } // If invalid/unrecognized tweet else { // Logging - LogService.log(ELogActions.WARNING, { - action: ELogActions.DESERIALIZE, + LogService.log(LogActions.WARNING, { + action: LogActions.DESERIALIZE, message: `Tweet not found, skipping`, }); } @@ -270,7 +272,7 @@ export class Tweet implements ITweet { * * @public */ -export class TweetEntities { +export class TweetEntities implements ITweetEntities { /** The list of hashtags mentioned in the tweet. */ public hashtags: string[] = []; @@ -323,12 +325,15 @@ export class TweetEntities { * * @public */ -export class TweetMedia { +export class TweetMedia implements ITweetMedia { + /** The ID of the media. */ + public id: string; + /** The thumbnail URL for the video content of the tweet. */ public thumbnailUrl?: string; /** The type of media. */ - public type: EMediaType; + public type: MediaType; /** The direct URL to the media. */ public url = ''; @@ -337,19 +342,21 @@ export class TweetMedia { * @param media - The raw media details. */ public constructor(media: IRawExtendedMedia) { + this.id = media.id_str; + // If the media is a photo - if (media.type == ERawMediaType.PHOTO) { - this.type = EMediaType.PHOTO; + if (media.type == RawMediaType.PHOTO) { + this.type = MediaType.PHOTO; this.url = media.media_url_https; } // If the media is a gif - else if (media.type == ERawMediaType.GIF) { - this.type = EMediaType.GIF; + else if (media.type == RawMediaType.GIF) { + this.type = MediaType.GIF; this.url = media.video_info?.variants[0].url as string; } // If the media is a video else { - this.type = EMediaType.VIDEO; + this.type = MediaType.VIDEO; this.thumbnailUrl = media.media_url_https; /** The highest bitrate of all variants. */ @@ -372,6 +379,7 @@ export class TweetMedia { */ public toJSON(): ITweetMedia { return { + id: this.id, thumbnailUrl: this.thumbnailUrl, type: this.type, url: this.url, diff --git a/src/models/data/User.ts b/src/models/data/User.ts index 05b32719..7259baae 100644 --- a/src/models/data/User.ts +++ b/src/models/data/User.ts @@ -1,4 +1,4 @@ -import { ELogActions } from '../../enums/Logging'; +import { LogActions } from '../../enums/Logging'; import { findByFilter } from '../../helper/JsonUtils'; import { LogService } from '../../services/internal/LogService'; import { IUser } from '../../types/data/User'; @@ -20,6 +20,8 @@ export class User implements IUser { public followingsCount: number; public fullName: string; public id: string; + public isFollowed?: boolean; + public isFollowing?: boolean; public isVerified: boolean; public likeCount: number; public location?: string; @@ -35,19 +37,21 @@ export class User implements IUser { public constructor(user: IRawUser) { this._raw = { ...user }; this.id = user.rest_id; - this.userName = user.legacy.screen_name; - this.fullName = user.legacy.name; - this.createdAt = new Date(user.legacy.created_at).toISOString(); + this.userName = user.core?.screen_name ?? user.legacy.screen_name ?? ''; + this.fullName = user.core?.name ?? user.legacy.name ?? ''; + this.createdAt = new Date(user.core?.created_at ?? user.legacy.created_at ?? 0).toISOString(); this.description = user.legacy.description.length ? user.legacy.description : undefined; + this.isFollowed = user.legacy.following; + this.isFollowing = user.legacy.followed_by; this.isVerified = user.is_blue_verified; this.likeCount = user.legacy.favourites_count; this.followersCount = user.legacy.followers_count; this.followingsCount = user.legacy.friends_count; this.statusesCount = user.legacy.statuses_count; - this.location = user.legacy.location.length ? user.legacy.location : undefined; + this.location = user.location?.location ?? user.legacy.location ?? undefined; this.pinnedTweet = user.legacy.pinned_tweet_ids_str[0]; this.profileBanner = user.legacy.profile_banner_url; - this.profileImage = user.legacy.profile_image_url_https; + this.profileImage = user.avatar?.image_url ?? user.legacy.profile_image_url_https ?? ''; } /** The raw user details. */ @@ -71,15 +75,15 @@ export class User implements IUser { // Deserializing valid data for (const item of extract) { - if (item.legacy && item.legacy.created_at) { + if (item.legacy && (item.core?.created_at || item.legacy.created_at)) { // Logging - LogService.log(ELogActions.DESERIALIZE, { id: item.rest_id }); + LogService.log(LogActions.DESERIALIZE, { id: item.rest_id }); users.push(new User(item)); } else { // Logging - LogService.log(ELogActions.WARNING, { - action: ELogActions.DESERIALIZE, + LogService.log(LogActions.WARNING, { + action: LogActions.DESERIALIZE, message: `User not found, skipping`, }); } @@ -108,15 +112,15 @@ export class User implements IUser { // Deserializing valid data for (const item of extract) { - if (item.legacy && item.legacy.created_at) { + if (item.legacy && (item.core?.created_at || item.legacy.created_at)) { // Logging - LogService.log(ELogActions.DESERIALIZE, { id: item.rest_id }); + LogService.log(LogActions.DESERIALIZE, { id: item.rest_id }); users.push(new User(item)); } else { // Logging - LogService.log(ELogActions.WARNING, { - action: ELogActions.DESERIALIZE, + LogService.log(LogActions.WARNING, { + action: LogActions.DESERIALIZE, message: `User not found, skipping`, }); } @@ -142,13 +146,13 @@ export class User implements IUser { for (const item of extract) { if (item.user_results?.result?.legacy) { // Logging - LogService.log(ELogActions.DESERIALIZE, { id: item.user_results.result.rest_id }); + LogService.log(LogActions.DESERIALIZE, { id: item.user_results.result.rest_id }); users.push(new User(item.user_results.result)); } else { // Logging - LogService.log(ELogActions.WARNING, { - action: ELogActions.DESERIALIZE, + LogService.log(LogActions.WARNING, { + action: LogActions.DESERIALIZE, message: `User not found, skipping`, }); } @@ -168,6 +172,8 @@ export class User implements IUser { followingsCount: this.followingsCount, fullName: this.fullName, id: this.id, + isFollowed: this.isFollowed, + isFollowing: this.isFollowing, isVerified: this.isVerified, likeCount: this.likeCount, location: this.location, diff --git a/src/requests/DirectMessage.ts b/src/requests/DirectMessage.ts new file mode 100644 index 00000000..b4398f59 --- /dev/null +++ b/src/requests/DirectMessage.ts @@ -0,0 +1,233 @@ +import qs from 'querystring'; + +import { AxiosRequestConfig } from 'axios'; + +/** + * Common parameter sets for DM requests + */ +const BaseDMParams = { + /* eslint-disable @typescript-eslint/naming-convention */ + + nsfw_filtering_enabled: false, + filter_low_quality: true, + include_quality: 'all', + dm_secret_conversations_enabled: false, + krs_registration_enabled: false, + cards_platform: 'Web-12', + include_cards: 1, + include_ext_alt_text: true, + include_ext_limited_action_results: true, + include_quote_count: true, + include_reply_count: 1, + tweet_mode: 'extended', + include_ext_views: true, + include_groups: true, + include_inbox_timelines: true, + include_ext_media_color: true, + supports_reactions: true, + supports_edit: true, + include_ext_edit_control: true, + include_ext_business_affiliations_label: true, + ext: 'mediaColor%2CaltText%2CbusinessAffiliationsLabel%2CmediaStats%2ChighlightedLabel%2CparodyCommentaryFanLabel%2CvoiceInfo%2CbirdwatchPivot%2CsuperFollowMetadata%2CunmentionInfo%2CeditControl%2Carticle', + + /* eslint-enable @typescript-eslint/naming-convention */ +}; + +const DMUserIncludeParams = { + /* eslint-disable @typescript-eslint/naming-convention */ + + include_profile_interstitial_type: 1, + include_blocking: 1, + include_blocked_by: 1, + include_followed_by: 1, + include_want_retweets: 1, + include_mute_edge: 1, + include_can_dm: 1, + include_can_media_tag: 1, + include_ext_is_blue_verified: 1, + include_ext_verified_type: 1, + include_ext_profile_image_shape: 1, + skip_status: 1, + + /* eslint-enable @typescript-eslint/naming-convention */ +}; + +/** + * Collection of requests related to direct messages. + * + * @public + */ +export class DMRequests { + /** + * Get a specific DM conversation + * @param conversationId - The conversation ID (e.g., "394028042-1712730991884689408") + * @param maxId - Maximum ID for pagination (optional) + */ + public static conversation(conversationId: string, maxId?: string): AxiosRequestConfig { + const context = maxId ? 'FETCH_DM_CONVERSATION_HISTORY' : 'FETCH_DM_CONVERSATION'; + + return { + method: 'get', + url: `https://x.com/i/api/1.1/dm/conversation/${conversationId}.json`, + params: { + ...BaseDMParams, + ...DMUserIncludeParams, + /* eslint-disable @typescript-eslint/naming-convention */ + max_id: maxId, + context: context, + dm_users: false, + include_conversation_info: true, + /* eslint-enable @typescript-eslint/naming-convention */ + }, + paramsSerializer: { encode: encodeURIComponent }, + }; + } + + /** + * Delete a DM conversation + * @param conversationId - The ID of the conversation to delete + */ + public static deleteConversation(conversationId: string): AxiosRequestConfig { + return { + method: 'post', + url: `https://x.com/i/api/1.1/dm/${conversationId}/delete.json`, + data: qs.stringify({ + /* eslint-disable @typescript-eslint/naming-convention */ + dm_secret_conversations_enabled: false, + krs_registration_enabled: false, + cards_platform: 'Web-12', + include_cards: 1, + include_ext_alt_text: true, + include_ext_limited_action_results: true, + include_quote_count: true, + include_reply_count: 1, + tweet_mode: 'extended', + include_ext_views: true, + dm_users: false, + include_groups: true, + include_inbox_timelines: true, + include_ext_media_color: true, + supports_reactions: true, + supports_edit: true, + include_conversation_info: true, + }), + }; + } + + /** + * Get the initial state of the DM inbox + */ + public static inboxInitial(): AxiosRequestConfig { + return { + method: 'get', + url: 'https://x.com/i/api/1.1/dm/inbox_initial_state.json', + params: { + ...BaseDMParams, + ...DMUserIncludeParams, + /* eslint-disable @typescript-eslint/naming-convention */ + dm_users: true, + include_ext_parody_commentary_fan_label: true, + ext: 'mediaColor%2CaltText%2CmediaStats%2ChighlightedLabel%2CparodyCommentaryFanLabel%2CvoiceInfo%2CbirdwatchPivot%2CsuperFollowMetadata%2CunmentionInfo%2CeditControl%2Carticle', + }, + paramsSerializer: { encode: encodeURIComponent }, + }; + } + + /** + * Get inbox timeline (pagination of conversations) + * @param maxId - Maximum ID for pagination + */ + public static inboxTimeline(maxId?: string): AxiosRequestConfig { + return { + method: 'get', + url: 'https://x.com/i/api/1.1/dm/inbox_timeline/trusted.json', + params: { + ...BaseDMParams, + ...DMUserIncludeParams, + /* eslint-disable @typescript-eslint/naming-convention */ + max_id: maxId, + dm_users: false, + }, + paramsSerializer: { encode: encodeURIComponent }, + }; + } + + /** + * Create a new DM or get DM creation interface + */ + // public static new(): AxiosRequestConfig { + // return { + // method: 'get', + // url: 'https://x.com/i/api/1.1/dm/new2.json', + // params: { + // /* eslint-disable @typescript-eslint/naming-convention */ + // ext: 'mediaColor%2CaltText%2CmediaStats%2ChighlightedLabel%2CparodyCommentaryFanLabel%2CvoiceInfo%2CbirdwatchPivot%2CsuperFollowMetadata%2CunmentionInfo%2CeditControl%2Carticle', + // include_ext_alt_text: true, + // include_ext_limited_action_results: true, + // include_reply_count: 1, + // tweet_mode: 'extended', + // include_ext_views: true, + // include_groups: true, + // include_inbox_timelines: true, + // include_ext_media_color: true, + // supports_reactions: true, + // supports_edit: true, + // }, + // paramsSerializer: { encode: encodeURIComponent }, + // }; + // } + + /** + * Check DM permissions for specific recipients + * @param recipientIds - Array of recipient user IDs + */ + // public static permissions(recipientIds: string[]): AxiosRequestConfig { + // return { + // method: 'get', + // url: 'https://x.com/i/api/1.1/dm/permissions.json', + // params: { + // /* eslint-disable @typescript-eslint/naming-convention */ + // recipient_ids: recipientIds.join(','), + // dm_users: true, + // }, + // paramsSerializer: { encode: encodeURIComponent }, + // }; + // } + + /** + * Update the last seen event ID for a conversation + * @param lastSeenEventId - The ID of the last seen event + * @param trustedLastSeenEventId - The trusted last seen event ID (usually same as lastSeenEventId) + */ + // public static updateLastSeenEventId(lastSeenEventId: string, trustedLastSeenEventId?: string): AxiosRequestConfig { + // return { + // method: 'post', + // url: 'https://x.com/i/api/1.1/dm/update_last_seen_event_id.json', + // data: qs.stringify({ + // /* eslint-disable @typescript-eslint/naming-convention */ + // last_seen_event_id: lastSeenEventId, + // trusted_last_seen_event_id: trustedLastSeenEventId ?? lastSeenEventId, + // }), + // }; + // } + + /** + * Get user updates for DMs (polling for new messages) + * @param cursor - Cursor for pagination + * @param activeConversationId - ID of the currently active conversation + */ + // public static userUpdates(cursor?: string, activeConversationId?: string): AxiosRequestConfig { + // return { + // method: 'get', + // url: 'https://x.com/i/api/1.1/dm/user_updates.json', + // params: { + // ...DM_BASE_PARAMS, + // /* eslint-disable @typescript-eslint/naming-convention */ + // cursor: cursor, + // active_conversation_id: activeConversationId, + // dm_users: false, + // }, + // paramsSerializer: { encode: encodeURIComponent }, + // }; + // } +} diff --git a/src/requests/List.ts b/src/requests/List.ts index 51aa19cf..9c8527f1 100644 --- a/src/requests/List.ts +++ b/src/requests/List.ts @@ -6,19 +6,49 @@ import { AxiosRequestConfig } from 'axios'; * @public */ export class ListRequests { + /** + * @param listId - The ID of the target list. + * @param userId - The ID of the user to be added as a member. + */ + public static addMember(listId: string, userId: string): AxiosRequestConfig { + return { + method: 'post', + url: 'https://x.com/i/api/graphql/EadD8ivrhZhYQr2pDmCpjA/ListAddMember', + data: { + /* eslint-disable @typescript-eslint/naming-convention */ + + variables: { + listId: listId, + userId: userId, + }, + features: { + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, + rweb_tipjar_consumption_enabled: true, + verified_phone_label_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + responsive_web_graphql_timeline_navigation_enabled: true, + }, + + /* eslint-enable @typescript-eslint/naming-convention */ + }, + }; + } + /** * @param id - The id of the list whose details are to be fetched. */ public static details(id: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/gO1_eYPohKYHwCG2m-1ZnQ/ListByRestId', + url: 'https://x.com/i/api/graphql/Tzkkg-NaBi_y1aAUUb6_eQ/ListByRestId', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ listId: id }), features: JSON.stringify({ - rweb_lists_timeline_redesign_enabled: true, - responsive_web_graphql_exclude_directive_enabled: true, + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, + rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, responsive_web_graphql_timeline_navigation_enabled: true, @@ -37,7 +67,7 @@ export class ListRequests { public static members(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/T7VZsrWpCoi4jWxFdwyNcg/ListMembers', + url: 'https://x.com/i/api/graphql/Bnhcen0kdsMAU1tW7U79qQ/ListMembers', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -48,6 +78,7 @@ export class ListRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -58,7 +89,7 @@ export class ListRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -76,6 +107,8 @@ export class ListRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ @@ -83,6 +116,35 @@ export class ListRequests { }; } + /** + * @param listId - The ID of the target list. + * @param userId - The ID of the user to remove as a member. + */ + public static removeMember(listId: string, userId: string): AxiosRequestConfig { + return { + method: 'post', + url: 'https://x.com/i/api/graphql/B5tMzrMYuFHJex_4EXFTSw/ListRemoveMember', + data: { + /* eslint-disable @typescript-eslint/naming-convention */ + + variables: { + listId: listId, + userId: userId, + }, + features: { + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, + rweb_tipjar_consumption_enabled: true, + verified_phone_label_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + responsive_web_graphql_timeline_navigation_enabled: true, + }, + + /* eslint-enable @typescript-eslint/naming-convention */ + }, + }; + } + /** * @param id - The id of the list whose tweets are to be fetched. * @param count - The number of tweets to fetch. Must be \<= 100. @@ -91,7 +153,7 @@ export class ListRequests { public static tweets(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/BkauSnPUDQTeeJsxq17opA/ListLatestTweetsTimeline', + url: 'https://x.com/i/api/graphql/fqNUs_6rqLf89u_2waWuqg/ListLatestTweetsTimeline', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -102,6 +164,7 @@ export class ListRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -112,7 +175,7 @@ export class ListRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -130,6 +193,8 @@ export class ListRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/src/requests/Tweet.ts b/src/requests/Tweet.ts index d24c53cf..f72f6778 100644 --- a/src/requests/Tweet.ts +++ b/src/requests/Tweet.ts @@ -1,6 +1,6 @@ import { AxiosRequestConfig } from 'axios'; -import { ERawTweetRepliesSortType, ERawTweetSearchResultType } from '../enums/raw/Tweet'; +import { RawTweetRepliesSortType, RawTweetSearchResultType } from '../enums/raw/Tweet'; import { TweetFilter } from '../models/args/FetchArgs'; import { NewTweet } from '../models/args/PostArgs'; import { MediaVariable, ReplyVariable } from '../models/params/Variables'; @@ -13,13 +13,28 @@ import { INewTweet } from '../types/args/PostArgs'; * @public */ export class TweetRequests { + /** + * @param id - The ID of the tweet to bookmark + */ + public static bookmark(id: string): AxiosRequestConfig { + return { + method: 'post', + url: 'https://x.com/i/api/graphql/aoDbu3RHznuiSkQ9aNM67Q/CreateBookmark', + data: { + /* eslint-disable @typescript-eslint/naming-convention */ + variables: JSON.stringify({ tweet_id: id }), + /* eslint-enable @typescript-eslint/naming-convention */ + }, + }; + } + /** * @param ids - The IDs of the tweets whose details are to be fetched. */ public static bulkDetails(ids: string[]): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/kPnxYjNX2HCKu8aY96er5w/TweetResultsByRestIds', + url: 'https://x.com/i/api/graphql/-R17e8UqwApFGdMxa3jASA/TweetResultsByRestIds', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -68,6 +83,9 @@ export class TweetRequests { responsive_web_grok_analyze_button_fetch_trends_enabled: false, articles_preview_enabled: false, responsive_web_grok_share_attachment_enabled: false, + responsive_web_grok_imagine_annotation_enabled: false, + responsive_web_grok_community_note_auto_translation_is_enabled: false, + responsive_web_profile_redirect_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ }, @@ -81,7 +99,7 @@ export class TweetRequests { public static details(id: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/zAz9764BcLZOJ0JU2wrd1A/TweetResultByRestId', + url: 'https://x.com/i/api/graphql/aFvUsJm2c-oDkJV75blV6g/TweetResultByRestId', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -158,18 +176,20 @@ export class TweetRequests { public static likers(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/4AzoFlLkEcs2bx5pO1mvsQ/Favoriters', + url: 'https://x.com/i/api/graphql/b3OrdeHDQfb9zRMC0fV3bw/Favoriters', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ tweetId: id, count: count, cursor: cursor, + enableRanking: false, includePromotedContent: false, }), features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -180,7 +200,7 @@ export class TweetRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -198,6 +218,8 @@ export class TweetRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ @@ -215,7 +237,7 @@ export class TweetRequests { return { method: 'post', - url: 'https://x.com/i/api/graphql/IID9x6WsdMnTlXnzXGq8ng/CreateTweet', + url: 'https://x.com/i/api/graphql/Uf3io9zVp1DsYxrmL5FJ7g/CreateTweet', data: { /* eslint-disable @typescript-eslint/naming-convention */ variables: { @@ -256,6 +278,9 @@ export class TweetRequests { responsive_web_grok_image_annotation_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_enhance_cards_enabled: false, + responsive_web_grok_imagine_annotation_enabled: false, + responsive_web_profile_redirect_enabled: false, + responsive_web_grok_community_note_auto_translation_is_enabled: false, }, /* eslint-enable @typescript-eslint/naming-convention */ }, @@ -266,10 +291,10 @@ export class TweetRequests { * @param id - The id of the tweet whose replies are to be fetched. * @param cursor - The cursor to the batch of replies to fetch. */ - public static replies(id: string, cursor?: string, sortBy?: ERawTweetRepliesSortType): AxiosRequestConfig { + public static replies(id: string, cursor?: string, sortBy?: RawTweetRepliesSortType): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/_8aYOgEDz35BrBcBal1-_w/TweetDetail', + url: 'https://x.com/i/api/graphql/97JF30KziU00483E_8elBA/TweetDetail', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -277,7 +302,7 @@ export class TweetRequests { cursor: cursor, referrer: 'tweet', with_rux_injections: false, - rankingMode: sortBy ?? ERawTweetRepliesSortType.RELEVACE, + rankingMode: sortBy ?? RawTweetRepliesSortType.RELEVACE, includePromotedContent: true, withCommunity: true, withQuickPromoteEligibilityTweetFields: true, @@ -287,6 +312,7 @@ export class TweetRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -297,7 +323,7 @@ export class TweetRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -315,6 +341,8 @@ export class TweetRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), fieldToggles: JSON.stringify({ @@ -335,7 +363,7 @@ export class TweetRequests { public static retweet(id: string): AxiosRequestConfig { return { method: 'post', - url: 'https://x.com/i/api/graphql/ojPdsZsimiJrUGLR1sjUtA/CreateRetweet', + url: 'https://x.com/i/api/graphql/LFho5rIi4xcKO90p9jwG7A/CreateRetweet', data: { variables: { /* eslint-disable @typescript-eslint/naming-convention */ @@ -355,7 +383,7 @@ export class TweetRequests { public static retweeters(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/i-CI8t2pJD15euZJErEDrg/Retweeters', + url: 'https://x.com/i/api/graphql/wfglZEC0MRgBdxMa_1a5YQ/Retweeters', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -367,6 +395,7 @@ export class TweetRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -377,7 +406,7 @@ export class TweetRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -395,6 +424,8 @@ export class TweetRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ @@ -443,7 +474,7 @@ export class TweetRequests { return { method: 'get', - url: 'https://x.com/i/api/graphql/nK1dw4oV3k4w5TdtcAdSww/SearchTimeline', + url: 'https://x.com/i/api/graphql/M1jEez78PEfVfbQLvlWMvQ/SearchTimeline', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -451,49 +482,44 @@ export class TweetRequests { count: count, cursor: cursor, querySource: 'typed_query', - product: parsedFilter.top ? ERawTweetSearchResultType.TOP : ERawTweetSearchResultType.LATEST, - withAuxiliaryUserLabels: false, - withArticleRichContentState: false, - withArticlePlainText: false, - withGrokAnalyze: false, - withDisallowedReplyControls: false, + product: parsedFilter.top ? RawTweetSearchResultType.TOP : RawTweetSearchResultType.LATEST, + withGrokTranslatedBio: false, }), features: JSON.stringify({ - rweb_lists_timeline_redesign_enabled: true, - responsive_web_graphql_exclude_directive_enabled: true, + rweb_video_screen_enabled: false, + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, + rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, - tweetypie_unmention_optimization_enabled: true, + premium_content_api_read_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: false, + responsive_web_grok_analyze_post_followups_enabled: true, + responsive_web_jetfuel_frame: true, + responsive_web_grok_share_attachment_enabled: true, + articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, view_counts_everywhere_api_enabled: true, longform_notetweets_consumption_enabled: true, - responsive_web_twitter_article_tweet_consumption_enabled: false, + responsive_web_twitter_article_tweet_consumption_enabled: true, tweet_awards_web_tipping_enabled: false, + responsive_web_grok_show_grok_translated_post: false, + responsive_web_grok_analysis_button_from_backend: true, + creator_subscriptions_quote_tweet_preview_enabled: false, freedom_of_speech_not_reach_fetch_enabled: true, standardized_nudges_misinfo: true, tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, - responsive_web_media_download_video_enabled: false, + responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, - c9s_tweet_anatomy_moderator_badge_enabled: false, - responsive_web_grok_show_grok_translated_post: false, - premium_content_api_read_enabled: false, - rweb_video_screen_enabled: false, - responsive_web_grok_analyze_post_followups_enabled: false, - creator_subscriptions_quote_tweet_preview_enabled: false, - communities_web_enable_tweet_community_results_fetch: false, - rweb_tipjar_consumption_enabled: false, - responsive_web_grok_analyze_button_fetch_trends_enabled: false, - profile_label_improvements_pcf_label_in_post_enabled: false, - responsive_web_grok_image_annotation_enabled: false, - responsive_web_jetfuel_frame: false, - articles_preview_enabled: false, - responsive_web_grok_share_attachment_enabled: false, - responsive_web_grok_analysis_button_from_backend: false, }), /* eslint-enable @typescript-eslint/naming-convention */ }, @@ -501,6 +527,23 @@ export class TweetRequests { }; } + /** + * @param id - The id of the tweet to be unbookmarked. + */ + public static unbookmark(id: string): AxiosRequestConfig { + return { + method: 'post', + url: 'https://x.com/i/api/graphql/Wlmlj2-xzyS1GN3a6cj-mQ/DeleteBookmark', + data: { + /* eslint-disable @typescript-eslint/naming-convention */ + variables: { + tweet_id: id, + }, + /* eslint-enable @typescript-eslint/naming-convention */ + }, + }; + } + /** * @param id - The id of the tweet to be unliked. */ diff --git a/src/requests/User.ts b/src/requests/User.ts index aa42247a..f719fcca 100644 --- a/src/requests/User.ts +++ b/src/requests/User.ts @@ -2,7 +2,8 @@ import qs from 'querystring'; import { AxiosRequestConfig } from 'axios'; -import { ERawAnalyticsGranularity, ERawAnalyticsMetric } from '../enums/raw/Analytics'; +import { RawAnalyticsGranularity, RawAnalyticsMetric } from '../enums/raw/Analytics'; +import { IProfileUpdateOptions } from '../types/args/ProfileArgs'; /** * Collection of requests related to users. @@ -18,7 +19,7 @@ export class UserRequests { public static affiliates(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/OVFfg1hExk_AygiMVSJd-Q/UserBusinessProfileTeamTimeline', + url: 'https://x.com/i/api/graphql/KFaAofDlKP7bnzskNWmjwA/UserBusinessProfileTeamTimeline', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -33,6 +34,7 @@ export class UserRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -43,7 +45,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -61,6 +63,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ @@ -73,22 +77,147 @@ export class UserRequests { * @param toTime - The end time of the analytic data to be fetched. * @param granularity - The granularity of the analytic data to be fetched. * @param requestedMetrics - The metrics to be fetched. + * @param showVerifiedFollowers - Whether to show verified followers in the analytics. */ public static analytics( fromTime: Date, toTime: Date, - granularity: ERawAnalyticsGranularity, - requestedMetrics: ERawAnalyticsMetric[], + granularity: RawAnalyticsGranularity, + requestedMetrics: RawAnalyticsMetric[], + showVerifiedFollowers: boolean, ): AxiosRequestConfig { + console.log( + `Fetching analytics from ${fromTime?.toString()} to ${toTime?.toString()} with granularity ${granularity} and metrics ${requestedMetrics.join(', ')}`, + ); + return { + method: 'get', + url: 'https://x.com/i/api/graphql/LwtiA7urqM6eDeBheAFi5w/AccountOverviewQuery', + params: { + variables: JSON.stringify({ + /* eslint-disable @typescript-eslint/naming-convention */ + from_time: fromTime, + to_time: toTime, + granularity: granularity, + requested_metrics: requestedMetrics, + show_verified_followers: showVerifiedFollowers, + /* eslint-enable @typescript-eslint/naming-convention */ + }), + }, + paramsSerializer: { encode: encodeURIComponent }, + }; + } + + /** + * Fetches tweets from a specific bookmark folder. + * + * @param folderId - The ID of the bookmark folder. + * @param count - The number of tweets to fetch. + * @param cursor - The cursor to the batch of tweets to fetch. + */ + public static bookmarkFolderTweets(folderId: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/NlJ6RM-hgHxt-iu9cPQz7A/overviewDataUserQuery', + url: 'https://x.com/i/api/graphql/KJIQpsvxrTfRIlbaRIySHQ/BookmarkFolderTimeline', params: { /* eslint-disable @typescript-eslint/naming-convention */ - from_time: fromTime, - to_time: toTime, - granularity: granularity, - requested_metrics: requestedMetrics, + variables: JSON.stringify({ + bookmark_collection_id: folderId, + count: count, + cursor: cursor, + includePromotedContent: true, + }), + features: JSON.stringify({ + rweb_video_screen_enabled: false, + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, + rweb_tipjar_consumption_enabled: true, + verified_phone_label_enabled: true, + creator_subscriptions_tweet_preview_api_enabled: true, + responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + premium_content_api_read_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: false, + responsive_web_grok_analyze_post_followups_enabled: true, + responsive_web_jetfuel_frame: true, + responsive_web_grok_share_attachment_enabled: true, + articles_preview_enabled: true, + responsive_web_edit_tweet_api_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + view_counts_everywhere_api_enabled: true, + longform_notetweets_consumption_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + tweet_awards_web_tipping_enabled: false, + responsive_web_grok_show_grok_translated_post: false, + responsive_web_grok_analysis_button_from_backend: true, + creator_subscriptions_quote_tweet_preview_enabled: false, + freedom_of_speech_not_reach_fetch_enabled: true, + responsive_web_grok_imagine_annotation_enabled: false, + responsive_web_grok_community_note_auto_translation_is_enabled: false, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + responsive_web_grok_image_annotation_enabled: true, + responsive_web_enhance_cards_enabled: false, + }), + /* eslint-enable @typescript-eslint/naming-convention */ + }, + paramsSerializer: { encode: encodeURIComponent }, + }; + } + + /** + * Fetches the list of bookmark folders for the logged-in user. + * + * @param cursor - The cursor to the batch of bookmark folders to fetch. + */ + public static bookmarkFolders(cursor?: string): AxiosRequestConfig { + return { + method: 'get', + url: 'https://x.com/i/api/graphql/i78YDd0Tza-dV4SYs58kRg/BookmarkFoldersSlice', + params: { + /* eslint-disable @typescript-eslint/naming-convention */ + variables: JSON.stringify({ + cursor: cursor, + }), + features: JSON.stringify({ + rweb_video_screen_enabled: false, + profile_label_improvements_pcf_label_in_post_enabled: true, + rweb_tipjar_consumption_enabled: true, + verified_phone_label_enabled: true, + creator_subscriptions_tweet_preview_api_enabled: true, + responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + premium_content_api_read_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: false, + responsive_web_grok_analyze_post_followups_enabled: true, + responsive_web_jetfuel_frame: false, + responsive_web_grok_share_attachment_enabled: true, + articles_preview_enabled: true, + responsive_web_edit_tweet_api_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + view_counts_everywhere_api_enabled: true, + longform_notetweets_consumption_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + tweet_awards_web_tipping_enabled: false, + responsive_web_grok_show_grok_translated_post: false, + responsive_web_grok_analysis_button_from_backend: true, + creator_subscriptions_quote_tweet_preview_enabled: false, + freedom_of_speech_not_reach_fetch_enabled: true, + responsive_web_grok_imagine_annotation_enabled: false, + responsive_web_grok_community_note_auto_translation_is_enabled: false, + responsive_web_profile_redirect_enabled: false, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + responsive_web_grok_image_annotation_enabled: true, + responsive_web_enhance_cards_enabled: false, + }), /* eslint-enable @typescript-eslint/naming-convention */ }, paramsSerializer: { encode: encodeURIComponent }, @@ -136,6 +265,9 @@ export class UserRequests { responsive_web_grok_analysis_button_from_backend: true, creator_subscriptions_quote_tweet_preview_enabled: false, freedom_of_speech_not_reach_fetch_enabled: true, + responsive_web_grok_imagine_annotation_enabled: false, + responsive_web_grok_community_note_auto_translation_is_enabled: false, + responsive_web_profile_redirect_enabled: false, standardized_nudges_misinfo: true, tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, longform_notetweets_rich_text_read_enabled: true, @@ -155,7 +287,7 @@ export class UserRequests { public static bulkDetailsByIds(ids: string[]): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/PyRggX3LQweP9nSF6PHliA/UsersByRestIds', + url: 'https://x.com/i/api/graphql/xavgLWWbFH8wm_8MQN8plQ/UsersByRestIds', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ userIds: ids }), @@ -171,6 +303,7 @@ export class UserRequests { responsive_web_graphql_timeline_navigation_enabled: true, profile_label_improvements_pcf_label_in_post_enabled: false, rweb_tipjar_consumption_enabled: false, + responsive_web_profile_redirect_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ }, @@ -184,7 +317,7 @@ export class UserRequests { public static detailsById(id: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/WJ7rCtezBVT6nk6VM5R8Bw/UserByRestId', + url: 'https://x.com/i/api/graphql/Bbaot8ySMtJD7K2t01gW7A/UserByRestId', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ userId: id, withSafetyModeUserFields: true }), @@ -199,6 +332,7 @@ export class UserRequests { creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_profile_redirect_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ }, @@ -212,13 +346,14 @@ export class UserRequests { public static detailsByUsername(userName: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/1VOOyvKkiI3FMmkeDNxM9A/UserByScreenName', + url: 'https://x.com/i/api/graphql/-oaLodhGbbnzJBACb1kk2Q/UserByScreenName', params: { /* eslint-disable @typescript-eslint/naming-convention */ - variables: JSON.stringify({ screen_name: userName, withSafetyModeUserFields: true }), + variables: JSON.stringify({ screen_name: userName, withGrokTranslatedBio: false }), features: JSON.stringify({ hidden_profile_subscriptions_enabled: true, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, subscriptions_verification_info_is_identity_verified_enabled: true, @@ -259,7 +394,7 @@ export class UserRequests { public static followed(count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/CRprHpVA12yhsub-KRERIg/HomeLatestTimeline', + url: 'https://x.com/i/api/graphql/_qO7FJzShSKYWi9gtboE6A/HomeLatestTimeline', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -272,6 +407,7 @@ export class UserRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -282,7 +418,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -300,6 +436,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ @@ -316,7 +454,7 @@ export class UserRequests { public static followers(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/Elc_-qTARceHpztqhI9PQA/Followers', + url: 'https://x.com/i/api/graphql/kuFUYP9eV1FPoEy4N-pi7w/Followers', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -324,10 +462,12 @@ export class UserRequests { count: count, cursor: cursor, includePromotedContent: false, + withGrokTranslatedBio: false, }), features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -338,7 +478,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -356,6 +496,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ @@ -428,7 +570,7 @@ export class UserRequests { public static highlights(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/cr8FsaThDCa9LKeD9CNZ4w/UserHighlightsTweets', + url: 'https://x.com/i/api/graphql/kzKWdUA6Y1LCqlvaVILZwQ/UserHighlightsTweets', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -441,6 +583,7 @@ export class UserRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -451,7 +594,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -469,6 +612,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), fieldToggles: { withArticlePlainText: false }, @@ -486,7 +631,7 @@ export class UserRequests { public static likes(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/eQl7iWsCr2fChppuJdAeRw/Likes', + url: 'https://x.com/i/api/graphql/JR2gceKucIKcVNB_9JkhsA/Likes', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -502,6 +647,7 @@ export class UserRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -512,7 +658,62 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, + responsive_web_grok_share_attachment_enabled: true, + articles_preview_enabled: true, + responsive_web_edit_tweet_api_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + view_counts_everywhere_api_enabled: true, + longform_notetweets_consumption_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + tweet_awards_web_tipping_enabled: false, + responsive_web_grok_show_grok_translated_post: false, + responsive_web_grok_analysis_button_from_backend: true, + creator_subscriptions_quote_tweet_preview_enabled: false, + freedom_of_speech_not_reach_fetch_enabled: true, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, + responsive_web_enhance_cards_enabled: false, + }), + fieldToggles: { withArticlePlainText: false }, + /* eslint-enable @typescript-eslint/naming-convention */ + }, + paramsSerializer: { encode: encodeURIComponent }, + }; + } + + /** + * @param id - The id of the user whose lists are to be fetched. + * @param count - The number of lists to fetch. Only works as a lower limit when used with a cursor. + * @param cursor - The cursor to the batch of lists to fetch. + */ + public static lists(id: string, count?: number, cursor?: string): AxiosRequestConfig { + return { + method: 'get', + url: 'https://x.com/i/api/graphql/9mQl9vR31wjodBP9b7_wyQ/ListsManagementPageTimeline', + params: { + /* eslint-disable @typescript-eslint/naming-convention */ + variables: JSON.stringify({ count: 100, cursor: cursor }), + features: JSON.stringify({ + rweb_video_screen_enabled: false, + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, + rweb_tipjar_consumption_enabled: true, + verified_phone_label_enabled: true, + creator_subscriptions_tweet_preview_api_enabled: true, + responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + premium_content_api_read_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: false, + responsive_web_grok_analyze_post_followups_enabled: true, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -530,6 +731,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), fieldToggles: { withArticlePlainText: false }, @@ -547,7 +750,7 @@ export class UserRequests { public static media(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/vFPc2LVIu7so2uA_gHQAdg/UserMedia', + url: 'https://x.com/i/api/graphql/MMnr49cP_nldzCTfeVDRtA/UserMedia', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -563,6 +766,7 @@ export class UserRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -573,7 +777,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -591,6 +795,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), fieldToggles: { withArticlePlainText: false }, @@ -607,7 +813,7 @@ export class UserRequests { public static notifications(count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/gaBVLalXDBRDJz6maKgdWg/NotificationsTimeline', + url: 'https://x.com/i/api/graphql/Ev6UMJRROInk_RMH2oVbBg/NotificationsTimeline', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -618,6 +824,7 @@ export class UserRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -628,7 +835,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -646,6 +853,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ @@ -660,7 +869,7 @@ export class UserRequests { public static recommended(count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/Q_P3YVnmHunGFkZ8ISM-7w/HomeTimeline', + url: 'https://x.com/i/api/graphql/V7xdnRnvW6a8vIsMr9xK7A/HomeTimeline', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -674,6 +883,7 @@ export class UserRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -684,7 +894,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -702,6 +912,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ @@ -721,6 +933,67 @@ export class UserRequests { }; } + /** + * @param userName - The username to search for. + * @param count - The number of user matches to fetch. Only works as a lower limit when used with a cursor. + * @param cursor - The cursor to the batch of results to fetch. + */ + public static search(userName: string, count?: number, cursor?: string): AxiosRequestConfig { + return { + method: 'get', + url: 'https://x.com/i/api/graphql/M1jEez78PEfVfbQLvlWMvQ/SearchTimeline', + params: { + /* eslint-disable @typescript-eslint/naming-convention */ + variables: JSON.stringify({ + rawQuery: userName, + count: count, + cursor: cursor, + querySource: 'typed_query', + product: 'People', + withGrokTranslatedBio: false, + }), + features: JSON.stringify({ + rweb_video_screen_enabled: false, + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, + rweb_tipjar_consumption_enabled: true, + verified_phone_label_enabled: true, + creator_subscriptions_tweet_preview_api_enabled: true, + responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + premium_content_api_read_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: false, + responsive_web_grok_analyze_post_followups_enabled: true, + responsive_web_jetfuel_frame: true, + responsive_web_grok_share_attachment_enabled: true, + articles_preview_enabled: true, + responsive_web_edit_tweet_api_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + view_counts_everywhere_api_enabled: true, + longform_notetweets_consumption_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + tweet_awards_web_tipping_enabled: false, + responsive_web_grok_show_grok_translated_post: false, + responsive_web_grok_analysis_button_from_backend: true, + creator_subscriptions_quote_tweet_preview_enabled: false, + freedom_of_speech_not_reach_fetch_enabled: true, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, + responsive_web_enhance_cards_enabled: false, + }), + /* eslint-enable @typescript-eslint/naming-convention */ + }, + paramsSerializer: { encode: encodeURIComponent }, + }; + } + /** * @param id - The id of the user whose subscriptions are to be fetched. * @param count - The number of subscriptions to fetch. Only works as a lower limit when used with a cursor. @@ -729,7 +1002,7 @@ export class UserRequests { public static subscriptions(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/UWlxAhUnBNK0BYmeqNPqAw/UserCreatorSubscriptions', + url: 'https://x.com/i/api/graphql/fl06vhYypYRcRxgLKO011Q/UserCreatorSubscriptions', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -739,25 +1012,39 @@ export class UserRequests { includePromotedContent: false, }), features: JSON.stringify({ - responsive_web_graphql_exclude_directive_enabled: true, + rweb_video_screen_enabled: false, + profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, + rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + premium_content_api_read_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, c9s_tweet_anatomy_moderator_badge_enabled: true, - tweetypie_unmention_optimization_enabled: true, + responsive_web_grok_analyze_button_fetch_trends_enabled: false, + responsive_web_grok_analyze_post_followups_enabled: true, + responsive_web_jetfuel_frame: true, + responsive_web_grok_share_attachment_enabled: true, + articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, view_counts_everywhere_api_enabled: true, longform_notetweets_consumption_enabled: true, responsive_web_twitter_article_tweet_consumption_enabled: true, tweet_awards_web_tipping_enabled: false, + responsive_web_grok_show_grok_translated_post: false, + responsive_web_grok_analysis_button_from_backend: true, + creator_subscriptions_quote_tweet_preview_enabled: false, freedom_of_speech_not_reach_fetch_enabled: true, standardized_nudges_misinfo: true, tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, - rweb_video_timestamps_enabled: true, longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, + responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), /* eslint-enable @typescript-eslint/naming-convention */ @@ -774,7 +1061,7 @@ export class UserRequests { public static tweets(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/HeWHY26ItCfUmm1e6ITjeA/UserTweets', + url: 'https://x.com/i/api/graphql/-V26I6Pb5xDZ3C7BWwCQ_Q/UserTweets', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -789,6 +1076,7 @@ export class UserRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -799,7 +1087,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -817,6 +1105,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), fieldToggles: { withArticlePlainText: false }, @@ -834,7 +1124,7 @@ export class UserRequests { public static tweetsAndReplies(id: string, count?: number, cursor?: string): AxiosRequestConfig { return { method: 'get', - url: 'https://x.com/i/api/graphql/OAx9yEcW3JA9bPo63pcYlA/UserTweetsAndReplies', + url: 'https://x.com/i/api/graphql/61HQnvcGP870hiE-hCbG4A/UserTweetsAndReplies', params: { /* eslint-disable @typescript-eslint/naming-convention */ variables: JSON.stringify({ @@ -849,6 +1139,7 @@ export class UserRequests { features: JSON.stringify({ rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, + responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: true, creator_subscriptions_tweet_preview_api_enabled: true, @@ -859,7 +1150,7 @@ export class UserRequests { c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: false, responsive_web_grok_analyze_post_followups_enabled: true, - responsive_web_jetfuel_frame: false, + responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true, @@ -877,6 +1168,8 @@ export class UserRequests { longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, + responsive_web_grok_imagine_annotation_enabled: true, + responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }), fieldToggles: { withArticlePlainText: false }, @@ -900,4 +1193,20 @@ export class UserRequests { }), }; } + + /** + * @param options - The profile update options. + */ + public static updateProfile(options: IProfileUpdateOptions): AxiosRequestConfig { + return { + method: 'post', + url: 'https://x.com/i/api/1.1/account/update_profile.json', + data: qs.stringify({ + ...(options.name && { name: options.name }), + ...(options.url && { url: options.url }), + ...(options.location && { location: options.location }), + ...(options.description && { description: options.description }), + }), + }; + } } diff --git a/src/services/internal/AuthService.ts b/src/services/internal/AuthService.ts index f4fd0253..4770c442 100644 --- a/src/services/internal/AuthService.ts +++ b/src/services/internal/AuthService.ts @@ -1,6 +1,6 @@ import axios from 'axios'; -import { EApiErrors } from '../../enums/Api'; +import { ApiErrors } from '../../enums/Api'; import { AuthCredential } from '../../models/auth/AuthCredential'; import { RettiwtConfig } from '../../models/RettiwtConfig'; @@ -67,7 +67,7 @@ export class AuthService { } // If user id was not found else { - throw new Error(EApiErrors.BAD_AUTHENTICATION); + throw new Error(ApiErrors.BAD_AUTHENTICATION); } } diff --git a/src/services/internal/ErrorService.ts b/src/services/internal/ErrorService.ts index eb8677a8..ef33f231 100644 --- a/src/services/internal/ErrorService.ts +++ b/src/services/internal/ErrorService.ts @@ -15,14 +15,14 @@ export class ErrorService implements IErrorHandler { * * @param error - The error response received from Twitter. */ - private handleAxiosError(error: AxiosError): void { + private _handleAxiosError(error: AxiosError): void { throw new TwitterError(error); } /** * Handle unknown error. */ - private handleUnknownError(): void { + private _handleUnknownError(): void { throw new Error('Unknown error'); } @@ -33,9 +33,9 @@ export class ErrorService implements IErrorHandler { */ public handle(error: unknown): void { if (isAxiosError(error)) { - this.handleAxiosError(error as AxiosError); + this._handleAxiosError(error as AxiosError); } else { - this.handleUnknownError(); + this._handleUnknownError(); } } } diff --git a/src/services/internal/LogService.ts b/src/services/internal/LogService.ts index e81225f1..090474d7 100644 --- a/src/services/internal/LogService.ts +++ b/src/services/internal/LogService.ts @@ -1,4 +1,4 @@ -import { ELogActions } from '../../enums/Logging'; +import { LogActions } from '../../enums/Logging'; /** * Handles logging of data for debug purpose. @@ -16,7 +16,7 @@ export class LogService { * * @param data - The data to be logged. */ - public static log(action: ELogActions, data: NonNullable): void { + public static log(action: LogActions, data: NonNullable): void { // Proceed to log only if logging is enabled if (this.enabled) { // Preparing the log message diff --git a/src/services/internal/TidService.ts b/src/services/internal/TidService.ts deleted file mode 100644 index 1ad1d19d..00000000 --- a/src/services/internal/TidService.ts +++ /dev/null @@ -1,140 +0,0 @@ -import axios from 'axios'; -import * as htmlParser from 'node-html-parser'; - -import { ELogActions } from '../../enums/Logging'; - -import { calculateClientTransactionIdHeader } from '../../helper/TidUtils'; - -import { RettiwtConfig } from '../../models/RettiwtConfig'; -import { ITidDynamicArgs } from '../../types/auth/TidDynamicArgs'; -import { ITidProvider } from '../../types/auth/TidProvider'; - -import { LogService } from './LogService'; - -/** - * Handles transaction ID generation for requests to Twitter. - * - * @internal - */ -export class TidService implements ITidProvider { - private readonly _cdnUrl: string; - private readonly _config: RettiwtConfig; - private _dynamicArgs?: ITidDynamicArgs; - - /** - * @param config - The config for Rettiwt. - */ - public constructor(config: RettiwtConfig) { - this._cdnUrl = 'https://abs.twimg.com/responsive-web/client-web'; - this._config = config; - } - - /** - * Fetches the dynamic args embedded in the homepage. - * - * @returns The new dynamic args. - */ - private async getDynamicArgs(): Promise { - const html = await this.getHomepageHtml(); - const root = htmlParser.parse(html); - const keyElement = root.querySelector("[name='twitter-site-verification']"); - const frameElements = root.querySelectorAll("[id^='loading-x-anim']"); - - return { - verificationKey: keyElement?.getAttribute('content') ?? '', - frames: frameElements.map((el) => this.parseFrameElement(el)), - indices: await this.getKeyBytesIndices(html), - }; - } - - /** - * Fetches the HTML content of Twitter's homepage. - * - * @returns The stringified HTML content of the homepage. - */ - private async getHomepageHtml(): Promise { - const response = await axios.get('https://x.com', { - headers: this._config.headers, - httpAgent: this._config.httpsAgent, - httpsAgent: this._config.httpsAgent, - }); - - return response.data; - } - - private async getKeyBytesIndices(html: string): Promise { - const ondemandFileMatch = html.match(/ondemand\.s":"([^"]+)"/); - if (!ondemandFileMatch || !ondemandFileMatch[1]) { - LogService.log(ELogActions.WARNING, { message: 'ondemand.s file not found' }); - - return [0, 0, 0, 0]; - } - - const onDemandFileHash = ondemandFileMatch ? ondemandFileMatch[1] : ''; - const response = await axios.get(`${this._cdnUrl}/ondemand.s.${onDemandFileHash}a.js`, { - httpAgent: this._config.httpsAgent, - httpsAgent: this._config.httpsAgent, - }); - const match = response.data.matchAll(/(\(\w\[(\d{1,2})],\s*16\))+?/gm); - - return Array.from(match).map((m) => Number(m[2])); - } - - private parseFrameElement(element: htmlParser.HTMLElement): number[][] { - const pathElement = element.children[0].children[1]; - const value = pathElement.getAttribute('d'); - if (!value) { - return [[]]; - } - - const rawFrames = value.substring(9).split('C'); - - return rawFrames.map((str) => str.replaceAll(/\D+/g, ' ').trim().split(' ')).map((arr) => arr.map(Number)); - } - - /** - * Generate an `x-client-transaction-id` for the specific URL method and path. - * - * @param method - The target method. - * @param path - The target path. - * - * @returns The specific `x-client-transaction-id` token. - */ - public async generate(method: string, path: string): Promise { - try { - // Refreshing dynamic args - await this.refreshDynamicArgs(); - - // If dynamic args weren't obtained, skip with error - if (!this._dynamicArgs) { - throw new Error('Dynamic args failed to generate'); - } - - const { verificationKey, frames, indices } = this._dynamicArgs; - - return calculateClientTransactionIdHeader({ - keyword: 'obfiowerehiring', - method: method, - path: path, - verificationKey: verificationKey, - frames: frames, - indices: indices, - extraByte: 3, - }); - } catch (err) { - LogService.log(ELogActions.WARNING, { - message: 'Failed to generated transaction token. Request may or may not work', - error: err, - }); - - return; - } - } - - /** - * Refreshes the dynamic args from the homepage. - */ - public async refreshDynamicArgs(): Promise { - this._dynamicArgs = await this.getDynamicArgs(); - } -} diff --git a/src/services/public/DirectMessageService.ts b/src/services/public/DirectMessageService.ts new file mode 100644 index 00000000..cd4a133c --- /dev/null +++ b/src/services/public/DirectMessageService.ts @@ -0,0 +1,159 @@ +import { Extractors } from '../../collections/Extractors'; +import { ResourceType } from '../../enums/Resource'; +import { Conversation } from '../../models/data/Conversation'; +import { Inbox } from '../../models/data/Inbox'; +import { RettiwtConfig } from '../../models/RettiwtConfig'; +import { IConversationTimelineResponse } from '../../types/raw/dm/Conversation'; +import { IInboxInitialResponse } from '../../types/raw/dm/InboxInitial'; +import { IInboxTimelineResponse } from '../../types/raw/dm/InboxTimeline'; + +import { FetcherService } from './FetcherService'; + +/** + * Handles interacting with resources related to direct messages + * + * @public + */ +export class DirectMessageService extends FetcherService { + /** + * @param config - The config object for configuring the Rettiwt instance. + * + * @internal + */ + public constructor(config: RettiwtConfig) { + super(config); + } + + /** + * Get the full conversation history for a specific conversation, ordered recent to oldest. + * Use this to load complete message history for a conversation identified from the inbox. + * + * @param conversationId - The ID of the conversation (e.g., "394028042-1712730991884689408"). + * @param cursor - The cursor for pagination. Is equal to the ID of the last message from previous batch. + * + * @returns The conversation with full message history, or undefined if not found. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Fetching a specific conversation + * rettiwt.dm.conversation('394028042-1712730991884689408') + * .then(conversation => { + * if (conversation) { + * console.log(`Conversation with ${conversation.participants.length} participants`); + * console.log(`${conversation.messages.length} messages loaded`); + * } + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async conversation(conversationId: string, cursor?: string): Promise { + const resource = ResourceType.DM_CONVERSATION; + + // Fetching raw conversation timeline + const response = await this.request(resource, { + conversationId, + maxId: cursor, + }); + + // Deserializing response + const data = Extractors[resource](response); + + return data; + } + + /** + * Delete a conversation. + * You will leave the conversation and it will be removed from your inbox. + * + * @param conversationId - The ID of the conversation to delete. + * + * @returns A promise that resolves when the conversation is deleted. + * + * @example + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * // Deleting a conversation + * rettiwt.dm.deleteConversation('394028042-1712730991884689408') + * .then(() => { + * console.log('Conversation deleted successfully'); + * }) + * .catch(err => { + * console.log('Failed to delete conversation:', err); + * }); + * ``` + **/ + public async deleteConversation(conversationId: string): Promise { + const resource = ResourceType.DM_DELETE_CONVERSATION; + + // Sending delete request + await this.request(resource, { + conversationId, + }); + } + + /** + * Get your inbox, ordered recent to oldest. + * + * @param cursor - The cursor to the inbox items to fetch. Is equal to the ID of the last inbox conversation. + * + * @returns The required inbox. Returns initial inbox if no cursor is provided. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Fetching the initial DM inbox state + * rettiwt.dm.inbox() + * .then(inbox => { + * console.log(`Found ${inbox.conversations.length} conversations`); + * console.log('First conversation:', inbox.conversations[0]); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async inbox(cursor?: string): Promise { + // If cursor is provided, fetch initial inbox + if (cursor !== undefined) { + const resource = ResourceType.DM_INBOX_TIMELINE; + + // Fetching raw inbox timeline + const response = await this.request(resource, { + maxId: cursor, + }); + + // Deserializing response + const data = Extractors[resource](response); + + return data; + } + // Else, fetch next inbox data + else { + const resource = ResourceType.DM_INBOX_INITIAL_STATE; + + // Fetching raw inbox initial state + const response = await this.request(resource, {}); + + // Deserializing response + const data = Extractors[resource](response); + + return data; + } + } +} diff --git a/src/services/public/FetcherService.ts b/src/services/public/FetcherService.ts index 08aa0deb..8c1947ba 100644 --- a/src/services/public/FetcherService.ts +++ b/src/services/public/FetcherService.ts @@ -1,25 +1,27 @@ -import axios from 'axios'; +import axios, { AxiosError, isAxiosError } from 'axios'; import { Cookie } from 'cookiejar'; - -import { allowGuestAuthentication, fetchResources, postResources } from '../../collections/Groups'; -import { requests } from '../../collections/Requests'; -import { EApiErrors } from '../../enums/Api'; -import { ELogActions } from '../../enums/Logging'; -import { EResourceType } from '../../enums/Resource'; +import { JSDOM } from 'jsdom'; +import { ClientTransaction } from 'x-client-transaction-id'; + +import { AllowGuestAuthenticationGroup, FetchResourcesGroup, PostResourcesGroup } from '../../collections/Groups'; +import { Requests } from '../../collections/Requests'; +import { ApiErrors } from '../../enums/Api'; +import { LogActions } from '../../enums/Logging'; +import { ResourceType } from '../../enums/Resource'; import { FetchArgs } from '../../models/args/FetchArgs'; import { PostArgs } from '../../models/args/PostArgs'; import { AuthCredential } from '../../models/auth/AuthCredential'; +import { TwitterError } from '../../models/errors/TwitterError'; import { RettiwtConfig } from '../../models/RettiwtConfig'; import { IFetchArgs } from '../../types/args/FetchArgs'; import { IPostArgs } from '../../types/args/PostArgs'; -import { ITidHeader } from '../../types/auth/TidHeader'; -import { ITidProvider } from '../../types/auth/TidProvider'; +import { ITransactionHeader } from '../../types/auth/TransactionHeader'; import { IErrorHandler } from '../../types/ErrorHandler'; +import { IErrorData } from '../../types/raw/base/Error'; import { AuthService } from '../internal/AuthService'; import { ErrorService } from '../internal/ErrorService'; import { LogService } from '../internal/LogService'; -import { TidService } from '../internal/TidService'; /** * The base service that handles all HTTP requests. @@ -36,9 +38,6 @@ export class FetcherService { /** The service used to handle HTTP and API errors */ private readonly _errorHandler: IErrorHandler; - /** Service responsible for generating the `x-client-transaction-id` header. */ - private readonly _tidProvider: ITidProvider; - /** The max wait time for a response. */ private readonly _timeout: number; @@ -53,7 +52,6 @@ export class FetcherService { this.config = config; this._delay = config.delay; this._errorHandler = config.errorHandler ?? new ErrorService(); - this._tidProvider = config.tidProvider ?? new TidService(config); this._timeout = config.timeout ?? 0; this._auth = new AuthService(config); } @@ -65,13 +63,13 @@ export class FetcherService { * * @throws An error if not authorized to access the requested resource. */ - private checkAuthorization(resource: EResourceType): void { + private _checkAuthorization(resource: ResourceType): void { // Logging - LogService.log(ELogActions.AUTHORIZATION, { authenticated: this.config.userId != undefined }); + LogService.log(LogActions.AUTHORIZATION, { authenticated: this.config.userId != undefined }); // Checking authorization status - if (!allowGuestAuthentication.includes(resource) && this.config.userId == undefined) { - throw new Error(EApiErrors.RESOURCE_NOT_ALLOWED); + if (!AllowGuestAuthenticationGroup.includes(resource) && this.config.userId == undefined) { + throw new Error(ApiErrors.RESOURCE_NOT_ALLOWED); } } @@ -80,10 +78,10 @@ export class FetcherService { * * @returns The generated AuthCredential */ - private async getCredential(): Promise { + private async _getCredential(): Promise { if (this.config.apiKey) { // Logging - LogService.log(ELogActions.GET, { target: 'USER_CREDENTIAL' }); + LogService.log(LogActions.GET, { target: 'USER_CREDENTIAL' }); return new AuthCredential( AuthService.decodeCookie(this.config.apiKey) @@ -92,7 +90,7 @@ export class FetcherService { ); } else { // Logging - LogService.log(ELogActions.GET, { target: 'NEW_GUEST_CREDENTIAL' }); + LogService.log(LogActions.GET, { target: 'NEW_GUEST_CREDENTIAL' }); return this._auth.guest(); } @@ -106,22 +104,105 @@ export class FetcherService { * * @returns The header containing the transaction ID. */ - private async getTransactionHeader(method: string, url: string): Promise { + private async _getTransactionHeader(method: string, url: string): Promise { + // Get the X homepage HTML document (using utility function) + const document = await this._handleXMigration(); + + // Create and initialize ClientTransaction instance + const transaction = await ClientTransaction.create(document); + // Getting the URL path excluding all params const path = new URL(url).pathname.split('?')[0].trim(); // Generating the transaction ID - const tid = await this._tidProvider.generate(method.toUpperCase(), path); - - if (tid) { - return { - /* eslint-disable @typescript-eslint/naming-convention */ - 'x-client-transaction-id': tid, - /* eslint-enable @typescript-eslint/naming-convention */ - }; - } else { - return undefined; + const tid = await transaction.generateTransactionId(method.toUpperCase(), path); + + return { + /* eslint-disable @typescript-eslint/naming-convention */ + 'x-client-transaction-id': tid, + /* eslint-enable @typescript-eslint/naming-convention */ + }; + } + + private async _handleXMigration(): Promise { + // Fetch X.com homepage + const homePageResponse = await axios.get('https://x.com', { + headers: this.config.headers, + httpAgent: this.config.httpsAgent, + httpsAgent: this.config.httpsAgent, + }); + + // Parse HTML using linkedom + let dom = new JSDOM(homePageResponse.data); + let document = dom.window.document; + + // Check for migration redirection links + const migrationRedirectionRegex = new RegExp( + '(http(?:s)?://(?:www\\.)?(twitter|x){1}\\.com(/x)?/migrate([/?])?tok=[a-zA-Z0-9%\\-_]+)+', + 'i', + ); + + const metaRefresh = document.querySelector("meta[http-equiv='refresh']"); + const metaContent = metaRefresh ? metaRefresh.getAttribute('content') || '' : ''; + + const migrationRedirectionUrl = + migrationRedirectionRegex.exec(metaContent) || migrationRedirectionRegex.exec(homePageResponse.data); + + if (migrationRedirectionUrl) { + // Follow redirection URL + const redirectResponse = await axios.get(migrationRedirectionUrl[0], { + httpAgent: this.config.httpsAgent, + httpsAgent: this.config.httpsAgent, + }); + + dom = new JSDOM(redirectResponse.data); + document = dom.window.document; + } + + // Handle migration form if present + const migrationForm = + document.querySelector("form[name='f']") || + document.querySelector("form[action='https://x.com/x/migrate']"); + + if (migrationForm) { + const url = migrationForm.getAttribute('action') || 'https://x.com/x/migrate'; + const method = migrationForm.getAttribute('method') || 'POST'; + + // Collect form input fields + const requestPayload = new FormData(); + + const inputFields = migrationForm.querySelectorAll('input'); + for (const element of Array.from(inputFields)) { + const name = element.getAttribute('name'); + const value = element.getAttribute('value'); + if (name && value) { + requestPayload.append(name, value); + } + } + + // Submit form using POST request + const formResponse = await axios.request({ + method: method, + url: url, + data: requestPayload, + headers: { + /* eslint-disable @typescript-eslint/naming-convention */ + + 'Content-Type': 'multipart/form-data', + ...this.config.headers, + + /* eslint-enable @typescript-eslint/naming-convention */ + }, + httpAgent: this.config.httpsAgent, + httpsAgent: this.config.httpsAgent, + }); + + dom = new JSDOM(formResponse.data); + document = dom.window.document; } + + // Return final DOM document + return document; } /** @@ -132,15 +213,15 @@ export class FetcherService { * * @returns The validated args. */ - private validateArgs(resource: EResourceType, args: IFetchArgs | IPostArgs): FetchArgs | PostArgs | undefined { - if (fetchResources.includes(resource)) { + private _validateArgs(resource: ResourceType, args: IFetchArgs | IPostArgs): FetchArgs | PostArgs | undefined { + if (FetchResourcesGroup.includes(resource)) { // Logging - LogService.log(ELogActions.VALIDATE, { target: 'FETCH_ARGS' }); + LogService.log(LogActions.VALIDATE, { target: 'FETCH_ARGS' }); return new FetchArgs(args); - } else if (postResources.includes(resource)) { + } else if (PostResourcesGroup.includes(resource)) { // Logging - LogService.log(ELogActions.VALIDATE, { target: 'POST_ARGS' }); + LogService.log(LogActions.VALIDATE, { target: 'POST_ARGS' }); return new PostArgs(args); } @@ -149,7 +230,7 @@ export class FetcherService { /** * Introduces a delay using the configured delay/delay function. */ - private async wait(): Promise { + private async _wait(): Promise { // If no delay is set, skip if (this._delay == undefined) { return; @@ -183,13 +264,13 @@ export class FetcherService { * * #### Fetching the raw details of a single user, using their username * ```ts - * import { FetcherService, EResourceType } from 'rettiwt-api'; + * import { FetcherService, ResourceType } from 'rettiwt-api'; * * // Creating a new FetcherService instance using the given 'API_KEY' * const fetcher = new FetcherService({ apiKey: API_KEY }); * * // Fetching the details of the User with username 'user1' - * fetcher.request(EResourceType.USER_DETAILS_BY_USERNAME, { id: 'user1' }) + * fetcher.request(ResourceType.USER_DETAILS_BY_USERNAME, { id: 'user1' }) * .then(res => { * console.log(res); * }) @@ -198,44 +279,94 @@ export class FetcherService { * }); * ``` */ - public async request(resource: EResourceType, args: IFetchArgs | IPostArgs): Promise { + public async request(resource: ResourceType, args: IFetchArgs | IPostArgs): Promise { + /** The current retry number. */ + let retry = 0; + + /** The error, if any. */ + let error: unknown = undefined; + // Logging - LogService.log(ELogActions.REQUEST, { resource: resource, args: args }); + LogService.log(LogActions.REQUEST, { resource: resource, args: args }); // Checking authorization for the requested resource - this.checkAuthorization(resource); + this._checkAuthorization(resource); // Validating args - args = this.validateArgs(resource, args)!; + args = this._validateArgs(resource, args)!; // Getting credentials from key - const cred: AuthCredential = await this.getCredential(); + const cred: AuthCredential = await this._getCredential(); // Getting request configuration - const config = requests[resource](args); + const config = Requests[resource](args); // Setting additional request parameters config.headers = { ...config.headers, ...cred.toHeader(), - ...(await this.getTransactionHeader(config.method ?? '', config.url ?? '')), ...this.config.headers, }; config.httpAgent = this.config.httpsAgent; config.httpsAgent = this.config.httpsAgent; config.timeout = this._timeout; - // Sending the request - try { - // Introducing a delay - await this.wait(); - - // Returning the reponse body - return (await axios(config)).data; - } catch (error) { - // If error, delegate handling to error handler - this._errorHandler.handle(error); - throw error; - } + // Using retries for error 404 + do { + // Sending the request + try { + // Getting and appending transaction information + config.headers = { + ...(await this._getTransactionHeader(config.method ?? '', config.url ?? '')), + ...config.headers, + }; + + // Introducing a delay + await this._wait(); + + // Getting the response body + const responseData = (await axios(config)).data; + + // Check for Twitter API errors in response body + // Type guard to check if response contains errors + const potentialErrorResponse = responseData as unknown as Partial; + if ( + potentialErrorResponse.errors && + Array.isArray(potentialErrorResponse.errors) && + (potentialErrorResponse.data === undefined || + JSON.stringify(potentialErrorResponse.data) === JSON.stringify({})) + ) { + // Throw TwitterError using existing error class + const axiosError = { + response: { + data: { errors: potentialErrorResponse.errors }, + status: 200, + }, + message: potentialErrorResponse.errors[0]?.message ?? 'Twitter API Error', + status: 200, + } as AxiosError; + throw new TwitterError(axiosError); + } + + // Returning the reponse body + return responseData; + } catch (err) { + // If it's an error 404, retry + if (isAxiosError(err) && err.status === 404) { + error = err; + continue; + } + // Else, delegate error handling + else { + this._errorHandler.handle(err); + } + } finally { + // Incrementing the number of retries done + retry++; + } + } while (retry < this.config.maxRetries); + + /** If request not successful even after retries, throw the error */ + throw error; } } diff --git a/src/services/public/ListService.ts b/src/services/public/ListService.ts index a178fd5f..72eaaabe 100644 --- a/src/services/public/ListService.ts +++ b/src/services/public/ListService.ts @@ -1,10 +1,14 @@ -import { extractors } from '../../collections/Extractors'; -import { EResourceType } from '../../enums/Resource'; +import { Extractors } from '../../collections/Extractors'; +import { ResourceType } from '../../enums/Resource'; import { CursoredData } from '../../models/data/CursoredData'; +import { List } from '../../models/data/List'; import { Tweet } from '../../models/data/Tweet'; import { User } from '../../models/data/User'; import { RettiwtConfig } from '../../models/RettiwtConfig'; +import { IListMemberAddResponse } from '../../types/raw/list/AddMember'; +import { IListDetailsResponse } from '../../types/raw/list/Details'; import { IListMembersResponse } from '../../types/raw/list/Members'; +import { IListMemberRemoveResponse } from '../../types/raw/list/RemoveMember'; import { IListTweetsResponse } from '../../types/raw/list/Tweets'; import { FetcherService } from './FetcherService'; @@ -19,6 +23,88 @@ export class ListService extends FetcherService { super(config); } + /** + * Add a user as a member of a list. + * + * @param listId - The ID of the target list. + * @param userId - The ID of the target user to be added as a member. + * + * @returns The new member count of the list. If adding was unsuccessful, return `undefined`. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Adding a user with ID '123456789' as a member to the list with ID '987654321' + * rettiwt.list.addMember('987654321', '123456789') + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async addMember(listId: string, userId: string): Promise { + const resource: ResourceType = ResourceType.LIST_MEMBER_ADD; + + // Adding the user as a member + const response = await this.request(resource, { + id: listId, + userId: userId, + }); + + // Deserializing response + const data = Extractors[resource](response); + + return data; + } + + /** + * Get the details of a list. + * + * @param id - The ID of the target list. + * + * @returns + * The details of the target list. + * + * If list not found, returns undefined. + * + * @example + * + * #### Fetching the details of a list + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Fetching the details of the list with the id '1234567890' + * rettiwt.list.details('1234567890') + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async details(id: string): Promise { + const resource: ResourceType = ResourceType.LIST_DETAILS; + + // Getting the details of the list + const response = await this.request(resource, { id: id }); + + // Deserializing response + const data = Extractors[resource](response, id); + + return data; + } + /** * Get the list of members of a tweet list. * @@ -49,7 +135,7 @@ export class ListService extends FetcherService { * @remarks Due a bug in Twitter API, the count is ignored when no cursor is provided and defaults to 100. */ public async members(id: string, count?: number, cursor?: string): Promise> { - const resource: EResourceType = EResourceType.LIST_MEMBERS; + const resource: ResourceType = ResourceType.LIST_MEMBERS; // Fetching the raw list of members const response = await this.request(resource, { @@ -59,7 +145,48 @@ export class ListService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); + + return data; + } + + /** + * Remove a member from a list. + * + * @param listId - The ID of the target list. + * @param userId - The ID of the target user to removed from the members. + * + * @returns The new member count of the list. If removal was unsuccessful, return `undefined`. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Removing a user with ID '123456789' from the member of the list with ID '987654321' + * rettiwt.list.removeMember('987654321', '123456789') + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async removeMember(listId: string, userId: string): Promise { + const resource: ResourceType = ResourceType.LIST_MEMBER_REMOVE; + + // Removing the member + const response = await this.request(resource, { + id: listId, + userId: userId, + }); + + // Deserializing response + const data = Extractors[resource](response); return data; } @@ -94,7 +221,7 @@ export class ListService extends FetcherService { * @remarks Due a bug in Twitter API, the count is ignored when no cursor is provided and defaults to 100. */ public async tweets(id: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.LIST_TWEETS; + const resource = ResourceType.LIST_TWEETS; // Fetching raw list tweets const response = await this.request(resource, { @@ -104,7 +231,7 @@ export class ListService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); // Sorting the tweets by date, from recent to oldest data.list.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf()); diff --git a/src/services/public/TweetService.ts b/src/services/public/TweetService.ts index 6b2e4034..4df02515 100644 --- a/src/services/public/TweetService.ts +++ b/src/services/public/TweetService.ts @@ -1,8 +1,8 @@ import { statSync } from 'fs'; -import { extractors } from '../../collections/Extractors'; -import { EResourceType } from '../../enums/Resource'; -import { ETweetRepliesSortType } from '../../enums/Tweet'; +import { Extractors } from '../../collections/Extractors'; +import { ResourceType } from '../../enums/Resource'; +import { TweetRepliesSortType } from '../../enums/Tweet'; import { CursoredData } from '../../models/data/CursoredData'; import { Tweet } from '../../models/data/Tweet'; import { User } from '../../models/data/User'; @@ -12,6 +12,7 @@ import { ITweetFilter } from '../../types/args/FetchArgs'; import { INewTweet } from '../../types/args/PostArgs'; import { IMediaInitializeUploadResponse } from '../../types/raw/media/InitalizeUpload'; +import { ITweetBookmarkResponse } from '../../types/raw/tweet/Bookmark'; import { ITweetDetailsResponse } from '../../types/raw/tweet/Details'; import { ITweetDetailsBulkResponse } from '../../types/raw/tweet/DetailsBulk'; import { ITweetLikeResponse } from '../../types/raw/tweet/Like'; @@ -22,6 +23,7 @@ import { ITweetRetweetResponse } from '../../types/raw/tweet/Retweet'; import { ITweetRetweetersResponse } from '../../types/raw/tweet/Retweeters'; import { ITweetScheduleResponse } from '../../types/raw/tweet/Schedule'; import { ITweetSearchResponse } from '../../types/raw/tweet/Search'; +import { ITweetUnbookmarkResponse } from '../../types/raw/tweet/Unbookmark'; import { ITweetUnlikeResponse } from '../../types/raw/tweet/Unlike'; import { ITweetUnpostResponse } from '../../types/raw/tweet/Unpost'; import { ITweetUnretweetResponse } from '../../types/raw/tweet/Unretweet'; @@ -44,6 +46,45 @@ export class TweetService extends FetcherService { super(config); } + /** + * Bookmark a tweet. + * + * @param id - The ID of the tweet to be bookmarked. + * + * @returns Whether bookmarking was successful or not. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Bookmarking the Tweet with id '1234567890' + * rettiwt.tweet.bookmark('1234567890') + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async bookmark(id: string): Promise { + const resource = ResourceType.TWEET_BOOKMARK; + + // Favoriting the tweet + const response = await this.request(resource, { + id: id, + }); + + // Deserializing response + const data = Extractors[resource](response) ?? false; + + return data; + } + /** * Get the details of one or more tweets. * @@ -95,41 +136,41 @@ export class TweetService extends FetcherService { * ``` */ public async details(id: T): Promise { - let resource: EResourceType; + let resource: ResourceType; // If user is authenticated and details of single tweet required if (this.config.userId != undefined && typeof id == 'string') { - resource = EResourceType.TWEET_DETAILS_ALT; + resource = ResourceType.TWEET_DETAILS_ALT; // Fetching raw tweet details const response = await this.request(resource, { id: id }); // Deserializing response - const data = extractors[resource](response, id); + const data = Extractors[resource](response, id); return data as T extends string ? Tweet | undefined : Tweet[]; } // If user is authenticated and details of multiple tweets required else if (this.config.userId != undefined && Array.isArray(id)) { - resource = EResourceType.TWEET_DETAILS_BULK; + resource = ResourceType.TWEET_DETAILS_BULK; // Fetching raw tweet details const response = await this.request(resource, { ids: id }); // Deserializing response - const data = extractors[resource](response, id); + const data = Extractors[resource](response, id); return data as T extends string ? Tweet | undefined : Tweet[]; } // If user is not authenticated else { - resource = EResourceType.TWEET_DETAILS; + resource = ResourceType.TWEET_DETAILS; // Fetching raw tweet details const response = await this.request(resource, { id: String(id) }); // Deserializing response - const data = extractors[resource](response, String(id)); + const data = Extractors[resource](response, String(id)); return data as T extends string ? Tweet | undefined : Tweet[]; } @@ -161,7 +202,7 @@ export class TweetService extends FetcherService { * ``` */ public async like(id: string): Promise { - const resource = EResourceType.TWEET_LIKE; + const resource = ResourceType.TWEET_LIKE; // Favoriting the tweet const response = await this.request(resource, { @@ -169,7 +210,7 @@ export class TweetService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response) ?? false; + const data = Extractors[resource](response) ?? false; return data; } @@ -202,7 +243,7 @@ export class TweetService extends FetcherService { * ``` */ public async likers(id: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.TWEET_LIKERS; + const resource = ResourceType.TWEET_LIKERS; // Fetching raw likers const response = await this.request(resource, { @@ -212,7 +253,7 @@ export class TweetService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -301,13 +342,13 @@ export class TweetService extends FetcherService { * ``` */ public async post(options: INewTweet): Promise { - const resource = EResourceType.TWEET_POST; + const resource = ResourceType.TWEET_POST; // Posting the tweet const response = await this.request(resource, { tweet: options }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -315,9 +356,13 @@ export class TweetService extends FetcherService { /** * Get the list of replies to a tweet. * + * If the target tweet is a thread, + * the first batch always contains all the tweets in the thread, + * if `sortBy` is set to {@link TweetRepliesSortType.RELEVANCE}. + * * @param id - The ID of the target tweet. * @param cursor - The cursor to the batch of replies to fetch. - * @param sortBy - The sorting order of the replies to fetch. Default is {@link ETweetRepliesSortType.RECENT}. + * @param sortBy - The sorting order of the replies to fetch. Default is {@link TweetRepliesSortType.RECENT}. * * @returns The list of replies to the given tweet. * @@ -346,9 +391,9 @@ export class TweetService extends FetcherService { public async replies( id: string, cursor?: string, - sortBy: ETweetRepliesSortType = ETweetRepliesSortType.LATEST, + sortBy: TweetRepliesSortType = TweetRepliesSortType.LATEST, ): Promise> { - const resource = EResourceType.TWEET_REPLIES; + const resource = ResourceType.TWEET_REPLIES; // Fetching raw list of replies const response = await this.request(resource, { @@ -358,7 +403,7 @@ export class TweetService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -389,13 +434,13 @@ export class TweetService extends FetcherService { * ``` */ public async retweet(id: string): Promise { - const resource = EResourceType.TWEET_RETWEET; + const resource = ResourceType.TWEET_RETWEET; // Retweeting the tweet const response = await this.request(resource, { id: id }); // Deserializing response - const data = extractors[resource](response) ?? false; + const data = Extractors[resource](response) ?? false; return data; } @@ -428,7 +473,7 @@ export class TweetService extends FetcherService { * ``` */ public async retweeters(id: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.TWEET_RETWEETERS; + const resource = ResourceType.TWEET_RETWEETERS; // Fetching raw list of retweeters const response = await this.request(resource, { @@ -438,7 +483,7 @@ export class TweetService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -474,13 +519,13 @@ export class TweetService extends FetcherService { * Scheduling a tweet is similar to {@link post}ing, except that an extra parameter called `scheduleFor` is used. */ public async schedule(options: INewTweet): Promise { - const resource = EResourceType.TWEET_SCHEDULE; + const resource = ResourceType.TWEET_SCHEDULE; // Scheduling the tweet const response = await this.request(resource, { tweet: options }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -518,7 +563,7 @@ export class TweetService extends FetcherService { * For details about available filters, refer to {@link TweetFilter} */ public async search(filter: ITweetFilter, count?: number, cursor?: string): Promise> { - const resource = EResourceType.TWEET_SEARCH; + const resource = ResourceType.TWEET_SEARCH; // Fetching raw list of filtered tweets const response = await this.request(resource, { @@ -528,7 +573,7 @@ export class TweetService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); // Sorting the tweets by date, from recent to oldest data.list.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf()); @@ -605,6 +650,43 @@ export class TweetService extends FetcherService { } } + /** + * Unbookmark a tweet. + * + * @param id - The ID of the target tweet. + * + * @returns Whether unbookmarking was successful or not. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Unbookmarking the tweet with id '1234567890' + * rettiwt.tweet.unbookmark('1234567890') + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async unbookmark(id: string): Promise { + const resource = ResourceType.TWEET_UNBOOKMARK; + + // Unliking the tweet + const response = await this.request(resource, { id: id }); + + // Deserializing the response + const data = Extractors[resource](response) ?? false; + + return data; + } + /** * Unlike a tweet. * @@ -631,13 +713,13 @@ export class TweetService extends FetcherService { * ``` */ public async unlike(id: string): Promise { - const resource = EResourceType.TWEET_UNLIKE; + const resource = ResourceType.TWEET_UNLIKE; // Unliking the tweet const response = await this.request(resource, { id: id }); // Deserializing the response - const data = extractors[resource](response) ?? false; + const data = Extractors[resource](response) ?? false; return data; } @@ -668,13 +750,13 @@ export class TweetService extends FetcherService { * ``` */ public async unpost(id: string): Promise { - const resource = EResourceType.TWEET_UNPOST; + const resource = ResourceType.TWEET_UNPOST; // Unposting the tweet const response = await this.request(resource, { id: id }); // Deserializing the response - const data = extractors[resource](response) ?? false; + const data = Extractors[resource](response) ?? false; return data; } @@ -705,13 +787,13 @@ export class TweetService extends FetcherService { * ``` */ public async unretweet(id: string): Promise { - const resource = EResourceType.TWEET_UNRETWEET; + const resource = ResourceType.TWEET_UNRETWEET; // Unretweeting the tweet const response = await this.request(resource, { id: id }); // Deserializing the response - const data = extractors[resource](response) ?? false; + const data = Extractors[resource](response) ?? false; return data; } @@ -742,13 +824,13 @@ export class TweetService extends FetcherService { * ``` */ public async unschedule(id: string): Promise { - const resource = EResourceType.TWEET_UNSCHEDULE; + const resource = ResourceType.TWEET_UNSCHEDULE; // Unscheduling the tweet const response = await this.request(resource, { id: id }); // Deserializing the response - const data = extractors[resource](response) ?? false; + const data = Extractors[resource](response) ?? false; return data; } @@ -788,16 +870,16 @@ export class TweetService extends FetcherService { // INITIALIZE const size = typeof media == 'string' ? statSync(media).size : media.byteLength; const id: string = ( - await this.request(EResourceType.MEDIA_UPLOAD_INITIALIZE, { + await this.request(ResourceType.MEDIA_UPLOAD_INITIALIZE, { upload: { size: size }, }) ).media_id_string; // APPEND - await this.request(EResourceType.MEDIA_UPLOAD_APPEND, { upload: { id: id, media: media } }); + await this.request(ResourceType.MEDIA_UPLOAD_APPEND, { upload: { id: id, media: media } }); // FINALIZE - await this.request(EResourceType.MEDIA_UPLOAD_FINALIZE, { upload: { id: id } }); + await this.request(ResourceType.MEDIA_UPLOAD_FINALIZE, { upload: { id: id } }); return id; } diff --git a/src/services/public/UserService.ts b/src/services/public/UserService.ts index 36097304..ca3bb714 100644 --- a/src/services/public/UserService.ts +++ b/src/services/public/UserService.ts @@ -1,11 +1,20 @@ -import { extractors } from '../../collections/Extractors'; -import { EResourceType } from '../../enums/Resource'; +import { Extractors } from '../../collections/Extractors'; +import { RawAnalyticsGranularity, RawAnalyticsMetric } from '../../enums/raw/Analytics'; +import { ResourceType } from '../../enums/Resource'; +import { ProfileUpdateOptions } from '../../models/args/ProfileArgs'; +import { Analytics } from '../../models/data/Analytics'; +import { BookmarkFolder } from '../../models/data/BookmarkFolder'; import { CursoredData } from '../../models/data/CursoredData'; +import { List } from '../../models/data/List'; import { Notification } from '../../models/data/Notification'; import { Tweet } from '../../models/data/Tweet'; import { User } from '../../models/data/User'; import { RettiwtConfig } from '../../models/RettiwtConfig'; +import { IProfileUpdateOptions } from '../../types/args/ProfileArgs'; import { IUserAffiliatesResponse } from '../../types/raw/user/Affiliates'; +import { IUserAnalyticsResponse } from '../../types/raw/user/Analytics'; +import { IUserBookmarkFoldersResponse } from '../../types/raw/user/BookmarkFolders'; +import { IUserBookmarkFolderTweetsResponse } from '../../types/raw/user/BookmarkFolderTweets'; import { IUserBookmarksResponse } from '../../types/raw/user/Bookmarks'; import { IUserDetailsResponse } from '../../types/raw/user/Details'; import { IUserDetailsBulkResponse } from '../../types/raw/user/DetailsBulk'; @@ -15,9 +24,12 @@ import { IUserFollowersResponse } from '../../types/raw/user/Followers'; import { IUserFollowingResponse } from '../../types/raw/user/Following'; import { IUserHighlightsResponse } from '../../types/raw/user/Highlights'; import { IUserLikesResponse } from '../../types/raw/user/Likes'; +import { IUserListsResponse } from '../../types/raw/user/Lists'; import { IUserMediaResponse } from '../../types/raw/user/Media'; import { IUserNotificationsResponse } from '../../types/raw/user/Notifications'; +import { IUserProfileUpdateResponse } from '../../types/raw/user/ProfileUpdate'; import { IUserRecommendedResponse } from '../../types/raw/user/Recommended'; +import { IUserSearchResponse } from '../../types/raw/user/Search'; import { IUserSubscriptionsResponse } from '../../types/raw/user/Subscriptions'; import { IUserTweetsResponse } from '../../types/raw/user/Tweets'; import { IUserTweetsAndRepliesResponse } from '../../types/raw/user/TweetsAndReplies'; @@ -68,7 +80,7 @@ export class UserService extends FetcherService { * ``` */ public async affiliates(id?: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_AFFILIATES; + const resource = ResourceType.USER_AFFILIATES; // Fetching raw list of affiliates const response = await this.request(resource, { @@ -78,7 +90,147 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); + + return data; + } + + /** + * Get the analytics overview of the logged in user. + * + * @param fromTime - The start time of the analytics period. Defaults to 7 days ago. + * @param toTime - The end time of the analytics period. Defaults to now. + * @param granularity - The granularity of the analytics data. Defaults to daily. + * @param metrics - The metrics to include in the analytics data. Defaults to all available metrics available. + * @param showVerifiedFollowers - Whether to include verified follower count and relationship counts in the response. Defaults to true. + * + * @returns The raw analytics data of the user. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Fetching the analytics overview of the logged in user + * rettiwt.user.analytics().then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async analytics( + fromTime?: Date, + toTime?: Date, + granularity?: RawAnalyticsGranularity, + metrics?: RawAnalyticsMetric[], + showVerifiedFollowers?: boolean, + ): Promise { + const resource = ResourceType.USER_ANALYTICS; + + // Define default values if not provided + fromTime = fromTime ?? new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); + toTime = toTime ?? new Date(); + granularity = granularity ?? RawAnalyticsGranularity.DAILY; + metrics = metrics ?? Object.values(RawAnalyticsMetric); + showVerifiedFollowers = showVerifiedFollowers ?? true; + + // Fetching raw analytics + const response = await this.request(resource, { + fromTime, + toTime, + granularity, + metrics, + showVerifiedFollowers, + }); + + const data = Extractors[resource](response); + + return data; + } + + /** + * Get the list of tweets in a specific bookmark folder of the logged in user. + * + * @param folderId - The ID of the bookmark folder. + * @param count - The number of tweets to fetch, must be \<= 100. + * @param cursor - The cursor to the batch of tweets to fetch. + * + * @returns The list of tweets in the bookmark folder. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Fetching the first 100 tweets from bookmark folder with ID '2001752149647049173' + * rettiwt.user.bookmarkFolderTweets('2001752149647049173') + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async bookmarkFolderTweets(folderId: string, count?: number, cursor?: string): Promise> { + const resource = ResourceType.USER_BOOKMARK_FOLDER_TWEETS; + + // Fetching raw list of tweets from folder + const response = await this.request(resource, { + id: folderId, + count: count, + cursor: cursor, + }); + + // Deserializing response + const data = Extractors[resource](response); + + return data; + } + + /** + * Get the list of bookmark folders of the logged in user. + * + * @param cursor - The cursor to the batch of bookmark folders to fetch. + * + * @returns The list of bookmark folders. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Fetching all bookmark folders of the logged in user + * rettiwt.user.bookmarkFolders() + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async bookmarkFolders(cursor?: string): Promise> { + const resource = ResourceType.USER_BOOKMARK_FOLDERS; + + // Fetching raw list of bookmark folders + const response = await this.request(resource, { + cursor: cursor, + }); + + // Deserializing response + const data = Extractors[resource](response); return data; } @@ -110,7 +262,7 @@ export class UserService extends FetcherService { * ``` */ public async bookmarks(count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_BOOKMARKS; + const resource = ResourceType.USER_BOOKMARKS; // Fetching raw list of likes const response = await this.request(resource, { @@ -119,22 +271,42 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } /** - * Get the details of a user. + * Get the details of the logged in user. * - * @param id - The username/ID/IDs of the target user/users. If no ID is provided, the logged-in user's ID is used. + * @returns The details of the user. * - * @returns - * The details of the given user. + * @example * - * If more than one ID is provided, returns a list. + * ```ts + * import { Rettiwt } from 'rettiwt-api'; * - * If no user matches the given id, returns `undefined`. + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Fetching the details of the User + * rettiwt.user.details() + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async details(): Promise; + + /** + * Get the details of a user. + * + * @param id - The ID/username of the target user. + * + * @returns The details of the user. * * @example * @@ -145,8 +317,8 @@ export class UserService extends FetcherService { * // Creating a new Rettiwt instance using the given 'API_KEY' * const rettiwt = new Rettiwt({ apiKey: API_KEY }); * - * // Fetching the details of the User with username 'user1' - * rettiwt.user.details('user1') + * // Fetching the details of the User with username 'user1' or '@user1' + * rettiwt.user.details('user1') // or @user1 * .then(res => { * console.log(res); * }) @@ -173,9 +345,18 @@ export class UserService extends FetcherService { * console.log(err); * }); * ``` - * * @example + */ + public async details(id: string): Promise; + + /** + * Get the details of multiple users in bulk. + * + * @param id - The list of IDs of the target users. + * + * @returns The details of the users. + * + * @example * - * #### Fetching the details of multiple users * ```ts * import { Rettiwt } from 'rettiwt-api'; * @@ -192,46 +373,49 @@ export class UserService extends FetcherService { * }); * ``` */ - public async details( - id: T, - ): Promise { - let resource: EResourceType; + public async details(id: string[]): Promise; + + public async details(id?: string | string[]): Promise { + let resource: ResourceType; // If details of multiple users required if (Array.isArray(id)) { - resource = EResourceType.USER_DETAILS_BY_IDS_BULK; + resource = ResourceType.USER_DETAILS_BY_IDS_BULK; // Fetching raw details const response = await this.request(resource, { ids: id }); // Deserializing response - const data = extractors[resource](response, id); + const data = Extractors[resource](response, id); - return data as T extends string | undefined ? User | undefined : User[]; + return data; } // If details of single user required else { // If username is given if (id && isNaN(Number(id))) { - resource = EResourceType.USER_DETAILS_BY_USERNAME; + resource = ResourceType.USER_DETAILS_BY_USERNAME; + if (id?.startsWith('@')) { + id = id.slice(1); + } } // If id is given (or not, for self details) else { - resource = EResourceType.USER_DETAILS_BY_ID; + resource = ResourceType.USER_DETAILS_BY_ID; } // If no ID is given, and not authenticated, skip if (!id && !this.config.userId) { - return undefined as T extends string | undefined ? User | undefined : User[]; + return undefined; } // Fetching raw details const response = await this.request(resource, { id: id ?? this.config.userId }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); - return data as T extends string | undefined ? User | undefined : User[]; + return data; } } @@ -263,13 +447,13 @@ export class UserService extends FetcherService { * ``` */ public async follow(id: string): Promise { - const resource = EResourceType.USER_FOLLOW; + const resource = ResourceType.USER_FOLLOW; // Following the user - const response = await this.request(EResourceType.USER_FOLLOW, { id: id }); + const response = await this.request(ResourceType.USER_FOLLOW, { id: id }); // Deserializing the response - const data = extractors[resource](response) ?? false; + const data = Extractors[resource](response) ?? false; return data; } @@ -302,7 +486,7 @@ export class UserService extends FetcherService { * @remarks Always returns 35 feed items, with no way to customize the count. */ public async followed(cursor?: string): Promise> { - const resource = EResourceType.USER_FEED_FOLLOWED; + const resource = ResourceType.USER_FEED_FOLLOWED; // Fetching raw list of tweets const response = await this.request(resource, { @@ -310,7 +494,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -343,7 +527,7 @@ export class UserService extends FetcherService { * ``` */ public async followers(id?: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_FOLLOWERS; + const resource = ResourceType.USER_FOLLOWERS; // Fetching raw list of followers const response = await this.request(resource, { @@ -353,7 +537,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -386,7 +570,7 @@ export class UserService extends FetcherService { * ``` */ public async following(id?: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_FOLLOWING; + const resource = ResourceType.USER_FOLLOWING; // Fetching raw list of following const response = await this.request(resource, { @@ -396,7 +580,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -428,8 +612,8 @@ export class UserService extends FetcherService { * }); * ``` */ - public async highlights(id: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_HIGHLIGHTS; + public async highlights(id?: string, count?: number, cursor?: string): Promise> { + const resource = ResourceType.USER_HIGHLIGHTS; // Fetching raw list of highlights const response = await this.request(resource, { @@ -439,7 +623,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -471,7 +655,7 @@ export class UserService extends FetcherService { * ``` */ public async likes(count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_LIKES; + const resource = ResourceType.USER_LIKES; // Fetching raw list of likes const response = await this.request(resource, { @@ -481,7 +665,49 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); + + return data; + } + + /** + * Get the list of of the the logged in user. Includes both followed and owned. + * + * @param count - The number of lists to fetch, must be \<= 100. + * @param cursor - The cursor to the batch of likes to fetch. + * + * @returns The list of tweets liked by the target user. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Fetching the first 100 Lists of the logged in User + * rettiwt.user.lists() + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async lists(count?: number, cursor?: string): Promise> { + const resource = ResourceType.USER_LISTS; + + // Fetching raw list of lists + const response = await this.request(resource, { + id: this.config.userId, + count: count, + cursor: cursor, + }); + + // Deserializing response + const data = Extractors[resource](response); return data; } @@ -514,7 +740,7 @@ export class UserService extends FetcherService { * ``` */ public async media(id?: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_MEDIA; + const resource = ResourceType.USER_MEDIA; // Fetching raw list of media const response = await this.request(resource, { @@ -524,7 +750,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -562,7 +788,7 @@ export class UserService extends FetcherService { * ``` */ public async *notifications(pollingInterval = 60000): AsyncGenerator { - const resource = EResourceType.USER_NOTIFICATIONS; + const resource = ResourceType.USER_NOTIFICATIONS; /** Whether it's the first batch of notifications or not. */ let first = true; @@ -581,7 +807,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const notifications = extractors[resource](response); + const notifications = Extractors[resource](response); // Sorting the notifications by time, from oldest to recent notifications.list.sort((a, b) => new Date(a.receivedAt).valueOf() - new Date(b.receivedAt).valueOf()); @@ -630,7 +856,7 @@ export class UserService extends FetcherService { * @remarks Always returns 35 feed items, with no way to customize the count. */ public async recommended(cursor?: string): Promise> { - const resource = EResourceType.USER_FEED_RECOMMENDED; + const resource = ResourceType.USER_FEED_RECOMMENDED; // Fetching raw list of tweets const response = await this.request(resource, { @@ -638,7 +864,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -675,7 +901,7 @@ export class UserService extends FetcherService { * If the target user has a pinned tweet, the returned reply timeline has one item extra and this is always the pinned tweet. */ public async replies(id?: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_TIMELINE_AND_REPLIES; + const resource = ResourceType.USER_TIMELINE_AND_REPLIES; // Fetching raw list of replies const response = await this.request(resource, { @@ -685,15 +911,56 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } /** - * Get the list of subscriptions of a user. + * Search for a username. + * + * @param userName - The username to search for. + * @param count - The number of results to fetch, must be \<= 20. + * @param cursor - The cursor to the batch of results to fetch. + * + * @returns The list of users that match the given username. + * + * @example + * + * ```ts + * import { Rettiwt } from 'rettiwt-api'; * - * @deprecated Currently not working. + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Fetching the top 5 matching users for the username 'user1' + * rettiwt.user.search('user1') + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async search(userName: string, count?: number, cursor?: string): Promise> { + const resource = ResourceType.USER_SEARCH; + + // Fetching raw list of filtered tweets + const response = await this.request(resource, { + id: userName, + count: count, + cursor: cursor, + }); + + // Deserializing response + const data = Extractors[resource](response); + + return data; + } + + /** + * Get the list of subscriptions of a user. * * @param id - The ID of the target user. If no ID is provided, the logged-in user's ID is used. * @param count - The number of subscriptions to fetch, must be \<= 100. @@ -719,8 +986,8 @@ export class UserService extends FetcherService { * }); * ``` */ - public async subscriptions(id: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_SUBSCRIPTIONS; + public async subscriptions(id?: string, count?: number, cursor?: string): Promise> { + const resource = ResourceType.USER_SUBSCRIPTIONS; // Fetching raw list of subscriptions const response = await this.request(resource, { @@ -730,7 +997,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -767,8 +1034,8 @@ export class UserService extends FetcherService { * - If the target user has a pinned tweet, the returned timeline has one item extra and this is always the pinned tweet. * - If timeline is fetched without authenticating, then the most popular tweets of the target user are returned instead. */ - public async timeline(id: string, count?: number, cursor?: string): Promise> { - const resource = EResourceType.USER_TIMELINE; + public async timeline(id?: string, count?: number, cursor?: string): Promise> { + const resource = ResourceType.USER_TIMELINE; // Fetching raw list of tweets const response = await this.request(resource, { @@ -778,7 +1045,7 @@ export class UserService extends FetcherService { }); // Deserializing response - const data = extractors[resource](response); + const data = Extractors[resource](response); return data; } @@ -809,13 +1076,78 @@ export class UserService extends FetcherService { * ``` */ public async unfollow(id: string): Promise { - const resource = EResourceType.USER_UNFOLLOW; + const resource = ResourceType.USER_UNFOLLOW; // Unfollowing the user - const response = await this.request(EResourceType.USER_UNFOLLOW, { id: id }); + const response = await this.request(ResourceType.USER_UNFOLLOW, { id: id }); + + // Deserializing the response + const data = Extractors[resource](response) ?? false; + + return data; + } + + /** + * Update the logged in user's profile. + * + * @param options - The profile update options. + * + * @returns Whether the profile update was successful or not. + * + * @example + * + * #### Updating only the display name + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Updating the display name of the logged in user + * rettiwt.user.updateProfile({ name: 'New Display Name' }) + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + * + * @example + * + * #### Updating multiple profile fields + * ```ts + * import { Rettiwt } from 'rettiwt-api'; + * + * // Creating a new Rettiwt instance using the given 'API_KEY' + * const rettiwt = new Rettiwt({ apiKey: API_KEY }); + * + * // Updating multiple profile fields + * rettiwt.user.updateProfile({ + * name: 'New Display Name', + * location: 'Istanbul', + * description: 'Hello world!', + * url: 'https://example.com' + * }) + * .then(res => { + * console.log(res); + * }) + * .catch(err => { + * console.log(err); + * }); + * ``` + */ + public async updateProfile(options: IProfileUpdateOptions): Promise { + const resource = ResourceType.USER_PROFILE_UPDATE; + + // Validating the options + const validatedOptions = new ProfileUpdateOptions(options); + + // Updating the profile + const response = await this.request(resource, { profileOptions: validatedOptions }); // Deserializing the response - const data = extractors[resource](response) ?? false; + const data = Extractors[resource](response) ?? false; return data; } diff --git a/src/types/RettiwtConfig.ts b/src/types/RettiwtConfig.ts index f39eda6e..5b48a91b 100644 --- a/src/types/RettiwtConfig.ts +++ b/src/types/RettiwtConfig.ts @@ -1,4 +1,3 @@ -import { ITidProvider } from './auth/TidProvider'; import { IErrorHandler } from './ErrorHandler'; /** @@ -26,9 +25,6 @@ export interface IRettiwtConfig { /** Optional custom error handler to define error conditions and process API/HTTP errors in responses. */ errorHandler?: IErrorHandler; - /** Optional custom `x-client-transaction-id` header provider. */ - tidProvider?: ITidProvider; - /** * Optional custom HTTP headers to add to all requests to Twitter API. * @@ -42,4 +38,11 @@ export interface IRettiwtConfig { * Can either be a number or a function that returns a number synchronously or asynchronously. */ delay?: number | (() => number | Promise); + + /** + * The maximum number of retries to use. + * + * @remarks Recommended to use a value of 5 combined with a `delay` of 1000 to prevent error 404. + */ + maxRetries?: number; } diff --git a/src/types/args/FetchArgs.ts b/src/types/args/FetchArgs.ts index 8c101a51..318f787c 100644 --- a/src/types/args/FetchArgs.ts +++ b/src/types/args/FetchArgs.ts @@ -1,4 +1,5 @@ -import { ETweetRepliesSortType } from '../../enums/Tweet'; +import { RawAnalyticsGranularity, RawAnalyticsMetric } from '../../enums/raw/Analytics'; +import { TweetRepliesSortType } from '../../enums/Tweet'; /** * Options specifying the data that is to be fetched. @@ -6,21 +7,45 @@ import { ETweetRepliesSortType } from '../../enums/Tweet'; * @public */ export interface IFetchArgs { + /** + * The id of the active conversation. + * + * @remarks + * - Required only for {@link ResourceType.DM_USER_UPDATES}. + */ + activeConversationId?: string; + + /** + * The maximum id of the data to fetch. + * + * @remarks + * - May be used for {@link ResourceType.DM_INBOX_TIMELINE} and {@link ResourceType.DM_CONVERSATION}. + */ + maxId?: string; + + /** + * The id of the conversation to fetch. + * + * @remarks + * - Required only for {@link ResourceType.DM_CONVERSATION} and {@link ResourceType.DM_DELETE_CONVERSATION}. + */ + conversationId?: string; + /** * The number of data items to fetch. * * @remarks * - Works only for cursored resources. - * - Does not work for {@link EResourceType.TWEET_REPLIES}. + * - Does not work for {@link ResourceType.TWEET_REPLIES}. * - Must be \<= 20 for: - * - {@link EResourceType.USER_TIMELINE} - * - {@link EResourceType.USER_TIMELINE} - * - {@link EResourceType.USER_TIMELINE_AND_REPLIES} + * - {@link ResourceType.USER_TIMELINE} + * - {@link ResourceType.USER_TIMELINE} + * - {@link ResourceType.USER_TIMELINE_AND_REPLIES} * - Must be \<= 100 for all other cursored resources. - * - Due a bug on Twitter's end, count does not work for {@link EResourceType.USER_FOLLOWERS} and {@link EResourceType.USER_FOLLOWING}. + * - Due a bug on Twitter's end, count does not work for {@link ResourceType.USER_FOLLOWERS} and {@link ResourceType.USER_FOLLOWING}. * - Has not effect for: - * - {@link EResourceType.USER_FEED_FOLLOWED} - * - {@link EResourceType.USER_FEED_RECOMMENDED} + * - {@link ResourceType.USER_FEED_FOLLOWED} + * - {@link ResourceType.USER_FEED_RECOMMENDED} */ count?: number; @@ -37,7 +62,7 @@ export interface IFetchArgs { * The filter for searching tweets. * * @remarks - * Required when searching for tweets using {@link EResourceType.TWEET_SEARCH}. + * Required when searching for tweets using {@link ResourceType.TWEET_SEARCH}. */ filter?: ITweetFilter; @@ -45,8 +70,8 @@ export interface IFetchArgs { * The id of the target resource. * * @remarks - * - Required for all resources except {@link EResourceType.TWEET_SEARCH} and {@link EResourceType.USER_TIMELINE_RECOMMENDED}. - * - For {@link EResourceType.USER_DETAILS_BY_USERNAME}, can be alphanumeric, while for others, is strictly numeric. + * - Required for all resources except {@link ResourceType.TWEET_SEARCH} and {@link ResourceType.USER_TIMELINE_RECOMMENDED}. + * - For {@link ResourceType.USER_DETAILS_BY_USERNAME} and {@link ResourceType.USER_SEARCH}, can be alphanumeric, while for others, is strictly numeric. */ id?: string; @@ -54,7 +79,7 @@ export interface IFetchArgs { * The IDs of the target resources. * * @remarks - * - Required only for {@link EResourceType.TWEET_DETAILS_BULK} and {@link EResourceType.USER_DETAILS_BY_IDS_BULK}. + * - Required only for {@link ResourceType.TWEET_DETAILS_BULK} and {@link ResourceType.USER_DETAILS_BY_IDS_BULK}. */ ids?: string[]; @@ -62,9 +87,49 @@ export interface IFetchArgs { * The sorting to use for tweet results. * * @remarks - * - Only works for {@link EResourceType.TWEET_REPLIES}. + * - Only works for {@link ResourceType.TWEET_REPLIES}. + */ + sortBy?: TweetRepliesSortType; + + /** + * The date to start fetching data from. + * + * @remarks + * - Only works for {@link EResourceType.USER_ANALYTICS}. + */ + fromTime?: Date; + + /** + * The date to end fetching data at. + * + * @remarks + * - Only works for {@link EResourceType.USER_ANALYTICS}. + */ + toTime?: Date; + + /** + * The granularity of the data to fetch. + * + * @remarks + * - Only works for {@link EResourceType.USER_ANALYTICS}. + */ + granularity?: RawAnalyticsGranularity; + + /** + * The metrics to fetch. + * + * @remarks + * - Only works for {@link EResourceType.USER_ANALYTICS}. + */ + metrics?: RawAnalyticsMetric[]; + + /** + * Show the verified follower count and relationship counts in the response. + * + * @remarks + * - Only works for {@link EResourceType.USER_ANALYTICS}. */ - sortBy?: ETweetRepliesSortType; + showVerifiedFollowers?: boolean; } /** diff --git a/src/types/args/PostArgs.ts b/src/types/args/PostArgs.ts index 7efde397..26bdf31e 100644 --- a/src/types/args/PostArgs.ts +++ b/src/types/args/PostArgs.ts @@ -1,3 +1,5 @@ +import { IProfileUpdateOptions } from './ProfileArgs'; + /** * Options specifying the data that is to be posted. * @@ -9,13 +11,15 @@ export interface IPostArgs { * * @remarks * Required only when posting using the following resources: - * - {@link EResourceType.TWEET_LIKE} - * - {@link EResourceType.TWEET_RETWEET} - * - {@link EResourceType.TWEET_UNLIKE} - * - {@link EResourceType.TWEET_UNPOST} - * - {@link EResourceType.TWEET_UNRETWEET} - * - {@link EResourceType.USER_FOLLOW} - * - {@link EResourceType.USER_UNFOLLOW} + * - {@link ResourceType.TWEET_BOOKMARK} + * - {@link ResourceType.TWEET_LIKE} + * - {@link ResourceType.TWEET_RETWEET} + * - {@link ResourceType.TWEET_UNBOOKMARK} + * - {@link ResourceType.TWEET_UNLIKE} + * - {@link ResourceType.TWEET_UNPOST} + * - {@link ResourceType.TWEET_UNRETWEET} + * - {@link ResourceType.USER_FOLLOW} + * - {@link ResourceType.USER_UNFOLLOW} */ id?: string; @@ -23,7 +27,7 @@ export interface IPostArgs { * The tweet that is to be posted. * * @remarks - * Required only when posting a tweet using {@link EResourceType.TWEET_POST} + * Required only when posting a tweet using {@link ResourceType.TWEET_POST} */ tweet?: INewTweet; @@ -32,11 +36,37 @@ export interface IPostArgs { * * @remarks * Required only when uploading a media using the following resources: - * - {@link EResourceType.MEDIA_UPLOAD_APPEND} - * - {@link EResourceType.MEDIA_UPLOAD_FINALIZE} - * - {@link EResourceType.MEDIA_UPLOAD_INITIALIZE} + * - {@link ResourceType.MEDIA_UPLOAD_APPEND} + * - {@link ResourceType.MEDIA_UPLOAD_FINALIZE} + * - {@link ResourceType.MEDIA_UPLOAD_INITIALIZE} */ upload?: IUploadArgs; + + /** + * The id of the target user. + * + * @remarks + * Required only for the following resources: + * - {@link ResourceType.LIST_MEMBER_ADD} + * - {@link ResourceType.LIST_MEMBER_REMOVE} + */ + userId?: string; + + /** + * The id of the conversation to delete. + * + * @remarks + * Required only when deleting a conversation using {@link ResourceType.DM_DELETE_CONVERSATION} + */ + conversationId?: string; + + /** + * Profile update options. + * + * @remarks + * Required only when updating user profile using {@link ResourceType.USER_PROFILE_UPDATE} + */ + profileOptions?: IProfileUpdateOptions; } /** diff --git a/src/types/args/ProfileArgs.ts b/src/types/args/ProfileArgs.ts new file mode 100644 index 00000000..25212941 --- /dev/null +++ b/src/types/args/ProfileArgs.ts @@ -0,0 +1,33 @@ +/** + * Profile update options. + * + * @public + */ +export interface IProfileUpdateOptions { + /** + * Bio/description of the user (max 160 characters). + */ + description?: string; + + /** + * Location of the user (max 30 characters). + */ + location?: string; + + /** + * Display name (max 50 characters). + * + * @remarks + * The name field represents the user's display name shown on their profile. + * This is different from the username (screen_name/handle). + */ + name?: string; + + /** + * URL associated with the profile. + * + * @remarks + * Will be prepended with http:// if not present. + */ + url?: string; +} diff --git a/src/types/auth/AuthCredential.ts b/src/types/auth/AuthCredential.ts index 7db29fad..580fe5e8 100644 --- a/src/types/auth/AuthCredential.ts +++ b/src/types/auth/AuthCredential.ts @@ -1,4 +1,4 @@ -import { EAuthenticationType } from '../../enums/Authentication'; +import { AuthenticationType } from '../../enums/Authentication'; /** * The credentials for authenticating against Twitter. @@ -15,7 +15,7 @@ export interface IAuthCredential { authToken?: string; /** The type of authentication. */ - authenticationType?: EAuthenticationType; + authenticationType?: AuthenticationType; /** The cookie of the twitter account, which is used to authenticate against twitter. */ cookies?: string; diff --git a/src/types/auth/TidDynamicArgs.ts b/src/types/auth/TidDynamicArgs.ts deleted file mode 100644 index abdb4538..00000000 --- a/src/types/auth/TidDynamicArgs.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Internal args used for generating trasaction ID. - * - * @internal - */ -export interface ITidDynamicArgs { - verificationKey: string; - frames: number[][][]; - indices: number[]; -} diff --git a/src/types/auth/TidHeader.ts b/src/types/auth/TidHeader.ts deleted file mode 100644 index a724c1f6..00000000 --- a/src/types/auth/TidHeader.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * The header for the transaction ID. - * - * @public - */ -export interface ITidHeader { - /* eslint-disable @typescript-eslint/naming-convention */ - - 'x-client-transaction-id': string; - - /* eslint-enable @typescript-eslint/naming-convention */ -} diff --git a/src/types/auth/TidParams.ts b/src/types/auth/TidParams.ts deleted file mode 100644 index cd25f34c..00000000 --- a/src/types/auth/TidParams.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * The parameters for generating the transaction ID. - * - * @internal - */ -export interface ITidParams { - /** Secret used for transaction ID calculation. */ - keyword: string; - - /** Request method. */ - method: string; - - /** Endpoint path without query parameters. */ - path: string; - - /** Twitter verification key received from HTML. */ - verificationKey: string; - - /** Animation frames extracted from HTML. */ - frames: number[][][]; - - /** Indices used for getting the correct verification key bytes during animation key calculation. */ - indices: number[]; - - /** Final byte of the transaction ID. */ - extraByte: number; - - /** Current time */ - time?: number; - - /** XOR byte used for final hash calculation, must be in 0-255 range. */ - xorByte?: number; - - /** Precomputed animation key. */ - animationKey?: string; -} diff --git a/src/types/auth/TidProvider.ts b/src/types/auth/TidProvider.ts deleted file mode 100644 index 541c9152..00000000 --- a/src/types/auth/TidProvider.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Service responsible for generating the `x-client-transaction-id` header. - * - * @public - */ -export interface ITidProvider { - /** - * Generates new `x-client-transaction-id` header. - * - * @param method - Request method. - * @param path - Endpoint path without query parameters. - */ - generate(method: string, path: string): Promise; - - /** - * Refresh arguments obtained from parsing the HTML page, if any. - */ - refreshDynamicArgs(): Promise; -} diff --git a/src/types/auth/TransactionHeader.ts b/src/types/auth/TransactionHeader.ts new file mode 100644 index 00000000..c75c9a18 --- /dev/null +++ b/src/types/auth/TransactionHeader.ts @@ -0,0 +1,8 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +/** + * The transaction information for Twitter. + */ +export interface ITransactionHeader { + 'x-client-transaction-id': string; +} diff --git a/src/types/data/Analytics.ts b/src/types/data/Analytics.ts new file mode 100644 index 00000000..619410ba --- /dev/null +++ b/src/types/data/Analytics.ts @@ -0,0 +1,58 @@ +import type { IAnalyticsMetric } from '../../types/raw/base/Analytic'; +/** + * The details of the analytic result of the connected User. + * + * @public + */ +export interface IAnalytics { + /** The creation date of user's account. */ + createdAt: string; + + /** Total followers number */ + followers: number; + + /** Total verified followers */ + verifiedFollowers: number; + + /** Total impressions on the given period */ + impressions: number; + + /** Total profile visits on the given period */ + profileVisits: number; + + /** Total Engagements on the given period */ + engagements: number; + + /** Total Follows on the given period */ + follows: number; + + /** Total Replies on the given period */ + replies: number; + + /** Total Likes on the given period */ + likes: number; + + /** Total Retweets on the given period */ + retweets: number; + + /** Total Bookmark on the given period */ + bookmarks: number; + + /** Total Shares on the given period */ + shares: number; + + /** Total CreateTweets on the given period */ + createTweets: number; + + /** Total CreateQuote on the given period */ + createQuote: number; + + /** Total Unfollows on the given period */ + unfollows: number; + + /** Total CreateReply on the given period */ + createReply: number; + + /** Organic metrics times series */ + organicMetricsTimeSeries: IAnalyticsMetric[]; +} diff --git a/src/types/data/BookmarkFolder.ts b/src/types/data/BookmarkFolder.ts new file mode 100644 index 00000000..3f80d5a7 --- /dev/null +++ b/src/types/data/BookmarkFolder.ts @@ -0,0 +1,12 @@ +/** + * The details of a single Bookmark Folder. + * + * @public + */ +export interface IBookmarkFolder { + /** The unique identifier for the folder. */ + id: string; + + /** The display name of the folder. */ + name: string; +} diff --git a/src/types/data/Conversation.ts b/src/types/data/Conversation.ts new file mode 100644 index 00000000..2a4ba811 --- /dev/null +++ b/src/types/data/Conversation.ts @@ -0,0 +1,44 @@ +import { IDirectMessage } from './DirectMessage'; + +/** + * The details of a single conversation. + * + * @public + */ +export interface IConversation { + /** The unique identifier of the conversation. */ + id: string; + + /** The type of conversation (ONE_TO_ONE or GROUP_DM). */ + type: 'ONE_TO_ONE' | 'GROUP_DM'; + + /** Array of participant user IDs. */ + participants: string[]; + + /** The name of the conversation (for group DMs). */ + name?: string; + + /** URL to the conversation avatar (for group DMs). */ + avatarUrl?: string; + + /** Whether the conversation is trusted. */ + trusted: boolean; + + /** Whether the conversation is muted. */ + muted: boolean; + + /** Whether notifications are disabled. */ + notificationsDisabled: boolean; + + /** The timestamp of the last activity (ISO 8601 format). */ + lastActivityAt: string; + + /** The ID of the last message. */ + lastMessageId?: string; + + /** Whether there are more messages to load. */ + hasMore: boolean; + + /** Array of messages in this conversation. */ + messages: IDirectMessage[]; +} diff --git a/src/types/data/CursoredData.ts b/src/types/data/CursoredData.ts index d2dc032d..79254889 100644 --- a/src/types/data/CursoredData.ts +++ b/src/types/data/CursoredData.ts @@ -1,3 +1,7 @@ +import { IBookmarkFolder } from './BookmarkFolder'; +import { IConversation } from './Conversation'; +import { IDirectMessage } from './DirectMessage'; +import { IList } from './List'; import { INotification } from './Notification'; import { ITweet } from './Tweet'; import { IUser } from './User'; @@ -9,7 +13,9 @@ import { IUser } from './User'; * * @public */ -export interface ICursoredData { +export interface ICursoredData< + T extends IDirectMessage | IConversation | INotification | ITweet | IUser | IList | IBookmarkFolder, +> { /** The batch of data of the given type. */ list: T[]; diff --git a/src/types/data/DirectMessage.ts b/src/types/data/DirectMessage.ts new file mode 100644 index 00000000..1212c922 --- /dev/null +++ b/src/types/data/DirectMessage.ts @@ -0,0 +1,33 @@ +/** + * The details of a single direct message. + * + * @public + */ +export interface IDirectMessage { + /** The unique identifier of the message. */ + id: string; + + /** The ID of the conversation this message belongs to. */ + conversationId: string; + + /** The ID of the user who sent the message. */ + senderId: string; + + /** The ID of the user who received the message (for one-to-one conversations). */ + recipientId?: string; + + /** The text content of the message. */ + text: string; + + /** The timestamp when the message was sent (ISO 8601 format). */ + createdAt: string; + + /** Array of media URLs attached to the message. */ + mediaUrls?: string[]; + + /** Number of times the message has been edited. */ + editCount?: number; + + /** Whether the message has been read. */ + read?: boolean; +} diff --git a/src/types/data/Inbox.ts b/src/types/data/Inbox.ts new file mode 100644 index 00000000..bf867d0a --- /dev/null +++ b/src/types/data/Inbox.ts @@ -0,0 +1,23 @@ +import { IConversation } from './Conversation'; + +/** + * The details of a DM inbox containing conversations and metadata. + * + * @public + */ +export interface IInbox { + /** List of conversations in the inbox. */ + conversations: IConversation[]; + + /** The cursor for pagination of conversations. */ + cursor: string; + + /** The ID of the last seen event. */ + lastSeenEventId: string; + + /** The ID of the last seen trusted event. */ + trustedLastSeenEventId: string; + + /** The ID of the last seen untrusted event. */ + untrustedLastSeenEventId: string; +} diff --git a/src/types/data/List.ts b/src/types/data/List.ts index a16090b0..7e02ebfd 100644 --- a/src/types/data/List.ts +++ b/src/types/data/List.ts @@ -7,15 +7,21 @@ export interface IList { /** The date and time of creation of the list, int UTC string format. */ createdAt: string; - /** The rest id of the user who created the list. */ + /** The ID of the user who created the list. */ createdBy: string; /** The list description. */ description?: string; + /** Whether the user is following the list or not. */ + isFollowing: boolean; + /** The rest id of the list. */ id: string; + /** Whether the user is a member of the list or not. */ + isMember: boolean; + /** The number of memeber of the list. */ memberCount: number; diff --git a/src/types/data/Notification.ts b/src/types/data/Notification.ts index 262b6576..221867b1 100644 --- a/src/types/data/Notification.ts +++ b/src/types/data/Notification.ts @@ -1,4 +1,4 @@ -import { ENotificationType } from '../../enums/Notification'; +import { NotificationType } from '../../enums/Notification'; /** * The details of a single notification. @@ -22,5 +22,5 @@ export interface INotification { target: string[]; /** The type of notification. */ - type?: ENotificationType; + type?: NotificationType; } diff --git a/src/types/data/Tweet.ts b/src/types/data/Tweet.ts index 87b0d44f..199b8276 100644 --- a/src/types/data/Tweet.ts +++ b/src/types/data/Tweet.ts @@ -1,4 +1,4 @@ -import { EMediaType } from '../../enums/Media'; +import { MediaType } from '../../enums/Media'; import { IUser } from './User'; @@ -9,7 +9,7 @@ import { IUser } from './User'; */ export interface ITweet { /** The number of bookmarks of a tweet. */ - bookmarkCount: number; + bookmarkCount?: number; /** The ID of tweet which started the current conversation. */ conversationId: string; @@ -30,25 +30,25 @@ export interface ITweet { lang: string; /** The number of likes of the tweet. */ - likeCount: number; + likeCount?: number; /** The urls of the media contents of the tweet (if any). */ media?: ITweetMedia[]; /** The number of quotes of the tweet. */ - quoteCount: number; + quoteCount?: number; /** The tweet which is quoted in the tweet. */ quoted?: ITweet; /** The number of replies to the tweet. */ - replyCount: number; + replyCount?: number; /** The rest id of the tweet to which the tweet is a reply. */ replyTo?: string; /** The number of retweets of the tweet. */ - retweetCount: number; + retweetCount?: number; /** The tweet which is retweeted in this tweet (if any). */ retweetedTweet?: ITweet; @@ -60,7 +60,7 @@ export interface ITweet { url: string; /** The number of views of a tweet. */ - viewCount: number; + viewCount?: number; } /** @@ -85,11 +85,14 @@ export interface ITweetEntities { * @public */ export interface ITweetMedia { + /** The ID of the media. */ + id: string; + /** The thumbnail URL for the video content of the tweet. */ thumbnailUrl?: string; /** The type of media. */ - type: EMediaType; + type: MediaType; /** The direct URL to the media. */ url: string; diff --git a/src/types/data/User.ts b/src/types/data/User.ts index 1505ff77..85d97c3a 100644 --- a/src/types/data/User.ts +++ b/src/types/data/User.ts @@ -22,6 +22,12 @@ export interface IUser { /** The rest id of the user. */ id: string; + /** Whether the user is being followed by the logged-in user. Available only when logged in. */ + isFollowed?: boolean; + + /** Whether the user is following the logged-in user. Available only when logged in. */ + isFollowing?: boolean; + /** Whether the account is verified or not. */ isVerified: boolean; diff --git a/src/types/raw/base/Analytic.ts b/src/types/raw/base/Analytic.ts index 9a057e61..9fda794a 100644 --- a/src/types/raw/base/Analytic.ts +++ b/src/types/raw/base/Analytic.ts @@ -8,11 +8,17 @@ export interface IAnalytics { __typename: string; organic_metrics_time_series: IAnalyticsMetric[]; + verified_follower_count: string; + relationship_counts: IAnalyticsRelationships; id: string; } +export interface IAnalyticsRelationships { + followers: number; +} + export interface IAnalyticsMetric { - metric_value: IAnalyticsMetricValue[]; + metric_values: IAnalyticsMetricValue[]; timestamp: IAnalyticsTimeStamp; } diff --git a/src/types/raw/base/BookmarkFolder.ts b/src/types/raw/base/BookmarkFolder.ts new file mode 100644 index 00000000..d3434652 --- /dev/null +++ b/src/types/raw/base/BookmarkFolder.ts @@ -0,0 +1,12 @@ +/** + * Represents the raw data of a single BookmarkFolder. + * + * @public + */ +export interface IBookmarkFolder { + /** The unique identifier for the folder. */ + id: string; + + /** The display name of the folder. */ + name: string; +} diff --git a/src/types/raw/base/Error.ts b/src/types/raw/base/Error.ts index 918d216b..c9ff814d 100644 --- a/src/types/raw/base/Error.ts +++ b/src/types/raw/base/Error.ts @@ -6,6 +6,7 @@ * @public */ export interface IErrorData { + data: unknown; errors: IErrorDetails[]; } diff --git a/src/types/raw/base/Media.ts b/src/types/raw/base/Media.ts index ae353774..25793c8b 100644 --- a/src/types/raw/base/Media.ts +++ b/src/types/raw/base/Media.ts @@ -1,6 +1,6 @@ /* eslint-disable */ -import { ERawMediaType } from '../../../enums/raw/Media'; +import { RawMediaType } from '../../../enums/raw/Media'; /** * Represents the raw data of a single Media. @@ -12,7 +12,7 @@ export interface IMedia { expanded_url: string; id_str: string; media_url_https: string; - type: ERawMediaType; + type: RawMediaType; url: string; } diff --git a/src/types/raw/base/Message.ts b/src/types/raw/base/Message.ts new file mode 100644 index 00000000..844a2bce --- /dev/null +++ b/src/types/raw/base/Message.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ + +import { ConversationMessage } from '../dm/Conversation'; +import { Message } from '../dm/InboxInitial'; +import { TimelineMessage } from '../dm/InboxTimeline'; + +// Extract the message_data types +type ConversationMessageData = ConversationMessage['message_data']; +type InboxMessageData = Message['message_data']; +type TimelineMessageData = TimelineMessage['message_data']; + +// Create unified message_data type that includes all possible fields +type UnifiedMessageData = InboxMessageData & Partial & Partial; + +export interface IMessage { + id: string; + time: string; + affects_sort?: boolean; + request_id: string; + conversation_id: string; + message_data: UnifiedMessageData; +} diff --git a/src/types/raw/base/Notification.ts b/src/types/raw/base/Notification.ts index 7a87bb72..ebd81215 100644 --- a/src/types/raw/base/Notification.ts +++ b/src/types/raw/base/Notification.ts @@ -1,6 +1,9 @@ /* eslint-disable */ -import { ERawNotificationType } from '../../../enums/raw/Notification'; +import { RawNotificationType } from '../../../enums/raw/Notification'; +import { IDataResult } from '../composite/DataResult'; +import { ITweet } from './Tweet'; +import { IUser } from './User'; /** * Represents the raw data of a single Notification. @@ -8,60 +11,56 @@ import { ERawNotificationType } from '../../../enums/raw/Notification'; * @public */ export interface INotification { + itemType: string; + __typename: string; id: string; - timestampMs: string; - icon: INotificationIcon; - message: INotificationMessage; - template: INotificationTemplate; + notification_icon: RawNotificationType; + rich_message: INotificationRichMessage; + notification_url: INotificationUrl; + template: INoticiationTemplate; + timestamp_ms: string; } -export interface INotificationIcon { - id: ERawNotificationType; -} - -export interface INotificationMessage { - text: string; - entities: INotificationMessageEntity[]; +export interface INotificationRichMessage { rtl: boolean; + text: string; + entities: INotificationEntity[]; } -export interface INotificationMessageEntity { +export interface INotificationEntity { fromIndex: number; toIndex: number; - format: string; + ref: INotificationEntityRef; } -export interface INotificationTemplate { - aggregateUserActionsV1: INotificationUserActions; +export interface INotificationEntityRef { + type: string; + user_results: IDataResult; } -export interface INotificationUserActions { - targetObjects: INotificationTargetObject[]; - fromUsers: INotificationFromUser[]; - additionalContext: INotificationAdditionalContext; +export interface INotificationUrl { + url: string; + urlType: string; + urtEndpointOptions?: INotificationUrtEndpointOptions; } -export interface INotificationTargetObject { - tweet: INotificationTweet; +export interface INotificationUrtEndpointOptions { + cacheId: string; + title: string; } -export interface INotificationTweet { - id: string; +export interface INoticiationTemplate { + __typename: string; + target_objects: INotificationTargetObject[]; + from_users: INotificationFromUser[]; } -export interface INotificationFromUser { - user: INotificationUser; -} - -export interface INotificationUser { - id: string; -} - -export interface INotificationAdditionalContext { - contextText: INotificationContextText; +export interface INotificationTargetObject { + __typename: string; + tweet_results: IDataResult; } -export interface INotificationContextText { - text: string; - entities: any[]; +export interface INotificationFromUser { + __typename: string; + user_results: IDataResult; } diff --git a/src/types/raw/base/Tweet.ts b/src/types/raw/base/Tweet.ts index 10bc5960..202190e3 100644 --- a/src/types/raw/base/Tweet.ts +++ b/src/types/raw/base/Tweet.ts @@ -17,7 +17,7 @@ export interface ITweet { edit_control: ITweetEditControl; edit_perspective: ITweetEditPerspective; is_translatable: boolean; - views: ITweetViews; + views?: ITweetViews; source: string; quoted_status_result: IDataResult; note_tweet: ITweetNote; @@ -78,14 +78,14 @@ export interface ITweetNoteMedia { } export interface ITweetLegacy { - bookmark_count: number; + bookmark_count?: number; bookmarked: boolean; created_at: string; conversation_id_str: string; display_text_range: number[]; entities: IEntities; extended_entities: IExtendedEntities; - favorite_count: number; + favorite_count?: number; favorited: boolean; full_text: string; in_reply_to_status_id_str: string; @@ -93,10 +93,10 @@ export interface ITweetLegacy { lang: string; possibly_sensitive: boolean; possibly_sensitive_editable: boolean; - quote_count: number; + quote_count?: number; quoted_status_id_str: string; - reply_count: number; - retweet_count: number; + reply_count?: number; + retweet_count?: number; retweeted: boolean; user_id_str: string; id_str: string; diff --git a/src/types/raw/base/User.ts b/src/types/raw/base/User.ts index ccd9f6c9..218c9359 100644 --- a/src/types/raw/base/User.ts +++ b/src/types/raw/base/User.ts @@ -11,11 +11,14 @@ export interface IUser { __typename: string; id: string; rest_id: string; + core?: IUserCore; + avatar?: IUserAvatar; affiliates_highlighted_label: IAffiliatesHighlightedLabel; has_graduated_access: boolean; is_blue_verified: boolean; profile_image_shape: string; legacy: IUserLegacy; + location?: IUserLocation; super_follow_eligible: boolean; smart_blocked_by: boolean; smart_blocking: boolean; @@ -28,6 +31,16 @@ export interface IUser { creator_subscriptions_count: number; } +export interface IUserCore { + created_at: string; + name: string; + screen_name: string; +} + +export interface IUserAvatar { + image_url: string; +} + export interface IAffiliatesHighlightedLabel { label: IAffiliateLabel; } @@ -82,10 +95,14 @@ export interface IAffiliateHighlightedMentionResultLegacy { } export interface IUserLegacy { - following: boolean; + created_at?: string; + name?: string; + screen_name?: string; + location?: string; + followed_by?: boolean; + following?: boolean; can_dm: boolean; can_media_tag: boolean; - created_at: string; default_profile: boolean; default_profile_image: boolean; description: string; @@ -97,16 +114,13 @@ export interface IUserLegacy { has_custom_timelines: boolean; is_translator: boolean; listed_count: number; - location: string; media_count: number; - name: string; normal_followers_count: number; pinned_tweet_ids_str: string[]; possibly_sensitive: boolean; profile_banner_url: string; - profile_image_url_https: string; + profile_image_url_https?: string; profile_interstitial_type: string; - screen_name: string; statuses_count: number; translator_type: string; verified: boolean; @@ -127,6 +141,10 @@ export interface IProfileUrl { urls: IUrl[]; } +export interface IUserLocation { + location: string; +} + export interface ILegacyExtendedProfile {} export interface IVerificationInfo { diff --git a/src/types/raw/composite/TimelineList.ts b/src/types/raw/composite/TimelineList.ts new file mode 100644 index 00000000..915f4226 --- /dev/null +++ b/src/types/raw/composite/TimelineList.ts @@ -0,0 +1,10 @@ +import { IList } from '../base/List'; + +/** + * Represents the raw data of a single timeline tweet. + * + * @public + */ +export interface ITimelineList { + list: IList; +} diff --git a/src/types/raw/dm/Conversation.ts b/src/types/raw/dm/Conversation.ts new file mode 100644 index 00000000..cc9084e4 --- /dev/null +++ b/src/types/raw/dm/Conversation.ts @@ -0,0 +1,59 @@ +/* eslint-disable */ + +import { Users, Conversations } from './InboxInitial'; + +/** + * The raw data received when fetching a specific conversation timeline. + * + * @public + */ +export interface IConversationTimelineResponse { + conversation_timeline: ConversationTimeline; +} + +interface ConversationTimeline { + status: 'HAS_MORE' | 'AT_END'; + min_entry_id: string; + max_entry_id: string; + entries: ConversationEntry[]; + users: Users; + conversations: Conversations; +} + +type ConversationEntry = { message: ConversationMessage } | { trust_conversation: TrustConversation }; + +export interface ConversationMessage { + id: string; + time: string; + request_id: string; + conversation_id: string; + message_data: ConversationMessageData; +} + +interface ConversationMessageData { + id: string; + time: string; + recipient_id: string; + sender_id: string; + text: string; + edit_count?: number; + message_reactions?: MessageReaction[]; +} + +interface MessageReaction { + id: string; + time: string; + conversation_id: string; + message_id: string; + reaction_key: string; + emoji_reaction: string; + sender_id: string; +} + +interface TrustConversation { + id: string; + time: string; + request_id: string; + conversation_id: string; + reason: string; // e.g., "accept" +} diff --git a/src/types/raw/dm/InboxInitial.ts b/src/types/raw/dm/InboxInitial.ts new file mode 100644 index 00000000..419ccf44 --- /dev/null +++ b/src/types/raw/dm/InboxInitial.ts @@ -0,0 +1,155 @@ +/* eslint-disable */ + +/** + * The raw data received when fetching the initial state of the DM inbox. + * + * @public + */ +export interface IInboxInitialResponse { + inbox_initial_state: InboxInitialState; +} + +export interface InboxInitialState { + last_seen_event_id: string; + trusted_last_seen_event_id: string; + untrusted_last_seen_event_id: string; + cursor: string; + inbox_timelines: InboxTimelines; + entries: Entry[]; + users: Users; + conversations: Conversations; +} + +interface InboxTimelines { + trusted: TimelineStatus; + untrusted: TimelineStatus; + untrusted_low_quality: TimelineStatus; +} + +interface TimelineStatus { + status: string; // "HAS_MORE" | "AT_END" + min_entry_id: string; +} + +interface Entry { + message: Message; +} + +export interface Message { + id: string; + time: string; + affects_sort: boolean; + request_id: string; + conversation_id: string; + message_data: MessageData; +} + +interface MessageData { + id: string; + time: string; + recipient_id: string; + sender_id: string; + text: string; + edit_count: number; +} + +export interface Users { + [userId: string]: User; +} + +interface User { + id: number; + id_str: string; + name: string; + screen_name: string; + profile_image_url: string; + profile_image_url_https: string; + following: boolean; + follow_request_sent: boolean; + description: string; + entities: UserEntities; + verified: boolean; + is_blue_verified: boolean; + protected: boolean; + blocking: boolean; + subscribed_by: boolean; + can_media_tag: boolean; + dm_blocked_by: boolean; + dm_blocking: boolean; + created_at: string; + friends_count: number; + followers_count: number; +} + +interface UserEntities { + url: UrlEntity; + description: DescriptionEntity; +} + +interface UrlEntity { + urls: UrlInfo[]; +} + +interface DescriptionEntity { + urls: UrlInfo[]; +} + +interface UrlInfo { + url: string; + expanded_url: string; + display_url: string; + indices: [number, number]; +} + +export interface Conversations { + [conversationId: string]: Conversation; +} + +export interface Conversation { + conversation_id: string; + type: 'GROUP_DM' | 'ONE_TO_ONE'; + sort_event_id: string; + sort_timestamp: string; + participants: Participant[]; + nsfw: boolean; + notifications_disabled: boolean; + mention_notifications_disabled: boolean; + last_read_event_id: string; + trusted: boolean; + low_quality: boolean; + muted: boolean; + status: 'HAS_MORE' | 'AT_END'; + min_entry_id: string; + max_entry_id: string; + create_time?: string; // Only for GROUP_DM + created_by_user_id?: string; // Only for GROUP_DM + name?: string; // Only for GROUP_DM + avatar_image_https?: string; // Only for GROUP_DM + avatar?: ConversationAvatar; // Only for GROUP_DM + read_only?: boolean; // Only for ONE_TO_ONE + social_proof?: SocialProof[]; // Only for untrusted conversations +} + +interface ConversationAvatar { + image: { + original_info: { + url: string; + width: number; + height: number; + }; + }; +} + +interface Participant { + user_id: string; + join_time?: string; // Only for GROUP_DM + last_read_event_id?: string; + join_conversation_event_id?: string; // Only for GROUP_DM + is_admin?: boolean; // Only for GROUP_DM +} + +export interface SocialProof { + proof_type: string; // e.g., "mutual_friends" + users: any[]; // Array of users (structure depends on proof_type) + total: number; +} diff --git a/src/types/raw/dm/InboxTimeline.ts b/src/types/raw/dm/InboxTimeline.ts new file mode 100644 index 00000000..d6478ace --- /dev/null +++ b/src/types/raw/dm/InboxTimeline.ts @@ -0,0 +1,301 @@ +/* eslint-disable */ + +import { Users, Conversations } from './InboxInitial'; + +/** + * The raw data received when fetching the inbox timeline. + * + * @public + */ +export interface IInboxTimelineResponse { + inbox_timeline: InboxTimeline; +} + +interface InboxTimeline { + status: 'HAS_MORE' | 'AT_END'; + min_entry_id: string; + entries: TimelineEntry[]; + users: Users; + conversations: Conversations; +} + +type TimelineEntry = + | { trust_conversation: TrustConversation } + | { message: TimelineMessage } + | { participants_leave: ParticipantsLeave }; + +interface TrustConversation { + id: string; + time: string; + affects_sort: boolean; + request_id: string; + conversation_id: string; + reason: string; // e.g., "accept" +} + +export interface TimelineMessage { + id: string; + time: string; + affects_sort: boolean; + request_id: string; + conversation_id: string; + message_data: TimelineMessageData; +} + +interface TimelineMessageData { + id: string; + time: string; + recipient_id?: string; + sender_id: string; + conversation_id?: string; + text: string; + edit_count: number; + entities?: MessageEntities; + reply_data?: ReplyData; + attachment?: MessageAttachment; +} + +interface MessageEntities { + hashtags: any[]; + symbols: any[]; + user_mentions: UserMention[]; + urls: UrlEntity[]; +} + +interface UserMention { + screen_name: string; + name: string; + id: number; + id_str: string; + indices: [number, number]; +} + +interface UrlEntity { + url: string; + expanded_url: string; + display_url: string; + indices: [number, number]; +} + +interface ReplyData { + id: string; + time: string; + recipient_id: string; + sender_id: string; + text: string; + edit_count: number; + entities?: MessageEntities; +} + +interface MessageAttachment { + card?: CardAttachment; + tweet?: TweetAttachment; +} + +interface CardAttachment { + name: string; + url: string; + card_type_url: string; + binding_values: CardBindingValues; +} + +interface CardBindingValues { + vanity_url?: StringValue; + domain?: StringValue; + title?: StringValue; + description?: StringValue; + thumbnail_image_small?: ImageValue; + thumbnail_image?: ImageValue; + thumbnail_image_large?: ImageValue; + thumbnail_image_x_large?: ImageValue; + thumbnail_image_color?: ImageColorValue; + thumbnail_image_original?: ImageValue; + summary_photo_image_small?: ImageValue; + summary_photo_image?: ImageValue; + summary_photo_image_large?: ImageValue; + summary_photo_image_x_large?: ImageValue; + summary_photo_image_color?: ImageColorValue; + summary_photo_image_original?: ImageValue; + photo_image_full_size_small?: ImageValue; + photo_image_full_size?: ImageValue; + photo_image_full_size_large?: ImageValue; + photo_image_full_size_x_large?: ImageValue; + photo_image_full_size_color?: ImageColorValue; + photo_image_full_size_original?: ImageValue; + card_url?: StringValue; +} + +interface StringValue { + type: 'STRING'; + string_value: string; + scribe_key?: string; +} + +interface ImageValue { + type: 'IMAGE'; + image_value: { + url: string; + width: number; + height: number; + alt: string | null; + }; +} + +interface ImageColorValue { + type: 'IMAGE_COLOR'; + image_color_value: { + palette: ColorPalette[]; + }; +} + +interface ColorPalette { + percentage: number; + rgb: { + red: number; + green: number; + blue: number; + }; +} + +interface TweetAttachment { + id: string; + url: string; + display_url: string; + expanded_url: string; + indices: [number, number]; + status: TwitterStatus; +} + +interface TwitterStatus { + created_at: string; + id: number; + id_str: string; + full_text: string; + truncated: boolean; + display_text_range: [number, number]; + entities: MessageEntities; + source: string; + in_reply_to_status_id: number | null; + in_reply_to_status_id_str: string | null; + in_reply_to_user_id: number | null; + in_reply_to_user_id_str: string | null; + in_reply_to_screen_name: string | null; + user: TwitterUser; + geo: any; + coordinates: any; + place: any; + contributors: any; + is_quote_status: boolean; + retweet_count: number; + favorite_count: number; + reply_count: number; + quote_count: number; + favorited: boolean; + retweeted: boolean; + lang: string; + supplemental_language: string | null; + ext: TwitterExtensions; +} + +interface TwitterUser { + id: number; + id_str: string; + name: string; + screen_name: string; + location: string; + description: string; + url: string; + entities: UserEntityInfo; + protected: boolean; + followers_count: number; + fast_followers_count: number; + normal_followers_count: number; + friends_count: number; + listed_count: number; + created_at: string; + favourites_count: number; + utc_offset: any; + time_zone: any; + geo_enabled: boolean; + verified: boolean; + statuses_count: number; + media_count: number; + lang: any; + contributors_enabled: boolean; + is_translator: boolean; + is_translation_enabled: boolean; + profile_background_color: string; + profile_background_image_url: string | null; + profile_background_image_url_https: string | null; + profile_background_tile: boolean; + profile_image_url: string; + profile_image_url_https: string; + profile_banner_url: string; + profile_link_color: string; + profile_sidebar_border_color: string; + profile_sidebar_fill_color: string; + profile_text_color: string; + profile_use_background_image: boolean; + default_profile: boolean; + default_profile_image: boolean; + pinned_tweet_ids: number[]; + pinned_tweet_ids_str: string[]; + has_custom_timelines: boolean; + can_dm: any; + can_media_tag: boolean; + following: boolean; + follow_request_sent: boolean; + notifications: boolean; + muting: any; + blocking: boolean; + blocked_by: boolean; + want_retweets: boolean; + advertiser_account_type: string; + advertiser_account_service_levels: any[]; + business_profile_state: string; + translator_type: string; + withheld_in_countries: any[]; + followed_by: boolean; + ext: TwitterExtensions; + require_some_consent: boolean; +} + +interface UserEntityInfo { + url: { + urls: UrlEntity[]; + }; + description: { + urls: UrlEntity[]; + }; +} + +interface TwitterExtensions { + businessAffiliationsLabel?: { + r: { ok: any }; + ttl: number; + }; + superFollowMetadata?: { + r: { ok: any }; + ttl: number; + }; + parodyCommentaryFanLabel?: { + r: { ok: string }; + ttl: number; + }; + highlightedLabel?: { + r: { ok: any }; + ttl: number; + }; +} + +interface ParticipantsLeave { + id: string; + time: string; + affects_sort: boolean; + conversation_id: string; + participants: ParticipantInfo[]; +} + +interface ParticipantInfo { + user_id: string; +} diff --git a/src/types/raw/dm/UserUpdates.ts b/src/types/raw/dm/UserUpdates.ts new file mode 100644 index 00000000..d951d1d3 --- /dev/null +++ b/src/types/raw/dm/UserUpdates.ts @@ -0,0 +1,46 @@ +/* eslint-disable */ + +import { Users, Conversations, InboxInitialState } from './InboxInitial'; + +/** + * The raw data received when fetching user updates from the DM system. + * The response structure varies based on query parameters. + * + * @public + */ +export interface IUserUpdatesResponse { + user_events?: UserEvents; + inbox_initial_state?: InboxInitialState; +} + +/** + * User events can have different structures based on the request type: + * - With active_conversation_id + cursor: Full data with users and conversations + * - Without active_conversation_id and cursor: Same as inbox initial (see IInboxInitialResponse) + * - With cursor only: Minimal data with just event IDs and cursor + */ +type UserEvents = UserEventsWithData | UserEventsMinimal; + +/** + * Full user events data returned when requesting with active_conversation_id and cursor. + * Used for conversation-specific updates with user and conversation context. + */ +interface UserEventsWithData { + cursor: string; + last_seen_event_id: string; + trusted_last_seen_event_id: string; + untrusted_last_seen_event_id: string; + users: Users; + conversations: Conversations; +} + +/** + * Minimal user events data returned when requesting with cursor only (no active_conversation_id). + * Used for lightweight polling of event state without full data. + */ +interface UserEventsMinimal { + cursor: string; + last_seen_event_id: string; + trusted_last_seen_event_id: string; + untrusted_last_seen_event_id: string; +} diff --git a/src/types/raw/list/AddMember.ts b/src/types/raw/list/AddMember.ts new file mode 100644 index 00000000..45ad8e8a --- /dev/null +++ b/src/types/raw/list/AddMember.ts @@ -0,0 +1,175 @@ +/* eslint-disable */ + +/** + * The raw data received after adding a member to a tweet list. + * + * @public + */ +export interface IListMemberAddResponse { + data: Data; +} + +export interface Data { + list: List; +} + +export interface List { + created_at: number; + default_banner_media: DefaultBannerMedia; + default_banner_media_results: DefaultBannerMediaResults; + description: string; + facepile_urls: any[]; + following: boolean; + id: string; + id_str: string; + is_member: boolean; + member_count: number; + members_context: string; + mode: string; + muting: boolean; + name: string; + pinning: boolean; + subscriber_count: number; + user_results: UserResults; +} + +export interface DefaultBannerMedia { + media_info: MediaInfo; +} + +export interface MediaInfo { + original_img_url: string; + original_img_width: number; + original_img_height: number; + salient_rect: SalientRect; +} + +export interface SalientRect { + left: number; + top: number; + width: number; + height: number; +} + +export interface DefaultBannerMediaResults { + result: Result; +} + +export interface Result { + id: string; + media_key: string; + media_id: string; + media_info: MediaInfo2; + __typename: string; +} + +export interface MediaInfo2 { + __typename: string; + original_img_height: number; + original_img_width: number; + original_img_url: string; + salient_rect: SalientRect2; +} + +export interface SalientRect2 { + height: number; + left: number; + top: number; + width: number; +} + +export interface UserResults { + result: Result2; +} + +export interface Result2 { + __typename: string; + id: string; + rest_id: string; + affiliates_highlighted_label: AffiliatesHighlightedLabel; + avatar: Avatar; + core: Core; + dm_permissions: DmPermissions; + has_graduated_access: boolean; + is_blue_verified: boolean; + legacy: Legacy; + location: Location; + media_permissions: MediaPermissions; + parody_commentary_fan_label: string; + profile_image_shape: string; + privacy: Privacy; + relationship_perspectives: RelationshipPerspectives; + tipjar_settings: TipjarSettings; + verification: Verification; + verified_phone_status: boolean; +} + +export interface AffiliatesHighlightedLabel {} + +export interface Avatar { + image_url: string; +} + +export interface Core { + created_at: string; + name: string; + screen_name: string; +} + +export interface DmPermissions { + can_dm: boolean; +} + +export interface Legacy { + default_profile: boolean; + default_profile_image: boolean; + description: string; + entities: Entities; + fast_followers_count: number; + favourites_count: number; + followers_count: number; + friends_count: number; + has_custom_timelines: boolean; + is_translator: boolean; + listed_count: number; + media_count: number; + needs_phone_verification: boolean; + normal_followers_count: number; + pinned_tweet_ids_str: any[]; + possibly_sensitive: boolean; + profile_interstitial_type: string; + statuses_count: number; + translator_type: string; + want_retweets: boolean; + withheld_in_countries: any[]; +} + +export interface Entities { + description: Description; +} + +export interface Description { + urls: any[]; +} + +export interface Location { + location: string; +} + +export interface MediaPermissions { + can_media_tag: boolean; +} + +export interface Privacy { + protected: boolean; +} + +export interface RelationshipPerspectives { + following: boolean; +} + +export interface TipjarSettings {} + +export interface Verification { + verified: boolean; +} diff --git a/src/types/raw/list/Details.ts b/src/types/raw/list/Details.ts index 40812484..1c70c91c 100644 --- a/src/types/raw/list/Details.ts +++ b/src/types/raw/list/Details.ts @@ -18,19 +18,20 @@ interface List { default_banner_media: DefaultBannerMedia; default_banner_media_results: DefaultBannerMediaResults; description: string; + facepile_urls: string[]; + followers_context: string; following: boolean; id: string; id_str: string; is_member: boolean; member_count: number; + members_context: string; mode: string; muting: boolean; name: string; + pinning: boolean; subscriber_count: number; user_results: UserResults; - facepile_urls: string[]; - followers_context: string; - members_context: string; } interface DefaultBannerMedia { @@ -87,19 +88,40 @@ interface Result2 { id: string; rest_id: string; affiliates_highlighted_label: AffiliatesHighlightedLabel; + avatar: Avatar; + core: Core; + dm_permissions: DmPermissions; has_graduated_access: boolean; is_blue_verified: boolean; - profile_image_shape: string; legacy: Legacy; + location: Location; + media_permissions: MediaPermissions; + parody_commentary_fan_label: string; + profile_image_shape: string; + privacy: Privacy; + relationship_perspectives: RelationshipPerspectives; + tipjar_settings: TipjarSettings; + verification: Verification; verified_phone_status: boolean; } interface AffiliatesHighlightedLabel {} -interface Legacy { - can_dm: boolean; - can_media_tag: boolean; +interface Avatar { + image_url: string; +} + +interface Core { created_at: string; + name: string; + screen_name: string; +} + +interface DmPermissions { + can_dm: boolean; +} + +interface Legacy { default_profile: boolean; default_profile_image: boolean; description: string; @@ -111,19 +133,14 @@ interface Legacy { has_custom_timelines: boolean; is_translator: boolean; listed_count: number; - location: string; media_count: number; - name: string; normal_followers_count: number; - pinned_tweet_ids_str: any[]; + pinned_tweet_ids_str: string[]; possibly_sensitive: boolean; profile_banner_url: string; - profile_image_url_https: string; profile_interstitial_type: string; - screen_name: string; statuses_count: number; translator_type: string; - verified: boolean; want_retweets: boolean; withheld_in_countries: any[]; } @@ -135,3 +152,25 @@ interface Entities { interface Description { urls: any[]; } + +interface Location { + location: string; +} + +interface MediaPermissions { + can_media_tag: boolean; +} + +interface Privacy { + protected: boolean; +} + +interface RelationshipPerspectives { + following: boolean; +} + +interface TipjarSettings {} + +interface Verification { + verified: boolean; +} diff --git a/src/types/raw/list/RemoveMember.ts b/src/types/raw/list/RemoveMember.ts new file mode 100644 index 00000000..1d8c0b68 --- /dev/null +++ b/src/types/raw/list/RemoveMember.ts @@ -0,0 +1,174 @@ +/* eslint-disable */ + +/** + * The raw data received after removing a member from a tweet list. + * + * @public + */ +export interface IListMemberRemoveResponse { + data: Data; +} + +export interface Data { + list: List; +} + +export interface List { + created_at: number; + default_banner_media: DefaultBannerMedia; + default_banner_media_results: DefaultBannerMediaResults; + description: string; + facepile_urls: any[]; + following: boolean; + id: string; + id_str: string; + is_member: boolean; + member_count: number; + mode: string; + muting: boolean; + name: string; + pinning: boolean; + subscriber_count: number; + user_results: UserResults; +} + +export interface DefaultBannerMedia { + media_info: MediaInfo; +} + +export interface MediaInfo { + original_img_url: string; + original_img_width: number; + original_img_height: number; + salient_rect: SalientRect; +} + +export interface SalientRect { + left: number; + top: number; + width: number; + height: number; +} + +export interface DefaultBannerMediaResults { + result: Result; +} + +export interface Result { + id: string; + media_key: string; + media_id: string; + media_info: MediaInfo2; + __typename: string; +} + +export interface MediaInfo2 { + __typename: string; + original_img_height: number; + original_img_width: number; + original_img_url: string; + salient_rect: SalientRect2; +} + +export interface SalientRect2 { + height: number; + left: number; + top: number; + width: number; +} + +export interface UserResults { + result: Result2; +} + +export interface Result2 { + __typename: string; + id: string; + rest_id: string; + affiliates_highlighted_label: AffiliatesHighlightedLabel; + avatar: Avatar; + core: Core; + dm_permissions: DmPermissions; + has_graduated_access: boolean; + is_blue_verified: boolean; + legacy: Legacy; + location: Location; + media_permissions: MediaPermissions; + parody_commentary_fan_label: string; + profile_image_shape: string; + privacy: Privacy; + relationship_perspectives: RelationshipPerspectives; + tipjar_settings: TipjarSettings; + verification: Verification; + verified_phone_status: boolean; +} + +export interface AffiliatesHighlightedLabel {} + +export interface Avatar { + image_url: string; +} + +export interface Core { + created_at: string; + name: string; + screen_name: string; +} + +export interface DmPermissions { + can_dm: boolean; +} + +export interface Legacy { + default_profile: boolean; + default_profile_image: boolean; + description: string; + entities: Entities; + fast_followers_count: number; + favourites_count: number; + followers_count: number; + friends_count: number; + has_custom_timelines: boolean; + is_translator: boolean; + listed_count: number; + media_count: number; + needs_phone_verification: boolean; + normal_followers_count: number; + pinned_tweet_ids_str: any[]; + possibly_sensitive: boolean; + profile_interstitial_type: string; + statuses_count: number; + translator_type: string; + want_retweets: boolean; + withheld_in_countries: any[]; +} + +export interface Entities { + description: Description; +} + +export interface Description { + urls: any[]; +} + +export interface Location { + location: string; +} + +export interface MediaPermissions { + can_media_tag: boolean; +} + +export interface Privacy { + protected: boolean; +} + +export interface RelationshipPerspectives { + following: boolean; +} + +export interface TipjarSettings {} + +export interface Verification { + verified: boolean; +} diff --git a/src/types/raw/tweet/Bookmark.ts b/src/types/raw/tweet/Bookmark.ts new file mode 100644 index 00000000..caa55d5c --- /dev/null +++ b/src/types/raw/tweet/Bookmark.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ + +/** + * The raw data received when bookmarking a given tweet. + * + * @public + */ +export interface ITweetBookmarkResponse { + data: Data; +} + +interface Data { + tweet_bookmark_put: string; +} diff --git a/src/types/raw/tweet/Unbookmark.ts b/src/types/raw/tweet/Unbookmark.ts new file mode 100644 index 00000000..05ac54c6 --- /dev/null +++ b/src/types/raw/tweet/Unbookmark.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ + +/** + * The raw data received when unbookmarking a given tweet. + * + * @public + */ +export interface ITweetUnbookmarkResponse { + data: Data; +} + +export interface Data { + tweet_bookmark_delete: string; +} diff --git a/src/types/raw/user/Analytics.ts b/src/types/raw/user/Analytics.ts index 35dc11e1..dae1f66c 100644 --- a/src/types/raw/user/Analytics.ts +++ b/src/types/raw/user/Analytics.ts @@ -1,5 +1,5 @@ /* eslint-disable */ - +import type { IAnalytics } from '../base/Analytic'; /** * The raw data received when fetching the analytic overview of the user. * @@ -10,30 +10,14 @@ export interface IUserAnalyticsResponse { } interface Data { - result: Result; + viewer_v2: ViewerV2; } -interface Result { - result: Result2; - id: string; +interface ViewerV2 { + user_results: UserResults; } -interface Result2 { - __typename: string; - organic_metrics_time_series: Series[]; +interface UserResults { id: string; -} - -interface Series { - metric_values: MetricValue[]; - timestamp: Timestamp; -} - -interface MetricValue { - metric_value: number; - metric_type: string; -} - -interface Timestamp { - iso8601_time: string; + result: IAnalytics; } diff --git a/src/types/raw/user/BookmarkFolderTweets.ts b/src/types/raw/user/BookmarkFolderTweets.ts new file mode 100644 index 00000000..01e13121 --- /dev/null +++ b/src/types/raw/user/BookmarkFolderTweets.ts @@ -0,0 +1,53 @@ +/* eslint-disable */ + +/** + * The raw data received when fetching tweets from a bookmark folder. + * + * @public + */ +export interface IUserBookmarkFolderTweetsResponse { + data: Data; +} + +interface Data { + bookmark_collection_timeline: BookmarkCollectionTimeline; +} + +interface BookmarkCollectionTimeline { + timeline: Timeline; +} + +interface Timeline { + instructions: Instruction[]; +} + +interface Instruction { + type: string; + entries?: Entry[]; +} + +interface Entry { + entryId: string; + sortIndex: string; + content: Content; +} + +interface Content { + entryType: string; + __typename: string; + itemContent?: ItemContent; + value?: string; + cursorType?: string; + stopOnEmptyResponse?: boolean; +} + +interface ItemContent { + itemType: string; + __typename: string; + tweet_results: TweetResults; + tweetDisplayType: string; +} + +interface TweetResults { + result: any; // Uses the same tweet structure as regular bookmarks +} diff --git a/src/types/raw/user/BookmarkFolders.ts b/src/types/raw/user/BookmarkFolders.ts new file mode 100644 index 00000000..870d69b9 --- /dev/null +++ b/src/types/raw/user/BookmarkFolders.ts @@ -0,0 +1,41 @@ +/* eslint-disable */ + +/** + * The raw data received when fetching bookmark folders. + * + * @public + */ +export interface IUserBookmarkFoldersResponse { + data: Data; +} + +interface Data { + viewer: Viewer; +} + +interface Viewer { + user_results: UserResults; +} + +interface UserResults { + result: Result; +} + +interface Result { + __typename: string; + bookmark_collections_slice: BookmarkCollectionsSlice; +} + +interface BookmarkCollectionsSlice { + items: BookmarkCollectionItem[]; + slice_info: SliceInfo; +} + +interface BookmarkCollectionItem { + id: string; + name: string; +} + +interface SliceInfo { + next_cursor?: string; +} diff --git a/src/types/raw/user/Lists.ts b/src/types/raw/user/Lists.ts new file mode 100644 index 00000000..6a3bd702 --- /dev/null +++ b/src/types/raw/user/Lists.ts @@ -0,0 +1,378 @@ +/* eslint-disable */ + +/** + * The raw data received when fetching the lists of the given user. + * + * @public + */ +export interface IUserListsResponse { + data: Data; +} + +export interface Data { + viewer: Viewer; +} + +export interface Viewer { + list_management_timeline: ListManagementTimeline; +} + +export interface ListManagementTimeline { + timeline: Timeline; +} + +export interface Timeline { + instructions: Instruction[]; + metadata: Metadata; +} + +export interface Instruction { + type: string; + direction?: string; + entries?: Entry[]; +} + +export interface Entry { + entryId: string; + sortIndex: string; + content: Content; +} + +export interface Content { + entryType: string; + __typename: string; + items?: Item[]; + displayType?: string; + header?: Header; + footer?: Footer; + clientEventInfo?: ClientEventInfo2; + value?: string; + cursorType?: string; +} + +export interface Item { + entryId: string; + item: Item2; +} + +export interface Item2 { + itemContent: ItemContent; + clientEventInfo: ClientEventInfo; +} + +export interface ItemContent { + itemType: string; + __typename: string; + displayType: string; + list: List; +} + +export interface List { + created_at: number; + default_banner_media: DefaultBannerMedia; + default_banner_media_results: DefaultBannerMediaResults; + description: string; + facepile_urls: string[]; + followers_context?: string; + following: boolean; + id: string; + id_str: string; + is_member: boolean; + member_count: number; + members_context?: string; + mode: string; + muting: boolean; + name: string; + pinning: boolean; + subscriber_count: number; + user_results: UserResults; + custom_banner_media?: CustomBannerMedia; + custom_banner_media_results?: CustomBannerMediaResults; +} + +export interface DefaultBannerMedia { + media_info: MediaInfo; +} + +export interface MediaInfo { + original_img_url: string; + original_img_width: number; + original_img_height: number; + salient_rect: SalientRect; +} + +export interface SalientRect { + left: number; + top: number; + width: number; + height: number; +} + +export interface DefaultBannerMediaResults { + result: Result; +} + +export interface Result { + id: string; + media_key: string; + media_id: string; + media_info: MediaInfo2; + __typename: string; +} + +export interface MediaInfo2 { + __typename: string; + original_img_height: number; + original_img_width: number; + original_img_url: string; + salient_rect: SalientRect2; +} + +export interface SalientRect2 { + height: number; + left: number; + top: number; + width: number; +} + +export interface UserResults { + result: Result2; +} + +export interface Result2 { + __typename: string; + id: string; + rest_id: string; + affiliates_highlighted_label: AffiliatesHighlightedLabel; + avatar: Avatar; + core: Core; + dm_permissions: DmPermissions; + has_graduated_access: boolean; + is_blue_verified: boolean; + legacy: Legacy; + location: Location; + media_permissions: MediaPermissions; + parody_commentary_fan_label: string; + profile_image_shape: string; + professional?: Professional; + privacy: Privacy; + relationship_perspectives: RelationshipPerspectives; + tipjar_settings: TipjarSettings; + verification: Verification; + verified_phone_status: boolean; +} + +export interface AffiliatesHighlightedLabel {} + +export interface Avatar { + image_url: string; +} + +export interface Core { + created_at: string; + name: string; + screen_name: string; +} + +export interface DmPermissions { + can_dm: boolean; +} + +export interface Legacy { + default_profile: boolean; + default_profile_image: boolean; + description: string; + entities: Entities; + fast_followers_count: number; + favourites_count: number; + followers_count: number; + friends_count: number; + has_custom_timelines: boolean; + is_translator: boolean; + listed_count: number; + media_count: number; + normal_followers_count: number; + pinned_tweet_ids_str: string[]; + possibly_sensitive: boolean; + profile_banner_url?: string; + profile_interstitial_type: string; + statuses_count: number; + translator_type: string; + url?: string; + want_retweets: boolean; + withheld_in_countries: any[]; + needs_phone_verification?: boolean; +} + +export interface Entities { + description: Description; + url?: Url; +} + +export interface Description { + urls: any[]; +} + +export interface Url { + urls: Url2[]; +} + +export interface Url2 { + display_url: string; + expanded_url: string; + url: string; + indices: number[]; +} + +export interface Location { + location: string; +} + +export interface MediaPermissions { + can_media_tag: boolean; +} + +export interface Professional { + rest_id: string; + professional_type: string; + category: Category[]; +} + +export interface Category { + id: number; + name: string; + icon_name: string; +} + +export interface Privacy { + protected: boolean; +} + +export interface RelationshipPerspectives { + following: boolean; +} + +export interface TipjarSettings { + is_enabled?: boolean; + bitcoin_handle?: string; + ethereum_handle?: string; +} + +export interface Verification { + verified: boolean; +} + +export interface CustomBannerMedia { + media_info: MediaInfo3; +} + +export interface MediaInfo3 { + original_img_url: string; + original_img_width: number; + original_img_height: number; + salient_rect: SalientRect3; +} + +export interface SalientRect3 { + left: number; + top: number; + width: number; + height: number; +} + +export interface CustomBannerMediaResults { + result: Result3; +} + +export interface Result3 { + id: string; + media_key: string; + media_id: string; + media_info: MediaInfo4; + __typename: string; +} + +export interface MediaInfo4 { + __typename: string; + original_img_height: number; + original_img_width: number; + original_img_url: string; + salient_rect: SalientRect4; + color_info: ColorInfo; +} + +export interface SalientRect4 { + height: number; + left: number; + top: number; + width: number; +} + +export interface ColorInfo { + palette: Palette[]; +} + +export interface Palette { + percentage: number; + rgb: Rgb; +} + +export interface Rgb { + blue: number; + green: number; + red: number; +} + +export interface ClientEventInfo { + component: string; + element: string; + details: Details; +} + +export interface Details { + timelinesDetails: TimelinesDetails; +} + +export interface TimelinesDetails { + injectionType: string; + controllerData?: string; +} + +export interface Header { + displayType: string; + text: string; + sticky: boolean; +} + +export interface Footer { + displayType: string; + text: string; + landingUrl: LandingUrl; +} + +export interface LandingUrl { + url: string; + urlType: string; +} + +export interface ClientEventInfo2 { + component: string; + details: Details2; +} + +export interface Details2 { + timelinesDetails: TimelinesDetails2; +} + +export interface TimelinesDetails2 { + injectionType: string; + controllerData?: string; +} + +export interface Metadata { + scribeConfig: ScribeConfig; +} + +export interface ScribeConfig { + page: string; +} diff --git a/src/types/raw/user/Notifications.ts b/src/types/raw/user/Notifications.ts index a2440a48..ef82e698 100644 --- a/src/types/raw/user/Notifications.ts +++ b/src/types/raw/user/Notifications.ts @@ -6,141 +6,481 @@ * @public */ export interface IUserNotificationsResponse { - globalObjects: GlobalObjects; - timeline: Timeline; + data: Data; +} + +interface Data { + viewer_v2: ViewerV2; +} + +interface ViewerV2 { + user_results: UserResults; } -interface GlobalObjects { - notifications: Notifications; +interface UserResults { + result: Result; } -interface Notifications { - [key: string]: Notification; +interface Result { + __typename: string; + rest_id: string; + notification_timeline: NotificationTimeline; } -interface Notification { +interface NotificationTimeline { id: string; - timestampMs: string; - icon: Icon; - message: Message; - template: Template; + timeline: Timeline; +} + +interface Timeline { + instructions: Instruction[]; +} + +interface Instruction { + type: string; + entries?: Entry[]; + sort_index?: string; } -interface Icon { +interface Entry { + entryId: string; + sortIndex: string; + content: Content; +} + +interface Content { + entryType: string; + __typename: string; + value?: string; + cursorType?: string; + itemContent?: ItemContent; + clientEventInfo?: ClientEventInfo; +} + +interface ItemContent { + itemType: string; + __typename: string; id: string; + notification_icon: string; + rich_message: RichMessage; + notification_url: NotificationUrl; + template: Template; + timestamp_ms: string; } -interface Message { +interface RichMessage { + rtl: boolean; text: string; entities: Entity[]; - rtl: boolean; } interface Entity { fromIndex: number; toIndex: number; - format: string; + ref: Ref; } -interface Template { - aggregateUserActionsV1: AggregateUserActionsV1; +interface Ref { + type: string; + user_results: UserResults2; +} + +interface UserResults2 { + result: Result2; +} + +interface Result2 { + __typename: string; + id: string; + rest_id: string; + affiliates_highlighted_label: AffiliatesHighlightedLabel; + avatar: Avatar; + core: Core; + dm_permissions: DmPermissions; + follow_request_sent: boolean; + has_graduated_access: boolean; + is_blue_verified: boolean; + legacy: Legacy; + location: Location; + media_permissions: MediaPermissions; + parody_commentary_fan_label: string; + profile_image_shape: string; + profile_bio: ProfileBio; + privacy: Privacy; + relationship_perspectives: RelationshipPerspectives; + tipjar_settings: TipjarSettings; + verification: Verification; + verified_phone_status: boolean; +} + +interface AffiliatesHighlightedLabel {} + +interface Avatar { + image_url: string; } -interface AggregateUserActionsV1 { - targetObjects: TargetObject[]; - fromUsers: FromUser[]; - additionalContext: AdditionalContext; +interface Core { + created_at: string; + name: string; + screen_name: string; +} + +interface DmPermissions { + can_dm: boolean; +} + +interface Legacy { + default_profile: boolean; + default_profile_image: boolean; + description: string; + entities: Entities; + fast_followers_count: number; + favourites_count: number; + followers_count: number; + friends_count: number; + has_custom_timelines: boolean; + is_translator: boolean; + listed_count: number; + media_count: number; + normal_followers_count: number; + notifications: boolean; + pinned_tweet_ids_str: any[]; + possibly_sensitive: boolean; + profile_interstitial_type: string; + statuses_count: number; + translator_type: string; + want_retweets: boolean; + withheld_in_countries: any[]; +} + +interface Entities { + description: Description; +} + +interface Description { + urls: any[]; +} + +interface Location { + location: string; +} + +interface MediaPermissions { + can_media_tag: boolean; +} + +interface ProfileBio { + description: string; +} + +interface Privacy { + protected: boolean; +} + +interface RelationshipPerspectives { + followed_by: boolean; + following: boolean; +} + +interface TipjarSettings {} + +interface Verification { + verified: boolean; +} + +interface NotificationUrl { + url: string; + urlType: string; + urtEndpointOptions?: UrtEndpointOptions; +} + +interface UrtEndpointOptions { + cacheId: string; + title: string; +} + +interface Template { + __typename: string; + target_objects: TargetObject[]; + from_users: FromUser[]; } interface TargetObject { - tweet: Tweet; + __typename: string; + tweet_results: TweetResults; } -interface Tweet { - id: string; +interface TweetResults { + result: Result3; } -interface FromUser { - user: User; +interface Result3 { + __typename: string; + rest_id: string; + core: Core2; + unmention_data: UnmentionData; + edit_control: EditControl; + is_translatable: boolean; + views: Views; + source: string; + grok_analysis_button: boolean; + legacy: Legacy3; +} + +interface Core2 { + user_results: UserResults3; +} + +interface UserResults3 { + result: Result4; } -interface User { +interface Result4 { + __typename: string; id: string; + rest_id: string; + affiliates_highlighted_label: AffiliatesHighlightedLabel2; + avatar: Avatar2; + core: Core3; + dm_permissions: DmPermissions2; + follow_request_sent: boolean; + has_graduated_access: boolean; + is_blue_verified: boolean; + legacy: Legacy2; + location: Location2; + media_permissions: MediaPermissions2; + parody_commentary_fan_label: string; + profile_image_shape: string; + profile_bio: ProfileBio2; + privacy: Privacy2; + relationship_perspectives: RelationshipPerspectives2; + tipjar_settings: TipjarSettings2; + verification: Verification2; + verified_phone_status: boolean; } -interface AdditionalContext { - contextText: ContextText; +interface AffiliatesHighlightedLabel2 {} + +interface Avatar2 { + image_url: string; } -interface ContextText { - text: string; - entities: any[]; +interface Core3 { + created_at: string; + name: string; + screen_name: string; } -interface Timeline { - id: string; - instructions: Instruction[]; +interface DmPermissions2 { + can_dm: boolean; } -interface Instruction { - clearCache?: ClearCache; - addEntries?: AddEntries; - clearEntriesUnreadState?: ClearEntriesUnreadState; - markEntriesUnreadGreaterThanSortIndex?: MarkEntriesUnreadGreaterThanSortIndex; +interface Legacy2 { + default_profile: boolean; + default_profile_image: boolean; + description: string; + entities: Entities2; + fast_followers_count: number; + favourites_count: number; + followers_count: number; + friends_count: number; + has_custom_timelines: boolean; + is_translator: boolean; + listed_count: number; + media_count: number; + needs_phone_verification: boolean; + normal_followers_count: number; + pinned_tweet_ids_str: any[]; + possibly_sensitive: boolean; + profile_interstitial_type: string; + statuses_count: number; + translator_type: string; + want_retweets: boolean; + withheld_in_countries: any[]; } -interface ClearCache {} +interface Entities2 { + description: Description2; +} -interface AddEntries { - entries: Entry[]; +interface Description2 { + urls: any[]; } -interface Entry { - entryId: string; - sortIndex: string; - content: Content; +interface Location2 { + location: string; } -interface Content { - operation?: Operation; - item?: Item; +interface MediaPermissions2 { + can_media_tag: boolean; } -interface Operation { - cursor: Cursor; +interface ProfileBio2 { + description: string; } -interface Cursor { - value: string; - cursorType: string; +interface Privacy2 { + protected: boolean; } -interface Item { - content: Content2; - clientEventInfo: ClientEventInfo; - feedbackInfo?: FeedbackInfo; +interface RelationshipPerspectives2 { + following: boolean; } -interface Content2 { - notification: Notification2; +interface TipjarSettings2 {} + +interface Verification2 { + verified: boolean; } -interface Notification2 { +interface UnmentionData {} + +interface EditControl { + edit_tweet_ids: string[]; + editable_until_msecs: string; + is_edit_eligible: boolean; + edits_remaining: string; +} + +interface Views { + count: string; + state: string; +} + +interface Legacy3 { + bookmark_count: number; + bookmarked: boolean; + created_at: string; + conversation_id_str: string; + display_text_range: number[]; + entities: Entities3; + favorite_count: number; + favorited: boolean; + full_text: string; + is_quote_status: boolean; + lang: string; + quote_count: number; + reply_count: number; + retweet_count: number; + retweeted: boolean; + user_id_str: string; + id_str: string; +} + +interface Entities3 { + hashtags: any[]; + symbols: any[]; + timestamps: any[]; + urls: any[]; + user_mentions: any[]; +} + +interface FromUser { + __typename: string; + user_results: UserResults4; +} + +interface UserResults4 { + result: Result5; +} + +interface Result5 { + __typename: string; id: string; - url: Url; - fromUsers: string[]; - targetTweets: string[]; + rest_id: string; + affiliates_highlighted_label: AffiliatesHighlightedLabel3; + avatar: Avatar3; + core: Core4; + dm_permissions: DmPermissions3; + follow_request_sent: boolean; + has_graduated_access: boolean; + is_blue_verified: boolean; + legacy: Legacy4; + location: Location3; + media_permissions: MediaPermissions3; + parody_commentary_fan_label: string; + profile_image_shape: string; + profile_bio: ProfileBio3; + privacy: Privacy3; + relationship_perspectives: RelationshipPerspectives3; + tipjar_settings: TipjarSettings3; + verification: Verification3; + verified_phone_status: boolean; } -interface Url { - urlType: string; - url: string; - urtEndpointOptions?: UrtEndpointOptions; +interface AffiliatesHighlightedLabel3 {} + +interface Avatar3 { + image_url: string; } -interface UrtEndpointOptions { - title: string; - cacheId: string; +interface Core4 { + created_at: string; + name: string; + screen_name: string; +} + +interface DmPermissions3 { + can_dm: boolean; +} + +interface Legacy4 { + default_profile: boolean; + default_profile_image: boolean; + description: string; + entities: Entities4; + fast_followers_count: number; + favourites_count: number; + followers_count: number; + friends_count: number; + has_custom_timelines: boolean; + is_translator: boolean; + listed_count: number; + media_count: number; + normal_followers_count: number; + notifications: boolean; + pinned_tweet_ids_str: any[]; + possibly_sensitive: boolean; + profile_interstitial_type: string; + statuses_count: number; + translator_type: string; + want_retweets: boolean; + withheld_in_countries: any[]; +} + +interface Entities4 { + description: Description3; +} + +interface Description3 { + urls: any[]; +} + +interface Location3 { + location: string; +} + +interface MediaPermissions3 { + can_media_tag: boolean; +} + +interface ProfileBio3 { + description: string; +} + +interface Privacy3 { + protected: boolean; +} + +interface RelationshipPerspectives3 { + followed_by: boolean; + following: boolean; +} + +interface TipjarSettings3 {} + +interface Verification3 { + verified: boolean; } interface ClientEventInfo { @@ -157,19 +497,3 @@ interface NotificationDetails { impressionId: string; metadata: string; } - -interface FeedbackInfo { - feedbackKeys: string[]; - feedbackMetadata: string; - clientEventInfo: ClientEventInfo2; -} - -interface ClientEventInfo2 { - element: string; -} - -interface ClearEntriesUnreadState {} - -interface MarkEntriesUnreadGreaterThanSortIndex { - sortIndex: string; -} diff --git a/src/types/raw/user/ProfileUpdate.ts b/src/types/raw/user/ProfileUpdate.ts new file mode 100644 index 00000000..11f20d7c --- /dev/null +++ b/src/types/raw/user/ProfileUpdate.ts @@ -0,0 +1,80 @@ +/* eslint-disable */ + +/** + * The raw data received when updating user profile. + * + * @public + */ +export interface IUserProfileUpdateResponse { + id: number; + id_str: string; + name: string; + screen_name: string; + location: string; + description: string; + url: string | null; + entities: Entities; + protected: boolean; + followers_count: number; + fast_followers_count: number; + normal_followers_count: number; + friends_count: number; + listed_count: number; + created_at: string; + favourites_count: number; + utc_offset: any; + time_zone: any; + geo_enabled: boolean; + verified: boolean; + statuses_count: number; + media_count: number; + lang: any; + contributors_enabled: boolean; + is_translator: boolean; + is_translation_enabled: boolean; + profile_background_color: string; + profile_background_image_url: string; + profile_background_image_url_https: string; + profile_background_tile: boolean; + profile_image_url: string; + profile_image_url_https: string; + profile_banner_url: string; + profile_link_color: string; + profile_sidebar_border_color: string; + profile_sidebar_fill_color: string; + profile_text_color: string; + profile_use_background_image: boolean; + has_extended_profile: boolean; + default_profile: boolean; + default_profile_image: boolean; + pinned_tweet_ids: number[]; + pinned_tweet_ids_str: string[]; + has_custom_timelines: boolean; + can_media_tag: boolean; + advertiser_account_type: string; + advertiser_account_service_levels: any[]; + business_profile_state: string; + translator_type: string; + withheld_in_countries: any[]; + require_some_consent: boolean; +} + +interface Entities { + description: Description; + url?: Url; +} + +interface Description { + urls: any[]; +} + +interface Url { + urls: UrlDetail[]; +} + +interface UrlDetail { + url: string; + expanded_url: string; + display_url: string; + indices: number[]; +} diff --git a/src/types/raw/user/Search.ts b/src/types/raw/user/Search.ts new file mode 100644 index 00000000..8efecd1c --- /dev/null +++ b/src/types/raw/user/Search.ts @@ -0,0 +1,230 @@ +/* eslint-disable */ + +/** + * The raw data received when search for users. + * + * @public + */ +export interface IUserSearchResponse { + data: Data; +} + +interface Data { + search_by_raw_query: SearchByRawQuery; +} + +interface SearchByRawQuery { + search_timeline: SearchTimeline; +} + +interface SearchTimeline { + timeline: Timeline; +} + +interface Timeline { + instructions: Instruction[]; +} + +interface Instruction { + type: string; + entries?: Entry[]; +} + +interface Entry { + entryId: string; + sortIndex: string; + content: Content; +} + +interface Content { + entryType: string; + __typename: string; + itemContent?: ItemContent; + clientEventInfo?: ClientEventInfo; + value?: string; + cursorType?: string; +} + +interface ItemContent { + itemType: string; + __typename: string; + user_results: UserResults; + userDisplayType: string; +} + +interface UserResults { + result: Result; +} + +interface Result { + __typename: string; + id: string; + rest_id: string; + affiliates_highlighted_label: AffiliatesHighlightedLabel; + avatar: Avatar; + core: Core; + dm_permissions: DmPermissions; + follow_request_sent: boolean; + has_graduated_access: boolean; + is_blue_verified: boolean; + legacy: Legacy; + location: Location; + media_permissions: MediaPermissions; + parody_commentary_fan_label: string; + profile_image_shape: string; + professional?: Professional; + profile_bio: ProfileBio; + privacy: Privacy; + relationship_perspectives: RelationshipPerspectives; + tipjar_settings: TipjarSettings; + super_follow_eligible?: boolean; + verification: Verification; + verified_phone_status: boolean; + profile_description_language?: string; +} + +interface AffiliatesHighlightedLabel { + label?: Label; +} + +interface Label { + url: Url; + badge: Badge; + description: string; + userLabelType: string; + userLabelDisplayType: string; +} + +interface Url { + url: string; + urlType: string; +} + +interface Badge { + url: string; +} + +interface Avatar { + image_url: string; +} + +interface Core { + created_at: string; + name: string; + screen_name: string; +} + +interface DmPermissions { + can_dm: boolean; +} + +interface Legacy { + default_profile: boolean; + default_profile_image: boolean; + description: string; + entities: Entities; + fast_followers_count: number; + favourites_count: number; + followers_count: number; + friends_count: number; + has_custom_timelines: boolean; + is_translator: boolean; + listed_count: number; + media_count: number; + normal_followers_count: number; + pinned_tweet_ids_str: string[]; + possibly_sensitive: boolean; + profile_banner_url?: string; + profile_interstitial_type: string; + statuses_count: number; + translator_type: string; + want_retweets: boolean; + withheld_in_countries: any[]; + url?: string; +} + +interface Entities { + description: Description; + url?: Url3; +} + +interface Description { + urls: Url2[]; +} + +interface Url2 { + display_url: string; + expanded_url: string; + url: string; + indices: number[]; +} + +interface Url3 { + urls: Url4[]; +} + +interface Url4 { + display_url: string; + expanded_url: string; + url: string; + indices: number[]; +} + +interface Location { + location: string; +} + +interface MediaPermissions { + can_media_tag: boolean; +} + +interface Professional { + rest_id: string; + professional_type: string; + category: Category[]; +} + +interface Category { + id: number; + name: string; + icon_name: string; +} + +interface ProfileBio { + description: string; +} + +interface Privacy { + protected: boolean; +} + +interface RelationshipPerspectives { + following: boolean; +} + +interface TipjarSettings { + is_enabled?: boolean; + bitcoin_handle?: string; + ethereum_handle?: string; + cash_app_handle?: string; + venmo_handle?: string; +} + +interface Verification { + verified: boolean; + verified_type?: string; +} + +interface ClientEventInfo { + component: string; + element: string; + details: Details; +} + +interface Details { + timelinesDetails: TimelinesDetails; +} + +interface TimelinesDetails { + controllerData: string; +} diff --git a/tsconfig.json b/tsconfig.json index 21ba8759..647354f8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -93,5 +93,5 @@ // "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "include": ["src/**/*"], - "exclude": ["node_modules", "**/*.spec.ts"] + "exclude": ["node_modules", "**/*.spec.ts", "playground/*"] }