├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── REFERENCES.md ├── package-lock.json ├── package.json ├── src ├── base │ ├── Cache.ts │ ├── IClientOptions.ts │ ├── Transport.ts │ ├── httpVerbs.ts │ └── utils.ts ├── generator │ ├── createFolderStructure.ts │ ├── template-models │ │ ├── collection-template.json │ │ ├── geo-reverse-template.json │ │ ├── geo-template.json │ │ ├── list-template.json │ │ ├── mention-template.json │ │ ├── size-template.json │ │ ├── tweet-template.json │ │ └── user-template.json │ ├── writeClients.ts │ ├── writeParamsInterfaces.ts │ ├── writeReferences.ts │ └── writeTypesInterfaces.ts ├── index.ts ├── interfaces │ └── IReferenceDirectory.ts ├── specs │ ├── twitter-api-spec.yml │ ├── v1 │ │ ├── accounts-and-users.yml │ │ ├── apps.yml │ │ ├── basic.yml │ │ ├── direct-messages.yml │ │ ├── geo.yml │ │ ├── media.yml │ │ ├── trends.yml │ │ └── tweets.yml │ └── v2 │ │ ├── metrics.yml │ │ ├── timelines.yml │ │ ├── tweets.yml │ │ └── users.yml ├── test │ ├── Cache.test.ts │ ├── createFolderStructure.test.ts │ ├── httpVerbs.test.ts │ └── utils.test.ts └── utils │ └── utils.ts └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "settings": { 8 | "version": "detect" 9 | }, 10 | "extends": [ 11 | "eslint:recommended", 12 | "plugin:react/recommended", 13 | "plugin:@typescript-eslint/eslint-recommended", 14 | "plugin:@typescript-eslint/recommended" 15 | ], 16 | "globals": { 17 | "Atomics": "readonly", 18 | "SharedArrayBuffer": "readonly" 19 | }, 20 | "parser": "@typescript-eslint/parser", 21 | "parserOptions": { 22 | "ecmaFeatures": { 23 | "jsx": true 24 | }, 25 | "ecmaVersion": 2018, 26 | "sourceType": "module" 27 | }, 28 | "plugins": ["@typescript-eslint"], 29 | "rules": { 30 | "@typescript-eslint/explicit-function-return-type": "off", 31 | "@typescript-eslint/no-use-before-define": "off", 32 | "@typescript-eslint/no-var-requires": "off", 33 | "@typescript-eslint/interface-name-prefix": "off", 34 | "@typescript-eslint/no-this-alias": "off", 35 | "@typescript-eslint/no-explicit-any": "off", 36 | "import/no-extraneous-dependencies": "off", 37 | "import/no-unresolved": "off", 38 | "no-async-promise-executor": "off", 39 | "no-console": "off", 40 | "no-param-reassign": "off", 41 | "no-underscore-dangle": "off", 42 | "global-require": "off", 43 | "arrow-body-style": "off", 44 | "implicit-arrow-linebreak": "off", 45 | "object-curly-newline": "off", 46 | "lines-between-class-members": "off", 47 | "function-paren-newline": "off", 48 | "linebreak-style": "off", 49 | "operator-linebreak": "off", 50 | "no-prototype-builtins": "off", 51 | "consistent-return": "off", 52 | "max-len": ["warn", { "code": 120 }] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: If you found a bug in Twitter API Client, please file a bug report 4 | title: '' 5 | labels: 'bug' 6 | assignees: 'Silind' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Install '...' 16 | 2. Open '....' 17 | 3. Build '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Package Manager:** 24 | To install Twitter API Client, I used... (npm / yarn / something else) 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for Twitter API Client 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: 'Silind' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **What does this pull request introduce? Please describe** 2 | A clear and concise description of what this pull requests includes. 3 | Please add a reference to the related issue, if relevant. 4 | 5 | **How did you verify that your changes work as expected? Please describe** 6 | Please describe your methods of testing your changes. 7 | 8 | **Example** 9 | Please describe how we can try out your changes 10 | 11 | 1. Create a new '...' 12 | 2. Build with '...' 13 | 3. See '...' 14 | 15 | **Screenshots** 16 | If applicable, add screenshots to demonstrate your changes. 17 | 18 | **Version** 19 | Which version is your changes included in? 20 | 21 | **PR Checklist** 22 | Please verify that you: 23 | 24 | - [ ] Ran all unit tests, and they are passing 25 | - [ ] Wrote new unit tests if appropriate 26 | - [ ] Installed the client locally and tested it manually 27 | - [ ] Updated the version in `package.json` 28 | - [ ] Set the branch base to `development` (not `main`) 29 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: 12.x 16 | registry-url: 'https://registry.npmjs.org' 17 | 18 | - name: Install 19 | run: | 20 | npm install 21 | 22 | - name: Test 23 | run: npm test 24 | 25 | - name: Generate 26 | run: npm run generate 27 | 28 | - name: Build 29 | run: npm run build 30 | 31 | - name: Publish to NPM registry 32 | run: npm publish 33 | env: 34 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: actions/setup-node@v1 11 | with: 12 | node-version: 12.x 13 | registry-url: 'https://registry.npmjs.org' 14 | 15 | - name: Install 16 | run: | 17 | npm install 18 | 19 | - name: Test 20 | run: npm test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | yarn.lock 9 | 10 | # Ignore generated files 11 | /generated 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # Next.js build output 83 | .next 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at contact@silind.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Issues 4 | 5 | In the case of a bug report, a suggestions, or if you just need help, please feel very free to open an issue. 6 | 7 | For general issues, please use the following labels: 8 | 9 | - Something is not working as intended: `bug` 10 | - Need help with something: `help wanted` 11 | - Have a question: `question` 12 | - Have a suggestion or want to request a feature: `enhancement` 13 | 14 | ## Pull Request 15 | 16 | Start by forking the repository. 17 | Clone your forked repository to your local machine. 18 | 19 | When creating a PR, please use the `development` branch as the base branch. 20 | 21 | ### Install 22 | 23 | Install the client by using 24 | 25 | ```console 26 | npm install 27 | ``` 28 | 29 | ### Generate client 30 | 31 | Generate a new client by using 32 | 33 | ```console 34 | npm run generate 35 | ``` 36 | 37 | This will create a new folder `generated` in the root. 38 | This folder will contain the new client source code in TypeScript. 39 | 40 | ### Build 41 | 42 | Run the command: 43 | 44 | ```console 45 | npm run build 46 | ``` 47 | 48 | This will produce yet another folder `dist` in the root. 49 | This folder will contain the transpiled source code based on the `generated` folder. 50 | 51 | ### Unit Tests 52 | 53 | Run unit tests by using the command: 54 | 55 | ```console 56 | npm test 57 | ``` 58 | 59 | ### Manual Testing 60 | 61 | Follow the above steps and make sure you have a `dist` folder with the transpiled source code. 62 | Create a new node project somewhere on your machine using the command: 63 | 64 | ```console 65 | npm init --y 66 | ``` 67 | 68 | In this new project, install the Twitter API Client from your local path. 69 | 70 | ```console 71 | npm install path/to/your/cloned/twitter-api-client/repository 72 | ``` 73 | 74 | Consume the client and test that the new functionality works as expected. 75 | 76 | ### Update the version 77 | 78 | Before creating the PR, make sure to update the version using the principles of semantic versioning. 79 | This is done by simple going to the `package.json` file and update the version. 80 | 81 | ## Adding changes to the client 82 | 83 | In the `src` folder you will find a file `spec/twitter-api-spec.yml`. 84 | This is the full specification of the Twitter API that is used in by this client. 85 | If you add a new endpoint group (eg, 'metrics' or 'tweets') you must add a new reference in the `spec/twitter-api-spec.yml` file. 86 | 87 | After adding the new reference, and the endpoint details in the `v1` or `v2` directories, run `npm run generate` to update the client (changes reflected in `REFERENCES.md`) 88 | 89 | When you run the command `npm run generate`, this file will be used to auto-generate a new client. 90 | Any changes you make in this file will be reflected by the Twitter API Client. 91 | 92 | :warning: The spec file is quite big, so be careful when you edit it. 93 | 94 | The client follows the format: 95 | 96 | ```yml 97 | # Title of a group 98 | - title: Accounts and users 99 | 100 | # List of subgroups 101 | subgroups: 102 | # Title of a subgroup 103 | - title: Create and manage lists 104 | 105 | # List of endpoints in the subgroup 106 | endpoints: 107 | # Title of the endpoint. Must follow the pattern VERB PATH 108 | - title: GET lists/list 109 | 110 | # The url to the Twitter Documentation page 111 | url: https://developer.twitter.com/en/docs/accounts-and-users/create-manage-lists/api-reference/get-lists-list 112 | 113 | # The url that the endpoint is using 114 | resourceUrl: https://api.twitter.com/1.1/lists/list.json 115 | 116 | # The description of the endpoint (from Twitter's docs) 117 | description: | 118 | Returns all lists the authenticating or specified user subscribes to, 119 | including their own. The user is specified using the user_id or screen_name parameters. 120 | 121 | # A list of parameters for the endpoint 122 | parameters: 123 | # Name of the parameter 124 | - name: user_id 125 | 126 | # Description of the parameter 127 | description: | 128 | The ID of the user for whom to return results. Helpful for disambiguating 129 | when a valid user ID is also a valid screen name. 130 | 131 | # Whether the paramter is required or not 132 | required: false 133 | 134 | # The type of the paramter: string, number or boolean 135 | type: number 136 | 137 | # An example response in JSON format. 138 | # This will be used to make type inference for the TypeScript interfaces 139 | # Certain words are used to reference bigger JSON objects in the 'generator/template-models' folder. 140 | # For instance, {user-object} references the file 'generator/template-models/user-template.json' 141 | exampleResponse: | 142 | { 143 | "users": [ {user-object} ], 144 | "next_cursor": 0, 145 | "next_cursor_str": "0", 146 | "previous_cursor": 0, 147 | "previous_cursor_str": "0", 148 | "total_count": null 149 | } 150 | ``` 151 | 152 | ## PR Checklist 153 | 154 | - I ran all unit tests, and they are passing 155 | - I wrote new unit tests if appropriate 156 | - I install the client locally and tested it manually 157 | - I updated the version in `package.json` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Silind Ltd 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twitter API Client 2 | 3 | Node.js client for Twitter API 4 | 5 | ![](https://i.imgur.com/NfnLHIM.png) 6 | 7 | [![NPM Version](https://img.shields.io/npm/v/twitter-api-client)](https://www.npmjs.com/package/twitter-api-client) 8 | ![Build Status](https://github.com/FeedHive/twitter-api-client/workflows/build/badge.svg) 9 | 10 | ### ⚠️ Important notice 11 | 12 | Twitter now has an [official TypeScript SDK](https://github.com/twitterdev/twitter-api-typescript-sdk). 13 | We recommend using that instead of this client. 14 | 15 | This project will be maintained, but will not be developed any further. 16 | 17 | To all contributors who added to this project: Thank you 🧡 18 | 19 | ## Table of content 20 | 21 | - [Features](#features) 22 | - [**Getting Started**](#getting-started) 23 | - [Usage](#usage) 24 | - [License](#license) 25 | - [Get Help](#get-help) 26 | - [Contribute](#contribute) 27 | 28 | ## Features 29 | 30 | ☑️ Includes 90% of the **official Twitter API** endpoints. 31 | ☑️ **Promise-based!** No ugly callbacks. 32 | ☑️ **Fully typed!** Both for query parameters and responses. 33 | ☑️ Inbuilt in-memory **cache** for rate-limit friendly usage. 34 | 35 | ## Getting Started 36 | 37 | ### Get your Twitter credentials 38 | 39 | You will need to create a set of Twitter developer credentials from your Twitter Developer account. 40 | If you don't have one already, apply for a developer account [here](https://developer.twitter.com/). 41 | It takes about 5 minutes. 42 | 43 | ### Install 44 | 45 | ```console 46 | npm i twitter-api-client 47 | ``` 48 | 49 | ## Usage 50 | 51 | ```javascript 52 | import { TwitterClient } from 'twitter-api-client'; 53 | 54 | const twitterClient = new TwitterClient({ 55 | apiKey: '', 56 | apiSecret: '', 57 | accessToken: '', 58 | accessTokenSecret: '', 59 | }); 60 | 61 | // Search for a user 62 | const data = await twitterClient.accountsAndUsers.usersSearch({ q: 'twitterDev' }); 63 | 64 | // Get message event by Id 65 | const data = await twitterClient.directMessages.eventsShow({ id: '1234' }); 66 | 67 | // Get most recent 25 retweets of a tweet 68 | const data = await twitterClient.tweets.statusesRetweetsById({ id: '12345', count: 25 }); 69 | 70 | // Get local trends 71 | const data = await twitterClient.trends.trendsAvailable(); 72 | ``` 73 | 74 | [See all available methods here](https://github.com/FeedHive/twitter-api-client/blob/main/REFERENCES.md). 75 | 76 | ### Configuration 77 | 78 | `twitter-api-client` comes with an inbuilt in-memory cache. 79 | The stale data is served by the cache-first principle. 80 | 81 | You can configure the caching behavior upon instantiation of the client: 82 | 83 | ```javascript 84 | const twitterClient = new TwitterClient({ 85 | apiKey: '', 86 | apiSecret: '', 87 | accessToken: '', 88 | accessTokenSecret: '', 89 | ttl: 120, // seconds. Defaults to 360 90 | disableCache: true, // Disables the caching behavior. Defaults to 'false' 91 | maxByteSize: 32000000, // Maximum (approximated) memory size for cache store. Defaults to 16000000. 92 | }); 93 | ``` 94 | 95 | ## License 96 | 97 | This project is licensed under the [MIT License](https://github.com/Silind/twitter-api-client/blob/main/LICENSE) 98 | 99 | ## Get Help 100 | 101 | - Reach out on [Twitter](https://twitter.com/SimonHoiberg) 102 | - Open an [issue on GitHub](https://github.com/Silind/twitter-api-client/issues/new) 103 | 104 | ## Contribute 105 | 106 | #### Issues 107 | 108 | In the case of a bug report, bugfix or a suggestions, please feel very free to open an issue. 109 | 110 | #### Pull request 111 | 112 | Pull requests are always welcome, and I'll do my best to do reviews as fast as I can. 113 | Please refer to the [contribution guide](https://github.com/Silind/twitter-api-client/blob/main/CONTRIBUTING.md) to see how to get started. 114 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitter-api-client", 3 | "version": "1.6.1", 4 | "description": "Node.js / JavaScript client for Twitter API", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "generate": "ts-node ./src/index.ts", 8 | "build": "tsc", 9 | "test": "jest" 10 | }, 11 | "files": [ 12 | "dist/*" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/FeedHive/twitter-api-client.git" 17 | }, 18 | "keywords": [ 19 | "node.js", 20 | "javascript", 21 | "twitter", 22 | "api" 23 | ], 24 | "author": "FeedHive", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/FeedHive/twitter-api-client/issues" 28 | }, 29 | "homepage": "https://github.com/FeedHive/twitter-api-client#readme", 30 | "dependencies": { 31 | "oauth": "^0.9.15", 32 | "object-sizeof": "^1.6.1" 33 | }, 34 | "devDependencies": { 35 | "@apidevtools/json-schema-ref-parser": "^9.0.6", 36 | "@sinonjs/fake-timers": "^6.0.1", 37 | "@types/jest": "^26.0.5", 38 | "@types/js-yaml": "^3.12.5", 39 | "@types/lodash.capitalize": "^4.2.6", 40 | "@types/mock-fs": "^4.10.0", 41 | "@types/node": "^13.11.1", 42 | "@types/oauth": "^0.9.1", 43 | "@types/rimraf": "^3.0.0", 44 | "@types/sinonjs__fake-timers": "^6.0.1", 45 | "@typescript-eslint/eslint-plugin": "^2.27.0", 46 | "@typescript-eslint/parser": "^2.27.0", 47 | "eslint": "^6.8.0", 48 | "eslint-plugin-node": "^11.1.0", 49 | "eslint-plugin-promise": "^4.2.1", 50 | "jest": "^26.1.0", 51 | "js-yaml": "^3.14.0", 52 | "json-to-ts": "^1.7.0", 53 | "lodash.capitalize": "^4.2.1", 54 | "mock-fs": "^4.13.0", 55 | "rimraf": "^3.0.2", 56 | "ts-jest": "^26.1.3", 57 | "ts-node": "^9.0.0", 58 | "typescript": "^3.8.3" 59 | }, 60 | "jest": { 61 | "roots": [ 62 | "/src" 63 | ], 64 | "testMatch": [ 65 | "**/?(*.)+(spec|test).+(ts|tsx|js)" 66 | ], 67 | "transform": { 68 | "^.+\\.(ts|tsx)$": "ts-jest" 69 | }, 70 | "collectCoverage": true 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/base/Cache.ts: -------------------------------------------------------------------------------- 1 | import sizeof from 'object-sizeof'; 2 | import { generateHash } from './utils'; 3 | 4 | interface ICacheEntry { 5 | added: Date; 6 | data: any; 7 | } 8 | 9 | const windowSessionStorage = typeof sessionStorage !== 'undefined' ? sessionStorage : undefined; 10 | 11 | class Cache { 12 | private ttl: number; 13 | private maxByteSize: number; 14 | private cache = new Map(); 15 | 16 | constructor(ttl = 360, maxByteSize = 16000000) { 17 | this.ttl = ttl; 18 | this.maxByteSize = maxByteSize; 19 | } 20 | 21 | public add(query: string, data: any) { 22 | const hashedKey = generateHash(query); 23 | 24 | const added = new Date(); 25 | const entry = { 26 | added, 27 | data, 28 | }; 29 | 30 | this.cache.set(hashedKey, entry); 31 | windowSessionStorage?.setItem(hashedKey, JSON.stringify(entry)); 32 | this.clearSpace(); 33 | } 34 | 35 | public get(query: string) { 36 | const hashedKey = generateHash(query); 37 | 38 | if (!this.has(query)) { 39 | return null; 40 | } 41 | 42 | try { 43 | const entry = this.cache.get(hashedKey); 44 | 45 | if (!entry) { 46 | const sessionData = windowSessionStorage?.getItem(hashedKey); 47 | 48 | if (!sessionData) { 49 | return; 50 | } 51 | 52 | return JSON.parse(sessionData); 53 | } 54 | 55 | return entry.data; 56 | } catch (error) { 57 | return null; 58 | } 59 | } 60 | 61 | public has(query: string) { 62 | const hashedKey = generateHash(query); 63 | 64 | try { 65 | const now = new Date(); 66 | let data = this.cache.get(hashedKey); 67 | 68 | if (!data) { 69 | const sessionData = windowSessionStorage?.getItem(hashedKey); 70 | 71 | if (!sessionData) { 72 | return false; 73 | } 74 | 75 | data = JSON.parse(sessionData) as ICacheEntry; 76 | } 77 | 78 | const entryAdded = new Date(data.added); 79 | 80 | if (now.getTime() > entryAdded.getTime() + this.ttl * 1000) { 81 | windowSessionStorage?.removeItem(hashedKey); 82 | this.cache.delete(hashedKey); 83 | return false; 84 | } 85 | 86 | return true; 87 | } catch (error) { 88 | return false; 89 | } 90 | } 91 | 92 | private clearSpace() { 93 | const cacheArray = Array.from(this.cache); 94 | 95 | if (sizeof(cacheArray) < this.maxByteSize) { 96 | return; 97 | } 98 | 99 | cacheArray.sort((a, b) => a[1].added.getTime() - b[1].added.getTime()); 100 | 101 | const [, ...reducedCacheArray] = cacheArray; 102 | 103 | this.cache = new Map(reducedCacheArray); 104 | this.clearSpace(); 105 | } 106 | } 107 | 108 | export default Cache; 109 | -------------------------------------------------------------------------------- /src/base/IClientOptions.ts: -------------------------------------------------------------------------------- 1 | export default interface IClientOptions { 2 | apiKey: string; 3 | apiSecret: string; 4 | accessToken?: string; 5 | accessTokenSecret?: string; 6 | ttl?: number; 7 | maxByteSize?: number; 8 | disableCache?: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /src/base/Transport.ts: -------------------------------------------------------------------------------- 1 | import OAuth from 'oauth'; 2 | import IClientOptions from './IClientOptions'; 3 | import Cache from './Cache'; 4 | import { formatURL, parse } from './utils'; 5 | 6 | class Transport { 7 | private oauth: OAuth.OAuth; 8 | private cache?: Cache; 9 | private credentials: IClientOptions & { [key: string]: any }; 10 | 11 | constructor(options: IClientOptions) { 12 | this.credentials = options; 13 | this.oauth = new OAuth.OAuth( 14 | 'https://api.twitter.com/oauth/request_token', 15 | 'https://api.twitter.com/oauth/access_token', 16 | this.credentials.apiKey, 17 | this.credentials.apiSecret, 18 | '1.0A', 19 | null, 20 | 'HMAC-SHA1', 21 | ); 22 | 23 | if (!options?.disableCache) { 24 | this.cache = new Cache(options?.ttl, options.maxByteSize); 25 | } 26 | } 27 | 28 | public updateOptions(options: Partial) { 29 | const { apiKey, apiSecret, ...rest } = options; 30 | const cleanOptions = rest as { [key: string]: any }; 31 | 32 | Object.keys(cleanOptions).forEach((key: string) => { 33 | if (cleanOptions[key]) { 34 | this.credentials[key] = cleanOptions[key]; 35 | } 36 | }); 37 | } 38 | 39 | public async doDeleteRequest(url: string): Promise { 40 | if (!this.oauth) { 41 | throw Error('Unable to make request. Authentication has not been established'); 42 | } 43 | return new Promise((resolve, reject) => { 44 | if (!this.credentials.accessToken || !this.credentials.accessTokenSecret) { 45 | reject(new Error('Unable to make request. Authentication has not been established')); 46 | return; 47 | } 48 | 49 | const formattedUrl = formatURL(url); 50 | 51 | this.oauth.delete( 52 | formattedUrl, 53 | this.credentials.accessToken, 54 | this.credentials.accessTokenSecret, 55 | (err: { statusCode: number; data?: any }, body?: string | Buffer) => { 56 | if (err) { 57 | reject(err); 58 | return; 59 | } 60 | 61 | if (!body) { 62 | resolve({} as T); 63 | return; 64 | } 65 | 66 | const result = parse(body.toString()); 67 | resolve(result); 68 | }, 69 | ); 70 | }); 71 | } 72 | 73 | public async doGetRequest(url: string): Promise { 74 | if (!this.oauth) { 75 | throw Error('Unable to make request. Authentication has not been established'); 76 | } 77 | 78 | if (this.cache?.has(url)) { 79 | return this.cache.get(url); 80 | } 81 | 82 | return new Promise((resolve, reject) => { 83 | if (!this.credentials.accessToken || !this.credentials.accessTokenSecret) { 84 | reject(new Error('Unable to make request. Authentication has not been established')); 85 | return; 86 | } 87 | 88 | const formattedUrl = formatURL(url); 89 | 90 | this.oauth.get( 91 | formattedUrl, 92 | this.credentials.accessToken, 93 | this.credentials.accessTokenSecret, 94 | (err: { statusCode: number; data?: any }, body?: string | Buffer) => { 95 | if (err) { 96 | reject(err); 97 | return; 98 | } 99 | 100 | if (!body) { 101 | resolve({} as T); 102 | return; 103 | } 104 | 105 | const result = parse(body.toString()); 106 | 107 | this.cache?.add(url, result); 108 | resolve(result); 109 | }, 110 | ); 111 | }); 112 | } 113 | 114 | public async doPostRequest( 115 | url: string, 116 | body?: any, 117 | contentType = 'application/x-www-form-urlencoded', 118 | ): Promise { 119 | if (!this.oauth || !this.credentials) { 120 | throw Error('Unable to make request. Authentication has not been established'); 121 | } 122 | 123 | return new Promise((resolve, reject) => { 124 | if (!this.credentials.accessToken || !this.credentials.accessTokenSecret) { 125 | reject(new Error('Unable to make request. Authentication has not been established')); 126 | return; 127 | } 128 | 129 | const formattedUrl = formatURL(url); 130 | const formattedBody = contentType === 'application/json' ? JSON.stringify(body) : body; 131 | 132 | this.oauth.post( 133 | formattedUrl, 134 | this.credentials.accessToken, 135 | this.credentials.accessTokenSecret, 136 | formattedBody, 137 | contentType, 138 | (err: { statusCode: number; data?: any }, body?: string | Buffer) => { 139 | if (err) { 140 | reject(err); 141 | return; 142 | } 143 | 144 | if (!body) { 145 | resolve({} as T); 146 | return; 147 | } 148 | 149 | const result = parse(body.toString()); 150 | resolve(result); 151 | }, 152 | ); 153 | }); 154 | } 155 | } 156 | 157 | export default Transport; 158 | -------------------------------------------------------------------------------- /src/base/httpVerbs.ts: -------------------------------------------------------------------------------- 1 | import capitalize from 'lodash.capitalize'; 2 | 3 | const supportedHttpVerbs = ['POST', 'GET', 'DELETE']; 4 | 5 | export const removeHttpVerbs = (text: string) => { 6 | const httpVerbsPattern = new RegExp(supportedHttpVerbs.join('|')); 7 | return text.replace(httpVerbsPattern, '').trim(); 8 | }; 9 | 10 | export const startWithHttpVerb = (text: string) => { 11 | const httpVerbsPattern = new RegExp(`^(${supportedHttpVerbs.join('|')})`); 12 | return httpVerbsPattern.test(text); 13 | }; 14 | 15 | export const getMethodName = (text: string) => { 16 | if (text.startsWith('GET')) return methodNameBuilder('GET'); 17 | if (text.startsWith('POST')) return methodNameBuilder('POST'); 18 | if (text.startsWith('DELETE')) return methodNameBuilder('DELETE'); 19 | }; 20 | 21 | export const methodNameBuilder = (verb: string) => { 22 | if (supportedHttpVerbs.includes(verb)) { 23 | return `this.transport.do${capitalize(verb)}Request`; 24 | } 25 | throw new Error('This verb is not supported'); 26 | }; 27 | -------------------------------------------------------------------------------- /src/base/utils.ts: -------------------------------------------------------------------------------- 1 | export const createParams = (params?: { [key: string]: any }, exclude?: string[]) => { 2 | if (!params) { 3 | return ''; 4 | } 5 | 6 | const searchParams = new URLSearchParams(); 7 | 8 | Object.entries(params).forEach(([key, value]) => { 9 | if (exclude?.includes(key)) { 10 | return; 11 | } 12 | 13 | if (typeof value === 'boolean') { 14 | searchParams.append(key, value ? 'true' : 'false'); 15 | return; 16 | } 17 | 18 | searchParams.append(key, `${value}`); 19 | }); 20 | 21 | return `?${searchParams.toString()}`; 22 | }; 23 | 24 | export const generateHash = (token: string): string => { 25 | const seed = 56852; 26 | 27 | let h1 = 0xdeadbeef ^ seed; 28 | let h2 = 0x41c6ce57 ^ seed; 29 | 30 | for (let i = 0, ch; i < token.length; i++) { 31 | ch = token.charCodeAt(i); 32 | h1 = Math.imul(h1 ^ ch, 2654435761); 33 | h2 = Math.imul(h2 ^ ch, 1597334677); 34 | } 35 | 36 | h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909); 37 | h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909); 38 | 39 | return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16); 40 | }; 41 | 42 | export const formatURL = (url: string): string => { 43 | return url 44 | .replace(/!/g, '%21') 45 | .replace(/'/g, '%27') 46 | .replace(/\(/g, '%28') 47 | .replace(/\)/g, '%29') 48 | .replace(/\*/g, '%2A'); 49 | }; 50 | 51 | export const parse = (body: string): T => { 52 | let parsed = undefined; 53 | 54 | try { 55 | parsed = JSON.parse(body); 56 | } catch (error) {} 57 | 58 | if (parsed) { 59 | return parsed; 60 | } 61 | 62 | try { 63 | parsed = JSON.parse('{"' + decodeURI(body).replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g, '":"') + '"}'); 64 | } catch (error) {} 65 | 66 | if (parsed) { 67 | return parsed; 68 | } 69 | 70 | return (body as any) as T; 71 | }; 72 | -------------------------------------------------------------------------------- /src/generator/createFolderStructure.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import rimraf from 'rimraf'; 3 | import { execSync } from 'child_process'; 4 | import { resolve } from 'path'; 5 | 6 | function createFolderStructure(basePath: string) { 7 | rimraf.sync(resolve(basePath, './generated')); 8 | const paths = [ 9 | resolve(basePath, './generated'), 10 | resolve(basePath, './generated/interfaces'), 11 | resolve(basePath, './generated/interfaces/params'), 12 | resolve(basePath, './generated/interfaces/types'), 13 | resolve(basePath, './generated/clients'), 14 | ]; 15 | 16 | for (const p of paths) { 17 | if (!fs.existsSync(p)) { 18 | fs.mkdirSync(p); 19 | } 20 | } 21 | } 22 | 23 | export function copyBase() { 24 | execSync(`cp -R ${resolve(__dirname, '../base')} ${resolve(__dirname, '../../generated/base')}`); 25 | } 26 | 27 | export default createFolderStructure; 28 | -------------------------------------------------------------------------------- /src/generator/template-models/collection-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "objects": { 3 | "timelines": { 4 | "custom-539487832448843776": { 5 | "collection_type": "user", 6 | "collection_url": "https://twitter.com/TwitterDev/timelines/539487832448843776", 7 | "description": "A collection of Tweets about National Parks in the United States.", 8 | "name": "National Park Tweets", 9 | "timeline_order": "curation_reverse_chron", 10 | "url": "", 11 | "user_id": "2244994945", 12 | "visibility": "public" 13 | } 14 | }, 15 | "tweets": { 16 | "504032379045179393": { 17 | "contributors": null, 18 | "coordinates": null, 19 | "created_at": "Mon Aug 25 22:27:38 +0000 2014", 20 | "entities": { 21 | "hashtags": [], 22 | "media": [ 23 | { 24 | "display_url": "pic.twitter.com/HtdvV0bPEu", 25 | "expanded_url": "http://twitter.com/Interior/status/504032379045179393/photo/1", 26 | "id": 504032378411446273, 27 | "id_str": "504032378411446273", 28 | "indices": [99, 121], 29 | "media_url": "http://pbs.twimg.com/media/Bv6uxxaCcAEjWHD.jpg", 30 | "media_url_https": "https://pbs.twimg.com/media/Bv6uxxaCcAEjWHD.jpg", 31 | "sizes": { 32 | "large": { 33 | "h": 695, 34 | "resize": "fit", 35 | "w": 1024 36 | }, 37 | "medium": { 38 | "h": 407, 39 | "resize": "fit", 40 | "w": 600 41 | }, 42 | "small": { 43 | "h": 230, 44 | "resize": "fit", 45 | "w": 340 46 | }, 47 | "thumb": { 48 | "h": 150, 49 | "resize": "crop", 50 | "w": 150 51 | } 52 | }, 53 | "type": "photo", 54 | "url": "http://t.co/HtdvV0bPEu" 55 | } 56 | ], 57 | "symbols": [], 58 | "urls": [], 59 | "user_mentions": [ 60 | { 61 | "id": 66453289, 62 | "id_str": "66453289", 63 | "indices": [47, 60], 64 | "name": "Lake Clark NP&P", 65 | "screen_name": "LakeClarkNPS" 66 | } 67 | ] 68 | }, 69 | "extended_entities": { 70 | "media": [ 71 | { 72 | "display_url": "pic.twitter.com/HtdvV0bPEu", 73 | "expanded_url": "http://twitter.com/Interior/status/504032379045179393/photo/1", 74 | "id": 504032378411446273, 75 | "id_str": "504032378411446273", 76 | "indices": [99, 121], 77 | "media_url": "http://pbs.twimg.com/media/Bv6uxxaCcAEjWHD.jpg", 78 | "media_url_https": "https://pbs.twimg.com/media/Bv6uxxaCcAEjWHD.jpg", 79 | "sizes": { 80 | "large": { 81 | "h": 695, 82 | "resize": "fit", 83 | "w": 1024 84 | }, 85 | "medium": { 86 | "h": 407, 87 | "resize": "fit", 88 | "w": 600 89 | }, 90 | "small": { 91 | "h": 230, 92 | "resize": "fit", 93 | "w": 340 94 | }, 95 | "thumb": { 96 | "h": 150, 97 | "resize": "crop", 98 | "w": 150 99 | } 100 | }, 101 | "type": "photo", 102 | "url": "http://t.co/HtdvV0bPEu" 103 | } 104 | ] 105 | }, 106 | "favorite_count": 639, 107 | "favorited": false, 108 | "geo": null, 109 | "id": 504032379045179393, 110 | "id_str": "504032379045179393", 111 | "in_reply_to_screen_name": null, 112 | "in_reply_to_status_id": null, 113 | "in_reply_to_status_id_str": null, 114 | "in_reply_to_user_id": null, 115 | "in_reply_to_user_id_str": null, 116 | "is_quote_status": false, 117 | "lang": "en", 118 | "place": null, 119 | "possibly_sensitive": false, 120 | "retweet_count": 606, 121 | "retweeted": false, 122 | "source": "Twitter for iPhone", 123 | "text": "How about a grizzly bear waving for the camera @LakeClarkNPS to end the day? Photo: Kevin Dietrich http://t.co/HtdvV0bPEu", 124 | "truncated": false, 125 | "user": { 126 | "id": 76348185, 127 | "id_str": "76348185" 128 | } 129 | } 130 | }, 131 | "response": { 132 | "position": { 133 | "max_position": "371578415352947200", 134 | "min_position": "371578380871797248", 135 | "was_truncated": false 136 | }, 137 | "timeline": [ 138 | { 139 | "feature_context": "HBgGY3VzdG9tFoCAktzo1NL8DgAA", 140 | "tweet": { 141 | "id": "504032379045179393", 142 | "sort_index": "371578415352947200" 143 | } 144 | }, 145 | { 146 | "feature_context": "HBgGY3VzdG9tFoCAktzo1NL8DgAA", 147 | "tweet": { 148 | "id": "532654992071852032", 149 | "sort_index": "371578393139797760" 150 | } 151 | }, 152 | { 153 | "feature_context": "HBgGY3VzdG9tFoCAktzo1NL8DgAA", 154 | "tweet": { 155 | "id": "524573263163572224", 156 | "sort_index": "371578380871797248" 157 | } 158 | } 159 | ], 160 | "timeline_id": "custom-539487832448843776" 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/generator/template-models/geo-reverse-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "query": { 3 | "params": { 4 | "accuracy": 0, 5 | "coordinates": { 6 | "coordinates": [-122.42284884, 37.76893497], 7 | "type": "Point" 8 | }, 9 | "granularity": "neighborhood" 10 | }, 11 | "type": "reverse_geocode", 12 | "url": "https://api.twitter.com/1.1/geo/reverse_geocode.json?accuracy=0&granularity=neighborhood&lat=37.76893497&long=-122.42284884" 13 | }, 14 | "result": { 15 | "places": [ 16 | { 17 | "attributes": {}, 18 | "bounding_box": { 19 | "coordinates": [ 20 | [ 21 | [-122.42676492, 37.75983003], 22 | [-122.420736, 37.75983003], 23 | [-122.420736, 37.77226299], 24 | [-122.42676492, 37.77226299] 25 | ] 26 | ], 27 | "type": "Polygon" 28 | }, 29 | "contained_within": [ 30 | { 31 | "attributes": {}, 32 | "bounding_box": { 33 | "coordinates": [ 34 | [ 35 | [-122.51368188, 37.70813196], 36 | [-122.35845384, 37.70813196], 37 | [-122.35845384, 37.83245301], 38 | [-122.51368188, 37.83245301] 39 | ] 40 | ], 41 | "type": "Polygon" 42 | }, 43 | "country": "United States", 44 | "country_code": "US", 45 | "full_name": "San Francisco, CA", 46 | "id": "5a110d312052166f", 47 | "name": "San Francisco", 48 | "place_type": "city", 49 | "url": "https://api.twitter.com/1.1/geo/id/5a110d312052166f.json" 50 | } 51 | ], 52 | "country": "United States", 53 | "country_code": "US", 54 | "full_name": "Mission Dolores, San Francisco", 55 | "id": "cf7afb4ee6011bca", 56 | "name": "Mission Dolores", 57 | "place_type": "neighborhood", 58 | "url": "https://api.twitter.com/1.1/geo/id/cf7afb4ee6011bca.json" 59 | }, 60 | { 61 | "attributes": {}, 62 | "bounding_box": { 63 | "coordinates": [ 64 | [ 65 | [-122.51368188, 37.70813196], 66 | [-122.35845384, 37.70813196], 67 | [-122.35845384, 37.83245301], 68 | [-122.51368188, 37.83245301] 69 | ] 70 | ], 71 | "type": "Polygon" 72 | }, 73 | "contained_within": [ 74 | { 75 | "attributes": {}, 76 | "bounding_box": { 77 | "coordinates": [ 78 | [ 79 | [-124.482003, 32.528832], 80 | [-114.131211, 32.528832], 81 | [-114.131211, 42.009517], 82 | [-124.482003, 42.009517] 83 | ] 84 | ], 85 | "type": "Polygon" 86 | }, 87 | "country": "United States", 88 | "country_code": "US", 89 | "full_name": "California, US", 90 | "id": "fbd6d2f5a4e4a15e", 91 | "name": "California", 92 | "place_type": "admin", 93 | "url": "https://api.twitter.com/1.1/geo/id/fbd6d2f5a4e4a15e.json" 94 | } 95 | ], 96 | "country": "United States", 97 | "country_code": "US", 98 | "full_name": "San Francisco, CA", 99 | "id": "5a110d312052166f", 100 | "name": "San Francisco", 101 | "place_type": "city", 102 | "url": "https://api.twitter.com/1.1/geo/id/5a110d312052166f.json" 103 | }, 104 | { 105 | "attributes": {}, 106 | "bounding_box": { 107 | "coordinates": [ 108 | [ 109 | [-124.482003, 32.528832], 110 | [-114.131211, 32.528832], 111 | [-114.131211, 42.009517], 112 | [-124.482003, 42.009517] 113 | ] 114 | ], 115 | "type": "Polygon" 116 | }, 117 | "contained_within": [ 118 | { 119 | "attributes": {}, 120 | "bounding_box": null, 121 | "country": "United States", 122 | "country_code": "US", 123 | "full_name": "United States", 124 | "id": "96683cc9126741d1", 125 | "name": "United States", 126 | "place_type": "country", 127 | "url": "https://api.twitter.com/1.1/geo/id/96683cc9126741d1.json" 128 | } 129 | ], 130 | "country": "United States", 131 | "country_code": "US", 132 | "full_name": "California, US", 133 | "id": "fbd6d2f5a4e4a15e", 134 | "name": "California", 135 | "place_type": "admin", 136 | "url": "https://api.twitter.com/1.1/geo/id/fbd6d2f5a4e4a15e.json" 137 | }, 138 | { 139 | "attributes": {}, 140 | "bounding_box": null, 141 | "contained_within": [], 142 | "country": "United States", 143 | "country_code": "US", 144 | "full_name": "United States", 145 | "id": "96683cc9126741d1", 146 | "name": "United States", 147 | "place_type": "country", 148 | "url": "https://api.twitter.com/1.1/geo/id/96683cc9126741d1.json" 149 | } 150 | ] 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/generator/template-models/geo-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "df51dec6f4ee2b2c", 3 | "url": "https://api.twitter.com/1.1/geo/id/df51dec6f4ee2b2c.json", 4 | "place_type": "neighborhood", 5 | "name": "Presidio", 6 | "full_name": "Presidio, San Francisco", 7 | "country_code": "US", 8 | "country": "United States", 9 | "contained_within": [ 10 | { 11 | "id": "5a110d312052166f", 12 | "url": "https://api.twitter.com/1.1/geo/id/5a110d312052166f.json", 13 | "place_type": "city", 14 | "name": "San Francisco", 15 | "full_name": "San Francisco, CA", 16 | "country_code": "US", 17 | "country": "United States", 18 | "centroid": [-122.4461400159226, 37.759828999999996], 19 | "bounding_box": { 20 | "type": "Polygon", 21 | "coordinates": [ 22 | [ 23 | [-122.514926, 37.708075], 24 | [-122.514926, 37.833238], 25 | [-122.357031, 37.833238], 26 | [-122.357031, 37.708075], 27 | [-122.514926, 37.708075] 28 | ] 29 | ] 30 | }, 31 | "attributes": {} 32 | } 33 | ], 34 | "geometry": null, 35 | "polylines": [], 36 | "centroid": [-122.46598425785236, 37.79989625], 37 | "bounding_box": { 38 | "type": "Polygon", 39 | "coordinates": [ 40 | [ 41 | [-122.4891333, 37.786925], 42 | [-122.4891333, 37.8128675], 43 | [-122.446306, 37.8128675], 44 | [-122.446306, 37.786925], 45 | [-122.4891333, 37.786925] 46 | ] 47 | ] 48 | }, 49 | "attributes": { 50 | "geotagCount": "6", 51 | "162834:id": "2202" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/generator/template-models/list-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 715919216927322112, 3 | "id_str": "715919216927322112", 4 | "name": "National-parks", 5 | "uri": "/TwitterDev/lists/national-parks", 6 | "subscriber_count": 92, 7 | "member_count": 8, 8 | "mode": "public", 9 | "description": "", 10 | "slug": "national-parks", 11 | "full_name": "@TwitterDev/national-parks", 12 | "created_at": "Fri Apr 01 15:10:17 +0000 2016", 13 | "following": false, 14 | "user": { 15 | "id": 2244994945, 16 | "id_str": "2244994945", 17 | "name": "Twitter Dev", 18 | "screen_name": "TwitterDev", 19 | "location": "Internet", 20 | "description": "Your official source for Twitter Platform news, updates & events. Need technical help? Visit https://t.co/mGHnxZU8c1 ⌨️ #TapIntoTwitter", 21 | "url": "https://t.co/FGl7VOULyL", 22 | "entities": { 23 | "url": { 24 | "urls": [ 25 | { 26 | "url": "https://t.co/FGl7VOULyL", 27 | "expanded_url": "https://developer.twitter.com/", 28 | "display_url": "developer.twitter.com", 29 | "indices": [0, 23] 30 | } 31 | ] 32 | }, 33 | "description": { 34 | "urls": [ 35 | { 36 | "url": "https://t.co/mGHnxZU8c1", 37 | "expanded_url": "https://twittercommunity.com/", 38 | "display_url": "twittercommunity.com", 39 | "indices": [93, 116] 40 | } 41 | ] 42 | } 43 | }, 44 | "protected": false, 45 | "followers_count": 502084, 46 | "friends_count": 1472, 47 | "listed_count": 1514, 48 | "created_at": "Sat Dec 14 04:35:55 +0000 2013", 49 | "favourites_count": 2203, 50 | "utc_offset": null, 51 | "time_zone": null, 52 | "geo_enabled": true, 53 | "verified": true, 54 | "statuses_count": 3393, 55 | "lang": "en", 56 | "contributors_enabled": false, 57 | "is_translator": false, 58 | "is_translation_enabled": null, 59 | "profile_background_color": "null", 60 | "profile_background_image_url": "null", 61 | "profile_background_image_url_https": "null", 62 | "profile_background_tile": null, 63 | "profile_image_url": "null", 64 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/880136122604507136/xHrnqf1T_normal.jpg", 65 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/2244994945/1498675817", 66 | "profile_link_color": "null", 67 | "profile_sidebar_border_color": "null", 68 | "profile_sidebar_fill_color": "null", 69 | "profile_text_color": "null", 70 | "profile_use_background_image": false, 71 | "has_extended_profile": null, 72 | "default_profile": false, 73 | "default_profile_image": false, 74 | "following": false, 75 | "follow_request_sent": false, 76 | "notifications": false, 77 | "translator_type": "null" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/generator/template-models/mention-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "coordinates": null, 3 | "favorited": false, 4 | "truncated": false, 5 | "created_at": "Mon Sep 03 13:24:14 +0000 2012", 6 | "id_str": "242613977966850048", 7 | "entities": { 8 | "urls": [], 9 | "hashtags": [], 10 | "user_mentions": [ 11 | { 12 | "name": "Jason Costa", 13 | "id_str": "14927800", 14 | "id": 14927800, 15 | "indices": [0, 11], 16 | "screen_name": "jasoncosta" 17 | }, 18 | { 19 | "name": "Matt Harris", 20 | "id_str": "777925", 21 | "id": 777925, 22 | "indices": [12, 26], 23 | "screen_name": "themattharris" 24 | }, 25 | { 26 | "name": "ThinkWall", 27 | "id_str": "117426578", 28 | "id": 117426578, 29 | "indices": [109, 119], 30 | "screen_name": "thinkwall" 31 | } 32 | ] 33 | }, 34 | "in_reply_to_user_id_str": "14927800", 35 | "contributors": null, 36 | "text": "@jasoncosta @themattharris Hey! Going to be in Frisco in October. Was hoping to have a meeting to talk about @thinkwall if you're around?", 37 | "retweet_count": 0, 38 | "in_reply_to_status_id_str": null, 39 | "id": 242613977966850048, 40 | "geo": null, 41 | "retweeted": false, 42 | "in_reply_to_user_id": 14927800, 43 | "place": null, 44 | "user": { 45 | "profile_sidebar_fill_color": "EEEEEE", 46 | "profile_sidebar_border_color": "000000", 47 | "profile_background_tile": false, 48 | "name": "Andrew Spode Miller", 49 | "profile_image_url": "http://a0.twimg.com/profile_images/1227466231/spode-balloon-medium_normal.jpg", 50 | "created_at": "Mon Sep 22 13:12:01 +0000 2008", 51 | "location": "London via Gravesend", 52 | "follow_request_sent": false, 53 | "profile_link_color": "F31B52", 54 | "is_translator": false, 55 | "id_str": "16402947", 56 | "entities": { 57 | "url": { 58 | "urls": [ 59 | { 60 | "expanded_url": null, 61 | "url": "http://www.linkedin.com/in/spode", 62 | "indices": [0, 32] 63 | } 64 | ] 65 | }, 66 | "description": { 67 | "urls": [] 68 | } 69 | }, 70 | "default_profile": false, 71 | "contributors_enabled": false, 72 | "favourites_count": 16, 73 | "url": "http://www.linkedin.com/in/spode", 74 | "profile_image_url_https": "https://si0.twimg.com/profile_images/1227466231/spode-balloon-medium_normal.jpg", 75 | "utc_offset": 0, 76 | "id": 16402947, 77 | "profile_use_background_image": false, 78 | "listed_count": 129, 79 | "profile_text_color": "262626", 80 | "lang": "en", 81 | "followers_count": 2013, 82 | "protected": false, 83 | "notifications": null, 84 | "profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/16420220/twitter-background-final.png", 85 | "profile_background_color": "FFFFFF", 86 | "verified": false, 87 | "geo_enabled": true, 88 | "time_zone": "London", 89 | "description": "Co-Founder/Dev (PHP/jQuery) @justFDI. Run @thinkbikes and @thinkwall for events. Ex tech journo, helps run @uktjpr. Passion for Linux and customises everything.", 90 | "default_profile_image": false, 91 | "profile_background_image_url": "http://a0.twimg.com/profile_background_images/16420220/twitter-background-final.png", 92 | "statuses_count": 11550, 93 | "friends_count": 770, 94 | "following": null, 95 | "show_all_inline_media": true, 96 | "screen_name": "spode" 97 | }, 98 | "in_reply_to_screen_name": "jasoncosta", 99 | "source": "JournoTwit", 100 | "in_reply_to_status_id": null 101 | } 102 | -------------------------------------------------------------------------------- /src/generator/template-models/size-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "sizes": { 3 | "ipad": { 4 | "h": 313, 5 | "w": 626, 6 | "url": "https://pbs.twimg.com/profile_banners/6253282/1347394302/ipad" 7 | }, 8 | "ipad_retina": { 9 | "h": 626, 10 | "w": 1252, 11 | "url": "https://pbs.twimg.com/profile_banners/6253282/1347394302/ipad_retina" 12 | }, 13 | "web": { 14 | "h": 260, 15 | "w": 520, 16 | "url": "https://pbs.twimg.com/profile_banners/6253282/1347394302/web" 17 | }, 18 | "web_retina": { 19 | "h": 520, 20 | "w": 1040, 21 | "url": "https://pbs.twimg.com/profile_banners/6253282/1347394302/web_retina" 22 | }, 23 | "mobile": { 24 | "h": 160, 25 | "w": 320, 26 | "url": "https://pbs.twimg.com/profile_banners/6253282/1347394302/mobile" 27 | }, 28 | "mobile_retina": { 29 | "h": 320, 30 | "w": 640, 31 | "url": "https://pbs.twimg.com/profile_banners/6253282/1347394302/mobile_retina" 32 | }, 33 | "300x100": { 34 | "h": 100, 35 | "w": 300, 36 | "url": "https://pbs.twimg.com/profile_banners/6253282/1347394302/300x100" 37 | }, 38 | "600x200": { 39 | "h": 200, 40 | "w": 600, 41 | "url": "https://pbs.twimg.com/profile_banners/6253282/1347394302/600x200" 42 | }, 43 | "1500x500": { 44 | "h": 500, 45 | "w": 1500, 46 | "url": "https://pbs.twimg.com/profile_banners/6253282/1347394302/1500x500" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/generator/template-models/tweet-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "created_at": "Tue Aug 11 05:14:08 +0000 2020", 3 | "id": 1293053062353948673, 4 | "id_str": "1293053062353948673", 5 | "text": "An amazing giveaway from an amazing person! Good luck for the winner!\n\n(RT just for reach) https://t.co/8IAbWVkh4A", 6 | "full_text": "An amazing giveaway from an amazing person! Good luck for the winner!\n\n(RT just for reach). I hope someone will take this home!", 7 | "truncated": false, 8 | "entities": { 9 | "hashtags": [], 10 | "symbols": [], 11 | "user_mentions": [], 12 | "urls": [ 13 | { 14 | "url": "https://t.co/8IAbWVkh4A", 15 | "expanded_url": "https://twitter.com/SimonHoiberg/status/1292831801648525314", 16 | "display_url": "twitter.com/SimonHoiberg/s…", 17 | "indices": [91, 114] 18 | } 19 | ] 20 | }, 21 | "extended_entities": { 22 | "media": [ 23 | { 24 | "id": 1293565706408038400, 25 | "id_str": "1293565706408038401", 26 | "indices": [219, 242], 27 | "additional_media_info": { 28 | "monetizable": false 29 | }, 30 | "media_url": "http://pbs.twimg.com/ext_tw_video_thumb/1293565706408038401/pu/img/66P2dvbU4a02jYbV.jpg", 31 | "media_url_https": "https://pbs.twimg.com/ext_tw_video_thumb/1293565706408038401/pu/img/66P2dvbU4a02jYbV.jpg", 32 | "url": "https://t.co/KaFSbjWUA8", 33 | "display_url": "pic.twitter.com/KaFSbjWUA8", 34 | "expanded_url": "https://twitter.com/TwitterDev/status/1293593516040269825/video/1", 35 | "type": "video", 36 | "video_info": { 37 | "aspect_ratio": [16, 9], 38 | "duration_millis": 34875, 39 | "variants": [ 40 | { 41 | "bitrate": 256000, 42 | "content_type": "video/mp4", 43 | "url": "https://video.twimg.com/ext_tw_video/1293565706408038401/pu/vid/480x270/Fg9lnGGsITO0uq2K.mp4?tag=10" 44 | }, 45 | { 46 | "bitrate": 832000, 47 | "content_type": "video/mp4", 48 | "url": "https://video.twimg.com/ext_tw_video/1293565706408038401/pu/vid/640x360/-crbtZE4y8vKN_uF.mp4?tag=10" 49 | }, 50 | { 51 | "content_type": "application/x-mpegURL", 52 | "url": "https://video.twimg.com/ext_tw_video/1293565706408038401/pu/pl/OvIqQojosF6sMIHR.m3u8?tag=10" 53 | }, 54 | { 55 | "bitrate": 2176000, 56 | "content_type": "video/mp4", 57 | "url": "https://video.twimg.com/ext_tw_video/1293565706408038401/pu/vid/1280x720/xkxyb-VPVY4OI0j9.mp4?tag=10" 58 | } 59 | ] 60 | }, 61 | "sizes": { 62 | "thumb": { 63 | "w": 150, 64 | "h": 150, 65 | "resize": "crop" 66 | }, 67 | "medium": { 68 | "w": 1200, 69 | "h": 675, 70 | "resize": "fit" 71 | }, 72 | "small": { 73 | "w": 680, 74 | "h": 383, 75 | "resize": "fit" 76 | }, 77 | "large": { 78 | "w": 1280, 79 | "h": 720, 80 | "resize": "fit" 81 | } 82 | } 83 | }, 84 | { 85 | "id": 861627472244162561, 86 | "id_str": "861627472244162561", 87 | "indices": [68, 91], 88 | "media_url": "http://pbs.twimg.com/media/C_UdnvPUwAE3Dnn.jpg", 89 | "media_url_https": "https://pbs.twimg.com/media/C_UdnvPUwAE3Dnn.jpg", 90 | "url": "https://t.co/9r69akA484", 91 | "display_url": "pic.twitter.com/9r69akA484", 92 | "expanded_url": "https://twitter.com/FloodSocial/status/861627479294746624/photo/1", 93 | "type": "photo", 94 | "sizes": { 95 | "medium": { 96 | "w": 1200, 97 | "h": 900, 98 | "resize": "fit" 99 | }, 100 | "small": { 101 | "w": 680, 102 | "h": 510, 103 | "resize": "fit" 104 | }, 105 | "thumb": { 106 | "w": 150, 107 | "h": 150, 108 | "resize": "crop" 109 | }, 110 | "large": { 111 | "w": 2048, 112 | "h": 1536, 113 | "resize": "fit" 114 | } 115 | } 116 | } 117 | ] 118 | }, 119 | "source": "Twitter for Android", 120 | "in_reply_to_status_id": null, 121 | "in_reply_to_status_id_str": null, 122 | "in_reply_to_user_id": null, 123 | "in_reply_to_user_id_str": null, 124 | "in_reply_to_screen_name": null, 125 | "user": { 126 | "id": 753911312468611072, 127 | "id_str": "753911312468611072", 128 | "name": "Deni Moka⚡", 129 | "screen_name": "dmokafa", 130 | "location": "visit my tech blog 👉 ", 131 | "description": "I help you to build better and elegant software by using\n- #design principles\n- #development methodologies\n- #agile frameworks\n- #programming disciplines", 132 | "url": "https://t.co/lCZW8wZs5C", 133 | "entities": { 134 | "url": { 135 | "urls": [ 136 | { 137 | "url": "https://t.co/lCZW8wZs5C", 138 | "expanded_url": "https://www.danielmoka.com", 139 | "display_url": "danielmoka.com", 140 | "indices": [0, 23] 141 | } 142 | ] 143 | }, 144 | "description": { 145 | "urls": [] 146 | } 147 | }, 148 | "protected": false, 149 | "followers_count": 4585, 150 | "friends_count": 1095, 151 | "listed_count": 41, 152 | "created_at": "Fri Jul 15 11:17:18 +0000 2016", 153 | "favourites_count": 12831, 154 | "utc_offset": null, 155 | "time_zone": null, 156 | "geo_enabled": false, 157 | "verified": false, 158 | "statuses_count": 3660, 159 | "lang": null, 160 | "contributors_enabled": false, 161 | "is_translator": false, 162 | "is_translation_enabled": false, 163 | "profile_background_color": "F5F8FA", 164 | "profile_background_image_url": null, 165 | "profile_background_image_url_https": null, 166 | "profile_background_tile": false, 167 | "profile_image_url": "http://pbs.twimg.com/profile_images/1223174599090810880/0ifheaFc_normal.jpg", 168 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/1223174599090810880/0ifheaFc_normal.jpg", 169 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/753911312468611072/1576097062", 170 | "profile_link_color": "1DA1F2", 171 | "profile_sidebar_border_color": "C0DEED", 172 | "profile_sidebar_fill_color": "DDEEF6", 173 | "profile_text_color": "333333", 174 | "profile_use_background_image": true, 175 | "has_extended_profile": true, 176 | "default_profile": true, 177 | "default_profile_image": false, 178 | "following": true, 179 | "follow_request_sent": false, 180 | "notifications": false, 181 | "translator_type": "none" 182 | }, 183 | "geo": null, 184 | "coordinates": null, 185 | "place": null, 186 | "contributors": null, 187 | "is_quote_status": true, 188 | "quoted_status_id--?": 1292831801648525314, 189 | "quoted_status_id_str--?": "1292831801648525314", 190 | "quoted_status--?": { 191 | "created_at": "Mon Aug 10 14:34:55 +0000 2020", 192 | "id": 1292831801648525314, 193 | "id_str": "1292831801648525314", 194 | "text": "🎁 10K GIVEAWAY 🎁\n\nYou will get:\n📚 The entire @YDKJS book series by @getify \n📦 The complete freelancing bundle by… https://t.co/Y15RmFLHyK", 195 | "truncated": true, 196 | "entities": { 197 | "hashtags": [], 198 | "symbols": [], 199 | "user_mentions": [ 200 | { 201 | "screen_name": "YDKJS", 202 | "name": "You Don't Know JS Yet", 203 | "id": 2412232099, 204 | "id_str": "2412232099", 205 | "indices": [45, 51] 206 | }, 207 | { 208 | "screen_name": "getify", 209 | "name": "getify", 210 | "id": 16686076, 211 | "id_str": "16686076", 212 | "indices": [67, 74] 213 | } 214 | ], 215 | "urls": [ 216 | { 217 | "url": "https://t.co/Y15RmFLHyK", 218 | "expanded_url": "https://twitter.com/i/web/status/1292831801648525314", 219 | "display_url": "twitter.com/i/web/status/1…", 220 | "indices": [114, 137] 221 | } 222 | ] 223 | }, 224 | "source": "Twitter Web App", 225 | "in_reply_to_status_id": null, 226 | "in_reply_to_status_id_str": null, 227 | "in_reply_to_user_id": null, 228 | "in_reply_to_user_id_str": null, 229 | "in_reply_to_screen_name": null, 230 | "user": { 231 | "id": 875776212341329920, 232 | "id_str": "875776212341329920", 233 | "name": "Simon Høiberg", 234 | "screen_name": "SimonHoiberg", 235 | "location": "Copenhagen, Denmark", 236 | "description": "Software Engineer ‧ Freelancer ‧ Starter.\nCreator → @sigmetic @direflowjs\n\n💡 Tips on how to become a better developer, and to start your own business.", 237 | "url": "https://t.co/7tFaIY51dW", 238 | "entities": { 239 | "url": { 240 | "urls": [ 241 | { 242 | "url": "https://t.co/7tFaIY51dW", 243 | "expanded_url": "http://www.silind.com", 244 | "display_url": "silind.com", 245 | "indices": [0, 23] 246 | } 247 | ] 248 | }, 249 | "description": { 250 | "urls": [] 251 | } 252 | }, 253 | "protected": false, 254 | "followers_count": 11226, 255 | "friends_count": 268, 256 | "listed_count": 65, 257 | "created_at": "Fri Jun 16 18:04:54 +0000 2017", 258 | "favourites_count": 9173, 259 | "utc_offset": null, 260 | "time_zone": null, 261 | "geo_enabled": false, 262 | "verified": false, 263 | "statuses_count": 2969, 264 | "lang": null, 265 | "contributors_enabled": false, 266 | "is_translator": false, 267 | "is_translation_enabled": false, 268 | "profile_background_color": "000000", 269 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", 270 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", 271 | "profile_background_tile": false, 272 | "profile_image_url": "http://pbs.twimg.com/profile_images/1082926208516530176/TsBWPhaf_normal.jpg", 273 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/1082926208516530176/TsBWPhaf_normal.jpg", 274 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/875776212341329920/1595152862", 275 | "profile_link_color": "FF691F", 276 | "profile_sidebar_border_color": "000000", 277 | "profile_sidebar_fill_color": "000000", 278 | "profile_text_color": "000000", 279 | "profile_use_background_image": false, 280 | "has_extended_profile": true, 281 | "default_profile": false, 282 | "default_profile_image": false, 283 | "following": false, 284 | "follow_request_sent": false, 285 | "notifications": false, 286 | "translator_type": "none" 287 | }, 288 | "geo": null, 289 | "coordinates": null, 290 | "place": null, 291 | "contributors": null, 292 | "is_quote_status": false, 293 | "retweet_count": 604, 294 | "favorite_count": 2174, 295 | "favorited": false, 296 | "retweeted": false, 297 | "possibly_sensitive": false, 298 | "lang": "en" 299 | }, 300 | "retweet_count": 0, 301 | "favorite_count": 11, 302 | "favorited": true, 303 | "retweeted": false, 304 | "possibly_sensitive": false, 305 | "lang": "en", 306 | "retweeted_status--?": { 307 | "created_at": "Fri Oct 15 00:35:07 +0000 2021", 308 | "id": 1448809628934934530, 309 | "id_str": "1448809628934934530", 310 | "text": "@nejsnave Yes and we were all really proud that the first film was @Adbusters readers top film along with Fight Clu… https://t.co/VWyvOeTwPy", 311 | "truncated": true, 312 | "entities": { 313 | "hashtags": [ 314 | { 315 | "text": "NowPlaying", 316 | "indices": [0, 11] 317 | }, 318 | { 319 | "text": "listen", 320 | "indices": [67, 74] 321 | } 322 | ], 323 | "symbols": [], 324 | "user_mentions": [ 325 | { 326 | "screen_name": "nejsnave", 327 | "name": "jennifer evans", 328 | "id": 12923812, 329 | "id_str": "12923812", 330 | "indices": [0, 9] 331 | }, 332 | { 333 | "screen_name": "Adbusters", 334 | "name": "Adbusters", 335 | "id": 14927297, 336 | "id_str": "14927297", 337 | "indices": [67, 77] 338 | } 339 | ], 340 | "urls": [ 341 | { 342 | "url": "https://t.co/VWyvOeTwPy", 343 | "expanded_url": "https://twitter.com/i/web/status/1448809628934934530", 344 | "display_url": "twitter.com/i/web/status/1…", 345 | "indices": [117, 140] 346 | } 347 | ] 348 | }, 349 | "source": "Twitter Web App", 350 | "in_reply_to_status_id": 1448808898979303424, 351 | "in_reply_to_status_id_str": "1448808898979303424", 352 | "in_reply_to_user_id": 12923812, 353 | "in_reply_to_user_id_str": "12923812", 354 | "in_reply_to_screen_name": "nejsnave", 355 | "user": { 356 | "id": 15275330, 357 | "id_str": "15275330", 358 | "name": "katatcoolworld", 359 | "screen_name": "katatcoolworld", 360 | "location": "Vancouver, Canada", 361 | "description": "Un-settler on unceded Coast Salish Territories. CEO @cooldotworld, Impact Producer for @Corporationfilm support our #1000Screenings; Plaintiff vs.Twitter", 362 | "url": "https://t.co/OAe7DPXfN0", 363 | "entities": { 364 | "url": { 365 | "urls": [ 366 | { 367 | "url": "https://t.co/OAe7DPXfN0", 368 | "expanded_url": "https://www.patreon.com/TheNewCorporation", 369 | "display_url": "patreon.com/TheNewCorporat…", 370 | "indices": [0, 23] 371 | } 372 | ] 373 | }, 374 | "description": { 375 | "urls": [ 376 | { 377 | "url": "https://t.co/VWyvOeTwPy", 378 | "expanded_url": "https://twitter.com/i/web/status/1448809628934934530", 379 | "display_url": "twitter.com/i/web/status/1…", 380 | "indices": [117, 140] 381 | } 382 | ] 383 | } 384 | }, 385 | "protected": false, 386 | "followers_count": 1162, 387 | "friends_count": 2094, 388 | "listed_count": 61, 389 | "created_at": "Mon Jun 30 02:27:10 +0000 2008", 390 | "favourites_count": 560, 391 | "utc_offset": null, 392 | "time_zone": null, 393 | "geo_enabled": false, 394 | "verified": false, 395 | "statuses_count": 8781, 396 | "lang": null, 397 | "contributors_enabled": false, 398 | "is_translator": false, 399 | "is_translation_enabled": false, 400 | "profile_background_color": "03FFE6", 401 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme3/bg.gif", 402 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme3/bg.gif", 403 | "profile_background_tile": true, 404 | "profile_image_url": "http://pbs.twimg.com/profile_images/378800000540170817/0264c99b62a279dd171375fb1dd09c40_normal.jpeg", 405 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/378800000540170817/0264c99b62a279dd171375fb1dd09c40_normal.jpeg", 406 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/15275330/1634219939", 407 | "profile_link_color": "2D86BD", 408 | "profile_sidebar_border_color": "FFFFFF", 409 | "profile_sidebar_fill_color": "E9F2EA", 410 | "profile_text_color": "141413", 411 | "profile_use_background_image": true, 412 | "has_extended_profile": false, 413 | "default_profile": false, 414 | "default_profile_image": false, 415 | "following": false, 416 | "follow_request_sent": false, 417 | "notifications": false, 418 | "translator_type": "none", 419 | "withheld_in_countries": [] 420 | }, 421 | "geo": null, 422 | "coordinates": null, 423 | "place": null, 424 | "contributors": null, 425 | "is_quote_status": true, 426 | "quoted_status_id--?": 1078376385440145408, 427 | "quoted_status_id_str--?": "1078376385440145408", 428 | "quoted_status--?": { 429 | "created_at": "Thu Dec 27 19:45:40 +0000 2018", 430 | "id": 1078376385440145408, 431 | "id_str": "1078376385440145408", 432 | "text": "When my kids are older I'm going to encourage them to go to @LambdaSchool. Following @AustenAllred & reading about… https://t.co/5zZNpqlxim", 433 | "truncated": true, 434 | "entities": { 435 | "hashtags": [ 436 | { 437 | "text": "NowPlaying", 438 | "indices": [0, 11] 439 | }, 440 | { 441 | "text": "listen", 442 | "indices": [67, 74] 443 | } 444 | ], 445 | "symbols": [], 446 | "user_mentions": [ 447 | { 448 | "screen_name": "LambdaSchool", 449 | "name": "Lambda School", 450 | "id": 733318062754004992, 451 | "id_str": "733318062754004992", 452 | "indices": [60, 73] 453 | }, 454 | { 455 | "screen_name": "AustenAllred", 456 | "name": "Austen Allred", 457 | "id": 1093528151123095552, 458 | "id_str": "1093528151123095552", 459 | "indices": [85, 98] 460 | } 461 | ], 462 | "urls": [ 463 | { 464 | "url": "https://t.co/5zZNpqlxim", 465 | "expanded_url": "https://twitter.com/i/web/status/1078376385440145408", 466 | "display_url": "twitter.com/i/web/status/1…", 467 | "indices": [120, 143] 468 | } 469 | ] 470 | }, 471 | "source": "Twitter Web Client", 472 | "in_reply_to_status_id": null, 473 | "in_reply_to_status_id_str": null, 474 | "in_reply_to_user_id": null, 475 | "in_reply_to_user_id_str": null, 476 | "in_reply_to_screen_name": null, 477 | "user": { 478 | "id": 1398479138, 479 | "id_str": "1398479138", 480 | "name": "Claire Lehmann", 481 | "screen_name": "clairlemon", 482 | "location": "Sydney, Australia", 483 | "description": "Founder @Quillette. Contributor @Australian.", 484 | "url": "https://t.co/fMrptONv8n", 485 | "entities": { 486 | "url": { 487 | "urls": [ 488 | { 489 | "url": "https://t.co/fMrptONv8n", 490 | "expanded_url": "https://linktr.ee/clairelehmann", 491 | "display_url": "linktr.ee/clairelehmann", 492 | "indices": [0, 23] 493 | } 494 | ] 495 | }, 496 | "description": { 497 | "urls": [ 498 | { 499 | "url": "https://t.co/fMrptONv8n", 500 | "expanded_url": "https://linktr.ee/clairelehmann", 501 | "display_url": "linktr.ee/clairelehmann", 502 | "indices": [0, 23] 503 | } 504 | ] 505 | } 506 | }, 507 | "protected": false, 508 | "followers_count": 227062, 509 | "friends_count": 4522, 510 | "listed_count": 2415, 511 | "created_at": "Fri May 03 00:18:34 +0000 2013", 512 | "favourites_count": 65670, 513 | "utc_offset": null, 514 | "time_zone": null, 515 | "geo_enabled": true, 516 | "verified": true, 517 | "statuses_count": 23557, 518 | "lang": null, 519 | "contributors_enabled": false, 520 | "is_translator": false, 521 | "is_translation_enabled": false, 522 | "profile_background_color": "EDECE9", 523 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme3/bg.gif", 524 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme3/bg.gif", 525 | "profile_background_tile": false, 526 | "profile_image_url": "http://pbs.twimg.com/profile_images/1370860449034498051/RaVOfBmt_normal.jpg", 527 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/1370860449034498051/RaVOfBmt_normal.jpg", 528 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/1398479138/1631429771", 529 | "profile_link_color": "088253", 530 | "profile_sidebar_border_color": "FFFFFF", 531 | "profile_sidebar_fill_color": "E3E2DE", 532 | "profile_text_color": "634047", 533 | "profile_use_background_image": true, 534 | "has_extended_profile": true, 535 | "default_profile": false, 536 | "default_profile_image": false, 537 | "following": false, 538 | "follow_request_sent": false, 539 | "notifications": false, 540 | "translator_type": "none", 541 | "withheld_in_countries": [] 542 | }, 543 | "geo": null, 544 | "coordinates": null, 545 | "place": null, 546 | "contributors": null, 547 | "is_quote_status": false, 548 | "retweet_count": 25, 549 | "favorite_count": 373, 550 | "favorited": false, 551 | "retweeted": false, 552 | "lang": "en" 553 | }, 554 | "retweet_count": 1, 555 | "favorite_count": 1, 556 | "favorited": false, 557 | "retweeted": false, 558 | "lang": "en" 559 | } 560 | } 561 | -------------------------------------------------------------------------------- /src/generator/template-models/user-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2244994945, 3 | "id_str": "2244994945", 4 | "name": "Twitter Dev", 5 | "screen_name": "TwitterDev", 6 | "location": "Internet", 7 | "description": "Your official source for Twitter Platform news, updates & events. Need technical help? Visit https://t.co/mGHnxZU8c1 ⌨️ #TapIntoTwitter", 8 | "url": "https://t.co/FGl7VOULyL", 9 | "entities": { 10 | "url": { 11 | "urls": [ 12 | { 13 | "url": "https://t.co/FGl7VOULyL", 14 | "expanded_url": "https://developer.twitter.com/", 15 | "display_url": "developer.twitter.com", 16 | "indices": [ 17 | 0, 18 | 23 19 | ] 20 | } 21 | ] 22 | }, 23 | "description": { 24 | "urls": [ 25 | { 26 | "url": "https://t.co/mGHnxZU8c1", 27 | "expanded_url": "https://twittercommunity.com/", 28 | "display_url": "twittercommunity.com", 29 | "indices": [ 30 | 93, 31 | 116 32 | ] 33 | } 34 | ] 35 | } 36 | }, 37 | "protected": false, 38 | "followers_count": 502017, 39 | "friends_count": 1472, 40 | "listed_count": 1513, 41 | "created_at": "Sat Dec 14 04:35:55 +0000 2013", 42 | "favourites_count": 2203, 43 | "utc_offset": null, 44 | "time_zone": null, 45 | "geo_enabled": true, 46 | "verified": true, 47 | "statuses_count": 3393, 48 | "lang": "en", 49 | "status": { 50 | "created_at": "Tue May 14 17:54:29 +0000 2019", 51 | "id": 1128357932238823424, 52 | "id_str": "1128357932238823424", 53 | "text": "We’ll release the first Labs endpoints to all eligible developers in the coming weeks. If you want to participate,… https://t.co/8q8sj87D5a", 54 | "truncated": true, 55 | "entities": { 56 | "hashtags": [], 57 | "symbols": [], 58 | "user_mentions": [], 59 | "urls": [ 60 | { 61 | "url": "https://t.co/8q8sj87D5a", 62 | "expanded_url": "https://twitter.com/i/web/status/1128357932238823424", 63 | "display_url": "twitter.com/i/web/status/1…", 64 | "indices": [ 65 | 116, 66 | 139 67 | ] 68 | } 69 | ] 70 | }, 71 | "source": "Twitter Web App", 72 | "in_reply_to_status_id": 1128357931026501633, 73 | "in_reply_to_status_id_str": "1128357931026501633", 74 | "in_reply_to_user_id": 2244994945, 75 | "in_reply_to_user_id_str": "2244994945", 76 | "in_reply_to_screen_name": "TwitterDev", 77 | "geo": null, 78 | "coordinates": null, 79 | "place": null, 80 | "contributors": null, 81 | "is_quote_status": false, 82 | "retweet_count": 12, 83 | "favorite_count": 37, 84 | "favorited": false, 85 | "retweeted": false, 86 | "possibly_sensitive": false, 87 | "lang": "en" 88 | }, 89 | "contributors_enabled": false, 90 | "is_translator": false, 91 | "is_translation_enabled": null, 92 | "profile_background_color": "null", 93 | "profile_background_image_url": "null", 94 | "profile_background_image_url_https": "null", 95 | "profile_background_tile": null, 96 | "profile_image_url": "null", 97 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/880136122604507136/xHrnqf1T_normal.jpg", 98 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/2244994945/1498675817", 99 | "profile_link_color": "null", 100 | "profile_sidebar_border_color": "null", 101 | "profile_sidebar_fill_color": "null", 102 | "profile_text_color": "null", 103 | "profile_use_background_image": null, 104 | "has_extended_profile": null, 105 | "default_profile": false, 106 | "default_profile_image": false, 107 | "following": false, 108 | "follow_request_sent": false, 109 | "notifications": false, 110 | "translator_type": "regular" 111 | } -------------------------------------------------------------------------------- /src/generator/writeClients.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { resolve } from 'path'; 3 | import IReferenceDirectory from '../interfaces/IReferenceDirectory'; 4 | import { createCamelCaseTitle } from '../utils/utils'; 5 | import { removeHttpVerbs, startWithHttpVerb, getMethodName } from '../base/httpVerbs'; 6 | 7 | function writeClients(dictionary: IReferenceDirectory[]) { 8 | const generatedPath = resolve(__dirname, '../../generated'); 9 | const clientsPath = `${generatedPath}/clients`; 10 | 11 | let superClientFile = `import IClientOptions from './base/IClientOptions';\n`; 12 | superClientFile += `import Transport from './base/Transport';\n\n`; 13 | const clients: string[] = []; 14 | 15 | dictionary.forEach((g) => { 16 | const fileName = createCamelCaseTitle(g.title); 17 | const interfacesParamsImportMap: { [key: string]: string[] } = {}; 18 | const interfacesTypesImports: string[] = []; 19 | const clientMethods: string[] = []; 20 | 21 | clients.push(`${fileName}Client`); 22 | 23 | let clientFile = ``; 24 | 25 | g.subgroups.forEach((s) => { 26 | const interfacesParamsImports: string[] = []; 27 | const subgroupFileName = createCamelCaseTitle(s.title); 28 | 29 | s.endpoints.forEach((e) => { 30 | if (!startWithHttpVerb(e.title)) { 31 | console.log(`❌ "${e.title}" not added. Verb is missing`); 32 | return; 33 | } 34 | const interfaceName = createCamelCaseTitle(removeHttpVerbs(e.title)); 35 | const methodName = interfaceName.replace(/^./, interfaceName[0].toLowerCase()); 36 | 37 | if (e.parameters) { 38 | interfacesParamsImports.push(`${interfaceName}Params`); 39 | } 40 | 41 | if (e.exampleResponse) { 42 | interfacesTypesImports.push(interfaceName); 43 | } 44 | 45 | const optional = !e.parameters?.some((p) => p.required) ? '?' : ''; 46 | const signature = e.parameters ? `(parameters${optional}: ${interfaceName}Params)` : '()'; 47 | const doMethod = getMethodName(e.title); 48 | const listed = e.exampleResponse?.startsWith('[') ? '[]' : ''; 49 | const returnType = e.exampleResponse ? `<${interfaceName}${listed}>` : ''; 50 | 51 | const matchPathParam = e.resourceUrl.match(/(:[a-zA-Z_]+)/); 52 | 53 | const pathParam = matchPathParam?.[0] ?? ''; 54 | const resourceUrl = pathParam 55 | ? e.resourceUrl.replace(pathParam, `' + parameters.${pathParam.slice(1)} + '`) 56 | : e.resourceUrl; 57 | 58 | let method = ' /**\n'; 59 | method += ` * ${e.description?.replace(/\n/g, ' ')}\n`; 60 | method += ' *\n'; 61 | method += ` * @link ${e.url}\n`; 62 | method += e.parameters ? ' * @param parameters\n' : ''; 63 | method += ' */\n'; 64 | 65 | method += ` public async ${methodName}${signature} {\n`; 66 | 67 | let requestParams = ''; 68 | 69 | if ((e.parameters && e.title.startsWith('GET')) || e.title.startsWith('DELETE')) { 70 | method += pathParam 71 | ? ` const params = createParams(parameters, ['${pathParam.slice(1)}']);\n` 72 | : ` const params = createParams(parameters);\n`; 73 | requestParams = ' + params'; 74 | } 75 | 76 | if (e.parameters && e.title.startsWith('POST')) { 77 | requestParams = ', parameters'; 78 | } 79 | 80 | if (e.contentType && e.title.startsWith('POST')) { 81 | requestParams += `, '${e.contentType}'`; 82 | } 83 | 84 | method += ` return await ${doMethod}${returnType}('${resourceUrl}'${requestParams});\n`; 85 | method += ' }\n\n'; 86 | 87 | clientMethods.push(method); 88 | console.log(`✅ "${e.title}" added.`); 89 | }); 90 | 91 | if (interfacesParamsImports.length) { 92 | interfacesParamsImportMap[subgroupFileName] = interfacesParamsImports; 93 | } 94 | }); 95 | 96 | if (Object.keys(interfacesParamsImportMap).length) { 97 | clientFile += `import { createParams } from '../base/utils';\n`; 98 | } 99 | 100 | Object.entries(interfacesParamsImportMap).forEach(([file, imports]) => { 101 | if (imports.length) { 102 | clientFile += `import {\n`; 103 | imports.forEach((param) => (clientFile += ` ${param},\n`)); 104 | clientFile += `} from '../interfaces/params/${file}Params';\n\n`; 105 | 106 | superClientFile += `export {\n`; 107 | imports.forEach((param) => (superClientFile += ` ${param},\n`)); 108 | superClientFile += `} from './interfaces/params/${file}Params';\n\n`; 109 | } 110 | }); 111 | 112 | interfacesTypesImports.forEach((type) => { 113 | clientFile += `import ${type} from '../interfaces/types/${type}Types';\n`; 114 | superClientFile += `import ${type} from './interfaces/types/${type}Types';\n`; 115 | superClientFile += `export { ${type} };\n`; 116 | }); 117 | 118 | clientFile += `import Transport from '../base/Transport'\n`; 119 | clientFile += `\nclass ${fileName}Client {\n`; 120 | clientFile += ` 121 | private transport: Transport; 122 | 123 | constructor(transport: Transport) { 124 | if (!transport) { 125 | throw Error('Transport class needs to be provided.'); 126 | } 127 | 128 | this.transport = transport; 129 | }\n`; 130 | 131 | clientMethods.forEach((method) => (clientFile += method)); 132 | 133 | clientFile += `}\n\nexport default ${fileName}Client;\n`; 134 | 135 | fs.writeFileSync(`${clientsPath}/${fileName}Client.ts`, clientFile); 136 | }); 137 | 138 | clients.forEach((client) => { 139 | superClientFile += `import ${client} from './clients/${client}';\n`; 140 | }); 141 | 142 | superClientFile += '\nclass TwitterClient {\n'; 143 | 144 | clients.forEach((client) => { 145 | const clientName = client.replace(/^./, client[0].toLowerCase()); 146 | superClientFile += ` private ${clientName}: ${client} | undefined;\n`; 147 | }); 148 | superClientFile += ` private transport: Transport;\n`; 149 | superClientFile += ` 150 | /** 151 | * Provide Twitter API Credentials and options 152 | * @param options 153 | */ 154 | constructor(options: IClientOptions) { 155 | if (!options.apiKey) { 156 | throw Error('API KEY needs to be provided.'); 157 | } 158 | 159 | if (!options.apiSecret) { 160 | throw Error('API SECRET needs to be provided.'); 161 | } 162 | 163 | if (!options.accessToken) { 164 | throw Error('ACCESS TOKEN needs to be provided.'); 165 | } 166 | 167 | if (!options.accessTokenSecret) { 168 | throw Error('ACCESS TOKEN SECRET needs to be provided.'); 169 | } 170 | this.transport = new Transport(options); 171 | }\n`; 172 | 173 | clients.forEach((client) => { 174 | const clientName = client.replace(/^./, client[0].toLowerCase()); 175 | superClientFile += ` 176 | public get ${clientName.replace('Client', '')}() { 177 | if (!this.${clientName}) { 178 | this.${clientName} = new ${client}(this.transport); 179 | } 180 | 181 | return this.${clientName}; 182 | } 183 | `; 184 | }); 185 | 186 | superClientFile += '}\n\nexport { TwitterClient };\n'; 187 | fs.writeFileSync(`${generatedPath}/index.ts`, superClientFile); 188 | } 189 | 190 | export default writeClients; 191 | -------------------------------------------------------------------------------- /src/generator/writeParamsInterfaces.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { resolve } from 'path'; 3 | import IReferenceDirectory from '../interfaces/IReferenceDirectory'; 4 | import { createCamelCaseTitle } from '../utils/utils'; 5 | import { removeHttpVerbs } from '../base/httpVerbs'; 6 | 7 | function writeParamsInterfaces(dictionary: IReferenceDirectory[]) { 8 | const generatedPath = resolve(__dirname, '../../generated'); 9 | const paramsPath = `${generatedPath}/interfaces/params`; 10 | 11 | dictionary.forEach((g) => { 12 | g.subgroups.forEach((s) => { 13 | const fileName = createCamelCaseTitle(s.title); 14 | let interfacesContent = ''; 15 | 16 | s.endpoints.forEach((e, i) => { 17 | if (!e.parameters) { 18 | return; 19 | } 20 | 21 | const titleWithoutVerb = removeHttpVerbs(e.title); 22 | const interfaceName = createCamelCaseTitle(titleWithoutVerb); 23 | 24 | let params = ''; 25 | 26 | e.parameters.forEach((p, i) => { 27 | params += ` /**\n * ${p.description}\n */\n '${p.name}'${!p.required ? '?' : ''}: ${ 28 | p.type === 'number' ? 'string | number' : p.type 29 | };`; 30 | 31 | if (i !== (e.parameters?.length ?? 0) - 1) { 32 | params += '\n'; 33 | } 34 | }); 35 | 36 | interfacesContent += `export interface ${interfaceName}Params {\n${params}\n}\n`; 37 | 38 | if (i !== s.endpoints.length - 1) { 39 | interfacesContent += '\n'; 40 | } 41 | }); 42 | 43 | if (!interfacesContent) { 44 | return; 45 | } 46 | 47 | fs.writeFileSync(`${paramsPath}/${fileName}Params.ts`, interfacesContent); 48 | }); 49 | }); 50 | } 51 | 52 | export default writeParamsInterfaces; 53 | -------------------------------------------------------------------------------- /src/generator/writeReferences.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { resolve } from 'path'; 3 | import IReferenceDirectory from '../interfaces/IReferenceDirectory'; 4 | import { createCamelCaseTitle } from '../utils/utils'; 5 | 6 | function writeReferences(dictionary: IReferenceDirectory[]) { 7 | const generatedPath = resolve(__dirname, '../..'); 8 | const readmeFile = `${generatedPath}/REFERENCES.md`; 9 | 10 | let readmeContent = '# Reference\n\n'; 11 | 12 | dictionary.forEach((g) => { 13 | const clientName = createCamelCaseTitle(g.title); 14 | const lowerClientName = clientName.replace(/^./, clientName[0].toLowerCase()); 15 | 16 | readmeContent += `## ${clientName}\n`; 17 | 18 | g.subgroups.forEach((s) => { 19 | s.endpoints.forEach((e) => { 20 | if (!e.title.startsWith('GET') && !e.title.startsWith('POST')) { 21 | return; 22 | } 23 | 24 | const titleWithoutVerb = e.title.replace('GET', '').replace('POST', ''); 25 | const interfaceName = createCamelCaseTitle(titleWithoutVerb); 26 | const methodName = interfaceName.replace(/^./, interfaceName[0].toLowerCase()); 27 | 28 | const signature = e.parameters ? `(parameters)` : '()'; 29 | 30 | readmeContent += `### \`TwitterClient.${lowerClientName}.${methodName}${signature}\`\n`; 31 | readmeContent += `#### Description\n`; 32 | readmeContent += `${e.description}\n\n`; 33 | 34 | if (e.parameters) { 35 | readmeContent += '#### Parameters\n\n'; 36 | readmeContent += '| Name | Required | type |\n'; 37 | readmeContent += '| ---- | -------- | ---- |\n'; 38 | e.parameters.forEach((param) => { 39 | readmeContent += `| ${param.name} | ${param.required} | ${param.type} |\n`; 40 | }); 41 | } 42 | 43 | readmeContent += ' \n'; 44 | 45 | readmeContent += '#### Link\n'; 46 | readmeContent += `${e.url} \n`; 47 | 48 | readmeContent += ' \n'; 49 | }); 50 | }); 51 | }); 52 | 53 | fs.writeFileSync(readmeFile, readmeContent); 54 | } 55 | 56 | export default writeReferences; 57 | -------------------------------------------------------------------------------- /src/generator/writeTypesInterfaces.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { resolve } from 'path'; 3 | import JsonToTS from 'json-to-ts'; 4 | import IReferenceDirectory from '../interfaces/IReferenceDirectory'; 5 | import { createCamelCaseTitle } from '../utils/utils'; 6 | import userTemplate from './template-models/user-template.json'; 7 | import tweetTemplate from './template-models/tweet-template.json'; 8 | import listTemplate from './template-models/list-template.json'; 9 | import sizeTemplate from './template-models/size-template.json'; 10 | import collectionTemplate from './template-models/collection-template.json'; 11 | import mentionTemplate from './template-models/mention-template.json'; 12 | import geoTemplate from './template-models/geo-template.json'; 13 | import geoReverseTemplate from './template-models/geo-reverse-template.json'; 14 | 15 | function writeTypesInterfaces(dictionary: IReferenceDirectory[]) { 16 | const generatedPath = resolve(__dirname, '../../generated'); 17 | const typesPath = `${generatedPath}/interfaces/types`; 18 | 19 | dictionary.forEach((g) => { 20 | g.subgroups.forEach((s) => { 21 | s.endpoints.forEach((e) => { 22 | if (!e.exampleResponse) { 23 | return; 24 | } 25 | 26 | const titleWithoutVerb = e.title.replace('GET', '').replace('POST', ''); 27 | const fileName = createCamelCaseTitle(titleWithoutVerb); 28 | let interfacesContent = ''; 29 | 30 | const exampleResponse = e.exampleResponse 31 | .replace(/\n/g, '') 32 | .replace(/\{user-object\}/g, JSON.stringify(userTemplate)) 33 | .replace(/\{tweet-object\}/g, JSON.stringify(tweetTemplate)) 34 | .replace(/\{list-object\}/g, JSON.stringify(listTemplate)) 35 | .replace(/\{size-object\}/g, JSON.stringify(sizeTemplate)) 36 | .replace(/\{collection-object\}/g, JSON.stringify(collectionTemplate)) 37 | .replace(/\{mention-object\}/g, JSON.stringify(mentionTemplate)) 38 | .replace(/\{geo-object\}/g, JSON.stringify(geoTemplate)) 39 | .replace(/\{geo-reverse-object\}/g, JSON.stringify(geoReverseTemplate)); 40 | 41 | try { 42 | const parsed = JSON.parse(exampleResponse); 43 | JsonToTS(parsed).forEach((interfaceContent: string) => { 44 | const [, name] = interfaceContent.split(' '); 45 | 46 | if (name === 'RootObject') { 47 | interfacesContent += `export default ${interfaceContent.replace( 48 | 'RootObject', 49 | fileName, 50 | )}\n\n`; 51 | } else { 52 | interfacesContent += `export ${interfaceContent}\n\n`; 53 | } 54 | }); 55 | } catch (error) { 56 | interfacesContent += `type ${fileName} = string;\n`; 57 | interfacesContent += `export default ${fileName};`; 58 | } 59 | 60 | if (!interfacesContent) { 61 | return; 62 | } 63 | 64 | fs.writeFileSync(`${typesPath}/${fileName}Types.ts`, interfacesContent); 65 | }); 66 | }); 67 | }); 68 | } 69 | 70 | export default writeTypesInterfaces; 71 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import $RefParser from '@apidevtools/json-schema-ref-parser'; 3 | import writeParamsInterfaces from './generator/writeParamsInterfaces'; 4 | import createFolderStructure, { copyBase } from './generator/createFolderStructure'; 5 | import writeTypesInterfaces from './generator/writeTypesInterfaces'; 6 | import writeClients from './generator/writeClients'; 7 | import writeReferences from './generator/writeReferences'; 8 | 9 | /** 10 | * Entry point 11 | */ 12 | start(); 13 | 14 | /** 15 | * Generate a new Twitter API Client 16 | */ 17 | async function start() { 18 | // Prepare folder structure for generated clients 19 | createFolderStructure(resolve(__dirname, '../')); 20 | copyBase(); 21 | 22 | // Parse the twitter-api-spec.yml file 23 | await parseDictionary(); 24 | 25 | // Generate clients based on the twitter-api-spec.yml file 26 | createParamsInterfaces(); 27 | createTypesInterfaces(); 28 | createClients(); 29 | createReadme(); 30 | } 31 | 32 | let dictionary: any; 33 | 34 | async function parseDictionary() { 35 | dictionary = await $RefParser.dereference( 36 | resolve(__dirname, './specs/twitter-api-spec.yml') 37 | ); 38 | } 39 | 40 | function createParamsInterfaces() { 41 | writeParamsInterfaces(dictionary); 42 | } 43 | 44 | function createTypesInterfaces() { 45 | return writeTypesInterfaces(dictionary); 46 | } 47 | 48 | function createClients() { 49 | writeClients(dictionary); 50 | } 51 | 52 | function createReadme() { 53 | writeReferences(dictionary); 54 | } 55 | -------------------------------------------------------------------------------- /src/interfaces/IReferenceDirectory.ts: -------------------------------------------------------------------------------- 1 | export default interface IReferenceDirectory { 2 | title: string; 3 | subgroups: IReferenceSubGroup[]; 4 | } 5 | 6 | export interface IReferenceSubGroup { 7 | title: string; 8 | endpoints: IReferenceEndpoint[]; 9 | } 10 | 11 | export interface IReferenceEndpoint { 12 | title: string; 13 | url: string; 14 | resourceUrl: string; 15 | description?: string; 16 | exampleResponse?: string; 17 | parameters?: IParameter[]; 18 | contentType?: string; 19 | } 20 | 21 | export interface IParameter { 22 | name: string; 23 | required: boolean; 24 | description: string; 25 | type: 'string' | 'number' | 'boolean'; 26 | } 27 | -------------------------------------------------------------------------------- /src/specs/twitter-api-spec.yml: -------------------------------------------------------------------------------- 1 | ### V1 ENDPOINTS #### 2 | 3 | # BASICS 4 | - $ref: v1/basic.yml 5 | 6 | # ACCOUNTS AND USERS 7 | - $ref: v1/accounts-and-users.yml 8 | 9 | # TWEETS 10 | - $ref: v1/tweets.yml 11 | 12 | # DIRECT MESSAGS 13 | - $ref: v1/direct-messages.yml 14 | 15 | # MEDIA 16 | - $ref: v1/media.yml 17 | 18 | # TRENDS 19 | - $ref: v1/trends.yml 20 | 21 | # GEO 22 | - $ref: v1/geo.yml 23 | 24 | ### V2 ENDPOINTS #### 25 | 26 | # METRICS 27 | - $ref: v2/metrics.yml 28 | 29 | # TWEETS 30 | - $ref: v2/tweets.yml 31 | 32 | # TIMELINES 33 | - $ref: v2/timelines.yml 34 | 35 | # USERS 36 | - $ref: v2/users.yml -------------------------------------------------------------------------------- /src/specs/v1/apps.yml: -------------------------------------------------------------------------------- 1 | title: Application 2 | subgroups: 3 | - title: Application 4 | endpoints: 5 | - title: GET application/rate_limit_status 6 | url: https://developer.twitter.com/en/docs/twitter-api/v1/developer-utilities/rate-limit-status/api-reference/get-application-rate_limit_status 7 | resourceUrl: https://api.twitter.com/1.1/application/rate_limit_status.json 8 | description: | 9 | Returns the current rate limits for methods belonging to the specified resource families. 10 | parameters: 11 | - name: resources 12 | description: | 13 | A comma-separated list of resource families you want to know the current rate limit disposition for. 14 | For best performance, only specify the resource families pertinent to your application. 15 | required: false 16 | type: string -------------------------------------------------------------------------------- /src/specs/v1/basic.yml: -------------------------------------------------------------------------------- 1 | title: Basics 2 | subgroups: 3 | - title: Authentication 4 | endpoints: 5 | - title: GET oauth/authenticate 6 | url: https://developer.twitter.com/en/docs/basics/authentication/api-reference/authenticate 7 | resourceUrl: https://api.twitter.com/oauth/authenticate 8 | description: | 9 | Allows a Consumer application to use an OAuth request_token to request user authorization. 10 | This method is a replacement of Section 6.2 of the OAuth 1.0 authentication flow for applications 11 | using the callback authentication flow. The method will use the currently logged in user as the account 12 | for access authorization unless the force_login parameter is set to true.This method differs from 13 | GET oauth / authorize in that if the user has already granted the application permission, 14 | the redirect will occur without the user having to re-approve the application. 15 | To realize this behavior, you must enable the Use Sign in with Twitter setting on your application record. 16 | parameters: 17 | - name: force_login 18 | description: Forces the user to enter their credentials to ensure the correct users account is authorized. 19 | required: false 20 | type: boolean 21 | - name: screen_name 22 | description: Prefills the username input box of the OAuth login screen with the given value. 23 | required: false 24 | type: string 25 | exampleResponse: | 26 | https://api.twitter.com/oauth/authenticate?oauth_token=Z6eEdO8MOmk394WozF5oKyuAv855l4Mlqo7hhlSLik 27 | - title: GET oauth/authorize 28 | url: https://developer.twitter.com/en/docs/basics/authentication/api-reference/authorize 29 | resourceUrl: https://api.twitter.com/oauth/authorize 30 | description: | 31 | Allows a Consumer application to use an OAuth Request Token to request user authorization. 32 | This method fulfills Section 6.2 of the OAuth 1.0 authentication flow. 33 | Desktop applications must use this method (and cannot use GET oauth / authenticate). 34 | Usage Note: An oauth_callback is never sent to this method, provide it to POST oauth / request_token instead. 35 | parameters: 36 | - name: force_login 37 | description: Forces the user to enter their credentials to ensure the correct users account is authorized. 38 | required: false 39 | type: boolean 40 | - name: screen_name 41 | description: Prefills the username input box of the OAuth login screen with the given value. 42 | required: false 43 | type: string 44 | exampleResponse: | 45 | https://api.twitter.com/oauth/authenticate?oauth_token=Z6eEdO8MOmk394WozF5oKyuAv855l4Mlqo7hhlSLik 46 | - title: POST oauth/access_token 47 | url: https://developer.twitter.com/en/docs/basics/authentication/api-reference/access_token 48 | resourceUrl: https://api.twitter.com/oauth/access_token 49 | description: | 50 | Allows a Consumer application to exchange the OAuth Request Token for an 51 | OAuth Access Token. This method fulfills Section 6.3 of the OAuth 1.0 authentication flow. 52 | parameters: 53 | - name: oauth_verifier 54 | description: | 55 | If using the OAuth web-flow, set this parameter to the value of the oauth_verifier 56 | returned in the callback URL. If you are using out-of-band OAuth, set this value to the pin-code. 57 | For OAuth 1.0a compliance this parameter is required. OAuth 1.0a is strictly enforced 58 | and applications not using the oauth_verifier will fail to complete the OAuth flow. 59 | required: false 60 | type: string 61 | exampleResponse: | 62 | { 63 | "oauth_token": "1234", 64 | "oauth_token_secret": "1234", 65 | "user_id": "1234", 66 | "screen_name": "testDev" 67 | } 68 | - title: POST oauth/invalidate_token 69 | url: https://developer.twitter.com/en/docs/basics/authentication/api-reference/invalidate_access_token 70 | resourceUrl: https://api.twitter.com/1.1/oauth/invalidate_token 71 | description: | 72 | Allows a registered application to revoke an issued OAuth access_token 73 | by presenting its client credentials. Once an access_token has been invalidated, 74 | new creation attempts will yield a different Access Token and usage of 75 | the invalidated token will no longer be allowed. 76 | parameters: 77 | - name: access_token 78 | description: The access_token of user to be invalidated 79 | required: true 80 | type: string 81 | - name: access_token_secret 82 | description: The access_token_secret of user to be invalidated 83 | required: true 84 | type: string 85 | exampleResponse: | 86 | { "access_token":"ACCESS_TOKEN" } 87 | - title: POST oauth2/invalidate_token 88 | url: https://developer.twitter.com/en/docs/basics/authentication/api-reference/invalidate_bearer_token 89 | resourceUrl: https://api.twitter.com/oauth2/invalidate_token 90 | description: | 91 | Allows a registered application to revoke an issued oAuth 2.0 Bearer Token by presenting 92 | its client credentials. Once a Bearer Token has been invalidated, new creation 93 | attempts will yield a different Bearer Token and usage of the invalidated 94 | token will no longer be allowed.Successful responses include a 95 | JSON-structure describing the revoked Bearer Token. 96 | parameters: 97 | - name: access_token 98 | description: The value of the bearer token to revoke 99 | required: true 100 | type: string 101 | exampleResponse: | 102 | { "access_token": "ACCESS_TOKEN" } 103 | - title: POST oauth/request_token 104 | url: https://developer.twitter.com/en/docs/basics/authentication/api-reference/request_token 105 | resourceUrl: https://api.twitter.com/oauth/request_token 106 | description: | 107 | Allows a Consumer application to obtain an OAuth Request Token to request user authorization. 108 | This method fulfills Section 6.1 of the OAuth 1.0 authentication flow. 109 | parameters: 110 | - name: oauth_callback 111 | description: | 112 | For OAuth 1.0a compliance this parameter is required. 113 | The value you specify here will be used as the URL a user is redirected to should they approve 114 | your application's access to their account. Set this to oob for out-of-band pin mode. 115 | This is also how you specify custom callbacks for use in desktop/mobile applications. 116 | Always send an oauth_callback on this step, regardless of a pre-registered callback. 117 | We require that any callback URL used with this endpoint will have to be 118 | whitelisted within the app settings on developer.twitter.com* 119 | required: false 120 | type: string 121 | - name: x_auth_access_type 122 | description: | 123 | Overrides the access level an application requests to a users account. 124 | Supported values are read or write. This parameter is intended to allow a developer to 125 | register a read/write application but also request read only access when appropriate. 126 | required: false 127 | type: string 128 | exampleResponse: | 129 | { 130 | "oauth_token": "1234", 131 | "oauth_token_secret": "1234", 132 | "oauth_callback_confirmed": "true" 133 | } 134 | - title: POST oauth2/token 135 | url: https://developer.twitter.com/en/docs/basics/authentication/api-reference/token 136 | resourceUrl: https://api.twitter.com/oauth2/token 137 | description: | 138 | Allows a registered application to obtain an OAuth 2 Bearer Token, 139 | which can be used to make API requests on an application's own behalf, 140 | without a user context. This is called Application-only authentication. 141 | A Bearer Token may be invalidated using oauth2/invalidate_token. 142 | Once a Bearer Token has been invalidated, new creation attempts will yield a different Bearer Token and 143 | usage of the previous token will no longer be allowed. 144 | Only one bearer token may exist outstanding for an application, and repeated requests to this method 145 | will yield the same already-existent token until it has been invalidated. 146 | Successful responses include a JSON-structure describing the awarded Bearer Token. 147 | Tokens received by this method should be cached. 148 | If attempted too frequently, requests will be rejected with a HTTP 403 with code 99. 149 | parameters: 150 | - name: grant_type 151 | description: | 152 | Specifies the type of grant being requested by the application. 153 | At this time, only client_credentials is allowed. See Application-Only Authentication 154 | for more information. 155 | required: true 156 | type: string 157 | exampleResponse: | 158 | {"token_type":"bearer","access_token":"AAAA%2FAAA%3DAAAAAAAA"} 159 | -------------------------------------------------------------------------------- /src/specs/v1/direct-messages.yml: -------------------------------------------------------------------------------- 1 | title: Direct Messages 2 | subgroups: 3 | - title: Custom profiles 4 | endpoints: 5 | - title: GET custom_profiles/:id 6 | url: https://developer.twitter.com/en/docs/direct-messages/custom-profiles/api-reference/get-profile 7 | resourceUrl: https://api.twitter.com/1.1/custom_profiles/:id.json 8 | description: Returns a custom profile that was created with POST custom_profiles/new.json. 9 | parameters: 10 | - name: id 11 | description: | 12 | The string ID of the custom profile that should be returned. 13 | Provided in resource URL. 14 | required: true 15 | type: string 16 | exampleResponse: | 17 | { 18 | "custom_profile": { 19 | "id": "100001", 20 | "created_timestamp": "1479767168196", 21 | "name": "Jon C, Partner Engineer", 22 | "avatar": { 23 | "media": { 24 | "url": "https://pbs.twimg.com/media/Cr7HZpvVYAAYZIX.jpg" 25 | } 26 | } 27 | } 28 | } 29 | 30 | - title: Sending and receiving events 31 | endpoints: 32 | - title: DELETE events/destroy 33 | url: https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/delete-message-event 34 | resourceUrl: https://api.twitter.com/1.1/direct_messages/events/destroy.json 35 | description: | 36 | Deletes the direct message specified in the required ID parameter. 37 | The authenticating user must be the recipient of the specified direct message. 38 | Direct Messages are only removed from the interface of the user context provided. 39 | Other members of the conversation can still access the Direct Messages. 40 | A successful delete will return a 204 http response code with no body content. 41 | Important: This method requires an access token with RWD 42 | (read, write & direct message) permissions. 43 | parameters: 44 | - name: id 45 | description: The id of the Direct Message event that should be deleted. 46 | required: true 47 | type: string 48 | - title: GET events/show 49 | url: https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/get-event 50 | resourceUrl: https://api.twitter.com/1.1/direct_messages/events/show.json 51 | description: Returns a single Direct Message event by the given id. 52 | parameters: 53 | - name: id 54 | description: The id of the Direct Message event that should be returned. 55 | required: true 56 | type: string 57 | exampleResponse: | 58 | { 59 | "event": { 60 | "id": "110", 61 | "created_timestamp": "5300", 62 | "type": "message_create", 63 | "message_create": {} 64 | } 65 | } 66 | - title: GET events/list 67 | url: https://developer.twitter.com/en/docs/twitter-api/v1/direct-messages/sending-and-receiving/api-reference/list-events 68 | resourceUrl: https://api.twitter.com/1.1/direct_messages/events/list.json 69 | description: Returns all Direct Message events (both sent and received) within the last 30 days. Sorted in reverse-chronological order. 70 | parameters: 71 | - name: count 72 | description: Max number of events to be returned. 20 default. 50 max. 73 | required: false 74 | type: number 75 | - name: cursor 76 | description: For paging through result sets greater than 1 page, use the “next_cursor” property from the previous request. 77 | required: false 78 | type: string 79 | exampleResponse: | 80 | { 81 | "next_cursor": "AB345dkfC", 82 | "events": [ 83 | { "id": "110", "created_timestamp": "5300", ... }, 84 | { "id": "109", "created_timestamp": "5200", ... }, 85 | { "id": "108", "created_timestamp": "5200", ... }, 86 | { "id": "107", "created_timestamp": "5200", ... }, 87 | { "id": "106", "created_timestamp": "5100", ... }, 88 | { "id": "105", "created_timestamp": "5100", ... }, 89 | ... 90 | ] 91 | } 92 | - title: POST events/new 93 | url: https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/new-event 94 | resourceUrl: https://api.twitter.com/1.1/direct_messages/events/new.json 95 | description: | 96 | Publishes a new message_create event resulting in a Direct Message sent to a 97 | specified user from the authenticating user. Returns an event if successful. 98 | Supports publishing Direct Messages with optional Quick Reply and media attachment. 99 | Replaces behavior currently provided by POST direct_messages/new.Requires a 100 | JSON POST body and Content-Type header to be set to application/json. 101 | Setting Content-Length may also be required if it is not automatically. 102 | parameters: 103 | - name: event 104 | description: Event object of the message event to send 105 | required: true 106 | type: | 107 | { 108 | type: string; 109 | message_create: { 110 | target: { 111 | recipient_id: string; 112 | }; 113 | message_data: { 114 | text: string; 115 | quick_reply?: { 116 | type: string[], 117 | }; 118 | attachment?: { 119 | type: string; 120 | media: { 121 | id: string; 122 | } 123 | }; 124 | } 125 | } 126 | } 127 | contentType: 'application/json' 128 | exampleResponse: | 129 | { 130 | "event": { 131 | "type": "message_create", 132 | "message_create": { 133 | "target": { 134 | "recipient_id": "RECIPIENT_USER_ID" 135 | }, 136 | "message_data": { 137 | "text": "Hello World!" 138 | } 139 | } 140 | } 141 | } 142 | 143 | - title: Typing indicator and read receipts 144 | endpoints: 145 | - title: POST indicate_typing 146 | url: https://developer.twitter.com/en/docs/direct-messages/typing-indicator-and-read-receipts/api-reference/new-typing-indicator 147 | resourceUrl: https://api.twitter.com/1.1/direct_messages/indicate_typing.json 148 | description: | 149 | Displays a visual typing indicator in the recipient’s 150 | Direct Message conversation view with the sender. 151 | Each request triggers a typing indicator animation 152 | with a duration of ~3 seconds. 153 | parameters: 154 | - name: recipient_id 155 | description: The user ID of the user to receive the typing indicator. 156 | required: true 157 | type: string 158 | 159 | - title: Welcome Messages 160 | endpoints: 161 | - title: GET welcome_messages/rules/show 162 | url: https://developer.twitter.com/en/docs/direct-messages/welcome-messages/api-reference/get-welcome-message-rule 163 | resourceUrl: https://api.twitter.com/1.1/direct_messages/welcome_messages/rules/show.json 164 | description: Returns a Welcome Message Rule by the given id. 165 | parameters: 166 | - name: id 167 | description: The id of the Welcome Message Rule that should be returned. 168 | required: true 169 | type: string 170 | exampleResponse: | 171 | { 172 | "welcome_message_rule" : { 173 | "id": "9910934913490319", 174 | "created_timestamp": "1470182394258", 175 | "welcome_message_id": "844385345234" 176 | } 177 | } 178 | 179 | - title: GET welcome_messages/show 180 | url: https://developer.twitter.com/en/docs/direct-messages/welcome-messages/api-reference/get-welcome-message 181 | resourceUrl: https://api.twitter.com/1.1/direct_messages/welcome_messages/show.json 182 | description: Returns a Welcome Message by the given id. 183 | parameters: 184 | - name: id 185 | description: The id of the Welcome Message that should be returned. 186 | required: true 187 | type: string 188 | exampleResponse: | 189 | { 190 | "welcome_message" : { 191 | "id": "844385345234", 192 | "created_timestamp": "1470182274821", 193 | "message_data": { 194 | "text": "Welcome!", 195 | "attachment": { 196 | "type": "media", 197 | "media": {} 198 | } 199 | } 200 | } 201 | } 202 | 203 | - title: POST welcome_messages/new 204 | url: https://developer.twitter.com/en/docs/direct-messages/welcome-messages/api-reference/new-welcome-message 205 | resourceUrl: https://api.twitter.com/1.1/direct_messages/welcome_messages/new.json 206 | description: | 207 | Creates a new Welcome Message that will be stored and sent in the future 208 | from the authenticating user in defined circumstances. 209 | Returns the message template if successful. Supports publishing with the same 210 | elements as Direct Messages (e.g. Quick Replies, media attachments). 211 | Requires a JSON POST body and Content-Type header to be set to application/json. 212 | Setting Content-Length may also be required if it is not automatically. 213 | See the Welcome Messages overview to learn how to work with Welcome Messages. 214 | parameters: 215 | - name: welcome_message 216 | description: | 217 | The Message Data Object defining the content of the message template. 218 | See POST direct_messages/events/new (message_create) for Message Data object 219 | details. 220 | required: true 221 | type: | 222 | { 223 | message_data: { 224 | text: string; 225 | quick_reply?: { 226 | type: string[], 227 | }; 228 | attachment?: { 229 | type: string; 230 | media: { 231 | id: string; 232 | } 233 | }; 234 | }; 235 | name?: string; 236 | } 237 | 238 | - name: name 239 | description: | 240 | A human readable name for the Welcome Message. This is not displayed 241 | to the user. Max length of 100 alpha numeric characters including hyphens, 242 | underscores, spaces, hashes and at signs. 243 | required: false 244 | type: string 245 | contentType: 'application/json' 246 | exampleResponse: | 247 | { 248 | "welcome_message" : { 249 | "id": "844385345234", 250 | "created_timestamp": "1470182274821", 251 | "message_data": { 252 | "text": "Welcome!", 253 | "attachment": { 254 | "type": "media", 255 | "media": {} 256 | } 257 | } 258 | }, 259 | "name": "simple_welcome-message 01" 260 | } 261 | 262 | - title: POST welcome_messages/rules/new 263 | url: https://developer.twitter.com/en/docs/direct-messages/welcome-messages/api-reference/new-welcome-message-rule 264 | resourceUrl: https://api.twitter.com/1.1/direct_messages/welcome_messages/rules/new.json 265 | description: | 266 | Creates a new Welcome Message Rule that determines which Welcome Message will be 267 | shown in a given conversation. Returns the created rule if successful. 268 | Requires a JSON POST body and Content-Type header to be set to application/json. 269 | Setting Content-Length may also be required if it is not automatically. 270 | Additional rule configurations are forthcoming. For the initial beta release, 271 | the most recently created Rule will always take precedence, and the assigned 272 | Welcome Message will be displayed in the conversation.See the Welcome Messages 273 | overview to learn how to work with Welcome Messages. 274 | parameters: 275 | - name: welcome_message_rule 276 | description: The rule to be triggered 277 | required: true 278 | type: | 279 | { 280 | welcome_message_id: string; 281 | } 282 | contentType: 'application/json' 283 | exampleResponse: | 284 | { 285 | "welcome_message_rule" : { 286 | "id": "9910934913490319", 287 | "created_timestamp": "1470182394258", 288 | "welcome_message_id": "844385345234" 289 | } 290 | } 291 | 292 | - title: GET welcome_messages/list 293 | url: https://developer.twitter.com/en/docs/twitter-api/v1/direct-messages/welcome-messages/api-reference/list-welcome-messages 294 | resourceUrl: https://api.twitter.com/1.1/direct_messages/welcome_messages/list.json 295 | description: Returns a list of Welcome Messages. 296 | parameters: 297 | - name: count 298 | description: Number of welcome messages to be returned. Max of 50. Default is 20. 299 | required: false 300 | type: number 301 | - name: cursor 302 | description: For paging through result sets greater than 1 page, use the “next_cursor” property from the previous request. 303 | required: false 304 | type: string 305 | exampleResponse: | 306 | { 307 | "welcome_messages": [ 308 | { 309 | "id": "844385345234", 310 | "created_timestamp": "1470182274821", 311 | "message_data": { 312 | "text": "Welcome!", 313 | "attachment": { 314 | "type": "media", 315 | "media": {} 316 | } 317 | } 318 | } 319 | ], 320 | "next_cursor": "NDUzNDUzNDY3Nzc3" 321 | } 322 | 323 | - title: DELETE welcome_messages/destroy 324 | url: https://developer.twitter.com/en/docs/twitter-api/v1/direct-messages/welcome-messages/api-reference/delete-welcome-message 325 | resourceUrl: https://api.twitter.com/1.1/direct_messages/welcome_messages/destroy.json 326 | description: Returns a list of Welcome Messages. 327 | parameters: 328 | - name: id 329 | description: The id of the Welcome Message that should be deleted. 330 | required: true 331 | type: string 332 | 333 | - title: DELETE welcome_messages/rules/destroy 334 | url: https://developer.twitter.com/en/docs/twitter-api/v1/direct-messages/welcome-messages/api-reference/delete-welcome-message-rule 335 | resourceUrl: https://api.twitter.com/1.1/direct_messages/welcome_messages/rules/destroy.json 336 | description: Returns a list of Welcome Messages. 337 | parameters: 338 | - name: id 339 | description: The id of the Welcome Message Rule that should be deleted. 340 | required: true 341 | type: string 342 | -------------------------------------------------------------------------------- /src/specs/v1/geo.yml: -------------------------------------------------------------------------------- 1 | title: Geo 2 | subgroups: 3 | - title: Get information about a place 4 | endpoints: 5 | - title: GET geo/id/:place_id 6 | url: https://developer.twitter.com/en/docs/geo/place-information/api-reference/get-geo-id-place_id 7 | resourceUrl: https://api.twitter.com/1.1/geo/id/:place_id.json 8 | description: | 9 | Returns all the information about a known place. 10 | parameters: 11 | - name: place_id 12 | description: A place in the world. These IDs can be retrieved from geo/reverse_geocode. 13 | required: true 14 | type: string 15 | exampleResponse: | 16 | {geo-object} 17 | - title: Get places near a location 18 | endpoints: 19 | - title: GET geo/reverse_geocode 20 | url: https://developer.twitter.com/en/docs/geo/places-near-location/api-reference/get-geo-reverse_geocode 21 | resourceUrl: https://api.twitter.com/1.1/geo/reverse_geocode.json 22 | description: | 23 | Given a latitude and a longitude, searches for up to 20 places that can be used as a place_id when updating a status.This request is an informative call and will deliver generalized results about geography. 24 | exampleResponse: | 25 | {geo-reverse-object} 26 | - title: GET geo/search 27 | url: https://developer.twitter.com/en/docs/geo/places-near-location/api-reference/get-geo-search 28 | resourceUrl: https://api.twitter.com/1.1/geo/search.json 29 | description: | 30 | Search for places that can be attached to a Tweet via POST statuses/update. Given a latitude and a longitude pair, an IP address, or a name, this request will return a list of all the valid places that can be used as the place_id when updating a status.Conceptually, a query can be made from the user's location, retrieve a list of places, have the user validate the location they are at, and then send the ID of this location with a call to POST statuses/update.This is the recommended method to use find places that can be attached to statuses/update. Unlike GET geo/reverse_geocode which provides raw data access, this endpoint can potentially re-order places with regards to the user who is authenticated. This approach is also preferred for interactive place matching with the user.Some parameters in this method are only required based on the existence of other parameters. For instance, "lat" is required if "long" is provided, and vice-versa. Authentication is recommended, but not required with this method. 31 | exampleResponse: | 32 | {geo-reverse-object} 33 | -------------------------------------------------------------------------------- /src/specs/v1/media.yml: -------------------------------------------------------------------------------- 1 | title: Media 2 | subgroups: 3 | - title: Upload media 4 | endpoints: 5 | - title: POST media/upload/init 6 | url: https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-upload-init 7 | resourceUrl: https://upload.twitter.com/1.1/media/upload.json 8 | description: | 9 | The INIT command request is used to initiate a file upload session. 10 | It returns a media_id which should be used to execute all subsequent requests. 11 | The next step after a successful return from INIT command is the APPEND command. 12 | See the Uploading media guide for constraints and requirements on media files. 13 | parameters: 14 | - name: command 15 | description: | 16 | Must be set to INIT (case sensitive). 17 | required: true 18 | type: string 19 | - name: total_bytes 20 | description: | 21 | The size of the media being uploaded in bytes. 22 | required: true 23 | type: number 24 | - name: media_type 25 | description: | 26 | The MIME type of the media being uploaded. 27 | required: true 28 | type: string 29 | - name: media_category 30 | description: | 31 | A string enum value which identifies a media usecase. 32 | This identifier is used to enforce usecase specific constraints 33 | (e.g. file size, video duration) and enable advanced features. 34 | required: false 35 | type: string 36 | - name: additional_owners 37 | description: | 38 | A comma-separated list of user IDs to set as additional owners allowed to use the returned 39 | media_id in Tweets or Cards. Up to 100 additional owners may be specified. 40 | required: false 41 | type: string 42 | exampleResponse: | 43 | { 44 | "media_id": 710511363345354753, 45 | "media_id_string": "710511363345354753", 46 | "size": 11065, 47 | "expires_after_secs": 86400, 48 | "image": { 49 | "image_type": "image/jpeg", 50 | "w": 800, 51 | "h": 320 52 | } 53 | } 54 | 55 | - title: POST media/upload/append 56 | url: https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-upload-append 57 | resourceUrl: https://upload.twitter.com/1.1/media/upload.json 58 | description: | 59 | The APPEND command is used to upload a chunk (consecutive byte range) of the media file. 60 | For example, a 3 MB file could be split into 3 chunks of size 1 MB, 61 | and uploaded using 3 APPEND command requests. 62 | After the entire file is uploaded, the next step is to call the FINALIZE command. 63 | parameters: 64 | - name: command 65 | description: | 66 | Must be set to APPEND (case sensitive). 67 | required: true 68 | type: string 69 | - name: media_id 70 | description: | 71 | The media_id returned from the INIT command. 72 | required: true 73 | type: string 74 | - name: media 75 | description: | 76 | The raw binary file content being uploaded. It must be <= 5 MB, and cannot be used with media_data. 77 | required: false 78 | type: string 79 | - name: media_data 80 | description: | 81 | The base64-encoded chunk of media file. It must be <= 5 MB and cannot be 82 | used with media. Use raw binary (media parameter) when possible. 83 | required: false 84 | type: string 85 | - name: segment_index 86 | description: | 87 | An ordered index of file chunk. It must be between 0-999 inclusive. 88 | The first segment has index 0, second segment has index 1, and so on. 89 | required: true 90 | type: string 91 | 92 | - title: GET media/upload/status 93 | url: https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/get-media-upload-status 94 | resourceUrl: https://upload.twitter.com/1.1/media/upload.json 95 | description: | 96 | The STATUS command is used to periodically poll for updates of media processing operation. 97 | After the STATUS command response returns succeeded, 98 | you can move on to the next step which is usually create Tweet with media_id. 99 | parameters: 100 | - name: command 101 | description: | 102 | Must be set to STATUS (case sensitive). 103 | required: true 104 | type: string 105 | - name: media_id 106 | description: | 107 | The media_id returned from the INIT command. 108 | required: true 109 | type: string 110 | exampleResponse: | 111 | { 112 | "media_id": 710511363345354753, 113 | "media_id_string": "710511363345354753", 114 | "expires_after_secs": 3595, 115 | "processing_info":{ 116 | "state": "in_progress", 117 | "check_after_secs": 10, 118 | "progress_percent": 8 119 | } 120 | } 121 | 122 | - title: POST media/upload/finalize 123 | url: https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-upload-finalize 124 | resourceUrl: https://upload.twitter.com/1.1/media/upload.json 125 | description: | 126 | The FINALIZE command should be called after the entire media file is uploaded 127 | using APPEND commands. If and (only if) the response of the FINALIZE command 128 | contains a processing_info field, it may also be necessary to use a STATUS 129 | command and wait for it to return success before proceeding to Tweet creation. 130 | parameters: 131 | - name: command 132 | description: | 133 | Must be set to FINALIZE (case sensitive). 134 | required: true 135 | type: string 136 | - name: media_id 137 | description: | 138 | The media_id returned from the INIT command. 139 | required: true 140 | type: string 141 | exampleResponse: | 142 | { 143 | "media_id": 710511363345354753, 144 | "media_id_string": "710511363345354753", 145 | "size": 11065, 146 | "expires_after_secs": 86400, 147 | "video": { 148 | "video_type": "video/mp4" 149 | }, 150 | "processing_info":{ 151 | "state": "in_progress", 152 | "check_after_secs": 10, 153 | "progress_percent": 8 154 | } 155 | } 156 | 157 | - title: POST media/upload 158 | url: https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-upload 159 | resourceUrl: https://upload.twitter.com/1.1/media/upload.json 160 | description: | 161 | Use this endpoint to upload images to Twitter. 162 | This endpoint returns a media_id by default and can optionally return a media_key 163 | when a media_category is specified. These values are used by Twitter endpoints that accept images. 164 | For example, a media_id value can be used to create a Tweet with an 165 | attached photo using the POST statuses/update endpoint. 166 | All Ads API endpoints require a media_key. 167 | For example, a media_key value can be used to create a Draft Tweet 168 | with a photo using the POST accounts/:account_id/draft_tweets endpoint. 169 | parameters: 170 | - name: media 171 | description: | 172 | The raw binary file content being uploaded. 173 | required: false 174 | type: string 175 | - name: media_data 176 | description: | 177 | The base64 encoded file content being uploaded. 178 | required: false 179 | type: string 180 | - name: media_category 181 | description: | 182 | The category that represents how the media will be used. This field is required when using the media with the Ads API 183 | required: false 184 | type: string 185 | exampleResponse: | 186 | { 187 | "media_id": 710511363345354753, 188 | "media_id_string": "710511363345354753", 189 | "media_key": "3_710511363345354753", 190 | "size": 11065, 191 | "expires_after_secs": 86400, 192 | "image": { 193 | "image_type": "image/jpeg", 194 | "w": 800, 195 | "h": 320 196 | } 197 | } 198 | 199 | - title: POST media/metadata/create 200 | url: https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-metadata-create 201 | resourceUrl: https://upload.twitter.com/1.1/media/metadata/create.json 202 | description: | 203 | This endpoint can be used to provide additional information about the uploaded media_id. 204 | This feature is currently only supported for images and GIFs. 205 | parameters: 206 | - name: media_id 207 | description: | 208 | The ID of the media to add metadata to 209 | required: true 210 | type: string 211 | - name: alt_text 212 | description: | 213 | An object containing { text: "the-alt-text" } 214 | required: true 215 | type: '{ text: string }' 216 | contentType: 'application/json' 217 | 218 | - title: POST media/subtitles/delete 219 | url: https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-subtitles-delete 220 | resourceUrl: https://upload.twitter.com/1.1/media/subtitles/delete.json 221 | description: | 222 | Use this endpoint to dissociate subtitles from a video and delete the subtitles. 223 | You can dissociate subtitles from a video before or after Tweeting. 224 | exampleResponse: | 225 | { 226 | "media_id":"692797692624265216", 227 | "media_category":"TweetVideo", 228 | "subtitle_info": { 229 | "subtitles": [ 230 | "language_code":"EN", //The language code should be a BCP47 code (e.g. 'en", "sp") 231 | ] 232 | } 233 | } 234 | 235 | - title: POST media/subtitles/create 236 | url: https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-subtitles-create 237 | resourceUrl: https://upload.twitter.com/1.1/media/subtitles/create.json 238 | description: | 239 | Use this endpoint to associate uploaded subtitles to an uploaded video. 240 | You can associate subtitles to video before or after Tweeting. 241 | Request flow for associating subtitle to video before the video is Tweeted : 1. 242 | Upload video using the chunked upload endpoint and get the video media_id. 2. 243 | Upload subtitle using the chunked upload endpoint with media category set to “Subtitles” 244 | and get the subtitle media_id. 245 | 3. Call this endpoint to associate the subtitle to the video. 246 | 4. Create Tweet with the video media_id. 247 | exampleResponse: | 248 | { 249 | "media_id":"692797692624265216", 250 | "media_category":"TweetVideo", 251 | "subtitle_info": { 252 | "subtitles": [ 253 | "media_id":"105195515189863968", 254 | "language_code":"EN", //The language code should be a BCP47 code (e.g. 'en", "sp"), 255 | "display_name":"English" 256 | ] 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/specs/v1/trends.yml: -------------------------------------------------------------------------------- 1 | title: Trends 2 | subgroups: 3 | - title: Get locations with trending topics 4 | endpoints: 5 | - title: GET trends/available 6 | url: https://developer.twitter.com/en/docs/trends/locations-with-trending-topics/api-reference/get-trends-available 7 | resourceUrl: https://api.twitter.com/1.1/trends/available.json 8 | description: | 9 | Returns the locations that Twitter has trending topic information for.The response is an array of "locations" that encode the location's WOEID and some other human-readable information such as a canonical name and country the location belongs in.A WOEID is a Yahoo! Where On Earth ID. 10 | exampleResponse: | 11 | [ 12 | { 13 | "country": "Sweden", 14 | "countryCode": "SE", 15 | "name": "Sweden", 16 | "parentid": 1, 17 | "placeType": { "code": 12, "name": "Country" }, 18 | "url": "http://where.yahooapis.com/v1/place/23424954", 19 | "woeid": 23424954 20 | } 21 | ] 22 | - title: GET trends/closest 23 | url: https://developer.twitter.com/en/docs/trends/locations-with-trending-topics/api-reference/get-trends-closest 24 | resourceUrl: https://api.twitter.com/1.1/trends/closest.json 25 | description: | 26 | Returns the locations that Twitter has trending topic information for, 27 | closest to a specified location.The response is an array of "locations" 28 | that encode the location's WOEID and some other human-readable information 29 | such as a canonical name and country the location belongs in.A WOEID is a Yahoo! 30 | Where On Earth ID. 31 | parameters: 32 | - name: lat 33 | description: | 34 | If provided with a long parameter the available trend locations 35 | will be sorted by distance, nearest to furthest, to the co-ordinate pair. 36 | The valid ranges for longitude is -180.0 to +180.0 (West is negative, East 37 | is positive) inclusive. 38 | required: true 39 | type: number 40 | - name: long 41 | description: | 42 | If provided with a lat parameter the available trend locations 43 | will be sorted by distance, nearest to furthest, to the co-ordinate pair. 44 | The valid ranges for longitude is -180.0 to +180.0 (West is negative, East 45 | is positive) inclusive. 46 | required: true 47 | type: number 48 | exampleResponse: | 49 | [ 50 | { 51 | "country": "Australia", 52 | "countryCode": "AU", 53 | "name": "Australia", 54 | "parentid": 1, 55 | "placeType": { 56 | "code": 12, 57 | "name": "Country" 58 | }, 59 | "url": "http://where.yahooapis.com/v1/place/23424748", 60 | "woeid": 23424748 61 | } 62 | ] 63 | - title: Get trends near a location 64 | endpoints: 65 | - title: GET trends/place 66 | url: https://developer.twitter.com/en/docs/trends/trends-for-location/api-reference/get-trends-place 67 | resourceUrl: https://api.twitter.com/1.1/trends/place.json 68 | description: | 69 | Returns the top 50 trending topics for a specific WOEID, if trending 70 | information is available for it.The response is an array of trend 71 | objects that encode the name of the trending topic, the query 72 | parameter that can be used to search for the topic on Twitter Search, 73 | and the Twitter Search URL.This information is cached for 5 minutes. 74 | Requesting more frequently than that will not return any more data, and 75 | will count against rate limit usage.The tweet_volume for the last 24 hours 76 | is also returned for many trends if this is available. 77 | parameters: 78 | - name: id 79 | description: | 80 | The Yahoo! Where On Earth ID of the location to return trending 81 | information for. Global information is available by using 1 as the WOEID. 82 | required: true 83 | type: number 84 | - name: exclude 85 | description: Setting this equal to hashtags will remove all hashtags from the trends list. 86 | required: false 87 | type: number 88 | exampleResponse: | 89 | [ 90 | { 91 | "trends": [ 92 | { 93 | "name": "#ChainedToTheRhythm", 94 | "url": "http://twitter.com/search?q=%23ChainedToTheRhythm", 95 | "promoted_content": null, 96 | "query": "%23ChainedToTheRhythm", 97 | "tweet_volume": 48857 98 | } 99 | ], 100 | "as_of": "2017-02-08T16:18:18Z", 101 | "created_at": "2017-02-08T16:10:33Z", 102 | "locations": [ 103 | { 104 | "name": "Worldwide", 105 | "woeid": 1 106 | } 107 | ] 108 | } 109 | ] 110 | -------------------------------------------------------------------------------- /src/specs/v2/metrics.yml: -------------------------------------------------------------------------------- 1 | title: metrics 2 | subgroups: 3 | - title: Get metrics from a tweet 4 | endpoints: 5 | - title: GET tweets 6 | url: https://developer.twitter.com/en/docs/twitter-api/metrics 7 | resourceUrl: https://api.twitter.com/2/tweets 8 | description: | 9 | The metrics field allows developers to access public and private engagement metrics for 10 | Tweet and media objects. Public metrics are accessible by anyone with a developer account while 11 | private metrics are accessible from owned/authorized accounts (definition below). 12 | parameters: 13 | - name: ids 14 | description: A comma-separated list of ids to get metrics for 15 | required: true 16 | type: string 17 | - name: tweet.fields 18 | description: A comma-separated list of fields to include for the tweet object 19 | required: false 20 | type: string 21 | - name: media.fields 22 | description: A comma-separated list of fields to include for the media object 23 | required: false 24 | type: string 25 | - name: expansions 26 | description: Use expansion for media objects 27 | required: false 28 | type: string 29 | exampleResponse: | 30 | { 31 | "data": [ 32 | { 33 | "attachments": { 34 | "media_keys": ["13_1204080851740315648"] 35 | }, 36 | "id": "1263145271946551300", 37 | "non_public_metrics": { 38 | "impression_count": 956, 39 | "url_link_clicks": 9, 40 | "user_profile_clicks": 34 41 | }, 42 | "organic_metrics": { 43 | "impression_count": 956, 44 | "like_count": 49, 45 | "reply_count": 2, 46 | "retweet_count": 9, 47 | "url_link_clicks": 9, 48 | "user_profile_clicks": 34 49 | }, 50 | "text": "test" 51 | } 52 | ], 53 | "includes": { 54 | "media": [ 55 | { 56 | "media_key": "13_1204080851740315648", 57 | "non_public_metrics": { 58 | "playback_0_count": 0, 59 | "playback_100_count": 1, 60 | "playback_25_count": 2, 61 | "playback_50_count": 1, 62 | "playback_75_count": 1 63 | }, 64 | "organic_metrics": { 65 | "playback_0_count": 0, 66 | "playback_100_count": 1, 67 | "playback_25_count": 2, 68 | "playback_50_count": 1, 69 | "playback_75_count": 1, 70 | "view_count": 1 71 | }, 72 | "type": "video" 73 | } 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/specs/v2/timelines.yml: -------------------------------------------------------------------------------- 1 | title: timelines 2 | subgroups: 3 | - title: Get tweets from a single user 4 | endpoints: 5 | - title: GET users/id/tweets 6 | url: https://developer.twitter.com/en/docs/twitter-api/tweets/timelines/api-reference/get-users-id-tweets 7 | resourceUrl: https://api.twitter.com/2/users/:id/tweets 8 | description: Returns Tweets composed by a single user, specified by the requested user ID. By default, the most recent ten Tweets are returned per request. Using pagination, the most recent 3,200 Tweets can be retrieved. The Tweets returned by this endpoint count towards the Project-level Tweet cap. 9 | parameters: 10 | - name: id 11 | description: Unique identifier of the Twitter account (user ID) for whom to return results. User ID can be referenced using the user/lookup endpoint. 12 | required: true 13 | type: string 14 | - name: expansions 15 | description: Expansions enable you to request additional data objects that relate to the originally returned Tweets. Submit a list of desired expansions in a comma-separated list without spaces. 16 | required: false 17 | type: string 18 | - name: max_results 19 | description: Specifies the number of Tweets to try and retrieve, up to a maximum of 100 per distinct request. By default, 10 results are returned if this parameter is not supplied. 20 | required: false 21 | type: number 22 | - name: pagination_token 23 | description: This parameter is used to move forwards or backwards through 'pages' of results, based on the value of the next_token or previous_token in the response. 24 | required: false 25 | type: string 26 | - name: until_id 27 | description: Returns results with a Tweet ID less less than (that is, older than) the specified 'until' Tweet ID. 28 | required: false 29 | type: string 30 | exampleResponse: | 31 | { 32 | "data": [ 33 | { 34 | "id": "1338971066773905408", 35 | "text": "💡 Using Twitter data for academic research? Join our next livestream this Friday @ 9am PT on https://t.co/GrtBOXh5Y1!n n@SuhemParack will show how to get started with recent search & filtered stream endpoints on the #TwitterAPI v2, the new Tweet payload, annotations, & more. https://t.co/IraD2Z7wEg" 36 | }, 37 | { 38 | "id": "1338923691497959425", 39 | "text": "📈 Live now with @jessicagarson and @i_am_daniele! https://t.co/Y1AFzsTTxb" 40 | }, 41 | { 42 | "id": "1337498609819021312", 43 | "text": "Thanks to everyone who tuned in today to make music with the #TwitterAPI!nnNext week on Twitch - @iamdaniele and @jessicagarson will show you how to integrate the #TwitterAPI and Google Sheets 📈. Tuesday, Dec 15th at 2pm ET. nnhttps://t.co/SQziic6eyp" 44 | }, 45 | { 46 | "id": "1337464482654793740", 47 | "text": "🎧💻 We're live! Tune in! 🎶 https://t.co/FSYP4rJdHr" 48 | }, 49 | { 50 | "id": "1337122535188652033", 51 | "text": "👂We want to hear what you think about our plans. As we continue to build our new product tracks, your feedback is essential to shaping the future of the Twitter API. Share your thoughts on this survey: https://t.co/dkIqFGPji7" 52 | }, 53 | { 54 | "id": "1337122534173663235", 55 | "text": "Is 2020 over yet?nDespite everything that happened this year, thousands of you still made the time to learn, play, and build incredible things on the new #TwitterAPI.nWe want to share some of your stories and give you a preview of what’s to come next year.nhttps://t.co/VpOKT22WgF" 56 | }, 57 | { 58 | "id": "1336463248510623745", 59 | "text": "🎧 Headphones on: watch @jessicagarson build an interactive app to write music using SuperCollider, Python, FoxDot, and the new Twitter API. Streaming Friday 1:30 ET on our new Twitch channel 🎶💻 https://t.co/SQziic6eyp" 60 | }, 61 | { 62 | "id": "1334987486343299072", 63 | "text": "console.log('Happy birthday, JavaScript!');" 64 | }, 65 | { 66 | "id": "1334920270587584521", 67 | "text": "Live now!nJoin the first ever @Twitch stream from TwitterDev https://t.co/x33fiVIi7B" 68 | }, 69 | { 70 | "id": "1334564488884862976", 71 | "text": "Before we release new #TwitterAPI endpoints, we let developers test drive a prototype of our intended design. @i_am_daniele takes you behind the scenes of an endpoint in the making. https://t.co/NNTDnciwNq" 72 | } 73 | ], 74 | "meta": { 75 | "oldest_id": "1334564488884862976", 76 | "newest_id": "1338971066773905408", 77 | "result_count": 10, 78 | "next_token": "7140dibdnow9c7btw3w29grvxfcgvpb9n9coehpk7xz5i" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/specs/v2/tweets.yml: -------------------------------------------------------------------------------- 1 | title: TweetsV2 2 | subgroups: 3 | - title: Post tweets 4 | endpoints: 5 | - title: POST createTweet 6 | url: https://developer.twitter.com/en/docs/twitter-api/tweets/manage-tweets/api-reference/post-tweets 7 | resourceUrl: https://api.twitter.com/2/tweets 8 | contentType: 'application/json' 9 | description: | 10 | Creates a Tweet on behalf of an authenticated user. 11 | parameters: 12 | - name: text 13 | description: The text of your Tweet. 14 | required: true 15 | type: string 16 | - name: direct_message_deep_link 17 | description: Tweets a link directly to a Direct Message conversation with an account. 18 | required: false 19 | type: string 20 | - name: for_super_followers_only 21 | description: Allows you to Tweet exclusively for Super Followers. 22 | required: false 23 | type: boolean 24 | - name: geo 25 | description: A place in the world. 26 | required: false 27 | type: | 28 | { 29 | place_id: string 30 | } 31 | - name: media 32 | description: A JSON object that contains media information being attached to created Tweet. This is mutually exclusive from Quote Tweet ID and Poll. 33 | required: false 34 | type: | 35 | { 36 | media_ids: string[]; 37 | tagged_user_ids?: string[]; 38 | } 39 | - name: poll 40 | description: A JSON object that contains options for a Tweet with a poll. This is mutually exclusive from Media and Quote Tweet ID. 41 | required: false 42 | type: | 43 | { 44 | options: string[]; 45 | duration_minutes: number; 46 | } 47 | - name: quote_tweet_id 48 | description: Link to the Tweet being quoted. 49 | required: false 50 | type: string 51 | - name: reply 52 | description: A JSON object that contains information of the Tweet being replied to. 53 | required: false 54 | type: | 55 | { 56 | exclude_reply_user_ids?: string; 57 | in_reply_to_tweet_id: string; 58 | } 59 | - name: reply_settings 60 | description: Settings to indicate who can reply to the Tweet. Options include "mentionedUsers" and "following". If the field isn’t specified, it will default to everyone. 61 | required: false 62 | type: string 63 | exampleResponse: | 64 | { 65 | "data": { 66 | "id": "1445880548472328192", 67 | "text": "Hello world!" 68 | } 69 | } 70 | - title: DELETE deleteTweet 71 | url: https://developer.twitter.com/en/docs/twitter-api/tweets/manage-tweets/api-reference/delete-tweets-id 72 | resourceUrl: https://api.twitter.com/2/tweets/ 73 | description: | 74 | Deletes a Tweet on behalf of an authenticated user. 75 | parameters: 76 | - name: id 77 | description: The ID of the Tweet to delete. 78 | required: true 79 | type: string 80 | - title: GET searchRecentTweets 81 | url: https://developer.twitter.com/en/docs/twitter-api/tweets/search/api-reference/get-tweets-search-recent 82 | resourceUrl: https://api.twitter.com/2/tweets/search/recent 83 | description: | 84 | Returns Tweets from the last seven days that match a search query 85 | parameters: 86 | - name: query 87 | description: A UTF-8, URL-encoded search query of 500 characters maximum, including operators. Queries may additionally be limited by complexity. 88 | required: true 89 | type: string 90 | - name: end_time 91 | description: | 92 | YYYY-MM-DDTHH:mm:ssZ (ISO 8601/RFC 3339). The newest, most recent UTC timestamp to which the Tweets will be provided. 93 | Timestamp is in second granularity and is exclusive (for example, 12:00:01 excludes the first second of the minute). 94 | By default, a request will return Tweets from as recent as 30 seconds ago if you do not include this parameter. 95 | required: false 96 | type: string 97 | - name: expansions 98 | description: | 99 | Expansions enable you to request additional data objects that relate to the originally returned Tweets. 100 | Submit a list of desired expansions in a comma-separated list without spaces. 101 | The ID that represents the expanded data object will be included directly in the Tweet data object, 102 | but the expanded object metadata will be returned within the includes response object, 103 | and will also include the ID so that you can match this data object to the original Tweet object. 104 | required: false 105 | type: string 106 | - name: max_results 107 | description: | 108 | The maximum number of search results to be returned by a request. A number between 10 and 100. By default, a request response will return 10 results 109 | required: false 110 | type: number 111 | - name: media.fields 112 | description: | 113 | This fields parameter enables you to select which specific media fields will deliver in each returned Tweet. 114 | Specify the desired fields in a comma-separated list without spaces between commas and fields. 115 | The Tweet will only return media fields if the Tweet contains media and if you've also included the expansions=attachments.media_keys query parameter in your request. 116 | While the media ID will be located in the Tweet object, you will find this ID and all additional media fields in the includes data object. 117 | required: false 118 | type: string 119 | - name: next_token 120 | description: | 121 | This parameter is used to get the next 'page' of results. 122 | The value used with the parameter is pulled directly from the response provided by the API, and should not be modified. 123 | required: false 124 | type: string 125 | - name: place.fields 126 | description: | 127 | This fields parameter enables you to select which specific place fields will deliver in each returned Tweet. 128 | Specify the desired fields in a comma-separated list without spaces between commas and fields. 129 | The Tweet will only return place fields if the Tweet contains a place and if you've also included the expansions=geo.place_id query parameter in your request. 130 | While the place ID will be located in the Tweet object, you will find this ID and all additional place fields in the includes data object. 131 | required: false 132 | type: string 133 | - name: poll.fields 134 | description: | 135 | This fields parameter enables you to select which specific poll fields will deliver in each returned Tweet. 136 | Specify the desired fields in a comma-separated list without spaces between commas and fields. 137 | The Tweet will only return poll fields if the Tweet contains a poll and if you've also included the expansions=attachments.poll_ids query parameter in your request. 138 | While the poll ID will be located in the Tweet object, you will find this ID and all additional poll fields in the includes data object. 139 | required: false 140 | type: string 141 | - name: since_id 142 | description: | 143 | Returns results with a Tweet ID greater than (that is, more recent than) the specified ID. 144 | The ID specified is exclusive and responses will not include it. 145 | If included with the same request as a start_time parameter, only since_id will be used. 146 | required: false 147 | type: string 148 | - name: sort_order 149 | description: | 150 | This parameter is used to specify the order in which you want the Tweets returned. 151 | By default, a request will return the most recent Tweets first (sorted by recency). 152 | required: false 153 | type: string 154 | - name: start_time 155 | description: | 156 | YYYY-MM-DDTHH:mm:ssZ (ISO 8601/RFC 3339). The oldest UTC timestamp (from most recent seven days) from which the Tweets will be provided. 157 | Timestamp is in second granularity and is inclusive (for example, 12:00:01 includes the first second of the minute). 158 | If included with the same request as a since_id parameter, only since_id will be used. 159 | By default, a request will return Tweets from up to seven days ago if you do not include this parameter. 160 | required: false 161 | type: string 162 | - name: tweet.fields 163 | description: | 164 | This fields parameter enables you to select which specific Tweet fields will deliver in each returned Tweet object. 165 | Specify the desired fields in a comma-separated list without spaces between commas and fields. 166 | You can also pass the expansions=referenced_tweets.id expansion to return the specified fields for both the original Tweet and any included referenced Tweets. 167 | The requested Tweet fields will display in both the original Tweet data object, as well as in the referenced Tweet expanded data object that will be located in the includes data object. 168 | required: false 169 | type: string 170 | - name: until_id 171 | description: | 172 | Returns results with a Tweet ID less than (that is, older than) the specified ID. 173 | The ID specified is exclusive and responses will not include it. 174 | required: false 175 | type: string 176 | - name: user.fields 177 | description: | 178 | This fields parameter enables you to select which specific user fields will deliver in each returned Tweet. 179 | required: false 180 | type: string 181 | - title: GET searchAllTweets 182 | url: https://developer.twitter.com/en/docs/twitter-api/tweets/search/api-reference/get-tweets-search-all 183 | resourceUrl: https://api.twitter.com/2/tweets/search/all 184 | description: | 185 | Full-archive search returns the complete history of public Tweets matching a search query; since the first Tweet was created March 26, 2006. 186 | parameters: 187 | - name: query 188 | description: A UTF-8, URL-encoded search query of 500 characters maximum, including operators. Queries may additionally be limited by complexity. 189 | required: true 190 | type: string 191 | - name: end_time 192 | description: | 193 | YYYY-MM-DDTHH:mm:ssZ (ISO 8601/RFC 3339). The newest, most recent UTC timestamp to which the Tweets will be provided. 194 | Timestamp is in second granularity and is exclusive (for example, 12:00:01 excludes the first second of the minute). 195 | By default, a request will return Tweets from as recent as 30 seconds ago if you do not include this parameter. 196 | required: false 197 | type: string 198 | - name: expansions 199 | description: | 200 | Expansions enable you to request additional data objects that relate to the originally returned Tweets. 201 | Submit a list of desired expansions in a comma-separated list without spaces. 202 | The ID that represents the expanded data object will be included directly in the Tweet data object, 203 | but the expanded object metadata will be returned within the includes response object, 204 | and will also include the ID so that you can match this data object to the original Tweet object. 205 | required: false 206 | type: string 207 | - name: max_results 208 | description: | 209 | The maximum number of search results to be returned by a request. A number between 10 and 100. By default, a request response will return 10 results 210 | required: false 211 | type: number 212 | - name: media.fields 213 | description: | 214 | This fields parameter enables you to select which specific media fields will deliver in each returned Tweet. 215 | Specify the desired fields in a comma-separated list without spaces between commas and fields. 216 | The Tweet will only return media fields if the Tweet contains media and if you've also included the expansions=attachments.media_keys query parameter in your request. 217 | While the media ID will be located in the Tweet object, you will find this ID and all additional media fields in the includes data object. 218 | required: false 219 | type: string 220 | - name: next_token 221 | description: | 222 | This parameter is used to get the next 'page' of results. 223 | The value used with the parameter is pulled directly from the response provided by the API, and should not be modified. 224 | required: false 225 | type: string 226 | - name: place.fields 227 | description: | 228 | This fields parameter enables you to select which specific place fields will deliver in each returned Tweet. 229 | Specify the desired fields in a comma-separated list without spaces between commas and fields. 230 | The Tweet will only return place fields if the Tweet contains a place and if you've also included the expansions=geo.place_id query parameter in your request. 231 | While the place ID will be located in the Tweet object, you will find this ID and all additional place fields in the includes data object. 232 | required: false 233 | type: string 234 | - name: poll.fields 235 | description: | 236 | This fields parameter enables you to select which specific poll fields will deliver in each returned Tweet. 237 | Specify the desired fields in a comma-separated list without spaces between commas and fields. 238 | The Tweet will only return poll fields if the Tweet contains a poll and if you've also included the expansions=attachments.poll_ids query parameter in your request. 239 | While the poll ID will be located in the Tweet object, you will find this ID and all additional poll fields in the includes data object. 240 | required: false 241 | type: string 242 | - name: since_id 243 | description: | 244 | Returns results with a Tweet ID greater than (that is, more recent than) the specified ID. 245 | The ID specified is exclusive and responses will not include it. 246 | If included with the same request as a start_time parameter, only since_id will be used. 247 | required: false 248 | type: string 249 | - name: sort_order 250 | description: | 251 | This parameter is used to specify the order in which you want the Tweets returned. 252 | By default, a request will return the most recent Tweets first (sorted by recency). 253 | required: false 254 | type: string 255 | - name: start_time 256 | description: | 257 | YYYY-MM-DDTHH:mm:ssZ (ISO 8601/RFC 3339). The oldest UTC timestamp (from most recent seven days) from which the Tweets will be provided. 258 | Timestamp is in second granularity and is inclusive (for example, 12:00:01 includes the first second of the minute). 259 | If included with the same request as a since_id parameter, only since_id will be used. 260 | By default, a request will return Tweets from up to seven days ago if you do not include this parameter. 261 | required: false 262 | type: string 263 | - name: tweet.fields 264 | description: | 265 | This fields parameter enables you to select which specific Tweet fields will deliver in each returned Tweet object. 266 | Specify the desired fields in a comma-separated list without spaces between commas and fields. 267 | You can also pass the expansions=referenced_tweets.id expansion to return the specified fields for both the original Tweet and any included referenced Tweets. 268 | The requested Tweet fields will display in both the original Tweet data object, as well as in the referenced Tweet expanded data object that will be located in the includes data object. 269 | required: false 270 | type: string 271 | - name: until_id 272 | description: | 273 | Returns results with a Tweet ID less than (that is, older than) the specified ID. 274 | The ID specified is exclusive and responses will not include it. 275 | required: false 276 | type: string 277 | - name: user.fields 278 | description: | 279 | This fields parameter enables you to select which specific user fields will deliver in each returned Tweet. 280 | required: false 281 | type: string 282 | - title: GET countRecentTweets 283 | url: https://developer.twitter.com/en/docs/twitter-api/tweets/counts/api-reference/get-tweets-counts-recent 284 | resourceUrl: https://api.twitter.com/2/tweets/counts/recent 285 | description: | 286 | The recent Tweet counts endpoint returns count of Tweets from the last seven days that match a query. 287 | parameters: 288 | - name: query 289 | description: A UTF-8, URL-encoded search query of 500 characters maximum, including operators. Queries may additionally be limited by complexity. 290 | required: true 291 | type: string 292 | - name: end_time 293 | description: | 294 | YYYY-MM-DDTHH:mm:ssZ (ISO 8601/RFC 3339). The newest, most recent UTC timestamp to which the Tweets will be provided. 295 | Timestamp is in second granularity and is exclusive (for example, 12:00:01 excludes the first second of the minute). 296 | By default, a request will return Tweets from as recent as 30 seconds ago if you do not include this parameter. 297 | required: false 298 | type: string 299 | - name: start_time 300 | description: | 301 | YYYY-MM-DDTHH:mm:ssZ (ISO 8601/RFC 3339). The oldest UTC timestamp (from most recent seven days) from which the Tweets will be provided. 302 | Timestamp is in second granularity and is inclusive (for example, 12:00:01 includes the first second of the minute). 303 | If included with the same request as a since_id parameter, only since_id will be used. 304 | By default, a request will return Tweets from up to seven days ago if you do not include this parameter. 305 | required: false 306 | type: string 307 | - name: granularity 308 | description: | 309 | This is the granularity that you want the timeseries count data to be grouped by. You can requeset minute, hour, or day granularity. 310 | The default granularity, if not specified is hour. 311 | required: false 312 | type: string 313 | - name: since_id 314 | description: | 315 | YYYY-MM-DDTHH:mm:ssZ (ISO 8601/RFC 3339). The oldest UTC timestamp (from most recent seven days) from which the Tweet counts will be provided. 316 | Timestamp is in second granularity and is inclusive (for example, 12:00:01 includes the first second of the minute). 317 | If included with the same request as a since_id parameter, only since_id will be used. 318 | By default, a request will return Tweet counts from up to seven days ago if you do not include this parameter. 319 | required: false 320 | type: string 321 | - name: until_id 322 | description: | 323 | Returns results with a Tweet ID less than (that is, older than) the specified ID. 324 | The ID specified is exclusive and responses will not include it. 325 | required: false 326 | type: string 327 | exampleResponse: | 328 | { 329 | "data": [ 330 | { 331 | "end": "2021-05-27T00:00:00.000Z", 332 | "start": "2021-05-26T23:00:00.000Z", 333 | "tweet_count": 345 334 | }, 335 | ], 336 | "meta": { 337 | "total_tweet_count": 744364 338 | } 339 | } 340 | - title: GET countAllTweets 341 | url: https://developer.twitter.com/en/docs/twitter-api/tweets/counts/api-reference/get-tweets-counts-all 342 | resourceUrl: https://api.twitter.com/2/tweets/counts/all 343 | description: | 344 | The recent Tweet counts endpoint returns count of Tweets from the last seven days that match a query. 345 | parameters: 346 | - name: query 347 | description: A UTF-8, URL-encoded search query of 500 characters maximum, including operators. Queries may additionally be limited by complexity. 348 | required: true 349 | type: string 350 | - name: end_time 351 | description: | 352 | YYYY-MM-DDTHH:mm:ssZ (ISO 8601/RFC 3339). The newest, most recent UTC timestamp to which the Tweets will be provided. 353 | Timestamp is in second granularity and is exclusive (for example, 12:00:01 excludes the first second of the minute). 354 | By default, a request will return Tweets from as recent as 30 seconds ago if you do not include this parameter. 355 | required: false 356 | type: string 357 | - name: start_time 358 | description: | 359 | YYYY-MM-DDTHH:mm:ssZ (ISO 8601/RFC 3339). The oldest UTC timestamp (from most recent seven days) from which the Tweets will be provided. 360 | Timestamp is in second granularity and is inclusive (for example, 12:00:01 includes the first second of the minute). 361 | If included with the same request as a since_id parameter, only since_id will be used. 362 | By default, a request will return Tweets from up to seven days ago if you do not include this parameter. 363 | required: false 364 | type: string 365 | - name: granularity 366 | description: | 367 | This is the granularity that you want the timeseries count data to be grouped by. You can requeset minute, hour, or day granularity. 368 | The default granularity, if not specified is hour. 369 | required: false 370 | type: string 371 | - name: since_id 372 | description: | 373 | YYYY-MM-DDTHH:mm:ssZ (ISO 8601/RFC 3339). The oldest UTC timestamp (from most recent seven days) from which the Tweet counts will be provided. 374 | Timestamp is in second granularity and is inclusive (for example, 12:00:01 includes the first second of the minute). 375 | If included with the same request as a since_id parameter, only since_id will be used. 376 | By default, a request will return Tweet counts from up to seven days ago if you do not include this parameter. 377 | required: false 378 | type: string 379 | - name: until_id 380 | description: | 381 | Returns results with a Tweet ID less than (that is, older than) the specified ID. 382 | The ID specified is exclusive and responses will not include it. 383 | required: false 384 | type: string 385 | - name: next_token 386 | description: | 387 | This parameter is used to get the next 'page' of results. 388 | The value used with the parameter is pulled directly from the response provided by the API, assuming that your request contains more than 31 days-worth of results, 389 | and should not be modified. You can learn more by visiting our page on pagination. 390 | required: false 391 | type: string 392 | exampleResponse: | 393 | { 394 | "data": [ 395 | { 396 | "end": "2021-05-27T00:00:00.000Z", 397 | "start": "2021-05-26T23:00:00.000Z", 398 | "tweet_count": 345 399 | }, 400 | ], 401 | "meta": { 402 | "total_tweet_count": 744364 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /src/specs/v2/users.yml: -------------------------------------------------------------------------------- 1 | title: users 2 | subgroups: 3 | - title: Get information about an authorized user 4 | endpoints: 5 | - title: GET /users/me 6 | url: https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me 7 | resourceUrl: https://api.twitter.com/2/users/me 8 | description: | 9 | Return user information about an authorized user. User rate limit for OAuth 2.0 and OAuth 1.0a: 75 requests per 15-minute window per each authenticated user. 10 | parameters: 11 | exampleResponse: | 12 | { 13 | "data": { 14 | "id": "2244994945", 15 | "name": "TwitterDev", 16 | "username": "Twitter Dev" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/Cache.test.ts: -------------------------------------------------------------------------------- 1 | import FakeTimers from '@sinonjs/fake-timers'; 2 | import Cache from '../base/Cache'; 3 | 4 | const clock = FakeTimers.install(); 5 | 6 | describe('requests - GitHubCache', () => { 7 | let cache = new Cache(100); 8 | 9 | beforeEach(() => { 10 | cache = new Cache(100); 11 | }); 12 | 13 | it('caches result from query', async () => { 14 | const result = { mock: 'result' }; 15 | cache.add('mock-query', result); 16 | 17 | const cachedResult = cache.get('mock-query'); 18 | expect(cachedResult).toEqual(result); 19 | }); 20 | 21 | it('does not use cache when ttl has expired', () => { 22 | const result = { mock: 'result' }; 23 | cache.add('mock-query', result); 24 | 25 | const hasDataBeforeExpire = cache.has('mock-query'); 26 | expect(hasDataBeforeExpire).toBeTruthy(); 27 | 28 | clock.tick(101 * 1000); 29 | 30 | const hasDataAfterExpire = cache.has('mock-query'); 31 | expect(hasDataAfterExpire).toBeFalsy(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/test/createFolderStructure.test.ts: -------------------------------------------------------------------------------- 1 | import mockFs from 'mock-fs'; 2 | import fs from 'fs'; 3 | import createFolderStructure from '../generator/createFolderStructure'; 4 | 5 | afterEach(() => { 6 | return mockFs.restore(); 7 | }); 8 | 9 | describe('Folder structure generator', () => { 10 | it('Deletes the content of folder', () => { 11 | mockFs({ 12 | '/generated/old-stuff': {}, 13 | }); 14 | createFolderStructure('/'); 15 | const result = fs.existsSync('/generated/old-stuff'); 16 | expect(result).toBeFalsy(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/test/httpVerbs.test.ts: -------------------------------------------------------------------------------- 1 | import Item from "mock-fs/lib/item"; 2 | import { 3 | removeHttpVerbs, 4 | startWithHttpVerb, 5 | methodNameBuilder, 6 | getMethodName 7 | } from "../base/httpVerbs"; 8 | 9 | describe("Utils", () => { 10 | it("Removes supported http verbs from string", () => { 11 | const query = "DELETE a"; 12 | const result = removeHttpVerbs(query); 13 | expect(result).toEqual("a"); 14 | }); 15 | it("Keep untouched a string without the supported http verbs", () => { 16 | const query = "PUT a"; 17 | const result = removeHttpVerbs(query); 18 | expect(result).toEqual(query); 19 | }); 20 | it("Check a string that have an http verb", () => { 21 | const query = "DELETE a"; 22 | const result = startWithHttpVerb(query); 23 | expect(result).toEqual(true); 24 | }); 25 | it("Check a string that doesnt have an http verb", () => { 26 | const query = "PUT a"; 27 | const result = startWithHttpVerb(query); 28 | expect(result).toEqual(false); 29 | }); 30 | it("returns false if text has a verb elsewhere but the start", () => { 31 | const query = "a GET"; 32 | const result = startWithHttpVerb(query); 33 | expect(result).toEqual(false); 34 | }); 35 | it("return the correct method name", () => { 36 | const result = methodNameBuilder("GET"); 37 | expect(result).toEqual("this.transport.doGetRequest"); 38 | }); 39 | it('should return the correct method name with GET',()=>{ 40 | const result = getMethodName('GET dd dd dd ') 41 | expect(result).toEqual("this.transport.doGetRequest") 42 | }) 43 | }); 44 | -------------------------------------------------------------------------------- /src/test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { parse } from '../base/utils'; 2 | 3 | describe('Utils', () => { 4 | it('should parse object regularly', () => { 5 | const query = { test: 'test' }; 6 | 7 | const result = parse(JSON.stringify(query)); 8 | 9 | expect(result).toEqual(query); 10 | }); 11 | 12 | it('should transform query result correctly', () => { 13 | const query = 'oauth_token=123&oauth_token_secret=456&oauth_callback_confirmed=true'; 14 | 15 | const result = parse(query); 16 | 17 | expect(result).toEqual({ 18 | oauth_token: '123', 19 | oauth_token_secret: '456', 20 | oauth_callback_confirmed: 'true', 21 | }); 22 | }); 23 | 24 | it('should return string if transformation fails', () => { 25 | const query = '1234abcd'; 26 | 27 | const result = parse(query); 28 | 29 | expect(result).toBe(query); 30 | }); 31 | }); -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export const createCamelCaseTitle = (slashedTitle: string) => { 2 | const transformedSlashedTitle = slashedTitle 3 | .trim() 4 | .replace(/ /g, '/') 5 | .replace(/:/g, 'By/') 6 | .replace(/\(/g, '/') 7 | .replace(/\)/g, '/') 8 | .replace(/\/\//g, '/') 9 | .replace(/_/g, '/') 10 | .replace(/,/g, '/'); 11 | 12 | const words = transformedSlashedTitle.split('/'); 13 | let title = ''; 14 | 15 | words.forEach((w) => { 16 | if (!w) { 17 | return; 18 | } 19 | 20 | title += w.replace(/^./, w[0].toUpperCase()); 21 | }); 22 | 23 | return title; 24 | }; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "outDir": "dist", 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "resolveJsonModule": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "module": "commonjs", 14 | "moduleResolution": "node", 15 | "declaration": true 16 | }, 17 | "include": ["generated"], 18 | "exclude": ["node_modules", "dist", "src", ".github"] 19 | } 20 | --------------------------------------------------------------------------------