├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .prettierrc ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── package.json ├── src ├── .eslintrc ├── TwitterClient.ts ├── index.ts ├── types │ ├── searchResults.ts │ ├── userFromIdInterface.ts │ └── userInterface.ts └── utils │ ├── clientSearch.ts │ ├── getApiClientInstance.ts │ ├── getBearerToken.ts │ ├── getGuestToken.ts │ ├── getHashflags.ts │ ├── getInitialOptions.ts │ ├── getTweetsFromUser.ts │ ├── getUser.ts │ └── parseSearch.ts ├── test └── client.test.ts ├── tsconfig.json ├── tsconfig.test.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "standard", 5 | "plugin:prettier/recommended", 6 | "prettier/standard", 7 | "plugin:@typescript-eslint/eslint-recommended" 8 | ], 9 | "env": { 10 | "node": true 11 | }, 12 | "parserOptions": { 13 | "ecmaVersion": 2020, 14 | "ecmaFeatures": { 15 | "legacyDecorators": true 16 | } 17 | }, 18 | "rules": { 19 | "space-before-function-paren": 0, 20 | "import/export": 0, 21 | "no-unused-vars": "off", 22 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }], 23 | // "indent": ["error", 2, { "SwitchCase": 1}], 24 | "quotes": ["error", "single"], 25 | "no-multi-spaces": ["error"] 26 | }, 27 | "plugins": [ 28 | "@typescript-eslint" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # We'll let Git's auto-detection algorithm infer if a file is text. If it is, 2 | # enforce LF line endings regardless of OS or git configurations. 3 | * text=auto eol=lf 4 | 5 | # Isolate binary files in case the auto-detection algorithm fails and 6 | # marks them as text files (which could brick them). 7 | *.{png,jpg,jpeg,gif,webp,woff,woff2} binary -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | 2 | # https://docs.github.com/en/actions/guides/publishing-nodejs-packages 3 | 4 | name: twitter-api-scraperpackage 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | publish-npm: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 12 18 | registry-url: https://registry.npmjs.org/ 19 | - run: yarn install --frozen-lockfile 20 | - run: yarn run build 21 | - run: yarn test 22 | - run: npm publish 23 | env: 24 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules/ 3 | 4 | dist/ 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "tabWidth": 2, 5 | "bracketSpacing": true, 6 | "arrowParens": "always", 7 | "trailingComma": "none" 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | - 10 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true 4 | }, 5 | "eslint.validate": [ 6 | "javascript" 7 | ], 8 | "files.eol": "\n", 9 | "editor.insertSpaces": true, 10 | "editor.tabSize": 2 11 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 matiasngf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # twitter-api-scraper 2 | 3 | > Get typed data from the public twitter api. No keys required. 4 | 5 | [![NPM](https://img.shields.io/npm/v/twitter-api-scraper.svg)](https://www.npmjs.com/package/twitter-api-scraper) 6 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 7 | [![License](https://img.shields.io/github/license/matiasngf/twitter-api-scraper?style=flat-square)](https://github.com/matiasngf/twitter-api-scraper/blob/main/LICENSE) 8 | [![Issues](https://img.shields.io/github/issues/matiasngf/twitter-api-scraper)](https://github.com/matiasngf/twitter-api-scraper/issues) 9 | 10 | - [x] Search tweets 11 | - [x] Get users by username 12 | - [x] Get users by id 13 | - [x] Get hashflags 14 | - [ ] Get tweets from user 15 | - [ ] Get replies from tweet 16 | - [ ] Get trending topics 17 | 18 | ## Install 19 | 20 | ```bash 21 | npm install --save twitter-api-scraper 22 | ``` 23 | 24 | ## Usage 25 | 26 | ### Start client 27 | 28 | First, connect() the client. 29 | 30 | ```ts 31 | import TwitterClient from 'twitter-api-scraper' 32 | const client = new TwitterClient() 33 | await client.connect() 34 | ``` 35 | 36 | ### Search 37 | 38 | ```ts 39 | const maxTweets = 3000 40 | const result = await client.search( 41 | { 42 | terms: 'hello world!' 43 | }, 44 | maxTweets 45 | ) 46 | 47 | const { tweets, users, nextToken } = result 48 | 49 | ``` 50 | 51 | ### Typescript 52 | 53 | ```ts 54 | import { SearchQuery, ParsedSearchResult } from 'twitter-api-scraper' 55 | 56 | const query: SearchQuery = { 57 | terms: '#typescript', 58 | dateFrom: '2021-02-15', 59 | dateTo: '2021-02-15' 60 | } 61 | 62 | const result: ParsedSearchResult = await client.search(query) 63 | ``` 64 | 65 | ### Search parameters 66 | 67 | ```ts 68 | const query: SearchQuery = { 69 | terms: '#typescript', 70 | dateFrom: '2021-02-15', 71 | dateTo: '2021-02-17', 72 | minReplies: 10, 73 | minRetweets: 10, 74 | minFaves: 10, 75 | lang: 'es' 76 | } 77 | client.search(query) 78 | ``` 79 | 80 | ### Search multiple pages 81 | 82 | ```ts 83 | const result = await client.search(query, 100) 84 | const { nextToken } = result 85 | 86 | const secondPageResult = await client.search(query, 100, nextToken) 87 | ``` 88 | 89 | ### Original api response 90 | 91 | ```ts 92 | const originalApiResult = await client.searchRaw( 93 | { 94 | terms: 'hello world!' 95 | } 96 | ) 97 | ``` 98 | 99 | ## Users 100 | 101 | ### Get user 102 | 103 | ```ts 104 | const user: UserInterface = await client.getUser('jack') 105 | ``` 106 | 107 | ### Get user by id 108 | 109 | ```ts 110 | const user: UserFromId = await client.getUserById('12') 111 | ``` 112 | 113 | ## Hashflags 114 | 115 | ```ts 116 | const hashflags: Hashflag[] = await client.getHashflags('2021-09-23') 117 | ``` 118 | 119 | 120 | ## License 121 | 122 | MIT © [matiasngf](https://github.com/matiasngf) 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitter-api-scraper", 3 | "version": "0.0.5", 4 | "description": "Scrape the twitter api, no keys required.", 5 | "author": "matiasngf", 6 | "license": "MIT", 7 | "repository": "matiasngf/twitter-api-scraper", 8 | "main": "dist/index.js", 9 | "module": "dist/index.modern.js", 10 | "source": "src/index.ts", 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "scripts": { 15 | "build": "microbundle-crl --no-compress --format modern,cjs", 16 | "start": "microbundle-crl watch --no-compress --format modern,cjs", 17 | "prepare": "run-s build", 18 | "test": "jest", 19 | "test:build": "run-s build", 20 | "test:lint": "eslint .", 21 | "predeploy": "cd example && yarn install && yarn run build", 22 | "deploy": "gh-pages -d example/build" 23 | }, 24 | "devDependencies": { 25 | "@types/jest": "^25.1.4", 26 | "@types/node": "^12.12.38", 27 | "@typescript-eslint/eslint-plugin": "^2.26.0", 28 | "@typescript-eslint/parser": "^2.26.0", 29 | "babel-eslint": "^10.0.3", 30 | "cross-env": "^7.0.2", 31 | "eslint": "^6.8.0", 32 | "eslint-config-prettier": "^6.7.0", 33 | "eslint-config-standard": "^14.1.0", 34 | "eslint-plugin-import": "^2.18.2", 35 | "eslint-plugin-node": "^11.0.0", 36 | "eslint-plugin-prettier": "^3.1.1", 37 | "eslint-plugin-promise": "^4.2.1", 38 | "eslint-plugin-standard": "^4.0.1", 39 | "gh-pages": "^2.2.0", 40 | "jest": "^27.2.0", 41 | "microbundle-crl": "^0.13.10", 42 | "npm-run-all": "^4.1.5", 43 | "prettier": "^2.0.4", 44 | "ts-jest": "^27.0.5", 45 | "ts-node": "^10.2.1", 46 | "typescript": "^3.7.5" 47 | }, 48 | "files": [ 49 | "dist" 50 | ], 51 | "dependencies": { 52 | "axios": "^0.21.4" 53 | }, 54 | "jest": { 55 | "moduleFileExtensions": [ 56 | "ts", 57 | "js" 58 | ], 59 | "transform": { 60 | "^.+\\.ts$": "ts-jest" 61 | }, 62 | "globals": { 63 | "ts-jest": { 64 | "tsconfig": "tsconfig.json" 65 | } 66 | }, 67 | "testMatch": [ 68 | "**/test/**/*.test.ts" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/TwitterClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClientOptionsInterface, 3 | getInitialOptions 4 | } from './utils/getInitialOptions' 5 | import { AxiosInstance } from 'axios' 6 | import { getApiClientInstance } from './utils/getApiClientInstance' 7 | import { getGuestToken } from './utils/getGuestToken' 8 | import { getBearerToken } from './utils/getBearerToken' 9 | import { clientSearch, SearchQuery } from './utils/clientSearch' 10 | import { ParsedSearchResult, parseSearch } from './utils/parseSearch' 11 | import { SearchResults } from './types/searchResults' 12 | import { getHashflags, Hashflag } from './utils/getHashflags' 13 | import { getUser, getUserById } from './utils/getUser' 14 | import { UserInterface } from './types/userInterface' 15 | import { UserFromId } from './types/userFromIdInterface' 16 | 17 | export default class TwitterClient { 18 | // Options 19 | expiredToken: boolean 20 | guestToken: string 21 | guestTokenUpdatedAt: number 22 | bearerToken: string 23 | bearerTokenUpdatedAt: number 24 | maxRetries: number 25 | log: boolean 26 | userAgents: string[] 27 | language: string 28 | 29 | // Internal 30 | userAgent: string = '' 31 | 32 | // Clients 33 | apiClient: AxiosInstance 34 | 35 | constructor(options: ClientOptionsInterface = {}) { 36 | // options 37 | this.setOptions(options) 38 | 39 | // apis 40 | this.apiClient = getApiClientInstance() 41 | this.getNewUserAgent() 42 | } 43 | 44 | setOptions = (options: ClientOptionsInterface) => { 45 | const finalOptions = getInitialOptions(options) 46 | this.guestToken = finalOptions.guestToken 47 | this.bearerToken = finalOptions.bearerToken 48 | this.maxRetries = finalOptions.maxRetries 49 | this.log = finalOptions.log 50 | this.userAgents = finalOptions.userAgents 51 | } 52 | 53 | getNewUserAgent = (): string => { 54 | const n = this.userAgents.length 55 | const sel: string | undefined = 56 | this.userAgents[Math.round(Math.random() * n - 1)] 57 | 58 | const selected = 59 | sel || 60 | 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0' 61 | 62 | this.userAgent = selected 63 | this.apiClient.defaults.headers['User-Agent'] = selected 64 | this.say(`Setting new User-Agent: ${selected}`) 65 | return selected 66 | } 67 | 68 | getBearerToken = async (): Promise => { 69 | const token = await getBearerToken(this.userAgent) 70 | if (token) { 71 | this.setBearerToken(token) 72 | return token 73 | } else { 74 | throw new Error('Token not found.') 75 | } 76 | } 77 | 78 | getGuestToken = async (): Promise => { 79 | const token = await getGuestToken() 80 | if (token) { 81 | this.setGuestToken(token) 82 | return token 83 | } else { 84 | throw new Error('Token not found.') 85 | } 86 | } 87 | 88 | setGuestToken = (token: string) => { 89 | this.guestToken = token 90 | this.apiClient.defaults.headers['x-guest-token'] = this.guestToken 91 | this.guestTokenUpdatedAt = Math.floor(Date.now() / 1000) 92 | this.say('New guest token setted.') 93 | } 94 | 95 | setBearerToken = (token: string) => { 96 | this.bearerToken = token 97 | this.bearerTokenUpdatedAt = Math.floor(Date.now() / 1000) 98 | this.apiClient.defaults.headers.authorization = `Bearer ${this.bearerToken}` 99 | this.say('New bearer token setted.') 100 | } 101 | 102 | connect = async () => { 103 | this.say('Connecting...') 104 | await this.getBearerToken() 105 | await this.getGuestToken() 106 | this.say('Connected to twitter') 107 | } 108 | 109 | private say = (message: any) => { 110 | if (this.log) { 111 | console.log(message) 112 | } 113 | } 114 | 115 | // Search 116 | search = async ( 117 | query: SearchQuery, 118 | maxTweets: number = 100, 119 | pageToken?: string 120 | ): Promise => { 121 | this.say('Searching...') 122 | const data = await clientSearch(this.apiClient, query, maxTweets, pageToken) 123 | const result = parseSearch(data) 124 | this.say('Searched tweets') 125 | return result 126 | } 127 | 128 | searchRaw = async ( 129 | query: SearchQuery, 130 | maxTweets: number = 100, 131 | pageToken?: string 132 | ): Promise => { 133 | this.say('Searching...') 134 | const data = await clientSearch(this.apiClient, query, maxTweets, pageToken) 135 | this.say('Searched tweets') 136 | return data 137 | } 138 | 139 | // Get User 140 | getUser = async (username: string): Promise => { 141 | if (typeof username !== 'string') { 142 | throw new Error('Username must be string.') 143 | } 144 | const name = username.replace('@', '') 145 | this.say(`Getting user: ${name}`) 146 | const data = await getUser(this.apiClient, name) 147 | this.say('User OK') 148 | return data 149 | } 150 | 151 | getUserById = async (userId: string | number): Promise => { 152 | this.say(`Getting user by id: ${userId}`) 153 | const data = await getUserById(this.apiClient, userId) 154 | this.say('User OK') 155 | return data 156 | } 157 | 158 | // Get hashflags 159 | getHashflags = async (date: string): Promise => { 160 | this.say('Getting hashflags...') 161 | const data = await getHashflags(date) 162 | this.say('Hashflags OK') 163 | return data 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import TwitterClient from './TwitterClient' 2 | 3 | // utils 4 | export * from './utils/clientSearch' 5 | export * from './utils/getApiClientInstance' 6 | export * from './utils/getBearerToken' 7 | export * from './utils/getGuestToken' 8 | export * from './utils/getHashflags' 9 | export * from './utils/getInitialOptions' 10 | export * from './utils/getTweetsFromUser' 11 | export * from './utils/getUser' 12 | export * from './utils/parseSearch' 13 | 14 | // Types 15 | export * from './types/searchResults' 16 | export * from './types/userFromIdInterface' 17 | export * from './types/userInterface' 18 | 19 | export default TwitterClient 20 | -------------------------------------------------------------------------------- /src/types/searchResults.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable quotes */ 2 | /* eslint-disable camelcase */ 3 | export interface SearchResults { 4 | globalObjects: GlobalObjects 5 | timeline: Timeline 6 | } 7 | 8 | export interface GlobalObjects { 9 | tweets: { [key: string]: TweetValue } 10 | users: { [key: string]: UserValue } 11 | moments: Broadcasts 12 | cards: Broadcasts 13 | places: Broadcasts 14 | media: Broadcasts 15 | broadcasts: Broadcasts 16 | topics: Broadcasts 17 | lists: Broadcasts 18 | } 19 | 20 | export interface Broadcasts {} 21 | 22 | export interface TweetValue { 23 | created_at: string 24 | id: number 25 | id_str: string 26 | full_text: string 27 | truncated: boolean 28 | display_text_range: number[] 29 | entities: TweetEntities 30 | extended_entities?: ExtendedEntities 31 | source: string 32 | in_reply_to_status_id: number | null 33 | in_reply_to_status_id_str: null | string 34 | in_reply_to_user_id: number | null 35 | in_reply_to_user_id_str: null | string 36 | in_reply_to_screen_name: null | string 37 | user_id: number 38 | user_id_str: string 39 | geo: null 40 | coordinates: null 41 | place: null 42 | contributors: null 43 | is_quote_status: boolean 44 | retweet_count: number 45 | favorite_count: number 46 | reply_count: number 47 | quote_count: number 48 | conversation_id: number 49 | conversation_id_str: string 50 | favorited: boolean 51 | retweeted: boolean 52 | possibly_sensitive?: boolean 53 | possibly_sensitive_editable?: boolean 54 | lang: Lang 55 | supplemental_language: null 56 | self_thread?: SelfThread 57 | quoted_status_id?: number 58 | quoted_status_id_str?: string 59 | quoted_status_permalink?: QuotedStatusPermalink 60 | } 61 | 62 | export interface TweetEntities { 63 | hashtags: Hashtag[] 64 | symbols: any[] 65 | user_mentions: any[] 66 | urls: any[] 67 | media?: EntitiesMedia[] 68 | } 69 | 70 | export interface Hashtag { 71 | text: string 72 | indices: number[] 73 | } 74 | 75 | export interface EntitiesMedia { 76 | id: number 77 | id_str: string 78 | indices: number[] 79 | media_url: string 80 | media_url_https: string 81 | url: string 82 | display_url: string 83 | expanded_url: string 84 | type: Type 85 | original_info: OriginalInfo 86 | sizes: Sizes 87 | source_status_id?: number 88 | source_status_id_str?: string 89 | source_user_id?: number 90 | source_user_id_str?: string 91 | } 92 | 93 | export interface OriginalInfo { 94 | width: number 95 | height: number 96 | focus_rects?: FocusRect[] 97 | } 98 | 99 | export interface FocusRect { 100 | x: number 101 | y: number 102 | h: number 103 | w: number 104 | } 105 | 106 | export interface Sizes { 107 | thumb: Large 108 | large: Large 109 | small: Large 110 | medium: Large 111 | } 112 | 113 | export interface Large { 114 | w: number 115 | h: number 116 | resize: Resize 117 | } 118 | 119 | export enum Resize { 120 | Crop = 'crop', 121 | Fit = 'fit' 122 | } 123 | 124 | export enum Type { 125 | Photo = 'photo', 126 | Video = 'video' 127 | } 128 | 129 | export interface ExtendedEntities { 130 | media: ExtendedEntitiesMedia[] 131 | } 132 | 133 | export interface ExtendedEntitiesMedia { 134 | id: number 135 | id_str: string 136 | indices: number[] 137 | media_url: string 138 | media_url_https: string 139 | url: string 140 | display_url: string 141 | expanded_url: string 142 | type: Type 143 | original_info: OriginalInfo 144 | sizes: Sizes 145 | media_key: string 146 | ext_alt_text: null 147 | ext_media_availability: EXTMediaAvailability 148 | ext_media_color: MediaColor 149 | ext: MediaEXT 150 | source_status_id?: number 151 | source_status_id_str?: string 152 | source_user_id?: number 153 | source_user_id_str?: string 154 | video_info?: VideoInfo 155 | additional_media_info?: AdditionalMediaInfo 156 | } 157 | 158 | export interface AdditionalMediaInfo { 159 | monetizable: boolean 160 | source_user?: UserValue 161 | } 162 | 163 | export interface UserValue { 164 | id: number 165 | id_str: string 166 | name: string 167 | screen_name: string 168 | location: string 169 | description: string 170 | url: null | string 171 | entities: UserEntities 172 | protected: boolean 173 | followers_count: number 174 | fast_followers_count: number 175 | normal_followers_count: number 176 | friends_count: number 177 | listed_count: number 178 | created_at: string 179 | favourites_count: number 180 | utc_offset: null 181 | time_zone: null 182 | geo_enabled: boolean 183 | verified: boolean 184 | statuses_count: number 185 | media_count: number 186 | lang: null 187 | contributors_enabled: boolean 188 | is_translator: boolean 189 | is_translation_enabled: boolean 190 | profile_background_color: string 191 | profile_background_image_url: null | string 192 | profile_background_image_url_https: null | string 193 | profile_background_tile: boolean 194 | profile_image_url: string 195 | profile_image_url_https: string 196 | profile_banner_url?: string 197 | profile_image_extensions_media_color: MediaColor 198 | profile_image_extensions_alt_text: null 199 | profile_image_extensions_media_availability: null 200 | profile_image_extensions: ProfileExtensions 201 | profile_banner_extensions_alt_text?: null 202 | profile_banner_extensions_media_availability?: null 203 | profile_banner_extensions_media_color?: MediaColor 204 | profile_banner_extensions?: ProfileExtensions 205 | profile_link_color: string 206 | profile_sidebar_border_color: ProfileSidebarBorderColor 207 | profile_sidebar_fill_color: ProfileSidebarFillColor 208 | profile_text_color: string 209 | profile_use_background_image: boolean 210 | has_extended_profile: boolean 211 | default_profile: boolean 212 | default_profile_image: boolean 213 | pinned_tweet_ids: number[] 214 | pinned_tweet_ids_str: string[] 215 | has_custom_timelines: boolean 216 | can_dm: null 217 | following: null 218 | follow_request_sent: null 219 | notifications: null 220 | muting: null 221 | blocking: null 222 | blocked_by: null 223 | want_retweets: null 224 | advertiser_account_type: AdvertiserAccountType 225 | advertiser_account_service_levels: string[] 226 | profile_interstitial_type: string 227 | business_profile_state: AdvertiserAccountType 228 | translator_type: TranslatorType 229 | withheld_in_countries: any[] 230 | followed_by: null 231 | ext: UserEXT 232 | require_some_consent: boolean 233 | } 234 | 235 | export enum AdvertiserAccountType { 236 | None = 'none', 237 | PromotableUser = 'promotable_user' 238 | } 239 | 240 | export interface UserEntities { 241 | description: Description 242 | url?: Description 243 | } 244 | 245 | export interface Description { 246 | urls: URL[] 247 | } 248 | 249 | export interface URL { 250 | url: string 251 | expanded_url: string 252 | display_url: string 253 | indices: number[] 254 | } 255 | 256 | export interface UserEXT { 257 | highlightedLabel: HighlightedLabel 258 | } 259 | 260 | export interface HighlightedLabel { 261 | r: HighlightedLabelR 262 | ttl: number 263 | } 264 | 265 | export interface HighlightedLabelR { 266 | ok: Broadcasts 267 | } 268 | 269 | export interface ProfileExtensions { 270 | mediaStats: ProfileBannerExtensionsMediaStats 271 | } 272 | 273 | export interface ProfileBannerExtensionsMediaStats { 274 | r: MediaStatsRClass 275 | ttl: number 276 | } 277 | 278 | export interface MediaStatsRClass { 279 | missing: null 280 | } 281 | 282 | export interface MediaColor { 283 | palette: Palette[] 284 | } 285 | 286 | export interface Palette { 287 | rgb: RGB 288 | percentage: number 289 | } 290 | 291 | export interface RGB { 292 | red: number 293 | green: number 294 | blue: number 295 | } 296 | 297 | export enum ProfileSidebarBorderColor { 298 | C0Deed = 'C0DEED', 299 | Ffffff = 'FFFFFF', 300 | The000000 = '000000' 301 | } 302 | 303 | export enum ProfileSidebarFillColor { 304 | Ddeef6 = 'DDEEF6', 305 | F5Deb3 = 'F5DEB3', 306 | The000000 = '000000' 307 | } 308 | 309 | export enum TranslatorType { 310 | None = 'none', 311 | Regular = 'regular' 312 | } 313 | 314 | export interface MediaEXT { 315 | mediaStats: EXTMediaStats 316 | } 317 | 318 | export interface EXTMediaStats { 319 | r: RRClass | REnum 320 | ttl: number 321 | } 322 | 323 | export interface RRClass { 324 | ok: Ok 325 | } 326 | 327 | export interface Ok { 328 | viewCount: string 329 | } 330 | 331 | export enum REnum { 332 | Missing = 'Missing' 333 | } 334 | 335 | export interface EXTMediaAvailability { 336 | status: Status 337 | } 338 | 339 | export enum Status { 340 | Available = 'available' 341 | } 342 | 343 | export interface VideoInfo { 344 | aspect_ratio: number[] 345 | duration_millis: number 346 | variants: Variant[] 347 | } 348 | 349 | export interface Variant { 350 | bitrate?: number 351 | content_type: ContentType 352 | url: string 353 | } 354 | 355 | export enum ContentType { 356 | ApplicationXMPEGURL = 'application/x-mpegURL', 357 | VideoMp4 = 'video/mp4' 358 | } 359 | 360 | export enum Lang { 361 | En = 'en' 362 | } 363 | 364 | export interface QuotedStatusPermalink { 365 | url: string 366 | expanded: string 367 | display: string 368 | } 369 | 370 | export interface SelfThread { 371 | id: number 372 | id_str: string 373 | } 374 | 375 | export interface Timeline { 376 | id: string 377 | instructions: Instruction[] 378 | responseObjects: ResponseObjects 379 | } 380 | 381 | export type Instruction = AddEntryInstruction 382 | 383 | export interface AddEntryInstruction { 384 | addEntries: AddEntries 385 | } 386 | 387 | export interface AddEntries { 388 | entries: Entry[] 389 | } 390 | 391 | export interface Entry { 392 | entryId: string 393 | sortIndex: string 394 | content: EntryContent 395 | } 396 | 397 | export interface EntryContent { 398 | timelineModule?: TimelineModule 399 | item?: ContentItem 400 | operation?: Operation 401 | } 402 | 403 | export interface ContentItem { 404 | content: PurpleContent 405 | clientEventInfo: ItemClientEventInfo 406 | feedbackInfo: FeedbackInfo 407 | } 408 | 409 | export interface ItemClientEventInfo { 410 | component: Component 411 | element: PurpleElement 412 | details: Details 413 | } 414 | 415 | export enum Component { 416 | ConversationModule = 'conversation_module', 417 | Result = 'result', 418 | UserModule = 'user_module' 419 | } 420 | 421 | export interface Details { 422 | timelinesDetails: TimelinesDetails 423 | } 424 | 425 | export interface TimelinesDetails { 426 | controllerData: string 427 | } 428 | 429 | export enum PurpleElement { 430 | Tweet = 'tweet', 431 | User = 'user' 432 | } 433 | 434 | export interface PurpleContent { 435 | tweet: ContentTweet 436 | } 437 | 438 | export interface ContentTweet { 439 | id: string 440 | displayType: DisplayType 441 | highlights?: Highlights 442 | } 443 | 444 | export enum DisplayType { 445 | Tweet = 'Tweet' 446 | } 447 | 448 | export interface Highlights { 449 | textHighlights: TextHighlight[] 450 | } 451 | 452 | export interface TextHighlight { 453 | startIndex: number 454 | endIndex: number 455 | } 456 | 457 | export interface FeedbackInfo { 458 | feedbackKeys: FeedbackKey[] 459 | displayContext: DisplayContext 460 | clientEventInfo: FeedbackInfoClientEventInfo 461 | } 462 | 463 | export interface FeedbackInfoClientEventInfo { 464 | component: Component 465 | element: FluffyElement 466 | action: Action 467 | } 468 | 469 | export enum Action { 470 | Click = 'click' 471 | } 472 | 473 | export enum FluffyElement { 474 | FeedbackGivefeedback = 'feedback_givefeedback', 475 | FeedbackNotcredible = 'feedback_notcredible', 476 | FeedbackNotrelevant = 'feedback_notrelevant' 477 | } 478 | 479 | export interface DisplayContext { 480 | reason: Reason 481 | } 482 | 483 | export enum Reason { 484 | ThisTweetSNotHelpful = "This Tweet's not helpful" 485 | } 486 | 487 | export enum FeedbackKey { 488 | Givefeedback = 'givefeedback' 489 | } 490 | 491 | export interface Operation { 492 | cursor: Cursor 493 | } 494 | 495 | export interface Cursor { 496 | value: string 497 | cursorType: string 498 | } 499 | 500 | export interface TimelineModule { 501 | items: ItemElement[] 502 | displayType: string 503 | header?: Header 504 | footer?: Footer 505 | clientEventInfo: TimelineModuleClientEventInfo 506 | } 507 | 508 | export interface TimelineModuleClientEventInfo { 509 | component: Component 510 | element: string 511 | } 512 | 513 | export interface Footer { 514 | text: string 515 | url: string 516 | displayType: string 517 | } 518 | 519 | export interface Header { 520 | text: string 521 | sticky: boolean 522 | displayType: string 523 | } 524 | 525 | export interface ItemElement { 526 | entryId: string 527 | item: ItemItem 528 | } 529 | 530 | export interface ItemItem { 531 | content: FluffyContent 532 | clientEventInfo: ItemClientEventInfo 533 | feedbackInfo?: FeedbackInfo 534 | } 535 | 536 | export interface FluffyContent { 537 | user?: ContentUser 538 | tweet?: ContentTweet 539 | } 540 | 541 | export interface ContentUser { 542 | id: string 543 | displayType: string 544 | } 545 | 546 | export interface ResponseObjects { 547 | feedbackActions: FeedbackActions 548 | } 549 | 550 | export interface FeedbackActions { 551 | givefeedback: Givefeedback 552 | notrelevant: Givefeedback 553 | notcredible: Givefeedback 554 | } 555 | 556 | export interface Givefeedback { 557 | feedbackType: string 558 | prompt: string 559 | confirmation: string 560 | childKeys?: string[] 561 | hasUndoAction: boolean 562 | confirmationDisplayType: string 563 | clientEventInfo: FeedbackInfoClientEventInfo 564 | icon?: string 565 | } 566 | -------------------------------------------------------------------------------- /src/types/userFromIdInterface.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable quotes */ 2 | /* eslint-disable camelcase */ 3 | export interface UserFromId { 4 | data: Data 5 | } 6 | 7 | interface Data { 8 | user: User 9 | } 10 | 11 | interface User { 12 | id: string 13 | rest_id: string 14 | affiliates_highlighted_label: AffiliatesHighlightedLabel 15 | legacy: Legacy 16 | } 17 | 18 | interface AffiliatesHighlightedLabel {} 19 | 20 | interface Legacy { 21 | created_at: string 22 | default_profile: boolean 23 | default_profile_image: boolean 24 | description: string 25 | entities: Entities 26 | fast_followers_count: number 27 | favourites_count: number 28 | followers_count: number 29 | friends_count: number 30 | has_custom_timelines: boolean 31 | is_translator: boolean 32 | listed_count: number 33 | location: string 34 | media_count: number 35 | name: string 36 | normal_followers_count: number 37 | pinned_tweet_ids_str: string[] 38 | profile_banner_extensions: ProfileExtensions 39 | profile_banner_url: string 40 | profile_image_extensions: ProfileExtensions 41 | profile_image_url_https: string 42 | profile_interstitial_type: string 43 | protected: boolean 44 | screen_name: string 45 | statuses_count: number 46 | translator_type: string 47 | verified: boolean 48 | withheld_in_countries: any[] 49 | } 50 | 51 | interface Entities { 52 | description: Description 53 | } 54 | 55 | interface Description { 56 | urls: any[] 57 | } 58 | 59 | interface ProfileExtensions { 60 | mediaColor: MediaColor 61 | } 62 | 63 | interface MediaColor { 64 | r: R 65 | } 66 | 67 | interface R { 68 | ok: Ok 69 | } 70 | 71 | interface Ok { 72 | palette: Palette[] 73 | } 74 | 75 | interface Palette { 76 | percentage: number 77 | rgb: RGB 78 | } 79 | 80 | interface RGB { 81 | blue: number 82 | green: number 83 | red: number 84 | } 85 | -------------------------------------------------------------------------------- /src/types/userInterface.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable quotes */ 2 | /* eslint-disable camelcase */ 3 | export interface UserInterface { 4 | data: Data 5 | } 6 | 7 | interface Data { 8 | user: UserClass 9 | } 10 | 11 | interface UserClass { 12 | result: Result 13 | } 14 | 15 | interface Result { 16 | __typename: string 17 | id: string 18 | rest_id: string 19 | affiliates_highlighted_label: AffiliatesHighlightedLabel 20 | legacy: Legacy 21 | smart_blocked_by: boolean 22 | smart_blocking: boolean 23 | legacy_extended_profile: LegacyExtendedProfile 24 | is_profile_translatable: boolean 25 | } 26 | 27 | interface AffiliatesHighlightedLabel {} 28 | 29 | interface Legacy { 30 | created_at: string 31 | default_profile: boolean 32 | default_profile_image: boolean 33 | description: string 34 | entities: Entities 35 | fast_followers_count: number 36 | favourites_count: number 37 | followers_count: number 38 | friends_count: number 39 | has_custom_timelines: boolean 40 | is_translator: boolean 41 | listed_count: number 42 | location: string 43 | media_count: number 44 | name: string 45 | normal_followers_count: number 46 | pinned_tweet_ids_str: string[] 47 | profile_banner_extensions: ProfileExtensions 48 | profile_banner_url: string 49 | profile_image_extensions: ProfileExtensions 50 | profile_image_url_https: string 51 | profile_interstitial_type: string 52 | protected: boolean 53 | screen_name: string 54 | statuses_count: number 55 | translator_type: string 56 | url: string 57 | verified: boolean 58 | withheld_in_countries: any[] 59 | } 60 | 61 | interface Entities { 62 | description: Description 63 | url: Description 64 | } 65 | 66 | interface Description { 67 | urls: URL[] 68 | } 69 | 70 | interface URL { 71 | display_url: string 72 | expanded_url: string 73 | url: string 74 | indices: number[] 75 | } 76 | 77 | interface ProfileExtensions { 78 | mediaColor: MediaColor 79 | } 80 | 81 | interface MediaColor { 82 | r: R 83 | } 84 | 85 | interface R { 86 | ok: Ok 87 | } 88 | 89 | interface Ok { 90 | palette: Palette[] 91 | } 92 | 93 | interface Palette { 94 | percentage: number 95 | rgb: RGB 96 | } 97 | 98 | interface RGB { 99 | blue: number 100 | green: number 101 | red: number 102 | } 103 | 104 | interface LegacyExtendedProfile { 105 | birthdate: Birthdate 106 | } 107 | 108 | interface Birthdate { 109 | day: number 110 | month: number 111 | year: number 112 | visibility: string 113 | year_visibility: string 114 | } 115 | -------------------------------------------------------------------------------- /src/utils/clientSearch.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios' 2 | import { SearchResults } from '../types/searchResults' 3 | 4 | export interface SearchQuery { 5 | terms: string 6 | dateFrom?: string 7 | dateTo?: string 8 | minReplies?: number 9 | minRetweets?: number 10 | minFaves?: number 11 | lang?: string 12 | } 13 | 14 | const buildSearchQueryString = (query: SearchQuery): string => { 15 | let q = '' 16 | if (query.terms !== undefined) { 17 | q = query.terms 18 | } 19 | if (query.dateFrom !== undefined) { 20 | q += ` since:${query.dateFrom}` 21 | } 22 | if (query.dateTo !== undefined) { 23 | q += ` until:${query.dateTo}` 24 | } 25 | if (query.minReplies !== undefined) { 26 | q += ` min_replies:${query.minReplies}` 27 | } 28 | if (query.minRetweets !== undefined) { 29 | q += ` min_retweets:${query.minRetweets}` 30 | } 31 | if (query.minFaves !== undefined) { 32 | q += ` min_faves:${query.minFaves}` 33 | } 34 | if (query.lang !== undefined) { 35 | q += ` lang:${query.lang}` 36 | } 37 | 38 | return q 39 | } 40 | 41 | const getSearchParams = ( 42 | query: SearchQuery, 43 | maxTweets: number, 44 | pageToken?: string 45 | ) => { 46 | const params: any = { 47 | include_profile_interstitial_type: 1, 48 | include_blocking: 1, 49 | include_blocked_by: 1, 50 | include_followed_by: 1, 51 | include_want_retweets: 1, 52 | include_mute_edge: 1, 53 | include_can_dm: 1, 54 | include_can_media_tag: 1, 55 | skip_status: 1, 56 | cards_platform: 'Web-12', 57 | include_cards: 1, 58 | include_ext_alt_text: true, 59 | include_quote_count: true, 60 | include_reply_count: 1, 61 | tweet_mode: 'extended', 62 | include_entities: true, 63 | include_user_entities: true, 64 | include_ext_media_color: true, 65 | include_ext_media_availability: true, 66 | send_error_codes: true, 67 | simple_quoted_tweet: true, 68 | q: buildSearchQueryString(query), 69 | count: maxTweets, 70 | query_source: 'typed_query', 71 | pc: 1, 72 | spelling_corrections: 1, 73 | ext: 'mediaStats,highlightedLabel' 74 | } 75 | if (pageToken) { 76 | params.cursor = pageToken 77 | } 78 | return params 79 | } 80 | 81 | const getSearchHeaders = (query: SearchQuery, lang: string) => { 82 | return { 83 | Referer: 'https://twitter.com/search?q=' + query.terms + '&src=typed_query', 84 | 'x-twitter-client-language': lang 85 | } 86 | } 87 | 88 | export const clientSearch = async ( 89 | instance: AxiosInstance, 90 | query: SearchQuery, 91 | maxTweets: number, 92 | pageToken?: string 93 | ): Promise => { 94 | const searchParmas = getSearchParams(query, maxTweets, pageToken) 95 | const queryUrl = 'https://twitter.com/i/api/2/search/adaptive.json' 96 | const lang = query.lang || 'en' 97 | const headers = getSearchHeaders(query, lang) 98 | // Call api 99 | const response = await instance.get(queryUrl, { 100 | params: searchParmas, 101 | headers 102 | }) 103 | return response.data 104 | } 105 | -------------------------------------------------------------------------------- /src/utils/getApiClientInstance.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from 'axios' 2 | 3 | export const getApiClientInstance = (): AxiosInstance => 4 | axios.create({ 5 | headers: { 6 | Host: 'twitter.com', 7 | origin: 'https://twitter.com', 8 | 'User-Agent': 9 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0', 10 | Accept: '*/*', 11 | 'Accept-Language': 'es-AR,es;q=0.8,en-US;q=0.5,en;q=0.3', 12 | 'Accept-Encoding': 'gzip, deflate', 13 | 'x-twitter-active-user': 'yes', 14 | DNT: '1', 15 | Connection: 'keep-alive', 16 | Pragma: 'no-cache', 17 | 'Cache-Control': 'no-cache', 18 | TE: 'Trailers', 19 | 'sec-fetch-dest': 'empty', 20 | 'sec-fetch-mode': 'cors', 21 | 'sec-fetch-site': 'same-site' 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /src/utils/getBearerToken.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export const getBearerToken = async ( 4 | userAgent: string 5 | ): Promise => { 6 | const response = await axios.get( 7 | 'https://abs.twimg.com/responsive-web/web/main.92eeeb04.js', 8 | { 9 | headers: { 10 | 'User-agent': userAgent 11 | } 12 | } 13 | ) 14 | const tokenMatch = response.data.match(/s="(AAAAAA[^"]+)"/) 15 | const token = tokenMatch ? tokenMatch[1] : null 16 | return token 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/getGuestToken.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export const getGuestToken = async (): Promise => { 4 | const response = await axios.get('https://mobile.twitter.com/', { 5 | withCredentials: true, 6 | headers: { 7 | 'User-agent': 8 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0', 9 | Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 10 | Host: 'mobile.twitter.com', 11 | 'Accept-Encoding': 'gzip, deflate', 12 | DNT: '1', 13 | Connection: 'keep-alive', 14 | 'Upgrade-Insecure-Requests': '1', 15 | origin: 'https://mobile.twitter.com', 16 | referer: 'https://mobile.twitter.com' 17 | } 18 | }) 19 | const tokenMatch = response.data.match(/decodeURIComponent\("gt=([^;]+);/) 20 | const token = tokenMatch ? tokenMatch[1] : null 21 | return token 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/getHashflags.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export interface Hashflag { 4 | campaignName: string 5 | hashtag: string 6 | assetUrl: string 7 | startingTimestampMs: string 8 | endingTimestampMs: string 9 | } 10 | 11 | export const hashflagHeaders = { 12 | Host: 'pbs.twimg.com', 13 | Accept: '*/*', 14 | 'User-Agent': 15 | 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0', 16 | 'Accept-Language': 'en-US,en;q=0.5', 17 | 'Accept-Encoding': 'gzip, deflate, br', 18 | Origin: 'https://mobile.twitter.com', 19 | DNT: '1', 20 | Connection: 'keep-alive', 21 | Referer: 'https://mobile.twitter.com/', 22 | 'Sec-Fetch-Dest': 'empty', 23 | 'Sec-Fetch-Mode': 'cors', 24 | 'Sec-Fetch-Site': 'cross-site' 25 | } 26 | 27 | export const getHashflags = async (date: string) => { 28 | const { data } = await axios.get( 29 | `https://pbs.twimg.com/hashflag/config-${date}-01.json`, 30 | { headers: hashflagHeaders } 31 | ) 32 | return data 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/getInitialOptions.ts: -------------------------------------------------------------------------------- 1 | export interface ClientRequiredOprionsInterface { 2 | guestToken: string 3 | bearerToken: string 4 | maxRetries: number 5 | log: boolean 6 | userAgents: string[] 7 | } 8 | 9 | export type ClientOptionsInterface = Partial 10 | 11 | export const defaultUserAgents = [ 12 | 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0', 13 | 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0', 14 | 'Mozilla/5.0 (Windows NT 6.3; WOW64; rv:57.0) Gecko/20100101 Firefox/57.0', 15 | 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0', 16 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0', 17 | 'Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0', 18 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0', 19 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0', 20 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/37.0 Mobile/15E148 Safari/605.1.15', 21 | 'Mozilla/5.0 (iPad; CPU OS 11_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/37.0 Mobile/15E148 Safari/605.1.15' 22 | ] 23 | 24 | const defaultOptions: ClientRequiredOprionsInterface = { 25 | guestToken: '', 26 | bearerToken: '', 27 | maxRetries: 3, 28 | log: false, 29 | userAgents: defaultUserAgents 30 | } 31 | 32 | export const getInitialOptions = ( 33 | options: ClientOptionsInterface 34 | ): ClientRequiredOprionsInterface => { 35 | const finalOptions: ClientRequiredOprionsInterface = defaultOptions 36 | Object.entries(finalOptions).forEach(([index, _value]) => { 37 | if (typeof options[index] !== 'undefined') { 38 | finalOptions[index] = options[index] 39 | } 40 | }) 41 | return finalOptions 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/getTweetsFromUser.ts: -------------------------------------------------------------------------------- 1 | export const getTweetsFromUsername = async (username: string) => { 2 | return username 3 | } 4 | 5 | export const getTweetsFromUserId = async ( 6 | userId: number, 7 | count: number = 300 8 | ) => { 9 | const url = `https://mobile.twitter.com/i/api/graphql/Lya9A5YxHQxhCQJ5IPtm7A/UserTweets?variables={"userId":"${userId}","count":${count},"withTweetQuoteCount":true,"includePromotedContent":true,"withSuperFollowsUserFields":false,"withUserResults":true,"withBirdwatchPivots":false,"withReactionsMetadata":false,"withReactionsPerspective":false,"withSuperFollowsTweetFields":false,"withVoice":true}` 10 | return url 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/getUser.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from 'axios' 2 | import { UserFromId } from '../types/userFromIdInterface' 3 | import { UserInterface } from '../types/userInterface' 4 | 5 | export const testUsername = (username: string) => { 6 | if (typeof username !== 'string') { 7 | return false 8 | } 9 | return /^([A-Za-z]+[A-Za-z0-9-_]+)$/.test(username) 10 | } 11 | 12 | export const getUser = async (instance: AxiosInstance, username: string) => { 13 | if (!testUsername(username)) { 14 | throw new Error('Invalid username') 15 | } 16 | const { data } = await instance.get( 17 | `https://twitter.com/i/api/graphql/B-dCk4ph5BZ0UReWK590tw/UserByScreenName?variables={"screen_name":"${username}","withSafetyModeUserFields":true,"withSuperFollowsUserFields":false}` 18 | ) 19 | return data 20 | } 21 | 22 | export const getUserById = async ( 23 | instance: AxiosInstance, 24 | userId: number | string 25 | ) => { 26 | const { data } = await instance.get( 27 | `https://twitter.com/i/api/graphql/WN6Hck-Pwm-YP0uxVj1oMQ/UserByRestIdWithoutResults?variables={"userId":"${userId}","withHighlightedLabel":true}` 28 | ) 29 | return data 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/parseSearch.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AddEntryInstruction, 3 | Instruction, 4 | SearchResults, 5 | TweetValue, 6 | UserValue 7 | } from '../types/searchResults' 8 | 9 | export const getNextToken = (d: SearchResults): string | undefined => { 10 | const instructions = d?.timeline?.instructions 11 | if (!instructions?.length) { 12 | return undefined 13 | } 14 | const lastInstruction: Instruction = instructions[instructions.length - 1] 15 | if (lastInstruction.addEntries) { 16 | return getTokenFromAddEntry(lastInstruction) 17 | } 18 | return undefined 19 | } 20 | 21 | const getTokenFromAddEntry = ( 22 | entry: AddEntryInstruction 23 | ): string | undefined => { 24 | const entries = entry.addEntries.entries 25 | const last = entries[entries.length - 1] 26 | return last.content.operation?.cursor.value || undefined 27 | } 28 | 29 | export interface ParsedSearchResult { 30 | users: { 31 | [key: string]: UserValue 32 | } 33 | tweets: { 34 | [key: string]: TweetValue 35 | } 36 | nextToken: string | undefined 37 | } 38 | 39 | export const parseSearch = (d: SearchResults): ParsedSearchResult => { 40 | const result = { 41 | users: d.globalObjects.users, 42 | tweets: d.globalObjects.tweets, 43 | nextToken: getNextToken(d) 44 | } 45 | return result 46 | } 47 | -------------------------------------------------------------------------------- /test/client.test.ts: -------------------------------------------------------------------------------- 1 | import TwitterClient from '../src' 2 | 3 | jest.setTimeout(20000) 4 | 5 | const client = new TwitterClient() 6 | beforeAll(async () => { 7 | await client.connect() 8 | }) 9 | 10 | describe('Test Main client', (): void => { 11 | test('search', async (): Promise => { 12 | const q = { terms: 'hello world!' } 13 | const result = await client.search(q, 5) 14 | const token = result.nextToken 15 | await client.search(q, 5, token) 16 | }) 17 | test('getUser', async (): Promise => { 18 | await client.getUser('jack') 19 | }) 20 | test('getUserById', async (): Promise => { 21 | await client.getUserById('12') 22 | }) 23 | test('getHashflags', async (): Promise => { 24 | await client.getHashflags('2021-09-23') 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "declaration": true, 9 | "esModuleInterop": true, 10 | "noImplicitReturns": true, 11 | "noImplicitThis": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "suppressImplicitAnyIndexErrors": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "allowSyntheticDefaultImports": true 18 | }, 19 | "include": ["src"], 20 | "exclude": ["node_modules", "dist", "example"] 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } --------------------------------------------------------------------------------