├── .github └── workflows │ ├── test-build.yaml │ └── test-python.yaml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CONTRIBUTING.md ├── LICENSE ├── LICENSE.AGPL ├── README.md ├── debug └── requests_sample.py ├── dist ├── compatible │ └── openapi-3.0.yaml └── docs │ └── openapi-3.0.yaml ├── other └── error │ └── GenericError.json ├── requirements.txt ├── src ├── config │ ├── component │ │ └── response_header.yaml │ └── placeholder.json └── openapi │ ├── openapi-3.0.yaml │ ├── paths │ ├── bookmarks.yaml │ ├── follow.yaml │ ├── other.yaml │ ├── post.yaml │ ├── profile.yaml │ ├── timeline.yaml │ ├── tweet.yaml │ ├── user.yaml │ ├── usertweets.yaml │ ├── v1.1-get.yaml │ ├── v1.1-post.yaml │ └── v2.0-get.yaml │ ├── resources │ └── parameters.yaml │ ├── response │ └── error.yaml │ └── schemas │ ├── content.yaml │ ├── general.yaml │ ├── instruction.yaml │ ├── timeline.yaml │ ├── tweet.yaml │ ├── typename.yaml │ └── user.yaml ├── test ├── dart-dio │ └── openapi-generator-config.yaml ├── local.sh ├── python │ ├── openapi-generator-config.yaml │ ├── test_serialize.py │ └── test_serialize_guest.py └── typescript-fetch │ └── openapi-generator-config.yaml └── tools ├── build.py ├── build_config.py ├── generater.py ├── hooks.py └── login.py /.github/workflows/test-build.yaml: -------------------------------------------------------------------------------- 1 | name: build-test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | # Openapi Generator Setup 19 | - name: Get OpenJDK 11 20 | uses: actions/setup-java@v2 21 | with: 22 | distribution: "temurin" 23 | java-version: 11 24 | 25 | - name: Get Java version 26 | run: java -version 27 | 28 | - name: Get Openapi Generator cache file 29 | id: openapi-generator-cache 30 | run: echo "file=openapi-generator-cli.jar" >> $GITHUB_OUTPUT 31 | 32 | - name: Openapi Generator cache 33 | uses: actions/cache@v3 34 | with: 35 | path: ${{ steps.openapi-generator-cache.outputs.file }} 36 | key: ${{ runner.os }}-openapi-generator-${{ hashFiles('openapi-generator-cli.jar') }} 37 | restore-keys: | 38 | ${{ runner.os }}-openapi-generator- 39 | 40 | - name: Get Openapi Generator 41 | run: | 42 | wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.12.0/openapi-generator-cli-7.12.0.jar -O openapi-generator-cli.jar --no-verbose 43 | if: steps.openapi-generator-cache.outputs.cache-hit != 'true' 44 | 45 | # Python Setup 46 | - name: Set up Python 3.10 47 | uses: actions/setup-python@v4 48 | with: 49 | python-version: "3.10" 50 | architecture: "x64" 51 | 52 | - name: Get Python version 53 | run: python -V 54 | 55 | - name: Get pip cache dir 56 | id: pip-cache 57 | run: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT 58 | 59 | - name: pip cache 60 | uses: actions/cache@v3 61 | with: 62 | path: ${{ steps.pip-cache.outputs.dir }} 63 | key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} 64 | restore-keys: | 65 | ${{ runner.os }}-pip- 66 | 67 | - name: Install dependencies 68 | run: | 69 | python -m pip install --upgrade pip 70 | pip install -r requirements.txt 71 | 72 | # generate 73 | - name: Build twitter-openapi 74 | run: | 75 | python tools/build.py 76 | 77 | # clean up dependencies 78 | - name: Clean up dependencies 79 | run: | 80 | pip uninstall -y -r requirements.txt 81 | 82 | # generate 83 | - name: Generate 84 | run: | 85 | java -jar openapi-generator-cli.jar generate -c test/python/openapi-generator-config.yaml -g python 86 | java -jar openapi-generator-cli.jar generate -c test/dart-dio/openapi-generator-config.yaml -g dart-dio 87 | java -jar openapi-generator-cli.jar generate -c test/typescript-fetch/openapi-generator-config.yaml -g typescript-fetch 88 | -------------------------------------------------------------------------------- /.github/workflows/test-python.yaml: -------------------------------------------------------------------------------- 1 | name: test-python 2 | 3 | on: 4 | pull_request_target: 5 | types: [labeled] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build_and_preview: 10 | runs-on: ubuntu-latest 11 | name: Test 12 | if: contains(github.event.pull_request.labels.*.name, 'safe to test') 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | ref: "${{ github.event.pull_request.merge_commit_sha }}" 18 | 19 | # Openapi Generator Setup 20 | - name: Get OpenJDK 11 21 | uses: actions/setup-java@v2 22 | with: 23 | distribution: "temurin" 24 | java-version: 11 25 | 26 | - name: Get Java version 27 | run: java -version 28 | 29 | - name: Get Openapi Generator cache file 30 | id: openapi-generator-cache 31 | run: echo "file=openapi-generator-cli.jar" >> $GITHUB_OUTPUT 32 | 33 | - name: Openapi Generator cache 34 | uses: actions/cache@v3 35 | with: 36 | path: ${{ steps.openapi-generator-cache.outputs.file }} 37 | key: ${{ runner.os }}-openapi-generator-${{ hashFiles('openapi-generator-cli.jar') }} 38 | restore-keys: | 39 | ${{ runner.os }}-openapi-generator- 40 | 41 | - name: Get Openapi Generator 42 | run: | 43 | wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.13.0/openapi-generator-cli-7.13.0.jar -O openapi-generator-cli.jar --no-verbose 44 | if: steps.openapi-generator-cache.outputs.cache-hit != 'true' 45 | 46 | # Python Setup 47 | - name: Set up Python 3.10 48 | uses: actions/setup-python@v4 49 | with: 50 | python-version: "3.10" 51 | architecture: "x64" 52 | 53 | - name: Get Python version 54 | run: python -V 55 | 56 | - name: Get pip cache dir 57 | id: pip-cache 58 | run: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT 59 | 60 | - name: pip cache 61 | uses: actions/cache@v3 62 | with: 63 | path: ${{ steps.pip-cache.outputs.dir }} 64 | key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} 65 | restore-keys: | 66 | ${{ runner.os }}-pip- 67 | 68 | - name: Install dependencies 69 | run: | 70 | python -m pip install --upgrade pip 71 | pip install -r requirements.txt 72 | 73 | # generate 74 | - name: Build twitter-openapi 75 | run: | 76 | python tools/build.py 77 | 78 | # clean up dependencies 79 | - name: Clean up dependencies 80 | run: | 81 | pip uninstall -y -r requirements.txt 82 | 83 | # generate 84 | - name: Generate Python client 85 | run: | 86 | java -jar openapi-generator-cli.jar generate -c test/python/openapi-generator-config.yaml -g python 87 | 88 | # test 89 | - name: Test Python client 90 | run: | 91 | pip install ./python_generated 92 | 93 | - name: Run Python client test 94 | run: | 95 | python test/python/test_serialize.py 96 | env: 97 | TWITTER_SESSION: ${{ secrets.TWITTER_SESSION }} 98 | ERROR_UNCATCHED: "False" 99 | SLEEP_TIME: 2 100 | CUESOR_TEST_COUNT: 5 101 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | __pycache__/ 3 | cookie.json 4 | *_generated 5 | *.jar 6 | cache/ 7 | tools/input.js -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "redhat.vscode-yaml", 4 | "ms-python.vscode-pylance", 5 | "ms-python.python", 6 | "ms-python.debugpy", 7 | "charliermarsh.ruff" 8 | ] 9 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Python: 現在のファイル", 6 | "type": "debugpy", 7 | "request": "launch", 8 | "program": "${file}", 9 | "console": "integratedTerminal", 10 | "justMyCode": false 11 | }, 12 | { 13 | "name": "build", 14 | "type": "debugpy", 15 | "request": "launch", 16 | "program": "tools/build.py", 17 | "console": "integratedTerminal", 18 | "justMyCode": true 19 | }, 20 | { 21 | "name": "test", 22 | "type": "debugpy", 23 | "request": "launch", 24 | "program": "test/python/test_serialize.py", 25 | "console": "integratedTerminal", 26 | "justMyCode": false, 27 | "preLaunchTask": "build-task", 28 | "env": { 29 | "ERROR_UNCATCHED": "True", 30 | "STRICT_MODE": "True", 31 | "MULTI_THREAD": "True", 32 | "CUESOR_TEST_COUNT": "3" 33 | } 34 | }, 35 | { 36 | "name": "test-guest", 37 | "type": "debugpy", 38 | "request": "launch", 39 | "program": "test/python/test_serialize_guest.py", 40 | "console": "integratedTerminal", 41 | "justMyCode": false, 42 | "preLaunchTask": "build-task" 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "yaml.format.enable": true, 3 | "yaml.completion": true, 4 | "yaml.validate": true, 5 | "yaml.hover": true, 6 | "yaml.schemas": { 7 | "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/refs/heads/main/schemas/v3.0/schema.yaml": "src/**/*.yaml" 8 | }, 9 | "[json]": { 10 | "editor.defaultFormatter": "esbenp.prettier-vscode" 11 | }, 12 | "[jsonc]": { 13 | "editor.defaultFormatter": "esbenp.prettier-vscode" 14 | }, 15 | "[yaml]": { 16 | "editor.defaultFormatter": "esbenp.prettier-vscode" 17 | }, 18 | "[markdown]": { 19 | "editor.defaultFormatter": "esbenp.prettier-vscode" 20 | }, 21 | // "python.analysis.typeCheckingMode": "basic", 22 | "[python]": { 23 | "editor.codeActionsOnSave": { 24 | "source.organizeImports": "explicit", 25 | "source.fixAll": "explicit" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build-task", 6 | "type": "shell", 7 | "osx": { 8 | "command": [ 9 | "source .venv/bin/activate;", 10 | "python3 tools/build.py;", 11 | "java -jar openapi-generator-cli.jar generate -c test/python/openapi-generator-config.yaml -g python;", 12 | "python3 -m pip install ./python_generated;" 13 | ] 14 | }, 15 | "linux": { 16 | "command": [ 17 | "source .venv/bin/activate;", 18 | "python3 tools/build.py;", 19 | "java -jar openapi-generator-cli.jar generate -c test/python/openapi-generator-config.yaml -g python;", 20 | "python3 -m pip install ./python_generated;" 21 | ] 22 | }, 23 | "windows": { 24 | "command": [ 25 | ".venv/Scripts/activate;", 26 | "python tools/build.py;", 27 | "scoop reset temurin17-jdk;", 28 | "java -jar openapi-generator-cli.jar generate -c test/python/openapi-generator-config.yaml -g python;", 29 | "python -m pip install ./python_generated;" 30 | ] 31 | } 32 | }, 33 | { 34 | "label": "init-venv", 35 | "type": "shell", 36 | "runOptions": { 37 | "runOn": "folderOpen" 38 | }, 39 | "osx": { 40 | "command": [ 41 | "python3.10 -m venv .venv;", 42 | ".venv/bin/python3 -m pip install -r requirements.txt;", 43 | "pip install urllib3>=2.1.0;", 44 | "curl https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.13.0/openapi-generator-cli-7.13.0.jar -o openapi-generator-cli.jar;" 45 | ] 46 | }, 47 | "linux": { 48 | "command": [ 49 | "python3 -m venv .venv;", 50 | ".venv/bin/python3 -m pip install -r requirements.txt;", 51 | "pip install urllib3>=2.1.0;", 52 | "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.13.0/openapi-generator-cli-7.13.0.jar -O openapi-generator-cli.jar;" 53 | ] 54 | }, 55 | "windows": { 56 | "command": [ 57 | "python -m venv .venv;", 58 | ".venv/Scripts/python -m pip install -r requirements.txt;", 59 | "pip install urllib3>=2.1.0;", 60 | "Invoke-WebRequest https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.13.0/openapi-generator-cli-7.13.0.jar -OutFile openapi-generator-cli.jar;" 61 | ] 62 | } 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | - `src` *.yaml files should be written according to the [v3.0/schema.json](https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/schemas/v3.0/schema.json) 4 | - `dist` Do not rewrite this file as it is an automatically generated OpenAPI file. 5 | - `dist` Do not include this in the pull request due to potential conflicts. 6 | 7 | Users using vscode can build and test with the GUI. 8 | [.vscode/launch.json](.vscode/launch.json) is a configuration file for vscode. 9 | 10 | ## Good first issues 11 | 12 | - Add a new endpoint 13 | - Add type to `type: object # todo` 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | twitter-openapi License 2 | 3 | Copyright (c) 2023 yuki 4 | 5 | 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | The Software shall not be used for any activity that involves the following types 18 | of behavior, commonly known as spam: 19 | 20 | 1. Sending unsolicited or excessive messages or posts. 21 | 2. Aggressively following, unfollowing, or liking tweets to artificially boost engagement. 22 | 3. Engaging in aggressive automated actions that disrupt or annoy other users. 23 | 4. Distributing false or misleading information. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | -------------------------------------------------------------------------------- /LICENSE.AGPL: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twitter OpenAPI 2 | 3 | Twitter OpenAPI(Swagger) specification 4 | 5 | [issues](https://github.com/fa0311/twitter-openapi/issues) / [discussions](https://github.com/fa0311/twitter-openapi/discussions) 6 | 7 | - [Python](https://github.com/fa0311/twitter_openapi_python) 8 | - [Dart](https://github.com/fa0311/twitter_openapi_dart) 9 | - [TypeScript](https://github.com/fa0311/twitter-openapi-typescript) 10 | - [React Documents](https://github.com/fa0311/twitter-openapi-docs) 11 | 12 | ## Usage 13 | 14 | ```shell 15 | openapi-generator-cli generate -g -i https://raw.githubusercontent.com/fa0311/twitter-openapi/main/dist/docs/openapi-3.0.yaml -o ./generated 16 | ``` 17 | 18 | There are several outputs, but the one that most closely follows the OpenAPI specification is `dist/docs`. 19 | However, a lot of syntax that is not supported by some generators is used. 20 | 21 | You can also modify the hook to make the generated results more user-friendly. [build_config.py](./tools/build_config.py) 22 | 23 | Note that the license also inherits to the output. 24 | 25 | ## Contribute 26 | 27 | [CONTRIBUTING.md](./CONTRIBUTING.md) 28 | 29 | ### build 30 | 31 | ```shell 32 | python -V # Python 3.10.8 33 | pip install -r requirements.txt 34 | python tools/build.py 35 | ``` 36 | 37 | ## License 38 | 39 | This project is dual licensed. You can choose one of the following licenses: 40 | 41 | - [Custom License](./LICENSE) 42 | - [GNU Affero General Public License v3.0](./LICENSE.AGPL) 43 | -------------------------------------------------------------------------------- /debug/requests_sample.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | import requests 5 | 6 | if Path("cookie.json").exists(): 7 | with open("cookie.json", "r") as f: 8 | cookies = json.load(f) 9 | cookies_dict = {k["name"]: k["value"] for k in cookies} 10 | 11 | 12 | data = requests.get( 13 | "https://twitter.com/i/api/graphql/BQ6xjFU6Mgm-WhEP3OiT9w/UserByScreenName", 14 | cookies=cookies_dict, 15 | headers={ 16 | "accept": "*/*", 17 | "accept-encoding": "identity", 18 | "accept-language": "en-US,en;q=0.9", 19 | "authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA", 20 | "content-type": "application/json", 21 | "referer": "https://twitter.com", 22 | "sec-ch-ua": '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"', 23 | "sec-ch-ua-mobile": "?0", 24 | "sec-ch-ua-platform": '"Windows"', 25 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/Chrome/130.0.0.0 Safari/537.36", 26 | "x-csrf-token": cookies_dict["ct0"], 27 | "x-twitter-active-user": "yes", 28 | "x-twitter-auth-type": "OAuth2Session", 29 | "x-twitter-client-language": "en", 30 | }, 31 | params={ 32 | "variables": json.dumps( 33 | { 34 | "screen_name": "5unsetpowerln", 35 | }, 36 | ), 37 | "features": json.dumps( 38 | { 39 | "hidden_profile_subscriptions_enabled": True, 40 | "rweb_tipjar_consumption_enabled": True, 41 | "responsive_web_graphql_exclude_directive_enabled": True, 42 | "verified_phone_label_enabled": False, 43 | "subscriptions_verification_info_is_identity_verified_enabled": True, 44 | "subscriptions_verification_info_verified_since_enabled": True, 45 | "highlights_tweets_tab_ui_enabled": True, 46 | "responsive_web_twitter_article_notes_tab_enabled": True, 47 | "subscriptions_feature_can_gift_premium": True, 48 | "creator_subscriptions_tweet_preview_api_enabled": True, 49 | "responsive_web_graphql_skip_user_profile_image_extensions_enabled": False, 50 | "responsive_web_graphql_timeline_navigation_enabled": True, 51 | } 52 | ), 53 | "fieldToggles": json.dumps( 54 | { 55 | "withAuxiliaryUserLabels": False, 56 | }, 57 | ), 58 | }, 59 | ) 60 | print(data.json()) 61 | -------------------------------------------------------------------------------- /other/error/GenericError.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "message": "_Missing: No status found with that ID.", 5 | "locations": [ 6 | { 7 | "line": 2, 8 | "column": 3 9 | } 10 | ], 11 | "path": ["threaded_conversation_with_injections_v2"], 12 | "extensions": { 13 | "name": "GenericError", 14 | "source": "Server", 15 | "code": 144, 16 | "kind": "NonFatal", 17 | "tracing": { 18 | "trace_id": "e6de8b2b694ae128" 19 | } 20 | }, 21 | "code": 144, 22 | "kind": "NonFatal", 23 | "name": "GenericError", 24 | "source": "Server", 25 | "tracing": { 26 | "trace_id": "e6de8b2b694ae128" 27 | } 28 | } 29 | ], 30 | "data": {} 31 | } 32 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fa0311/twitter-openapi/bc6ead94833764f54a6a4623871f7ad29926a7d9/requirements.txt -------------------------------------------------------------------------------- /src/config/component/response_header.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | paths: {} 6 | 7 | components: 8 | headers: 9 | x-connection-hash: 10 | schema: 11 | type: string 12 | x-rate-limit-limit: 13 | schema: 14 | type: integer 15 | x-rate-limit-remaining: 16 | schema: 17 | type: integer 18 | x-rate-limit-reset: 19 | schema: 20 | type: integer 21 | x-response-time: 22 | schema: 23 | type: integer 24 | x-tfe-preserve-body: 25 | schema: 26 | type: boolean 27 | x-transaction-id: 28 | schema: 29 | type: string 30 | x-twitter-response-tags: 31 | schema: 32 | type: string 33 | -------------------------------------------------------------------------------- /src/openapi/openapi-3.0.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | description: |- 5 | Twitter OpenAPI(Swagger) specification 6 | termsOfService: https://github.com/fa0311 7 | contact: 8 | email: yuki@yuki0311.com 9 | license: 10 | name: custom license or AGPL-3.0-or-later 11 | url: https://github.com/fa0311/twitter-openapi#license 12 | version: 0.0.1 13 | servers: 14 | - url: https://x.com/i/api 15 | - url: https://twitter.com/i/api 16 | 17 | paths: {} 18 | components: 19 | schemas: {} 20 | 21 | securitySchemes: 22 | Accept: 23 | description: "*/*" 24 | in: header 25 | name: Accept 26 | type: apiKey 27 | AcceptEncoding: 28 | description: "gzip, deflate, br, zstd" 29 | in: header 30 | name: Accept-Encoding 31 | type: apiKey 32 | AcceptLanguage: 33 | description: "en-US,en;q=0.9" 34 | in: header 35 | name: Accept-Language 36 | type: apiKey 37 | Priority: 38 | description: "u=1, i" 39 | in: header 40 | name: Priority 41 | type: apiKey 42 | Referer: 43 | description: "https://x.com/home" 44 | in: header 45 | name: Referer 46 | type: apiKey 47 | SecChUa: 48 | description: '"Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"' 49 | in: header 50 | name: Sec-Ch-Ua 51 | type: apiKey 52 | SecChUaMobile: 53 | description: "?0" 54 | in: header 55 | name: Sec-Ch-Ua-Mobile 56 | type: apiKey 57 | SecChUaPlatform: 58 | description: '"Windows"' 59 | in: header 60 | name: Sec-Ch-Ua-Platform 61 | type: apiKey 62 | SecFetchDest: 63 | description: "empty" 64 | in: header 65 | name: Sec-Fetch-Dest 66 | type: apiKey 67 | SecFetchMode: 68 | description: "cors" 69 | in: header 70 | name: Sec-Fetch-Mode 71 | type: apiKey 72 | SecFetchSite: 73 | description: "same-origin" 74 | in: header 75 | name: Sec-Fetch-Site 76 | type: apiKey 77 | UserAgent: 78 | description: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" 79 | in: header 80 | name: user-agent 81 | type: apiKey 82 | BearerAuth: 83 | description: AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA 84 | scheme: bearer 85 | type: http 86 | ClientTransactionId: 87 | type: apiKey 88 | in: header 89 | name: x-client-transaction-id 90 | ClientUuid: 91 | type: apiKey 92 | in: header 93 | name: x-client-uuid 94 | description: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 95 | CsrfToken: 96 | type: apiKey 97 | in: header 98 | name: x-csrf-token 99 | description: "document.cookie.split('; ').find(row => row.startsWith('ct0='));" 100 | GuestToken: 101 | type: apiKey 102 | in: header 103 | name: x-guest-token 104 | description: "document.cookie.split('; ').find(row => row.startsWith('gt='));" 105 | ActiveUser: 106 | description: "yes" 107 | in: header 108 | name: x-twitter-active-user 109 | type: apiKey 110 | AuthType: 111 | type: apiKey 112 | in: header 113 | name: x-twitter-auth-type 114 | description: "OAuth2Session if you are logged in" 115 | ClientLanguage: 116 | description: en 117 | in: header 118 | name: x-twitter-client-language 119 | type: apiKey 120 | CookieAuthToken: 121 | type: apiKey 122 | in: cookie 123 | name: auth_token 124 | description: "HttpOnly cookie" 125 | CookieCt0: 126 | type: apiKey 127 | in: cookie 128 | name: ct0 129 | description: "document.cookie.split('; ').find(row => row.startsWith('ct0='));" 130 | CookieGt0: 131 | type: apiKey 132 | in: cookie 133 | name: gt0 134 | description: "document.cookie.split('; ').find(row => row.startsWith('gt0='));" 135 | 136 | security: 137 | # Browser Schemes 138 | - Accept: [] 139 | - AcceptEncoding: [] 140 | - AcceptLanguage: [] 141 | - Priority: [] 142 | - Referer: [] 143 | - SecChUa: [] 144 | - SecChUaMobile: [] 145 | - SecChUaPlatform: [] 146 | - SecFetchDest: [] 147 | - SecFetchMode: [] 148 | - SecFetchSite: [] 149 | # Other Auth Schemes 150 | - UserAgent: [] 151 | - BearerAuth: [] 152 | # X Schemes 153 | - ClientTransactionId: [] 154 | - ClientUuid: [] 155 | - CsrfToken: [] 156 | - GuestToken: [] 157 | # Twitter Schemes 158 | - ActiveUser: [] 159 | - AuthType: [] 160 | - ClientLanguage: [] 161 | # Cookie Schemes 162 | - CookieCt0: [] 163 | - CookieAuthToken: [] 164 | - CookieGt0: [] 165 | 166 | tags: 167 | - name: user 168 | description: Responses containing User objects. 169 | - name: users 170 | description: Responses containing List[User] objects. 171 | - name: user-list 172 | description: Responses containing instruction objects. 173 | - name: tweet 174 | description: Responses containing instruction objects. 175 | - name: post 176 | description: post 177 | - name: v1.1-get 178 | description: legacy APIs get 179 | - name: v1.1-post 180 | description: legacy APIs post 181 | - name: v2.0-get 182 | description: legacy APIs get 183 | - name: v2.0-post 184 | description: legacy APIs post 185 | - name: other 186 | description: other 187 | -------------------------------------------------------------------------------- /src/openapi/paths/bookmarks.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | 6 | paths: 7 | /graphql/{pathQueryId}/Bookmarks: 8 | get: 9 | operationId: getBookmarks 10 | description: get bookmarks 11 | responses: 12 | "200": 13 | description: Successful operation 14 | content: 15 | application/json: 16 | schema: 17 | $ref: "#/components/schemas/BookmarksResponse" 18 | tags: 19 | - "tweet" 20 | 21 | components: 22 | schemas: 23 | BookmarksResponse: 24 | properties: 25 | data: 26 | $ref: "#/components/schemas/BookmarksResponseData" 27 | errors: 28 | type: array 29 | items: 30 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 31 | 32 | BookmarksResponseData: 33 | required: 34 | - "bookmark_timeline_v2" 35 | properties: 36 | bookmark_timeline_v2: 37 | $ref: "#/components/schemas/BookmarksTimeline" 38 | 39 | BookmarksTimeline: 40 | required: 41 | - "timeline" 42 | properties: 43 | timeline: 44 | $ref: "./../schemas/timeline.yaml#/components/schemas/Timeline" 45 | -------------------------------------------------------------------------------- /src/openapi/paths/follow.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | 6 | paths: 7 | /graphql/{pathQueryId}/Following: 8 | get: 9 | operationId: getFollowing 10 | description: get user list of following 11 | responses: 12 | "200": 13 | description: Successful operation 14 | content: 15 | application/json: 16 | schema: 17 | $ref: "#/components/schemas/FollowResponse" 18 | tags: 19 | - "user-list" 20 | 21 | /graphql/{pathQueryId}/Followers: 22 | get: 23 | operationId: getFollowers 24 | description: get user list of followers 25 | responses: 26 | "200": 27 | description: Successful operation 28 | content: 29 | application/json: 30 | schema: 31 | $ref: "#/components/schemas/FollowResponse" 32 | tags: 33 | - "user-list" 34 | 35 | /graphql/{pathQueryId}/FollowersYouKnow: 36 | get: 37 | operationId: getFollowersYouKnow 38 | description: "get followers you know" 39 | responses: 40 | "200": 41 | description: Successful operation 42 | content: 43 | application/json: 44 | schema: 45 | $ref: "#/components/schemas/FollowResponse" 46 | tags: 47 | - "user-list" 48 | 49 | components: 50 | schemas: 51 | FollowResponse: 52 | required: 53 | - "data" 54 | properties: 55 | data: 56 | $ref: "#/components/schemas/FollowResponseData" 57 | errors: 58 | type: array 59 | items: 60 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 61 | 62 | FollowResponseData: 63 | properties: 64 | user: 65 | $ref: "#/components/schemas/FollowResponseUser" 66 | 67 | FollowResponseUser: 68 | required: 69 | - "result" 70 | properties: 71 | result: 72 | $ref: "#/components/schemas/FollowResponseResult" 73 | 74 | FollowResponseResult: 75 | required: 76 | - "__typename" 77 | - "timeline" 78 | properties: 79 | __typename: 80 | $ref: "./../schemas/typename.yaml#/components/schemas/TypeName" # User 81 | timeline: 82 | $ref: "#/components/schemas/FollowTimeline" 83 | 84 | FollowTimeline: 85 | required: 86 | - "timeline" 87 | properties: 88 | timeline: 89 | $ref: "./../schemas/timeline.yaml#/components/schemas/Timeline" 90 | -------------------------------------------------------------------------------- /src/openapi/paths/other.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | 6 | paths: 7 | /other: 8 | get: 9 | operationId: other 10 | description: This is not an actual endpoint 11 | responses: 12 | "200": 13 | description: Successful operation 14 | content: 15 | application/json: 16 | schema: 17 | $ref: "#/components/schemas/OtherObjectAll" 18 | tags: 19 | - "other" 20 | 21 | components: 22 | schemas: 23 | OtherObjectAll: 24 | type: object 25 | properties: 26 | Session: 27 | $ref: "#/components/schemas/Session" # window.__INITIAL_STATE__ 28 | Session: 29 | type: object 30 | required: 31 | - "country" 32 | - "communitiesActions" 33 | - "isActiveCreator" 34 | - "isRestrictedSession" 35 | - "guestId" 36 | - "hasCommunityMemberships" 37 | - "language" 38 | - "oneFactorLoginEligibility" 39 | - "ssoInitTokens" 40 | - "superFollowersCount" 41 | - "superFollowsApplicationStatus" 42 | - "user_id" 43 | - "userFeatures" 44 | - "isSuperFollowSubscriber" 45 | properties: 46 | country: 47 | type: string 48 | pattern: "^[A-Z]{2}$" 49 | communitiesActions: 50 | $ref: "#/components/schemas/CommunitiesActions" 51 | isActiveCreator: 52 | type: boolean 53 | isRestrictedSession: 54 | type: boolean 55 | guestId: 56 | type: string 57 | pattern: "^[0-9]+$" 58 | hasCommunityMemberships: 59 | type: boolean 60 | language: 61 | type: string 62 | pattern: "^[a-z]{2}$" 63 | oneFactorLoginEligibility: 64 | $ref: "#/components/schemas/OneFactorLoginEligibility" 65 | SsoInitTokens: 66 | type: object 67 | superFollowersCount: 68 | type: integer 69 | superFollowsApplicationStatus: 70 | type: string 71 | enum: [NotStarted] 72 | user_id: 73 | type: string 74 | pattern: "^[0-9]+$" 75 | userFeatures: 76 | $ref: "#/components/schemas/UserFeatures" 77 | isSuperFollowSubscriber: 78 | type: boolean 79 | 80 | CommunitiesActions: 81 | type: object 82 | required: 83 | - "create" 84 | properties: 85 | create: 86 | type: boolean 87 | 88 | OneFactorLoginEligibility: 89 | type: object 90 | required: 91 | - "fetchStatus" 92 | properties: 93 | fetchStatus: 94 | type: string # enum: none 95 | 96 | UserFeatures: 97 | type: object 98 | required: 99 | - "mediatool_studio_library" 100 | properties: 101 | mediatool_studio_library: 102 | type: boolean 103 | -------------------------------------------------------------------------------- /src/openapi/paths/post.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | 6 | paths: 7 | /graphql/{pathQueryId}/FavoriteTweet: 8 | post: 9 | operationId: postFavoriteTweet 10 | description: favorite Tweet 11 | responses: 12 | "200": 13 | description: Successful operation 14 | content: 15 | application/json: 16 | schema: 17 | $ref: "#/components/schemas/FavoriteTweetResponse" 18 | tags: 19 | - "post" 20 | 21 | /graphql/{pathQueryId}/UnfavoriteTweet: 22 | post: 23 | operationId: postUnfavoriteTweet 24 | description: unfavorite Tweet 25 | responses: 26 | "200": 27 | description: Successful operation 28 | content: 29 | application/json: 30 | schema: 31 | $ref: "#/components/schemas/UnfavoriteTweetResponse" 32 | tags: 33 | - "post" 34 | 35 | /graphql/{pathQueryId}/CreateRetweet: 36 | post: 37 | operationId: postCreateRetweet 38 | description: create Retweet 39 | responses: 40 | "200": 41 | description: Successful operation 42 | content: 43 | application/json: 44 | schema: 45 | $ref: "#/components/schemas/CreateRetweetResponse" 46 | tags: 47 | - "post" 48 | 49 | /graphql/{pathQueryId}/DeleteRetweet: 50 | post: 51 | operationId: postDeleteRetweet 52 | description: delete Retweet 53 | responses: 54 | "200": 55 | description: Successful operation 56 | content: 57 | application/json: 58 | schema: 59 | $ref: "#/components/schemas/DeleteRetweetResponse" 60 | tags: 61 | - "post" 62 | 63 | /graphql/{pathQueryId}/CreateTweet: 64 | post: 65 | operationId: postCreateTweet 66 | description: create Tweet 67 | responses: 68 | "200": 69 | description: Successful operation 70 | content: 71 | application/json: 72 | schema: 73 | $ref: "#/components/schemas/CreateTweetResponse" 74 | tags: 75 | - "post" 76 | 77 | /graphql/{pathQueryId}/DeleteTweet: 78 | post: 79 | operationId: postDeleteTweet 80 | description: delete Retweet 81 | responses: 82 | "200": 83 | description: Successful operation 84 | content: 85 | application/json: 86 | schema: 87 | $ref: "#/components/schemas/DeleteTweetResponse" 88 | tags: 89 | - "post" 90 | 91 | /graphql/{pathQueryId}/CreateBookmark: 92 | post: 93 | operationId: postCreateBookmark 94 | description: create Bookmark 95 | responses: 96 | "200": 97 | description: Successful operation 98 | content: 99 | application/json: 100 | schema: 101 | $ref: "#/components/schemas/CreateBookmarkResponse" 102 | tags: 103 | - "post" 104 | 105 | /graphql/{pathQueryId}/DeleteBookmark: 106 | post: 107 | operationId: postDeleteBookmark 108 | description: delete Bookmark 109 | responses: 110 | "200": 111 | description: Successful operation 112 | content: 113 | application/json: 114 | schema: 115 | $ref: "#/components/schemas/DeleteBookmarkResponse" 116 | tags: 117 | - "post" 118 | 119 | components: 120 | schemas: 121 | # ---Favorite--- 122 | FavoriteTweetResponse: 123 | required: 124 | - "data" 125 | properties: 126 | data: 127 | $ref: "#/components/schemas/FavoriteTweet" 128 | errors: 129 | type: array 130 | items: 131 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 132 | 133 | FavoriteTweet: 134 | properties: 135 | favorite_tweet: 136 | type: string 137 | 138 | # ---Unfavorite--- 139 | 140 | UnfavoriteTweetResponse: 141 | required: 142 | - "data" 143 | properties: 144 | data: 145 | $ref: "#/components/schemas/UnfavoriteTweet" 146 | errors: 147 | type: array 148 | items: 149 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 150 | 151 | UnfavoriteTweet: 152 | properties: 153 | unfavorite_tweet: 154 | type: string 155 | 156 | # ---Retweet--- 157 | 158 | CreateRetweetResponse: 159 | required: 160 | - "data" 161 | properties: 162 | data: 163 | $ref: "#/components/schemas/CreateRetweetResponseData" 164 | errors: 165 | type: array 166 | items: 167 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 168 | 169 | CreateRetweetResponseData: 170 | properties: 171 | create_retweet: 172 | $ref: "#/components/schemas/CreateRetweetResponseResult" 173 | 174 | CreateRetweetResponseResult: 175 | required: 176 | - "retweet_results" 177 | properties: 178 | retweet_results: 179 | $ref: "#/components/schemas/CreateRetweet" 180 | 181 | CreateRetweet: 182 | required: 183 | - "result" 184 | properties: 185 | result: 186 | $ref: "#/components/schemas/Retweet" 187 | 188 | Retweet: 189 | required: 190 | - "rest_id" 191 | - "legacy" 192 | properties: 193 | rest_id: 194 | type: string 195 | pattern: "^[0-9]+$" 196 | legacy: 197 | type: object 198 | required: 199 | - "full_text" 200 | properties: 201 | full_text: 202 | type: string 203 | 204 | # ---DeleteRetweet--- 205 | 206 | DeleteRetweetResponse: 207 | required: 208 | - "data" 209 | properties: 210 | data: 211 | $ref: "#/components/schemas/DeleteRetweetResponseData" 212 | errors: 213 | type: array 214 | items: 215 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 216 | 217 | DeleteRetweetResponseData: 218 | properties: 219 | create_retweet: 220 | $ref: "#/components/schemas/DeleteRetweetResponseResult" 221 | 222 | DeleteRetweetResponseResult: 223 | required: 224 | - "source_tweet_results" 225 | properties: 226 | retweet_results: 227 | $ref: "#/components/schemas/DeleteRetweet" 228 | 229 | DeleteRetweet: 230 | required: 231 | - "result" 232 | properties: 233 | result: 234 | type: object 235 | items: 236 | $ref: "#/components/schemas/Retweet" 237 | 238 | # ---Tweet--- 239 | 240 | CreateTweetResponse: 241 | required: 242 | - "data" 243 | properties: 244 | data: 245 | $ref: "#/components/schemas/CreateTweetResponseData" 246 | errors: 247 | type: array 248 | items: 249 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 250 | 251 | CreateTweetResponseData: 252 | properties: 253 | create_tweet: 254 | $ref: "#/components/schemas/CreateTweetResponseResult" 255 | 256 | CreateTweetResponseResult: 257 | required: 258 | - "tweet_results" 259 | properties: 260 | tweet_results: 261 | $ref: "#/components/schemas/CreateTweet" 262 | 263 | CreateTweet: 264 | required: 265 | - "result" 266 | properties: 267 | result: 268 | $ref: "./../schemas/tweet.yaml#/components/schemas/Tweet" 269 | 270 | # ---DeleteTweet--- 271 | 272 | DeleteTweetResponse: 273 | required: 274 | - "data" 275 | properties: 276 | data: 277 | $ref: "#/components/schemas/DeleteTweetResponseData" 278 | errors: 279 | type: array 280 | items: 281 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 282 | 283 | DeleteTweetResponseData: 284 | properties: 285 | delete_retweet: 286 | $ref: "#/components/schemas/DeleteTweetResponseResult" 287 | 288 | DeleteTweetResponseResult: 289 | required: 290 | - "tweet_results" 291 | properties: 292 | tweet_results: 293 | type: object 294 | 295 | # ---Bookmark--- 296 | 297 | CreateBookmarkResponse: 298 | required: 299 | - "data" 300 | properties: 301 | data: 302 | $ref: "#/components/schemas/CreateBookmarkResponseData" 303 | errors: 304 | type: array 305 | items: 306 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 307 | 308 | CreateBookmarkResponseData: 309 | properties: 310 | tweet_bookmark_put: 311 | type: string 312 | 313 | # ---DeleteBookmark--- 314 | 315 | DeleteBookmarkResponse: 316 | required: 317 | - "data" 318 | properties: 319 | data: 320 | $ref: "#/components/schemas/DeleteBookmarkResponseData" 321 | errors: 322 | type: array 323 | items: 324 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 325 | 326 | DeleteBookmarkResponseData: 327 | properties: 328 | tweet_bookmark_delete: 329 | type: string 330 | -------------------------------------------------------------------------------- /src/openapi/paths/profile.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | 6 | paths: 7 | /graphql/{pathQueryId}/ProfileSpotlightsQuery: 8 | get: 9 | operationId: getProfileSpotlightsQuery 10 | description: "get user by screen name" 11 | responses: 12 | "200": 13 | description: Successful operation 14 | content: 15 | application/json: 16 | schema: 17 | $ref: "#/components/schemas/ProfileResponse" 18 | 19 | components: 20 | schemas: 21 | ProfileResponse: 22 | required: 23 | - "data" 24 | properties: 25 | data: 26 | $ref: "#/components/schemas/ProfileResponseData" 27 | errors: 28 | type: array 29 | items: 30 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 31 | 32 | ProfileResponseData: 33 | properties: 34 | user_result_by_screen_name: 35 | $ref: "#/components/schemas/UserResultByScreenName" 36 | 37 | UserResultByScreenName: 38 | required: 39 | - "id" 40 | - "result" 41 | properties: 42 | id: 43 | type: string 44 | pattern: "^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$" # base64 45 | result: 46 | $ref: "#/components/schemas/UserResultByScreenNameResult" 47 | 48 | UserResultByScreenNameResult: 49 | required: 50 | - "__typename" 51 | - "id" 52 | - "legacy" 53 | - "profilemodules" 54 | - "rest_id" 55 | properties: 56 | __typename: 57 | $ref: "./../schemas/typename.yaml#/components/schemas/TypeName" # User 58 | id: 59 | type: string 60 | pattern: "^[0-9a-zA-Z=]+$" 61 | legacy: 62 | $ref: "#/components/schemas/UserResultByScreenNameLegacy" 63 | profilemodules: 64 | type: object 65 | additionalProperties: true # todo 66 | rest_id: 67 | type: string 68 | pattern: "^[0-9]+$" 69 | UserResultByScreenNameLegacy: 70 | properties: 71 | blocking: 72 | type: boolean 73 | blocked_by: 74 | type: boolean 75 | protected: 76 | type: boolean 77 | following: 78 | type: boolean 79 | followed_by: 80 | type: boolean 81 | name: 82 | type: string 83 | screen_name: 84 | type: string 85 | -------------------------------------------------------------------------------- /src/openapi/paths/timeline.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | 6 | paths: 7 | /graphql/{pathQueryId}/HomeTimeline: 8 | get: 9 | operationId: getHomeTimeline 10 | description: get tweet list of timeline 11 | responses: 12 | "200": 13 | description: Successful operation 14 | content: 15 | application/json: 16 | schema: 17 | $ref: "#/components/schemas/TimelineResponse" 18 | tags: 19 | - "tweet" 20 | 21 | /graphql/{pathQueryId}/HomeLatestTimeline: 22 | get: 23 | operationId: getHomeLatestTimeline 24 | description: get tweet list of timeline 25 | responses: 26 | "200": 27 | description: Successful operation 28 | content: 29 | application/json: 30 | schema: 31 | $ref: "#/components/schemas/TimelineResponse" 32 | tags: 33 | - "tweet" 34 | 35 | /graphql/{pathQueryId}/ListLatestTweetsTimeline: 36 | get: 37 | operationId: getListLatestTweetsTimeline 38 | description: get tweet list of timeline 39 | responses: 40 | "200": 41 | description: Successful operation 42 | content: 43 | application/json: 44 | schema: 45 | $ref: "#/components/schemas/ListLatestTweetsTimelineResponse" 46 | tags: 47 | - "tweet" 48 | 49 | /graphql/{pathQueryId}/SearchTimeline: 50 | get: 51 | operationId: getSearchTimeline 52 | description: search tweet list. product:[Top, Latest, People, Photos, Videos] 53 | responses: 54 | "200": 55 | description: Successful operation 56 | content: 57 | application/json: 58 | schema: 59 | $ref: "#/components/schemas/SearchTimelineResponse" 60 | tags: 61 | - "tweet" 62 | 63 | /graphql/{pathQueryId}/CommunityTweetsTimeline: 64 | get: 65 | operationId: getCommunityTweetsTimeline 66 | description: get tweet list of community. rankingMode:[Recency, Relevance] 67 | responses: 68 | "200": 69 | description: Successful operation 70 | content: 71 | application/json: 72 | schema: 73 | $ref: "#/components/schemas/CommunityTweetsTimelineResponse" 74 | tags: 75 | - "tweet" 76 | 77 | /graphql/{pathQueryId}/CommunityMediaTimeline: 78 | get: 79 | operationId: getCommunityMediaTimeline 80 | description: get media list of community 81 | responses: 82 | "200": 83 | description: Successful operation 84 | content: 85 | application/json: 86 | schema: 87 | $ref: "#/components/schemas/CommunityMediaTimelineResponse" 88 | tags: 89 | - "tweet" 90 | 91 | /graphql/{pathQueryId}/CommunityAboutTimeline: 92 | get: 93 | operationId: getCommunityAboutTimeline 94 | description: get about of community 95 | responses: 96 | "200": 97 | description: Successful operation 98 | content: 99 | application/json: 100 | schema: 101 | $ref: "#/components/schemas/CommunityAboutTimelineResponse" 102 | tags: 103 | - "tweet" 104 | 105 | /graphql/{pathQueryId}/NotificationsTimeline: 106 | get: 107 | operationId: getNotificationsTimeline 108 | description: get notification list. timeline_type:[All, Verified, Mentions] 109 | responses: 110 | "200": 111 | description: Successful operation 112 | content: 113 | application/json: 114 | schema: 115 | $ref: "#/components/schemas/NotificationsTimelineResponse" 116 | tags: 117 | - "tweet" 118 | 119 | components: 120 | schemas: 121 | TimelineResponse: 122 | required: 123 | - "data" 124 | properties: 125 | data: 126 | $ref: "#/components/schemas/HomeTimelineResponseData" 127 | errors: 128 | type: array 129 | items: 130 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 131 | 132 | HomeTimelineResponseData: 133 | required: 134 | - "home" 135 | properties: 136 | home: 137 | $ref: "#/components/schemas/HomeTimelineHome" 138 | 139 | HomeTimelineHome: 140 | required: 141 | - "home_timeline_urt" 142 | properties: 143 | home_timeline_urt: 144 | $ref: "./../schemas/timeline.yaml#/components/schemas/Timeline" 145 | 146 | ListLatestTweetsTimelineResponse: 147 | required: 148 | - "data" 149 | properties: 150 | data: 151 | $ref: "#/components/schemas/ListTweetsTimelineData" 152 | errors: 153 | type: array 154 | items: 155 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 156 | 157 | ListTweetsTimelineData: 158 | required: 159 | - "list" 160 | properties: 161 | list: 162 | $ref: "#/components/schemas/ListTweetsTimelineList" 163 | 164 | ListTweetsTimelineList: 165 | required: 166 | - "tweets_timeline" 167 | properties: 168 | tweets_timeline: 169 | $ref: "#/components/schemas/TimelineResult" 170 | 171 | SearchTimelineResponse: 172 | required: 173 | - "data" 174 | properties: 175 | data: 176 | $ref: "#/components/schemas/SearchTimelineData" 177 | errors: 178 | type: array 179 | items: 180 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 181 | 182 | SearchTimelineData: 183 | required: 184 | - "search_by_raw_query" 185 | properties: 186 | search_by_raw_query: 187 | $ref: "#/components/schemas/SearchByRawQuery" 188 | 189 | SearchByRawQuery: 190 | required: 191 | - "search_timeline" 192 | properties: 193 | search_timeline: 194 | $ref: "#/components/schemas/SearchTimeline" 195 | 196 | SearchTimeline: 197 | required: 198 | - "timeline" 199 | properties: 200 | timeline: 201 | $ref: "./../schemas/timeline.yaml#/components/schemas/Timeline" 202 | 203 | CommunityTweetsTimelineResponse: 204 | required: 205 | - "data" 206 | properties: 207 | data: 208 | $ref: "#/components/schemas/RankedCommunityTweetData" 209 | errors: 210 | type: array 211 | items: 212 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 213 | 214 | RankedCommunityTweetData: 215 | required: 216 | - "communityResults" 217 | properties: 218 | communityResults: 219 | $ref: "#/components/schemas/RankedCommunityResults" 220 | 221 | RankedCommunityResults: 222 | required: 223 | - "result" 224 | properties: 225 | result: 226 | $ref: "#/components/schemas/RankedCommunityResult" 227 | 228 | RankedCommunityResult: 229 | required: 230 | - "__typename" 231 | - "ranked_community_timeline" 232 | properties: 233 | __typename: 234 | $ref: "./../schemas/typename.yaml#/components/schemas/TypeName" # Community 235 | ranked_community_timeline: 236 | $ref: "./../schemas/timeline.yaml#/components/schemas/TimelineResult" 237 | 238 | CommunityMediaTimelineResponse: 239 | required: 240 | - "data" 241 | properties: 242 | data: 243 | $ref: "#/components/schemas/MediaCommunityTweetData" 244 | errors: 245 | type: array 246 | items: 247 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 248 | 249 | MediaCommunityTweetData: 250 | required: 251 | - "__typename" 252 | - "communityResults" 253 | properties: 254 | communityResults: 255 | $ref: "#/components/schemas/MediaCommunityResults" 256 | 257 | MediaCommunityResults: 258 | required: 259 | - "result" 260 | properties: 261 | result: 262 | $ref: "#/components/schemas/MediaCommunityResult" 263 | 264 | MediaCommunityResult: 265 | required: 266 | - "__typename" 267 | - "community_media_timeline" 268 | properties: 269 | __typename: 270 | $ref: "./../schemas/typename.yaml#/components/schemas/TypeName" # Community 271 | community_media_timeline: 272 | $ref: "./../schemas/timeline.yaml#/components/schemas/TimelineResult" 273 | 274 | CommunityAboutTimelineResponse: 275 | required: 276 | - "data" 277 | properties: 278 | data: 279 | $ref: "#/components/schemas/AboutCommunityTweetData" 280 | errors: 281 | type: array 282 | items: 283 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 284 | 285 | AboutCommunityTweetData: 286 | required: 287 | - "communityResults" 288 | properties: 289 | communityResults: 290 | $ref: "#/components/schemas/AboutCommunityResults" 291 | 292 | AboutCommunityResults: 293 | required: 294 | - "result" 295 | properties: 296 | result: 297 | $ref: "#/components/schemas/AboutCommunityResult" 298 | 299 | AboutCommunityResult: 300 | required: 301 | - "__typename" 302 | - "about_timeline" 303 | properties: 304 | __typename: 305 | $ref: "./../schemas/typename.yaml#/components/schemas/TypeName" # Community 306 | about_timeline: 307 | $ref: "./../schemas/timeline.yaml#/components/schemas/TimelineResult" 308 | 309 | NotificationsTimelineResponse: 310 | required: 311 | - "data" 312 | properties: 313 | data: 314 | $ref: "#/components/schemas/NotificationsTimelineData" 315 | errors: 316 | type: array 317 | items: 318 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 319 | 320 | NotificationsTimelineData: 321 | required: 322 | - "viewer_v2" 323 | properties: 324 | viewer_v2: 325 | $ref: "#/components/schemas/NotificationsViewerV2" 326 | 327 | NotificationsViewerV2: 328 | required: 329 | - "user_results" 330 | properties: 331 | user_results: 332 | $ref: "#/components/schemas/NotificationsUserResults" 333 | 334 | NotificationsUserResults: 335 | required: 336 | - "result" 337 | properties: 338 | result: 339 | $ref: "#/components/schemas/NotificationsResult" 340 | 341 | NotificationsResult: 342 | required: 343 | - "__typename" 344 | - "rest_id" 345 | - "notification_timeline" 346 | properties: 347 | __typename: 348 | $ref: "./../schemas/typename.yaml#/components/schemas/TypeName" # User 349 | rest_id: 350 | type: string 351 | pattern: "^[0-9]+$" 352 | notification_timeline: 353 | $ref: "./../schemas/timeline.yaml#/components/schemas/TimelineResult" 354 | -------------------------------------------------------------------------------- /src/openapi/paths/tweet.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | 6 | paths: 7 | /graphql/{pathQueryId}/TweetDetail: 8 | get: 9 | operationId: getTweetDetail 10 | description: get TweetDetail 11 | responses: 12 | "200": 13 | description: Successful operation 14 | content: 15 | application/json: 16 | schema: 17 | $ref: "#/components/schemas/TweetDetailResponse" 18 | tags: 19 | - "tweet" 20 | 21 | /graphql/{pathQueryId}/TweetResultByRestId: 22 | get: 23 | operationId: getTweetResultByRestId 24 | description: get TweetResultByRestId 25 | responses: 26 | "200": 27 | description: Successful operation 28 | content: 29 | application/json: 30 | schema: 31 | $ref: "#/components/schemas/TweetResultByRestIdResponse" 32 | # tags: 33 | # - "tweet" 34 | 35 | /graphql/{pathQueryId}/Favoriters: 36 | get: 37 | operationId: getFavoriters 38 | description: get tweet favoriters 39 | responses: 40 | "200": 41 | description: Successful operation 42 | content: 43 | application/json: 44 | schema: 45 | $ref: "#/components/schemas/TweetFavoritersResponse" 46 | tags: 47 | - "user-list" 48 | 49 | /graphql/{pathQueryId}/Retweeters: 50 | get: 51 | operationId: getRetweeters 52 | description: get tweet retweeters 53 | responses: 54 | "200": 55 | description: Successful operation 56 | content: 57 | application/json: 58 | schema: 59 | $ref: "#/components/schemas/TweetRetweetersResponse" 60 | tags: 61 | - "user-list" 62 | 63 | components: 64 | schemas: 65 | TweetDetailResponse: 66 | required: 67 | - "data" 68 | properties: 69 | data: 70 | $ref: "#/components/schemas/TweetDetailResponseData" 71 | errors: 72 | type: array 73 | items: 74 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 75 | 76 | TweetDetailResponseData: 77 | properties: 78 | threaded_conversation_with_injections_v2: 79 | $ref: "./../schemas/timeline.yaml#/components/schemas/Timeline" 80 | 81 | TweetResultByRestIdResponse: 82 | required: 83 | - "data" 84 | properties: 85 | data: 86 | $ref: "#/components/schemas/TweetResultByRestIdData" 87 | errors: 88 | type: array 89 | items: 90 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 91 | 92 | TweetResultByRestIdData: 93 | properties: 94 | tweetResult: 95 | $ref: "./../schemas/content.yaml#/components/schemas/ItemResult" 96 | 97 | TweetFavoritersResponse: 98 | required: 99 | - "data" 100 | properties: 101 | data: 102 | $ref: "#/components/schemas/TweetFavoritersResponseData" 103 | errors: 104 | type: array 105 | items: 106 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 107 | 108 | TweetFavoritersResponseData: 109 | properties: 110 | favoriters_timeline: 111 | $ref: "./../schemas/timeline.yaml#/components/schemas/TimelineResult" 112 | 113 | TweetRetweetersResponse: 114 | required: 115 | - "data" 116 | properties: 117 | data: 118 | $ref: "#/components/schemas/TweetRetweetersResponseData" 119 | errors: 120 | type: array 121 | items: 122 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 123 | 124 | TweetRetweetersResponseData: 125 | properties: 126 | retweeters_timeline: 127 | $ref: "./../schemas/timeline.yaml#/components/schemas/TimelineResult" 128 | -------------------------------------------------------------------------------- /src/openapi/paths/user.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | 6 | paths: 7 | /graphql/{pathQueryId}/UserByScreenName: 8 | get: 9 | operationId: getUserByScreenName 10 | description: "get user by screen name" 11 | responses: 12 | "200": 13 | description: Successful operation 14 | content: 15 | application/json: 16 | schema: 17 | $ref: "#/components/schemas/UserResponse" 18 | tags: 19 | - "user" 20 | 21 | /graphql/{pathQueryId}/UserByRestId: 22 | get: 23 | operationId: getUserByRestId 24 | description: "get user by rest id" 25 | responses: 26 | "200": 27 | description: Successful operation 28 | content: 29 | application/json: 30 | schema: 31 | $ref: "#/components/schemas/UserResponse" 32 | tags: 33 | - "user" 34 | 35 | /graphql/{pathQueryId}/UsersByRestIds: 36 | get: 37 | operationId: getUsersByRestIds 38 | description: "get users by rest ids" 39 | responses: 40 | "200": 41 | description: Successful operation 42 | content: 43 | application/json: 44 | schema: 45 | $ref: "#/components/schemas/UsersResponse" 46 | tags: 47 | - "users" 48 | 49 | components: 50 | schemas: 51 | UserResponse: 52 | required: 53 | - "data" 54 | properties: 55 | data: 56 | $ref: "#/components/schemas/UserResponseData" 57 | errors: 58 | type: array 59 | items: 60 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 61 | 62 | UserResponseData: 63 | properties: 64 | user: 65 | $ref: "./../schemas/user.yaml#/components/schemas/UserResults" 66 | 67 | UsersResponse: 68 | required: 69 | - "data" 70 | properties: 71 | data: 72 | $ref: "#/components/schemas/UsersResponseData" 73 | errors: 74 | type: array 75 | items: 76 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 77 | 78 | UsersResponseData: 79 | properties: 80 | users: 81 | type: array 82 | items: 83 | $ref: "./../schemas/user.yaml#/components/schemas/UserResults" 84 | -------------------------------------------------------------------------------- /src/openapi/paths/usertweets.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | 6 | paths: 7 | /graphql/{pathQueryId}/UserTweets: 8 | get: 9 | operationId: getUserTweets 10 | description: "get user tweets" 11 | responses: 12 | "200": 13 | description: Successful operation 14 | content: 15 | application/json: 16 | schema: 17 | $ref: "#/components/schemas/UserTweetsResponse" 18 | tags: 19 | - "tweet" 20 | 21 | /graphql/{pathQueryId}/UserTweetsAndReplies: 22 | get: 23 | operationId: getUserTweetsAndReplies 24 | description: "get user replies tweets" 25 | responses: 26 | "200": 27 | description: Successful operation 28 | content: 29 | application/json: 30 | schema: 31 | $ref: "#/components/schemas/UserTweetsResponse" 32 | tags: 33 | - "tweet" 34 | 35 | /graphql/{pathQueryId}/UserHighlightsTweets: 36 | get: 37 | operationId: getUserHighlightsTweets 38 | description: "get user highlights tweets" 39 | responses: 40 | "200": 41 | description: Successful operation 42 | content: 43 | application/json: 44 | schema: 45 | $ref: "#/components/schemas/UserHighlightsTweetsResponse" 46 | tags: 47 | - "tweet" 48 | 49 | /graphql/{pathQueryId}/UserMedia: 50 | get: 51 | operationId: getUserMedia 52 | description: "get user media tweets" 53 | responses: 54 | "200": 55 | description: Successful operation 56 | content: 57 | application/json: 58 | schema: 59 | $ref: "#/components/schemas/UserTweetsResponse" 60 | tags: 61 | - "tweet" 62 | 63 | /graphql/{pathQueryId}/Likes: 64 | get: 65 | operationId: getLikes 66 | description: "get user likes tweets" 67 | responses: 68 | "200": 69 | description: Successful operation 70 | content: 71 | application/json: 72 | schema: 73 | $ref: "#/components/schemas/UserTweetsResponse" 74 | tags: 75 | - "tweet" 76 | 77 | components: 78 | schemas: 79 | UserTweetsResponse: 80 | required: 81 | - "data" 82 | properties: 83 | data: 84 | $ref: "#/components/schemas/UserTweetsData" 85 | errors: 86 | type: array 87 | items: 88 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 89 | 90 | UserTweetsData: 91 | properties: 92 | user: 93 | $ref: "#/components/schemas/UserTweetsUser" 94 | 95 | UserTweetsUser: 96 | required: 97 | - "result" 98 | properties: 99 | result: 100 | $ref: "#/components/schemas/UserTweetsResultV1" 101 | 102 | UserTweetsResultV1: 103 | required: 104 | - "__typename" 105 | - "timeline" 106 | properties: 107 | __typename: 108 | $ref: "./../schemas/typename.yaml#/components/schemas/TypeName" # User 109 | timeline: 110 | $ref: "./../schemas/timeline.yaml#/components/schemas/TimelineResult" 111 | 112 | UserTweetsResultV2: 113 | required: 114 | - "__typename" 115 | - "timeline_v2" 116 | properties: 117 | __typename: 118 | $ref: "./../schemas/typename.yaml#/components/schemas/TypeName" # User 119 | timeline_v2: 120 | $ref: "./../schemas/timeline.yaml#/components/schemas/TimelineResult" 121 | 122 | UserHighlightsTweetsResponse: 123 | required: 124 | - "data" 125 | properties: 126 | data: 127 | $ref: "#/components/schemas/UserHighlightsTweetsData" 128 | errors: 129 | type: array 130 | items: 131 | $ref: "./../response/error.yaml#/components/schemas/ErrorResponse" 132 | 133 | UserHighlightsTweetsData: 134 | properties: 135 | user: 136 | $ref: "#/components/schemas/UserHighlightsTweetsUser" 137 | 138 | UserHighlightsTweetsUser: 139 | required: 140 | - "result" 141 | properties: 142 | result: 143 | $ref: "#/components/schemas/UserHighlightsTweetsResult" 144 | 145 | UserHighlightsTweetsResult: 146 | required: 147 | - "timeline" 148 | - "__typename" 149 | properties: 150 | timeline: 151 | $ref: "#/components/schemas/UserHighlightsTweetsTimeline" 152 | __typename: 153 | $ref: "./../schemas/typename.yaml#/components/schemas/TypeName" # User 154 | 155 | UserHighlightsTweetsTimeline: 156 | required: 157 | - "timeline" 158 | properties: 159 | timeline: 160 | $ref: "./../schemas/timeline.yaml#/components/schemas/Timeline" 161 | -------------------------------------------------------------------------------- /src/openapi/paths/v1.1-get.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | 6 | paths: 7 | /1.1/friends/following/list.json: 8 | get: 9 | operationId: getFriendsFollowingList 10 | description: get friends following list 11 | responses: 12 | "200": 13 | description: Successful operation 14 | tags: 15 | - "v1.1-get" 16 | 17 | /1.1/search/typeahead.json: 18 | get: 19 | operationId: getSearchTypeahead 20 | description: get search typeahead 21 | responses: 22 | "200": 23 | description: Successful operation 24 | tags: 25 | - "v1.1-get" 26 | -------------------------------------------------------------------------------- /src/openapi/paths/v1.1-post.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | 6 | paths: 7 | /1.1/friendships/create.json: 8 | post: 9 | operationId: postCreateFriendships 10 | description: post create friendships 11 | responses: 12 | "200": 13 | description: Successful operation 14 | tags: 15 | - "v1.1-post" 16 | 17 | /1.1/friendships/destroy.json: 18 | post: 19 | operationId: postDestroyFriendships 20 | description: post destroy friendships 21 | responses: 22 | "200": 23 | description: Successful operation 24 | tags: 25 | - "v1.1-post" 26 | -------------------------------------------------------------------------------- /src/openapi/paths/v2.0-get.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | 6 | paths: 7 | /2/search/adaptive.json: 8 | get: 9 | operationId: getSearchAdaptive 10 | description: get search adaptive 11 | responses: 12 | "200": 13 | description: Successful operation 14 | tags: 15 | - "v2.0-get" 16 | -------------------------------------------------------------------------------- /src/openapi/resources/parameters.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | 6 | paths: {} 7 | 8 | components: 9 | parameters: 10 | queryId: 11 | name: queryId 12 | in: path 13 | required: true 14 | schema: 15 | type: string 16 | -------------------------------------------------------------------------------- /src/openapi/response/error.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | 6 | paths: {} 7 | components: 8 | schemas: 9 | ErrorResponse: 10 | type: object 11 | required: 12 | - message 13 | - locations 14 | - path 15 | - extensions 16 | - code 17 | - kind 18 | - name 19 | - source 20 | - tracing 21 | properties: 22 | message: 23 | type: string 24 | locations: 25 | type: array 26 | items: 27 | $ref: "#/components/schemas/Location" 28 | path: 29 | type: array 30 | items: 31 | oneOf: 32 | - type: string 33 | - type: integer 34 | extensions: 35 | $ref: "#/components/schemas/ErrorExtensions" 36 | code: 37 | type: integer 38 | kind: 39 | type: string 40 | name: 41 | type: string 42 | source: 43 | type: string 44 | retry_after: 45 | type: integer 46 | tracing: 47 | $ref: "#/components/schemas/Tracing" 48 | 49 | Location: 50 | type: object 51 | required: 52 | - line 53 | - column 54 | properties: 55 | line: 56 | type: integer 57 | column: 58 | type: integer 59 | 60 | ErrorExtensions: 61 | type: object 62 | required: 63 | - name 64 | - source 65 | - code 66 | - kind 67 | - tracing 68 | properties: 69 | name: 70 | type: string 71 | source: 72 | type: string 73 | retry_after: 74 | type: integer 75 | code: 76 | type: integer 77 | kind: 78 | type: string 79 | tracing: 80 | $ref: "#/components/schemas/Tracing" 81 | 82 | Tracing: 83 | type: object 84 | required: 85 | - trace_id 86 | properties: 87 | trace_id: 88 | type: string 89 | pattern: "^[0-9a-f]{16}$" 90 | -------------------------------------------------------------------------------- /src/openapi/schemas/content.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | 6 | paths: {} 7 | components: 8 | schemas: 9 | ContentUnion: 10 | oneOf: 11 | - $ref: "#/components/schemas/TimelineTimelineItem" 12 | - $ref: "#/components/schemas/TimelineTimelineModule" 13 | - $ref: "#/components/schemas/TimelineTimelineCursor" 14 | discriminator: 15 | propertyName: entryType 16 | mapping": # deprecated 17 | TimelineTimelineItem: "#/components/schemas/TimelineTimelineItem" 18 | TimelineTimelineModule: "#/components/schemas/TimelineTimelineModule" 19 | TimelineTimelineCursor: "#/components/schemas/TimelineTimelineCursor" 20 | 21 | ContentEntryType: 22 | type: string 23 | enum: 24 | [TimelineTimelineItem, TimelineTimelineCursor, TimelineTimelineModule] 25 | 26 | CursorType: 27 | type: string 28 | enum: [Top, Bottom, ShowMore, ShowMoreThreads, Gap, ShowMoreThreadsPrompt] # Gap??? 29 | 30 | TimelineTimelineItem: 31 | required: 32 | - "__typename" 33 | - "entryType" 34 | - "itemContent" 35 | properties: 36 | __typename: 37 | $ref: "./typename.yaml#/components/schemas/TypeName" # TimelineTimelineItem 38 | entryType: 39 | $ref: "#/components/schemas/ContentEntryType" # TimelineTimelineItem 40 | itemContent: 41 | $ref: "#/components/schemas/ItemContentUnion" 42 | clientEventInfo: 43 | $ref: "#/components/schemas/ClientEventInfo" 44 | feedbackInfo: 45 | type: object 46 | additionalProperties: true # todo 47 | 48 | TimelineTimelineModule: 49 | required: 50 | - "__typename" 51 | - "entryType" 52 | - "displayType" 53 | # - "items" 54 | # - "clientEventInfo" 55 | properties: 56 | __typename: 57 | type: string 58 | $ref: "./typename.yaml#/components/schemas/TypeName" # TimelineTimelineModule 59 | entryType: 60 | $ref: "#/components/schemas/ContentEntryType" # TimelineTimelineCursor 61 | displayType: 62 | $ref: "#/components/schemas/DisplayType" 63 | items: 64 | type: array 65 | items: 66 | $ref: "#/components/schemas/ModuleItem" 67 | footer: 68 | type: object 69 | additionalProperties: true # todo 70 | header: 71 | type: object 72 | additionalProperties: true # todo 73 | clientEventInfo: 74 | $ref: "#/components/schemas/ClientEventInfo" 75 | metadata: 76 | type: object 77 | additionalProperties: true # todo 78 | feedbackInfo: 79 | $ref: "#/components/schemas/FeedbackInfo" 80 | 81 | TimelineTimelineCursor: 82 | required: 83 | - "__typename" 84 | - "cursorType" 85 | - "value" 86 | properties: 87 | __typename: 88 | $ref: "./typename.yaml#/components/schemas/TypeName" # TimelineTimelineCursor 89 | entryType: 90 | $ref: "#/components/schemas/ContentEntryType" # null | TimelineTimelineCursor 91 | itemType: 92 | $ref: "#/components/schemas/ContentEntryType" # null | TimelineTimelineCursor 93 | cursorType: 94 | $ref: "#/components/schemas/CursorType" 95 | value: 96 | type: string 97 | stopOnEmptyResponse: 98 | type: boolean 99 | displayTreatment: 100 | $ref: "#/components/schemas/DisplayTreatment" 101 | 102 | DisplayTreatment: 103 | type: object 104 | required: 105 | - "actionText" 106 | properties: 107 | actionText: 108 | type: string 109 | labelText: 110 | type: string 111 | 112 | # ================= Module ================= 113 | 114 | DisplayType: 115 | type: string 116 | enum: 117 | - Vertical 118 | - VerticalConversation 119 | - VerticalGrid 120 | - Carousel 121 | 122 | ModuleItem: 123 | required: 124 | - "entryId" 125 | - "item" 126 | properties: 127 | entryId: 128 | type: string 129 | pattern: "^(([a-zA-Z]+|[0-9]+|[0-9a-f]+)(-|$))+" 130 | item: 131 | $ref: "#/components/schemas/ModuleEntry" 132 | dispensable: 133 | type: boolean 134 | 135 | ModuleEntry: 136 | required: 137 | - "itemContent" 138 | properties: 139 | clientEventInfo: 140 | $ref: "#/components/schemas/ClientEventInfo" 141 | itemContent: 142 | $ref: "#/components/schemas/ItemContentUnion" 143 | feedbackInfo: 144 | $ref: "#/components/schemas/FeedbackInfo" 145 | 146 | FeedbackInfo: 147 | required: 148 | - "feedbackType" 149 | properties: 150 | feedbackKeys: 151 | type: array 152 | items: 153 | type: string 154 | clientEventInfo: 155 | $ref: "#/components/schemas/ClientEventInfo" 156 | 157 | # ================= ContentItem ================= 158 | 159 | ItemContentUnion: 160 | oneOf: 161 | - $ref: "#/components/schemas/TimelineTweet" 162 | - $ref: "#/components/schemas/TimelineTimelineCursor" 163 | - $ref: "#/components/schemas/TimelineUser" 164 | - $ref: "#/components/schemas/TimelinePrompt" 165 | - $ref: "#/components/schemas/TimelineMessagePrompt" 166 | - $ref: "#/components/schemas/TimelineCommunity" 167 | - $ref: "#/components/schemas/TimelineTombstone" 168 | - $ref: "#/components/schemas/TimelineTrend" 169 | - $ref: "#/components/schemas/TimelineNotification" 170 | 171 | discriminator: 172 | propertyName: __typename 173 | mapping": # deprecated 174 | TimelineTweet: "#/components/schemas/TimelineTweet" 175 | TimelineTimelineCursor: "#/components/schemas/TimelineTimelineCursor" 176 | TimelineUser: "#/components/schemas/TimelineUser" 177 | TimelinePrompt: "#/components/schemas/TimelinePrompt" 178 | TimelineMessagePrompt: "#/components/schemas/TimelineMessagePrompt" 179 | TimelineCommunity: "#/components/schemas/TimelineCommunity" 180 | TimelineTombstone: "#/components/schemas/TimelineTombstone" 181 | TimelineTrend: "#/components/schemas/TimelineTrend" 182 | TimelineNotification: "#/components/schemas/TimelineNotification" 183 | 184 | ContentItemType: 185 | type: string 186 | enum: 187 | [ 188 | TimelineTweet, 189 | TimelineTimelineCursor, 190 | TimelineUser, 191 | TimelinePrompt, 192 | TimelineMessagePrompt, 193 | TimelineCommunity, 194 | TimelineTombstone, 195 | TimelineTrend, 196 | TimelineNotification, 197 | ] 198 | 199 | TimelineTweet: 200 | required: 201 | - "__typename" 202 | - "itemType" 203 | - "tweetDisplayType" 204 | - "tweet_results" 205 | properties: 206 | __typename: 207 | $ref: "./typename.yaml#/components/schemas/TypeName" # TimelineTweet 208 | itemType: 209 | $ref: "#/components/schemas/ContentItemType" # TimelineTweet 210 | tweetDisplayType: 211 | type: string 212 | enum: [Tweet, SelfThread, MediaGrid, CondensedTweet] 213 | tweet_results: 214 | $ref: "#/components/schemas/ItemResult" 215 | socialContext: 216 | $ref: "#/components/schemas/SocialContextUnion" 217 | promotedMetadata: 218 | type: object 219 | additionalProperties: true # todo 220 | highlights: 221 | $ref: "#/components/schemas/Highlight" 222 | hasModeratedReplies: 223 | type: boolean 224 | 225 | TimelineUser: 226 | required: 227 | - "__typename" 228 | - "itemType" 229 | - "userDisplayType" 230 | - "user_results" 231 | properties: 232 | __typename: 233 | $ref: "./typename.yaml#/components/schemas/TypeName" # TimelineUser 234 | itemType: 235 | $ref: "#/components/schemas/ContentItemType" # TimelineUser 236 | socialContext: 237 | $ref: "#/components/schemas/SocialContextUnion" 238 | userDisplayType: 239 | type: string 240 | enum: [User, UserDetailed, SubscribableUser, UserConcise] 241 | user_results: 242 | $ref: "./user.yaml#/components/schemas/UserResults" 243 | 244 | ItemResult: 245 | properties: 246 | __typename: 247 | $ref: "./typename.yaml#/components/schemas/TypeName" # null | TimelineTweet | TweetUnavailable 248 | result: 249 | $ref: "./tweet.yaml#/components/schemas/TweetUnion" 250 | 251 | SocialContextUnion: 252 | oneOf: 253 | - $ref: "#/components/schemas/TimelineGeneralContext" 254 | - $ref: "#/components/schemas/TimelineTopicContext" 255 | discriminator: 256 | propertyName: type 257 | mapping": # deprecated 258 | TimelineGeneralContext: "#/components/schemas/TimelineGeneralContext" 259 | TimelineTopicContext: "#/components/schemas/TimelineTopicContext" 260 | 261 | SocialContextUnionType: 262 | type: string 263 | enum: 264 | - TimelineGeneralContext 265 | - TimelineTopicContext 266 | 267 | TimelineGeneralContext: 268 | type: object 269 | properties: 270 | type: 271 | $ref: "#/components/schemas/SocialContextUnionType" 272 | contextType: 273 | type: string # enum 274 | enum: 275 | - "Follow" 276 | - "Pin" 277 | - "Like" 278 | - "Location" 279 | - "Sparkle" 280 | - "Conversation" 281 | - "List" 282 | - "Community" 283 | - "Facepile" 284 | text: 285 | type: string 286 | landingUrl: 287 | $ref: "#/components/schemas/SocialContextLandingUrl" 288 | contextImageUrls: 289 | type: array 290 | items: 291 | type: string 292 | format: uri 293 | 294 | TimelineTopicContext: 295 | type: object 296 | properties: 297 | type: 298 | $ref: "#/components/schemas/SocialContextUnionType" 299 | topic: 300 | $ref: "#/components/schemas/TopicContext" 301 | functionalityType: 302 | type: string 303 | enum: ["Basic"] 304 | 305 | TopicContext: 306 | type: object 307 | properties: 308 | id: 309 | type: string 310 | topic_id: 311 | type: string 312 | name: 313 | type: string 314 | description: 315 | type: string 316 | icon_url: 317 | type: string 318 | format: uri 319 | following: 320 | type: boolean 321 | not_interested: 322 | type: boolean 323 | 324 | SocialContextLandingUrl: 325 | type: object 326 | properties: 327 | urlType: 328 | type: string # enum 329 | enum: ["DeepLink", "UrtEndpoint", "ExternalUrl"] 330 | url: 331 | type: string # twitter://user?id=900282258736545792 332 | format: uri 333 | urtEndpointOptions: 334 | $ref: "#/components/schemas/UrtEndpointOptions" 335 | 336 | UrtEndpointOptions: 337 | type: object 338 | required: 339 | - "title" 340 | - "requestParams" 341 | properties: 342 | title: 343 | type: string 344 | requestParams: 345 | type: array 346 | items: 347 | $ref: "#/components/schemas/UrtEndpointRequestParams" 348 | 349 | UrtEndpointRequestParams: 350 | type: object 351 | required: 352 | - "key" 353 | - "value" 354 | properties: 355 | key: 356 | type: string 357 | value: 358 | type: string 359 | 360 | Highlight: 361 | type: object 362 | required: 363 | - "textHighlights" 364 | properties: 365 | textHighlights: 366 | type: array 367 | items: 368 | $ref: "#/components/schemas/TextHighlight" 369 | 370 | TextHighlight: 371 | type: object 372 | required: 373 | - "startIndex" 374 | - "endIndex" 375 | properties: 376 | startIndex: 377 | type: integer 378 | endIndex: 379 | type: integer 380 | 381 | TimelinePrompt: 382 | properties: 383 | __typename: 384 | $ref: "./typename.yaml#/components/schemas/TypeName" # TimelinePrompt 385 | additionalProperties: true # todo 386 | 387 | TimelineMessagePrompt: 388 | properties: 389 | __typename: 390 | $ref: "./typename.yaml#/components/schemas/TypeName" # TimelineMessagePrompt 391 | additionalProperties: true # todo 392 | 393 | TimelineCommunity: 394 | properties: 395 | __typename: 396 | $ref: "./typename.yaml#/components/schemas/TypeName" # TimelineCommunity 397 | additionalProperties: true # todo 398 | 399 | ClientEventInfo: 400 | properties: 401 | component: 402 | # enum half_cover 403 | type: string 404 | element: 405 | type: string 406 | # august-2023-privacy-prompt-candidate 407 | # pattern: "^(([a-zA-Z]+|[0-9]+|[0-9a-f]+)(-|$))+" 408 | # pattern: "^(january|february|march|april|may|june|july|august|september|october|november|december)-[0-9]{4}-([a-z]-)+[a-z]+$" 409 | details: 410 | type: object 411 | additionalProperties: true # todo 412 | 413 | TimelineTombstone: 414 | properties: 415 | __typename: 416 | $ref: "./typename.yaml#/components/schemas/TypeName" # TimelineTombstone 417 | itemType: 418 | $ref: "#/components/schemas/ContentItemType" # TimelineTombstone 419 | tombstoneDisplayType: 420 | type: string 421 | enum: [Inline] 422 | tombstoneInfo: 423 | $ref: "#/components/schemas/TombstoneInfo" 424 | 425 | TombstoneInfo: 426 | type: object 427 | properties: 428 | text: 429 | type: string 430 | richText: 431 | $ref: "#/components/schemas/TombstoneRichText" 432 | 433 | TombstoneRichText: 434 | type: object 435 | properties: 436 | rtl: 437 | type: boolean 438 | text: 439 | type: string 440 | entities: 441 | type: array 442 | items: 443 | $ref: "#/components/schemas/TombstoneEntity" 444 | 445 | TombstoneEntity: 446 | type: object 447 | properties: 448 | fromIndex: 449 | type: integer 450 | toIndex: 451 | type: integer 452 | ref: 453 | $ref: "#/components/schemas/TombstoneRef" 454 | 455 | TombstoneRef: 456 | type: object 457 | properties: 458 | type: 459 | type: string 460 | enum: [TimelineUrl] 461 | url: 462 | type: string 463 | format: uri 464 | urlType: 465 | type: string 466 | enum: [ExternalUrl] 467 | 468 | TimelineTrend: 469 | required: 470 | - "__typename" 471 | - "name" 472 | - "trend_url" 473 | - "trend_metadata" 474 | properties: 475 | __typename: 476 | $ref: "./typename.yaml#/components/schemas/TypeName" # TimelineTrend 477 | itemType: 478 | $ref: "#/components/schemas/ContentItemType" # TimelineTrend 479 | social_context: 480 | $ref: "#/components/schemas/SocialContextUnion" 481 | is_ai_trend: 482 | type: boolean 483 | name: 484 | type: string 485 | trend_url: 486 | $ref: "#/components/schemas/SocialContextLandingUrl" 487 | trend_metadata: 488 | $ref: "#/components/schemas/TrendMetadata" 489 | thumbnail_image: 490 | $ref: "#/components/schemas/ThumbnailImage" 491 | images: 492 | type: array 493 | items: 494 | $ref: "#/components/schemas/TrendImage" 495 | 496 | TrendMetadata: 497 | type: object 498 | properties: 499 | url: 500 | $ref: "#/components/schemas/SocialContextLandingUrl" 501 | 502 | ThumbnailImage: 503 | type: object 504 | properties: 505 | original_img_url: 506 | type: string 507 | format: uri 508 | original_img_width: 509 | type: integer 510 | original_img_height: 511 | type: integer 512 | 513 | TrendImage: 514 | type: object 515 | properties: 516 | url: 517 | type: string 518 | format: uri 519 | 520 | TimelineNotification: 521 | required: 522 | - "__typename" 523 | - "itemType" 524 | - "id" 525 | - "notification_icon" 526 | - "rich_message" 527 | - "notification_url" 528 | - "template" 529 | - "timestamp_ms" 530 | properties: 531 | __typename: 532 | $ref: "./typename.yaml#/components/schemas/TypeName" # TimelineNotification 533 | itemType: 534 | $ref: "#/components/schemas/ContentItemType" # TimelineNotification 535 | id: 536 | type: string 537 | notification_icon: 538 | type: string # enum milestone_icon 539 | rich_message: 540 | $ref: "#/components/schemas/RichMessage" 541 | notification_url: 542 | $ref: "#/components/schemas/SocialContextLandingUrl" 543 | template: 544 | $ref: "#/components/schemas/NotificationTemplate" 545 | timestamp_ms: 546 | type: string # 2025-05-05T01:18:21.657Z 547 | 548 | RichMessage: 549 | type: object 550 | properties: 551 | rtl: 552 | type: boolean 553 | text: 554 | type: string 555 | 556 | NotificationTemplate: 557 | type: object 558 | properties: 559 | __typename: 560 | $ref: "./typename.yaml#/components/schemas/TypeName" # NotificationTemplate 561 | target_objects: 562 | type: array 563 | items: 564 | type: object 565 | from_users: 566 | type: array 567 | items: 568 | type: object 569 | -------------------------------------------------------------------------------- /src/openapi/schemas/general.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | 6 | paths: {} 7 | components: 8 | schemas: 9 | TwitterTimeFormat: 10 | type: string 11 | pattern: "^(Sun|Mon|Tue|Wed|Thu|Fri|Sat) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (0[1-9]|[12][0-9]|3[01]) (0[0-9]|1[0-9]|2[0-3])(: ?)([0-5][0-9])(: ?)([0-5][0-9]) ([+-][0-9]{4}) ([0-9]{4})$" 12 | example: "Sat Dec 31 23:59:59 +0000 2023" # Wed Sep 20 02: 55: 44 +0000 2023 13 | -------------------------------------------------------------------------------- /src/openapi/schemas/instruction.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | 6 | paths: {} 7 | components: 8 | schemas: 9 | InstructionUnion: 10 | oneOf: 11 | - $ref: "#/components/schemas/TimelineAddEntries" 12 | - $ref: "#/components/schemas/TimelineAddToModule" 13 | - $ref: "#/components/schemas/TimelineClearCache" 14 | - $ref: "#/components/schemas/TimelinePinEntry" 15 | - $ref: "#/components/schemas/TimelineReplaceEntry" 16 | - $ref: "#/components/schemas/TimelineShowAlert" 17 | - $ref: "#/components/schemas/TimelineTerminateTimeline" 18 | - $ref: "#/components/schemas/TimelineShowCover" 19 | - $ref: "#/components/schemas/TimelineClearEntriesUnreadState" 20 | - $ref: "#/components/schemas/TimelineMarkEntriesUnreadGreaterThanSortIndex" 21 | 22 | discriminator: 23 | propertyName: type 24 | mapping": # deprecated 25 | TimelineAddEntries: "#/components/schemas/TimelineAddEntries" 26 | TimelineAddToModule: "#/components/schemas/TimelineAddToModule" 27 | TimelineClearCache: "#/components/schemas/TimelineClearCache" 28 | TimelinePinEntry: "#/components/schemas/TimelinePinEntry" 29 | TimelineReplaceEntry: "#/components/schemas/TimelineReplaceEntry" 30 | TimelineShowAlert: "#/components/schemas/TimelineShowAlert" 31 | TimelineTerminateTimeline: "#/components/schemas/TimelineTerminateTimeline" 32 | TimelineShowCover: "#/components/schemas/TimelineShowCover" 33 | TimelineClearEntriesUnreadState: "#/components/schemas/TimelineClearEntriesUnreadState" 34 | TimelineMarkEntriesUnreadGreaterThanSortIndex: "#/components/schemas/TimelineMarkEntriesUnreadGreaterThanSortIndex" 35 | 36 | InstructionType: 37 | type: string 38 | enum: 39 | [ 40 | TimelineAddEntries, 41 | TimelineAddToModule, 42 | TimelineClearCache, 43 | TimelinePinEntry, 44 | TimelineReplaceEntry, 45 | TimelineShowAlert, 46 | TimelineTerminateTimeline, 47 | TimelineShowCover, 48 | TimelineClearEntriesUnreadState, 49 | TimelineMarkEntriesUnreadGreaterThanSortIndex, 50 | ] 51 | 52 | TimelineAddEntries: 53 | required: 54 | - type 55 | - entries 56 | properties: 57 | type: 58 | $ref: "#/components/schemas/InstructionType" # TimelineAddEntries 59 | entries: 60 | type: array 61 | items: 62 | $ref: "#/components/schemas/TimelineAddEntry" 63 | 64 | TimelineAddToModule: 65 | required: 66 | - type 67 | - moduleItems 68 | - moduleEntryId 69 | properties: 70 | type: 71 | $ref: "#/components/schemas/InstructionType" # TimelineAddToModule 72 | moduleItems: 73 | type: array 74 | items: 75 | $ref: "./content.yaml#/components/schemas/ModuleItem" 76 | moduleEntryId: 77 | type: string 78 | prepend: 79 | type: boolean 80 | 81 | TimelineClearCache: 82 | required: 83 | - type 84 | properties: 85 | type: 86 | $ref: "#/components/schemas/InstructionType" # TimelineClearCache 87 | 88 | TimelinePinEntry: 89 | required: 90 | - type 91 | - entry 92 | properties: 93 | type: 94 | $ref: "#/components/schemas/InstructionType" # TimelinePinEntry 95 | entry: 96 | $ref: "#/components/schemas/TimelineAddEntry" 97 | 98 | TimelineReplaceEntry: 99 | required: 100 | - type 101 | - entry_id_to_replace 102 | - entry 103 | properties: 104 | type: 105 | $ref: "#/components/schemas/InstructionType" # TimelineReplaceEntry 106 | entry_id_to_replace: 107 | type: string 108 | entry: 109 | $ref: "#/components/schemas/TimelineAddEntry" 110 | 111 | TimelineShowAlert: 112 | required: 113 | - type 114 | - usersResults 115 | - richText 116 | properties: 117 | type: 118 | $ref: "#/components/schemas/InstructionType" # TimelineShowAlert 119 | alertType: 120 | type: string 121 | enum: [NewTweets] # which else? 122 | triggerDelayMs: 123 | type: integer 124 | displayDurationMs: 125 | type: integer 126 | usersResults: 127 | type: array 128 | items: 129 | $ref: "./user.yaml#/components/schemas/UserResults" 130 | richText: 131 | type: object 132 | properties: 133 | text: 134 | type: string 135 | entities: 136 | type: array 137 | items: 138 | type: object 139 | additionalProperties: true # todo 140 | iconDisplayInfo: 141 | type: object 142 | additionalProperties: true # todo 143 | colorConfig: 144 | type: object 145 | additionalProperties: true # todo 146 | displayLocation: 147 | type: string 148 | enum: [Top] # which else? 149 | 150 | TimelineTerminateTimeline: 151 | required: 152 | - type 153 | - direction 154 | properties: 155 | type: 156 | $ref: "#/components/schemas/InstructionType" # TimelineTerminateTimeline 157 | direction: 158 | type: string 159 | enum: [Top, Bottom, TopAndBottom] 160 | 161 | TimelineAddEntry: 162 | required: 163 | - "content" 164 | - "entryId" 165 | - "sortIndex" 166 | properties: 167 | content: 168 | $ref: "./content.yaml#/components/schemas/ContentUnion" 169 | entryId: 170 | type: string 171 | pattern: "^(([a-zA-Z]+|[0-9]+|[0-9a-f]+)(-|$))+" 172 | sortIndex: 173 | type: string 174 | pattern: "[0-9]+$" 175 | 176 | TimelineShowCover: 177 | required: 178 | - type 179 | - clientEventInfo 180 | - cover 181 | properties: 182 | type: 183 | $ref: "#/components/schemas/InstructionType" # TimelineShowCover 184 | clientEventInfo: 185 | $ref: "./content.yaml#/components/schemas/ClientEventInfo" 186 | cover: 187 | $ref: "#/components/schemas/TimelineHalfCover" 188 | 189 | TimelineHalfCover: 190 | required: 191 | - type 192 | - halfCoverDisplayType 193 | - primaryText 194 | - primaryCoverCta 195 | - secondaryText 196 | - impressionCallbacks 197 | - dismissible 198 | properties: 199 | type: 200 | type: string 201 | enum: [TimelineHalfCover] 202 | halfCoverDisplayType: 203 | type: string 204 | enum: [Cover] 205 | primaryText: 206 | $ref: "#/components/schemas/Text" 207 | secondaryText: 208 | $ref: "#/components/schemas/Text" 209 | primaryCoverCta: 210 | $ref: "#/components/schemas/CoverCta" 211 | impressionCallbacks: 212 | type: array 213 | items: 214 | $ref: "#/components/schemas/Callback" 215 | dismissible: 216 | type: boolean 217 | 218 | Text: 219 | required: 220 | - text 221 | - entities 222 | properties: 223 | text: 224 | type: string 225 | entities: 226 | type: array 227 | items: 228 | $ref: "#/components/schemas/TextEntity" 229 | 230 | TextEntity: 231 | required: 232 | - fromIndex 233 | - toIndex 234 | - ref 235 | properties: 236 | fromIndex: 237 | type: integer 238 | toIndex: 239 | type: integer 240 | ref: 241 | $ref: "#/components/schemas/TextEntityRef" 242 | 243 | TextEntityRef: 244 | required: 245 | - type 246 | - url 247 | - urlType 248 | properties: 249 | type: 250 | type: string 251 | enum: [TimelineUrl] 252 | url: 253 | type: string 254 | format: uri 255 | urlType: 256 | type: string 257 | enum: [ExternalUrl] 258 | 259 | CoverCta: 260 | required: 261 | - text 262 | - ctaBehavior 263 | - callbacks 264 | - clientEventInfo 265 | properties: 266 | Text: 267 | type: string 268 | ctaBehavior: 269 | $ref: "#/components/schemas/TimelineCoverBehavior" 270 | callbacks: 271 | type: array 272 | items: 273 | $ref: "#/components/schemas/Callback" 274 | clientEventInfo: 275 | $ref: "#/components/schemas/CtaClientEventInfo" 276 | buttonStyle: 277 | type: string 278 | enum: ["Primary"] 279 | 280 | TimelineCoverBehavior: 281 | required: 282 | - type 283 | properties: 284 | type: 285 | type: string 286 | enum: [TimelineCoverBehaviorDismiss, TimelineCoverBehaviorNavigate] 287 | url: 288 | $ref: "#/components/schemas/TimelineCoverBehaviorUrl" 289 | 290 | TimelineCoverBehaviorUrl: 291 | required: 292 | - "url" 293 | - "url_type" 294 | properties: 295 | url: 296 | type: string 297 | format: uri 298 | url_type: 299 | type: string 300 | enum: ["ExternalUrl"] 301 | 302 | Callback: 303 | required: 304 | - endpoint 305 | properties: 306 | endpoint: 307 | type: string 308 | format: uri 309 | # pattern: '^/1\.1/[a-z]+/[a-z]+\.json\?[a-z_]+=[a-z0-9-]+(&[a-z_]+=[a-z0-9-]+)+?$' #/1.1/onboarding/fatigue.json?{params} 310 | 311 | CtaClientEventInfo: 312 | required: 313 | - action 314 | properties: 315 | action: 316 | type: string 317 | enum: [primary_cta] 318 | 319 | TimelineClearEntriesUnreadState: 320 | required: 321 | - type 322 | properties: 323 | type: 324 | $ref: "#/components/schemas/InstructionType" # TimelineClearEntriesUnreadState 325 | 326 | TimelineMarkEntriesUnreadGreaterThanSortIndex: 327 | required: 328 | - type 329 | properties: 330 | type: 331 | $ref: "#/components/schemas/InstructionType" # TimelineMarkEntriesUnreadGreaterThanSortIndex 332 | sort_index: 333 | type: string 334 | pattern: "[0-9]+$" 335 | -------------------------------------------------------------------------------- /src/openapi/schemas/timeline.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | 6 | paths: {} 7 | components: 8 | schemas: 9 | TimelineResult: 10 | properties: 11 | id: 12 | type: string 13 | timeline: 14 | $ref: "#/components/schemas/Timeline" 15 | 16 | Timeline: 17 | required: 18 | - "instructions" 19 | properties: 20 | instructions: 21 | type: array 22 | items: 23 | $ref: "./../schemas/instruction.yaml#/components/schemas/InstructionUnion" 24 | metadata: 25 | type: object 26 | additionalProperties: true # todo 27 | responseObjects: 28 | type: object 29 | additionalProperties: true # todo 30 | -------------------------------------------------------------------------------- /src/openapi/schemas/typename.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | paths: {} 6 | components: 7 | schemas: 8 | TypeName: 9 | type: string 10 | enum: 11 | [ 12 | TimelineTweet, 13 | TimelineTimelineItem, 14 | TimelineUser, 15 | TimelineTimelineCursor, 16 | TweetWithVisibilityResults, 17 | ContextualTweetInterstitial, 18 | TimelineTimelineModule, 19 | TweetTombstone, 20 | TimelinePrompt, 21 | TimelineMessagePrompt, 22 | TimelineCommunity, 23 | TimelineTombstone, 24 | TimelineTrend, 25 | TimelineNotification, 26 | TimelineNotificationAggregateUserActions, 27 | TweetUnavailable, 28 | TweetPreviewDisplay, 29 | Tweet, 30 | User, 31 | UserUnavailable, 32 | Community, 33 | CommunityDeleteActionUnavailable, 34 | CommunityJoinAction, 35 | CommunityJoinActionUnavailable, 36 | CommunityLeaveActionUnavailable, 37 | CommunityTweetPinActionUnavailable, 38 | CommunityTweetUnpinActionUnavailable, 39 | CommunityInvitesUnavailable, 40 | CommunityJoinRequestsUnavailable, 41 | ApiImage, 42 | ] 43 | -------------------------------------------------------------------------------- /src/openapi/schemas/user.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Twitter OpenAPI 4 | version: 0.0.1 5 | paths: {} 6 | components: 7 | schemas: 8 | UserResultCore: 9 | required: 10 | - "user_results" 11 | properties: 12 | user_results: 13 | $ref: "#/components/schemas/UserResults" 14 | 15 | UserResults: 16 | properties: 17 | result: 18 | $ref: "#/components/schemas/UserUnion" 19 | 20 | UserUnion: 21 | oneOf: 22 | - $ref: "#/components/schemas/User" 23 | - $ref: "#/components/schemas/UserUnavailable" 24 | discriminator: 25 | propertyName: __typename 26 | mapping": # deprecated 27 | User: "#/components/schemas/User" 28 | UserUnavailable: "#/components/schemas/UserUnavailable" 29 | 30 | User: 31 | required: 32 | - "__typename" 33 | - "id" 34 | - "is_blue_verified" 35 | - "legacy" 36 | - "rest_id" 37 | - "profile_image_shape" 38 | 39 | properties: 40 | __typename: 41 | $ref: "./typename.yaml#/components/schemas/TypeName" # User 42 | affiliates_highlighted_label: 43 | type: object 44 | additionalProperties: true # todo 45 | has_graduated_access: 46 | type: boolean 47 | has_nft_avatar: 48 | type: boolean 49 | id: 50 | type: string 51 | pattern: "^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$" # base64 52 | is_blue_verified: 53 | type: boolean 54 | legacy: 55 | $ref: "#/components/schemas/UserLegacy" 56 | rest_id: 57 | type: string 58 | pattern: "^[0-9]+$" 59 | business_account: 60 | type: object 61 | additionalProperties: true # todo 62 | super_follow_eligible: 63 | type: boolean 64 | super_followed_by: 65 | type: boolean 66 | super_following: 67 | type: boolean 68 | profile_image_shape: 69 | type: string 70 | enum: 71 | - "Circle" 72 | - "Square" 73 | - "Hexagon" 74 | professional: 75 | $ref: "#/components/schemas/UserProfessional" 76 | user_seed_tweet_count: 77 | type: integer 78 | highlights_info: 79 | $ref: "#/components/schemas/UserHighlightsInfo" 80 | creator_subscriptions_count: 81 | type: integer 82 | verification_info: 83 | $ref: "#/components/schemas/UserVerificationInfo" 84 | is_profile_translatable: 85 | type: boolean 86 | tipjar_settings: 87 | $ref: "#/components/schemas/UserTipJarSettings" 88 | legacy_extended_profile: 89 | $ref: "#/components/schemas/UserLegacyExtendedProfile" 90 | has_hidden_likes_on_profile: 91 | type: boolean 92 | premium_gifting_eligible: 93 | type: boolean 94 | has_hidden_subscriptions_on_profile: 95 | type: boolean 96 | parody_commentary_fan_label: 97 | type: string 98 | enum: ["None", "Parody", "Commentary"] 99 | 100 | UserProfessional: 101 | required: 102 | - "rest_id" 103 | - "professional_type" 104 | - "category" 105 | properties: 106 | rest_id: 107 | type: string 108 | pattern: "^[0-9]+$" 109 | professional_type: 110 | type: string 111 | enum: ["Business", "Creator"] 112 | category: 113 | type: array 114 | items: 115 | $ref: "#/components/schemas/UserProfessionalCategory" 116 | 117 | UserProfessionalCategory: 118 | required: 119 | - "id" 120 | - "name" 121 | - "icon_name" 122 | properties: 123 | id: 124 | type: integer 125 | name: 126 | type: string # enum 127 | icon_name: 128 | type: string # IconBriefcaseStroke ? 129 | 130 | UserHighlightsInfo: 131 | required: 132 | - "can_highlight_tweets" 133 | - "highlighted_tweets" 134 | properties: 135 | can_highlight_tweets: 136 | type: boolean 137 | highlighted_tweets: 138 | type: string 139 | 140 | UserVerificationInfo: 141 | required: 142 | - "is_identity_verified" 143 | properties: 144 | is_identity_verified: 145 | type: boolean 146 | reason: 147 | $ref: "#/components/schemas/UserVerificationInfoReason" 148 | 149 | UserVerificationInfoReason: 150 | required: 151 | - "description" 152 | - "verified_since_msec" 153 | properties: 154 | description: 155 | $ref: "#/components/schemas/UserVerificationInfoReasonDescription" 156 | verified_since_msec: 157 | type: string 158 | pattern: "^-?[0-9]+$" 159 | override_verified_year: 160 | type: integer 161 | 162 | UserVerificationInfoReasonDescription: 163 | required: 164 | - "text" 165 | - "entities" 166 | properties: 167 | text: 168 | type: string 169 | entities: 170 | type: array 171 | items: 172 | $ref: "#/components/schemas/UserVerificationInfoReasonDescriptionEntities" 173 | 174 | UserVerificationInfoReasonDescriptionEntities: 175 | required: 176 | - "from_index" 177 | - "to_index" 178 | - "ref" 179 | properties: 180 | from_index: 181 | type: integer 182 | to_index: 183 | type: integer 184 | ref: 185 | $ref: "#/components/schemas/UserVerificationInfoReasonDescriptionEntitiesRef" 186 | 187 | UserVerificationInfoReasonDescriptionEntitiesRef: 188 | required: 189 | - "url" 190 | - "url_type" 191 | properties: 192 | url: 193 | type: string 194 | format: uri 195 | url_type: 196 | type: string 197 | enum: ["ExternalUrl"] 198 | 199 | UserTipJarSettings: 200 | properties: 201 | is_enabled: 202 | type: boolean 203 | patreon_handle: 204 | type: string 205 | bitcoin_handle: 206 | type: string 207 | ethereum_handle: 208 | type: string 209 | cash_app_handle: 210 | type: string 211 | venmo_handle: 212 | type: string 213 | gofundme_handle: 214 | type: string # uri 215 | bandcamp_handle: 216 | type: string # uri 217 | pay_pal_handle: 218 | type: string 219 | 220 | UserLegacyExtendedProfile: 221 | properties: 222 | birthdate: 223 | $ref: "#/components/schemas/UserLegacyExtendedProfileBirthdate" 224 | 225 | UserLegacyExtendedProfileBirthdate: 226 | required: 227 | - "day" 228 | - "month" 229 | - "visibility" 230 | - "year_visibility" 231 | properties: 232 | day: 233 | type: integer 234 | month: 235 | type: integer 236 | year: 237 | type: integer 238 | visibility: 239 | type: string 240 | enum: ["Self", "Public", "MutualFollow", "Followers", "Following"] 241 | year_visibility: 242 | type: string 243 | enum: ["Self", "Public", "MutualFollow", "Followers", "Following"] 244 | 245 | UserLegacy: 246 | required: 247 | - "created_at" 248 | - "default_profile" 249 | - "default_profile_image" 250 | - "description" 251 | - "entities" 252 | - "fast_followers_count" 253 | - "favourites_count" 254 | - "followers_count" 255 | - "friends_count" 256 | - "has_custom_timelines" 257 | - "is_translator" 258 | - "listed_count" 259 | - "location" 260 | - "media_count" 261 | - "name" 262 | - "normal_followers_count" 263 | - "pinned_tweet_ids_str" 264 | - "possibly_sensitive" 265 | - "profile_image_url_https" 266 | - "profile_interstitial_type" 267 | - "screen_name" 268 | - "status" 269 | - "statuses_count" 270 | - "translator_type" 271 | - "verified" 272 | properties: 273 | blocked_by: 274 | type: boolean 275 | blocking: 276 | type: boolean 277 | can_dm: 278 | type: boolean 279 | can_media_tag: 280 | type: boolean 281 | created_at: 282 | $ref: "./general.yaml#/components/schemas/TwitterTimeFormat" 283 | default_profile: 284 | type: boolean 285 | default_profile_image: 286 | type: boolean 287 | description: 288 | type: string 289 | entities: 290 | type: object 291 | additionalProperties: true # todo 292 | fast_followers_count: 293 | type: integer 294 | favourites_count: 295 | type: integer 296 | follow_request_sent: 297 | type: boolean 298 | followed_by: 299 | type: boolean 300 | followers_count: 301 | type: integer 302 | following: 303 | type: boolean 304 | friends_count: 305 | type: integer 306 | has_custom_timelines: 307 | type: boolean 308 | is_translator: 309 | type: boolean 310 | listed_count: 311 | type: integer 312 | location: 313 | type: string 314 | media_count: 315 | type: integer 316 | muting: 317 | type: boolean 318 | name: 319 | type: string 320 | normal_followers_count: 321 | type: integer 322 | notifications: 323 | type: boolean 324 | pinned_tweet_ids_str: 325 | type: array 326 | items: 327 | type: string 328 | possibly_sensitive: 329 | type: boolean 330 | profile_banner_extensions: 331 | type: object 332 | profile_banner_url: 333 | type: string 334 | format: uri 335 | profile_image_extensions: 336 | type: object 337 | profile_image_url_https: 338 | type: string 339 | format: uri 340 | profile_interstitial_type: 341 | type: string 342 | protected: 343 | type: boolean 344 | screen_name: 345 | type: string 346 | statuses_count: 347 | type: integer 348 | translator_type: 349 | type: string 350 | url: 351 | type: string 352 | verified: 353 | type: boolean 354 | want_retweets: 355 | type: boolean 356 | verified_type: 357 | type: string 358 | enum: ["Business", "Government"] 359 | withheld_in_countries: 360 | type: array 361 | items: 362 | type: string # enum DE 363 | 364 | UserUnavailable: 365 | required: 366 | - "__typename" 367 | - "reason" 368 | properties: 369 | __typename: 370 | $ref: "./typename.yaml#/components/schemas/TypeName" # UserUnavailable 371 | reason: 372 | type: string 373 | message: 374 | type: string 375 | -------------------------------------------------------------------------------- /test/dart-dio/openapi-generator-config.yaml: -------------------------------------------------------------------------------- 1 | inputSpec: dist/compatible/openapi-3.0.yaml 2 | outputDir: dart_dio_generated 3 | -------------------------------------------------------------------------------- /test/local.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | python3 tools/build.py 4 | java -jar openapi-generator-cli.jar generate -c test/python/openapi-generator-config.yaml -g python 5 | python3 -m pip install ./python_generated 6 | python3 test/python/test_serialize.py -------------------------------------------------------------------------------- /test/python/openapi-generator-config.yaml: -------------------------------------------------------------------------------- 1 | inputSpec: dist/compatible/openapi-3.0.yaml 2 | outputDir: python_generated 3 | 4 | useOneOfDiscriminatorLookup: true 5 | -------------------------------------------------------------------------------- /test/python/test_serialize.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import concurrent.futures 3 | import glob 4 | import inspect 5 | import json 6 | import logging 7 | import os 8 | import time 9 | import traceback 10 | import warnings 11 | from enum import Enum 12 | from pathlib import Path 13 | from typing import Any 14 | 15 | import bs4 16 | import openapi_client as pt 17 | import requests 18 | import urllib3 19 | from x_client_transaction import ClientTransaction 20 | from x_client_transaction.utils import ( 21 | generate_headers, 22 | get_ondemand_file_url, 23 | handle_x_migration, 24 | ) 25 | 26 | warnings.filterwarnings("ignore") 27 | logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(levelname)s %(message)s") 28 | logger = logging.getLogger("test_serialize") 29 | 30 | TWITTER_SESSION = os.environ.get("TWITTER_SESSION", None) 31 | ERROR_UNCATCHED = os.environ.get("ERROR_UNCATCHED", "false").lower() == "true" 32 | SLEEP_TIME = float(os.environ.get("SLEEP", "0")) 33 | CUESOR_TEST_COUNT = int(os.environ.get("CUESOR_TEST_COUNT", "3")) 34 | STRICT_MODE = os.environ.get("STRICT_MODE", "false").lower() == "true" 35 | MULTI_THREAD = os.environ.get("MULTI_THREAD", "true").lower() == "true" 36 | 37 | 38 | def get_key(snake_str): 39 | components = snake_str.split("_") 40 | return "".join(x.title() for x in components[1:]) 41 | 42 | 43 | def get_cursor(obj, fn): 44 | res = [] 45 | if isinstance(obj, dict): 46 | callback = fn(obj) 47 | if callback is not None: 48 | res.extend(callback) 49 | else: 50 | for v in obj.values(): 51 | res.extend(get_cursor(v, fn)) 52 | elif isinstance(obj, list): 53 | for v in obj: 54 | res.extend(get_cursor(v, fn)) 55 | return res 56 | 57 | 58 | def find_cursor(x): 59 | if x.get("__typename") is pt.TypeName.TIMELINETIMELINECURSOR: 60 | return [x["value"]] 61 | 62 | 63 | def find_name(x): 64 | if x.get("name") is not None: 65 | return [x["name"]] 66 | 67 | 68 | def get_transaction_base(): 69 | session = requests.Session() 70 | session.headers = generate_headers() 71 | home_page_response = handle_x_migration(session=session) 72 | home_page = session.get(url="https://x.com") 73 | home_page_response = bs4.BeautifulSoup(home_page.content, "html.parser") 74 | ondemand_file_url = get_ondemand_file_url(response=home_page_response) 75 | ondemand_file = session.get(url=ondemand_file_url) 76 | ondemand_file_response = bs4.BeautifulSoup(ondemand_file.content, "html.parser") 77 | ct = ClientTransaction(home_page_response, ondemand_file_response) 78 | return ct 79 | 80 | 81 | def get_transaction_id(key, ct=get_transaction_base()): 82 | return ct.generate_transaction_id( 83 | method=placeholder[key]["@method"], path=placeholder[key]["@path"] 84 | ) 85 | 86 | 87 | def get_kwargs(key, additional): 88 | kwargs = {"path_query_id": placeholder[key]["queryId"], "_headers": {}} 89 | if placeholder[key].get("variables") is not None: 90 | kwargs["variables"] = json.dumps(placeholder[key]["variables"] | additional) 91 | if placeholder[key].get("features") is not None: 92 | kwargs["features"] = json.dumps(placeholder[key]["features"]) 93 | if placeholder[key].get("fieldToggles") is not None: 94 | kwargs["field_toggles"] = json.dumps(placeholder[key]["fieldToggles"]) 95 | if placeholder[key].get("@path") is not None: 96 | kwargs["_headers"]["x-client-transaction-id"] = get_transaction_id(key) 97 | return kwargs 98 | 99 | 100 | def match_rate_zero(a, b, base, key): 101 | def get(obj, key): 102 | if isinstance(obj, list): 103 | return get(obj[key[0]], key[1:]) 104 | if obj.__dict__.get("actual_instance") is not None: 105 | return get(obj.actual_instance, key) 106 | if len(key) == 0: 107 | return obj 108 | return get(super_get(obj.__dict__, key[0]), key[1:]) 109 | 110 | if STRICT_MODE: 111 | obj_name = type(get(base, key[:-1])) 112 | obj_key = f"{obj_name.__name__}.{key[-1]}" 113 | raise Exception(f"Not defined: {obj_key}\nContents: {b}") 114 | 115 | return 0 116 | 117 | 118 | def match_rate(a, b, base, key=""): 119 | if isinstance(a, Enum): 120 | a = a.value 121 | if isinstance(b, Enum): 122 | b = b.value 123 | if a is None and b is False: 124 | return 1 125 | if a is False and b is None: 126 | return 1 127 | if a is None and isinstance(b, list) and len(b) == 0: 128 | return 1 129 | if isinstance(a, list) and b is None and len(a) == 0: 130 | return 1 131 | if a is None and isinstance(b, dict) and len(b) == 0: 132 | return 1 133 | if isinstance(a, dict) and b is None and len(a) == 0: 134 | return 1 135 | if isinstance(a, dict) and isinstance(b, dict): 136 | if len(a) == 0 and len(b) == 0: 137 | return 1 138 | marge_key = set(a.keys()) | set(b.keys()) 139 | data = [match_rate(a.get(k), b.get(k), base, [*key, k]) for k in marge_key] 140 | return sum(data) / len(b) 141 | if isinstance(a, list) and isinstance(b, list): 142 | if len(a) == 0 and len(b) == 0: 143 | return 1 144 | if len(a) != len(b): 145 | return match_rate_zero(a, b, base, key) 146 | data = [match_rate(a[i], b[i], base, [*key, i]) for i in range(len(a))] 147 | return sum(data) / len(a) 148 | if a == b: 149 | return 1 150 | return match_rate_zero(a, b, base, key) 151 | 152 | 153 | def save_cache(data): 154 | rand = time.time_ns() 155 | os.makedirs("cache", exist_ok=True) 156 | with open(f"cache/{rand}.json", "w+") as f: 157 | json.dump(data, f, indent=4) 158 | 159 | 160 | def super_get(obj: dict, key: str): 161 | keys = [ 162 | key, 163 | "".join(["_" + c.lower() if c.isupper() else c for c in key]).lstrip("_"), 164 | ] 165 | 166 | for k in keys: 167 | if obj.get(k) is not None: 168 | return obj[k] 169 | raise KeyError(key) 170 | 171 | 172 | def task_callback(file, thread=True): 173 | try: 174 | with open(file, "r") as f: 175 | cache = json.load(f) 176 | data = pt.__dict__[cache["type"]].from_json(cache["raw"]) 177 | 178 | rate = match_rate( 179 | data.to_dict(), 180 | json.loads(cache["raw"]), 181 | base=data, 182 | ) 183 | return rate, file 184 | except Exception: 185 | if thread: 186 | return 0, file 187 | else: 188 | raise 189 | 190 | 191 | def kebab_to_upper_camel(text: dict[str, Any]) -> dict[str, Any]: 192 | res = {} 193 | for key, value in text.items(): 194 | new_key = "".join([x.capitalize() for x in remove_prefix(key).split("-")]) 195 | res[new_key] = value 196 | return res 197 | 198 | 199 | def get_header(data: dict, name: str): 200 | ignore = ["host", "connection"] 201 | return {key: value for key, value in data[name].items() if key not in ignore} 202 | 203 | 204 | def remove_prefix(text: str) -> str: 205 | if text.startswith("x-twitter-"): 206 | return text[10:] 207 | if text.startswith("x-"): 208 | return text[2:] 209 | return text 210 | 211 | 212 | def error_dump(e): 213 | if ERROR_UNCATCHED: 214 | raise 215 | 216 | logger.error("==========[STACK TRACE]==========") 217 | for trace in traceback.format_exc().split("\n"): 218 | logger.error(trace) 219 | logger.info("================================") 220 | 221 | 222 | if __name__ == "__main__": 223 | if Path("cookie.json").exists(): 224 | with open("cookie.json", "r") as f: 225 | cookies = json.load(f) 226 | elif TWITTER_SESSION is not None: 227 | data = base64.b64decode(TWITTER_SESSION).decode("utf-8") 228 | cookies = json.loads(data) 229 | else: 230 | commands = ["python -m pip install tweepy_authlib", "python tools/login.py"] 231 | raise Exception( 232 | f"cookie.json not found. Please run `{'; '.join(commands)}` first." 233 | ) 234 | 235 | if isinstance(cookies, list): 236 | cookies = {k["name"]: k["value"] for k in cookies} 237 | cookies_str = "; ".join([f"{k}={v}" for k, v in cookies.items()]) 238 | 239 | with open("src/config/placeholder.json", "r") as f: 240 | placeholder = json.load(f) 241 | 242 | fail = [] 243 | files = glob.glob("cache/*.json") 244 | if MULTI_THREAD: 245 | with concurrent.futures.ProcessPoolExecutor() as executor: 246 | tasks = [executor.submit(task_callback, x) for x in files] 247 | for task in concurrent.futures.as_completed(tasks): 248 | rate, file = task.result() 249 | if rate < 1: 250 | fail.append(file) 251 | logger.info(f"Match rate: {rate}") 252 | else: 253 | for file in files: 254 | rate, file = task_callback(file, thread=False) 255 | if rate < 1: 256 | fail.append(file) 257 | logger.info(f"Match rate: {rate}") 258 | 259 | logger.info(f"Fail: {len(fail)} / {len(glob.glob('cache/*.json'))}") 260 | 261 | for file in fail: 262 | task_callback(file, thread=False) 263 | logger.info(f"Match rate: {rate}") 264 | 265 | for file in glob.glob("other/**/*.json", recursive=True): 266 | with open(file, "r") as f: 267 | data = json.load(f) 268 | 269 | try: 270 | _ = pt.TweetDetailResponse.from_dict(data) 271 | except Exception as e: 272 | error_dump(e) 273 | access_token = "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA" 274 | twitter_url = "https://x.com/home" 275 | latest_user_agent_res = urllib3.PoolManager().request( 276 | "GET", 277 | "https://raw.githubusercontent.com/fa0311/latest-user-agent/refs/heads/main/header.json", 278 | ) 279 | latest_user_agent = json.loads(latest_user_agent_res.data.decode("utf-8")) 280 | api_conf = pt.Configuration( 281 | api_key=kebab_to_upper_camel( 282 | { 283 | **get_header(latest_user_agent, "chrome-fetch"), 284 | "sec-ch-ua-platform": '"Windows"', 285 | "accept-encoding": "identity", 286 | "referer": twitter_url, 287 | "priority": "u=1, i", 288 | "authorization": f"Bearer {access_token}", 289 | "x-twitter-auth-type": "OAuth2Session", 290 | "x-twitter-client-language": "en", 291 | "x-twitter-active-user": "yes", 292 | "x-csrf-token": cookies["ct0"], 293 | }, 294 | ), 295 | ) 296 | api_conf.access_token = access_token 297 | api_client = pt.ApiClient(configuration=api_conf, cookie=cookies_str) 298 | api_client.user_agent = get_header(latest_user_agent, "chrome-fetch")["user-agent"] 299 | 300 | session = requests.Session() 301 | session.headers = get_header(latest_user_agent, "chrome") 302 | 303 | error_count = 0 304 | 305 | for x in [pt.DefaultApi, pt.TweetApi, pt.UserApi, pt.UsersApi, pt.UserListApi]: 306 | for props, fn in inspect.getmembers(x): 307 | if not callable(fn): 308 | continue 309 | if props.startswith("__") or not props.endswith("_with_http_info"): 310 | continue 311 | 312 | key = get_key(props[:-15]) 313 | cursor_list = set([None]) 314 | cursor_history = set() 315 | 316 | try: 317 | for _ in range(CUESOR_TEST_COUNT): 318 | cursor = cursor_list.pop() 319 | cursor_history.add(cursor) 320 | logger.info(f"Try: {key} {cursor}") 321 | 322 | kwargs = get_kwargs( 323 | key, {} if cursor is None else {"cursor": cursor} 324 | ) 325 | res: pt.ApiResponse = getattr(x(api_client), props)(**kwargs) 326 | data = res.data.to_dict() 327 | 328 | save_cache( 329 | { 330 | "raw": res.raw_data.decode("utf-8"), 331 | "type": res.data.__class__.__name__, 332 | } 333 | ) 334 | 335 | new_cursor = set(get_cursor(data, find_cursor)) - cursor_history 336 | cursor_list.update(new_cursor) 337 | 338 | rate = match_rate( 339 | data, 340 | json.loads(res.raw_data), 341 | res.data, 342 | ) 343 | logger.info(f"Match rate: {rate}") 344 | 345 | if data.get("errors") is not None: 346 | logger.error(data) 347 | error_count += 1 348 | 349 | if len(cursor_list) == 0: 350 | break 351 | time.sleep(SLEEP_TIME) 352 | 353 | except Exception as e: 354 | error_dump(e) 355 | error_count += 1 356 | 357 | try: 358 | logger.info("Try: Self UserByScreenName Test") 359 | kwargs = get_kwargs("UserByScreenName", {"screen_name": "ptcpz3"}) 360 | res = pt.UserApi(api_client).get_user_by_screen_name_with_http_info(**kwargs) 361 | data = res.data.to_dict() 362 | 363 | rate = match_rate( 364 | data, 365 | json.loads(res.raw_data), 366 | res.data, 367 | ) 368 | logger.info(f"Match rate: {rate}") 369 | screen_name = data["data"]["user"]["result"]["legacy"]["screen_name"] 370 | if not screen_name == "ptcpz3": 371 | raise Exception("UserByScreenName failed") 372 | except Exception as e: 373 | error_dump(e) 374 | error_count += 1 375 | 376 | ids = [ 377 | # "1180389371481976833", banned 378 | "900282258736545792", 379 | "1212617657003859968", 380 | "2455740283", 381 | "2326837940", 382 | "1788224200639160320", 383 | "1500128450186985472", 384 | "25073877", 385 | ] 386 | for id in ids: 387 | try: 388 | logger.info("Try: Self UserTweets Test") 389 | kwargs = get_kwargs("UserTweets", {"userId": id}) 390 | res = pt.TweetApi(api_client).get_user_tweets_with_http_info(**kwargs) 391 | data = res.data.to_dict() 392 | 393 | rate = match_rate( 394 | data, 395 | json.loads(res.raw_data), 396 | res.data, 397 | ) 398 | logger.info(f"Match rate: {rate}") 399 | 400 | except Exception as e: 401 | error_dump(e) 402 | error_count += 1 403 | 404 | ids = [ 405 | "1720975693524377759", 406 | "1721006592303251551", 407 | "1739194269477331076", 408 | # "1697450269259522256", 409 | # "1697450278742884799", 410 | "1749500209061663043", 411 | "1759056048764469303", 412 | "1349129669258448897", 413 | "1810188416812019999", 414 | "1851981523207299417", 415 | "1853879226987901408", 416 | "1866022435549757577", 417 | "1866103697148887145", 418 | "1866036470303309863", 419 | "1862405433639804958", 420 | "1869983867521904840", 421 | "1875050002046726519", 422 | "1848219562136801480", 423 | "1881993128288399684", 424 | "1899104692577489182", 425 | ] 426 | for id in ids: 427 | try: 428 | logger.info(f"Try: Self TweetDetail {id} Test") 429 | kwargs = get_kwargs("TweetDetail", {"focalTweetId": id}) 430 | res = pt.TweetApi(api_client).get_tweet_detail_with_http_info(**kwargs) 431 | data = res.data.to_dict() 432 | 433 | save_cache( 434 | { 435 | "raw": res.raw_data.decode("utf-8"), 436 | "type": res.data.__class__.__name__, 437 | } 438 | ) 439 | 440 | rate = match_rate( 441 | data, 442 | json.loads(res.raw_data), 443 | res.data, 444 | ) 445 | logger.info(f"Match rate: {rate}") 446 | except Exception as e: 447 | error_dump(e) 448 | error_count += 1 449 | 450 | if error_count > 0: 451 | exit(1) 452 | -------------------------------------------------------------------------------- /test/python/test_serialize_guest.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | from urllib.parse import urlencode, urlparse 4 | 5 | import openapi_client as pt 6 | import requests 7 | import urllib3 8 | from x_client_transaction import ClientTransaction 9 | from x_client_transaction.utils import handle_x_migration 10 | 11 | 12 | def get_kwargs(key, additional): 13 | kwargs = {"path_query_id": placeholder[key]["queryId"], "_headers": {}} 14 | if placeholder[key].get("variables") is not None: 15 | kwargs["variables"] = json.dumps(placeholder[key]["variables"] | additional) 16 | if placeholder[key].get("features") is not None: 17 | kwargs["features"] = json.dumps(placeholder[key]["features"]) 18 | if placeholder[key].get("fieldToggles") is not None: 19 | kwargs["field_toggles"] = json.dumps(placeholder[key]["fieldToggles"]) 20 | if placeholder[key].get("@path") is not None: 21 | kwargs["_headers"]["x-client-transaction-id"] = ct.generate_transaction_id( 22 | method=placeholder[key]["@method"], path=placeholder[key]["@path"] 23 | ) 24 | return kwargs 25 | 26 | 27 | class SessionManager: 28 | def __init__(self) -> None: 29 | header = "https://raw.githubusercontent.com/fa0311/latest-user-agent/refs/heads/main/header.json" 30 | self.http = urllib3.PoolManager() 31 | self.chorome_header = json.loads(self.http.request("GET", header).data) 32 | 33 | def child(self): 34 | return SessionManagerChild(self.http, self.chorome_header) 35 | 36 | 37 | class SessionManagerChild: 38 | def __init__(self, http, chorome_header) -> None: 39 | self.http = http 40 | self.chorome_header = chorome_header 41 | self.session = {} 42 | 43 | def cookie_normalize(self, cookie: list[str]) -> dict[str, str]: 44 | value = { 45 | x.split("; ")[0].split("=")[0]: x.split("; ")[0].split("=")[1] 46 | for x in cookie 47 | } 48 | return {key: value[key] for key in value if len(value[key]) > 0} 49 | 50 | def cookie_to_str(self, cookie: dict[str, str]) -> str: 51 | return "; ".join([f"{key}={value}" for key, value in cookie.items()]) 52 | 53 | def getHader(self, additionals={}) -> dict[str, str]: 54 | ignore = ["host", "connection"] 55 | base = { 56 | key: value 57 | for key, value in self.chorome_header["chrome"].items() 58 | if key not in ignore 59 | } 60 | return base | {"cookie": self.cookie_to_str(self.session)} | additionals 61 | 62 | def update_normalize(self, cookie: list[str]): 63 | self.update(self.cookie_normalize(cookie)) 64 | 65 | def update(self, cookie: dict[str, str]): 66 | self.session.update(cookie) 67 | 68 | def pop(self, key: str): 69 | self.session.pop(key) 70 | 71 | def get(self, key: str): 72 | return self.session.get(key) 73 | 74 | def to_str(self): 75 | return self.cookie_to_str(self.session) 76 | 77 | 78 | def get_guest_token(): 79 | twitter_url = "https://x.com/elonmusk" 80 | http = urllib3.PoolManager() 81 | chrome = SessionManager() 82 | x = chrome.child() 83 | twitter = chrome.child() 84 | 85 | def regex(str: str, **kwargs) -> str: 86 | return str.format( 87 | quote=r"[\'\"]", 88 | space=r"\s*", 89 | dot=r"\.", 90 | any=r".*?", 91 | target=r"([\s\S]*?)", 92 | **kwargs, 93 | ) 94 | 95 | def redirect( 96 | method: str, url: str, body: str = None, headers: dict[str, str] = {} 97 | ) -> urllib3.HTTPResponse: 98 | for _ in range(10): 99 | if urlparse(url).netloc == "x.com": 100 | res = http.request( 101 | method, url, headers=x.getHader(headers), body=body, redirect=False 102 | ) 103 | x.update_normalize(res.headers._container["set-cookie"][1:]) 104 | elif urlparse(url).netloc == "twitter.com": 105 | res = http.request( 106 | method, 107 | url, 108 | headers=twitter.getHader(headers), 109 | body=body, 110 | redirect=False, 111 | ) 112 | twitter.update_normalize(res.headers._container["set-cookie"][1:]) 113 | else: 114 | raise Exception("Invalid domain") 115 | 116 | method = "GET" 117 | body = None 118 | headers = {} 119 | location = "document{dot}location{space}={space}{quote}{target}{quote}" 120 | submit = "document{dot}forms{dot}{target}{dot}submit" 121 | form = "{target}" 122 | input = "" 123 | 124 | if res.status >= 300 and res.status < 400: 125 | new_path = res.headers._container["location"][1] 126 | if new_path.startswith("/"): 127 | domain = f"{urlparse(url).scheme}://{urlparse(url).netloc}" 128 | url = f"{domain}{new_path}" 129 | else: 130 | url = new_path 131 | 132 | elif re.findall(regex(location), res.data.decode()): 133 | url = re.findall(regex(location), res.data.decode())[0] 134 | elif re.findall(regex(submit), res.data.decode()): 135 | name = re.findall(regex(submit), res.data.decode()) 136 | form_html = re.findall(regex(form, name=name[0]), res.data.decode()) 137 | input_html = re.findall(regex(input), form_html[0][1]) 138 | method = "POST" 139 | url = form_html[0][0] 140 | body = urlencode({k: v for k, v in input_html}) 141 | headers = {"content-type": "application/x-www-form-urlencoded"} 142 | elif res.status == 200: 143 | return res 144 | else: 145 | raise Exception("Failed to redirect") 146 | else: 147 | raise Exception("Failed to redirect") 148 | 149 | res = redirect("GET", twitter_url) 150 | reg = "document{dot}cookie{space}={space}{quote}{target}{quote}" 151 | if re.findall(regex(reg), res.data.decode()): 152 | find = re.findall(regex(reg), res.data.decode()) 153 | x.update_normalize(find) 154 | 155 | if x.get("gt") is None: 156 | raise Exception("Failed to get guest token") 157 | 158 | return x 159 | 160 | 161 | if __name__ == "__main__": 162 | cookies = get_guest_token() 163 | cookies_str = cookies.to_str() 164 | 165 | with open("src/config/placeholder.json", "r") as f: 166 | placeholder = json.load(f) 167 | 168 | api_conf = pt.Configuration( 169 | api_key={ 170 | "ClientLanguage": "en", 171 | "ActiveUser": "yes", 172 | "GuestToken": cookies.get("gt"), 173 | }, 174 | ) 175 | 176 | latest_user_agent_res = urllib3.PoolManager().request( 177 | "GET", 178 | "https://raw.githubusercontent.com/fa0311/latest-user-agent/main/output.json", 179 | ) 180 | 181 | latest_user_agent = json.loads(latest_user_agent_res.data.decode("utf-8")) 182 | 183 | api_conf.access_token = "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA" 184 | api_client = pt.ApiClient(configuration=api_conf, cookie=cookies_str) 185 | api_client.user_agent = latest_user_agent["chrome-fetch"] 186 | 187 | session = requests.Session() 188 | session.headers = latest_user_agent["chrome"] 189 | ct = ClientTransaction(handle_x_migration(session)) 190 | 191 | res = ( 192 | pt.TweetApi(api_client) 193 | .get_user_tweets_with_http_info( 194 | **get_kwargs("UserTweets", {}), 195 | ) 196 | .model_dump_json() 197 | ) 198 | res = ( 199 | pt.TweetApi(api_client) 200 | .get_user_highlights_tweets_with_http_info( 201 | **get_kwargs("UserHighlightsTweets", {}), 202 | ) 203 | .model_dump_json() 204 | ) 205 | res = ( 206 | pt.DefaultApi(api_client) 207 | .get_tweet_result_by_rest_id_with_http_info( 208 | **get_kwargs("TweetResultByRestId", {}), 209 | ) 210 | .model_dump_json() 211 | ) 212 | res = ( 213 | pt.UserApi(api_client) 214 | .get_user_by_screen_name_with_http_info(**get_kwargs("UserByScreenName", {})) 215 | .model_dump_json() 216 | ) 217 | -------------------------------------------------------------------------------- /test/typescript-fetch/openapi-generator-config.yaml: -------------------------------------------------------------------------------- 1 | inputSpec: dist/compatible/openapi-3.0.yaml 2 | outputDir: typescript_fetch_generated 3 | 4 | legacyDiscriminatorBehavior: false 5 | -------------------------------------------------------------------------------- /tools/build.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | import yaml 4 | import shutil 5 | from build_config import Config 6 | from hooks import OpenapiHookBase, RequestHookBase, SchemasHookBase, OtherHookBase 7 | from tqdm import tqdm 8 | 9 | 10 | def replace_ref(x): 11 | if isinstance(x, dict): 12 | return { 13 | key: "#" + value.split("#")[-1] if key == "$ref" else replace_ref(value) 14 | for key, value in x.items() 15 | } 16 | elif isinstance(x, list): 17 | return [replace_ref(value) for value in x] 18 | return x 19 | 20 | 21 | print("=== Build Start ===") 22 | 23 | 24 | config = Config() 25 | 26 | 27 | shutil.rmtree("dist", ignore_errors=True) 28 | 29 | 30 | for lang, profile in tqdm(config.main().items(), leave=False): 31 | paths = {} 32 | schemas = {} 33 | files = glob.glob(os.path.join(config.INPUT_DIR, "**/*.yaml")) 34 | for file in tqdm(files, leave=False): 35 | file = file.replace(os.path.sep, "/") 36 | with open(file, mode="r", encoding="utf-8") as f: 37 | load = yaml.safe_load(f) 38 | 39 | # RequestHookBase hook 40 | for path in list(load["paths"]): 41 | for method in list(load["paths"][path]): 42 | key, value = path, load["paths"][path][method] 43 | for tag in list(load["paths"][path][method].get("tags", ["default"])): 44 | for hook in profile["request"][tag]: 45 | hook: RequestHookBase 46 | key, value = hook.hook(key, value) 47 | paths.update({key: {}}) 48 | paths[key].update({method: value}) 49 | 50 | # SchemasHookBase hook 51 | for name in list(load.get("components", {}).get("schemas", {})): 52 | value = load["components"]["schemas"][name] 53 | for hook in profile["schemas"]: 54 | hook: SchemasHookBase 55 | value = hook.hook(value) 56 | schemas.update({name: value}) 57 | 58 | # OtherHookBase hook 59 | if file == "src/openapi/paths/other.yaml": 60 | for hook in profile["other"]: 61 | hook: OtherHookBase 62 | key, value = hook.hook() 63 | schemas["OtherResponse"]["properties"].append({key: value}) 64 | 65 | file = "src/openapi/openapi-3.0.yaml" 66 | 67 | with open(file, mode="r", encoding="utf-8") as f: 68 | openapi = yaml.safe_load(f) 69 | 70 | # OpenapiHookBase hook 71 | for hook in profile["openapi"]: 72 | hook: OpenapiHookBase 73 | openapi = hook.hook(openapi) 74 | 75 | openapi["paths"] = replace_ref(paths) 76 | openapi["components"]["schemas"] = replace_ref(schemas) 77 | output = config.OUTPUT_DIR.format(lang) 78 | os.makedirs(output, exist_ok=True) 79 | 80 | with open(output + "/openapi-3.0.yaml", mode="w+", encoding="utf-8") as f: 81 | f.write(yaml.dump(openapi)) 82 | 83 | print("=== Build End ===") 84 | -------------------------------------------------------------------------------- /tools/build_config.py: -------------------------------------------------------------------------------- 1 | from hooks import ( 2 | AddParametersOnBody, 3 | AddParametersOnContent, 4 | AddParametersOnParameters, 5 | AddPathQueryIdOnParameters, 6 | SetResponsesHeader, 7 | ) 8 | 9 | 10 | class Config: 11 | OUTPUT_DIR = "dist/{0}" 12 | INPUT_DIR = "src/openapi" 13 | 14 | def hooks_generator(self, queryParameterJson=True): 15 | # https://stackoverflow.com/questions/34820064/defining-an-api-with-swagger-get-call-that-uses-json-in-parameters/45223964 16 | if queryParameterJson: 17 | # ["parameters"][0]["content"]["application/json"]["schema"] 18 | getParamHook = AddParametersOnContent( 19 | split=-1, 20 | contentType="application/json", 21 | ignoreKeys=["queryId", "@path", "@method"], 22 | ) 23 | else: 24 | # ["parameters"][0]["schema"] 25 | getParamHook = AddParametersOnParameters( 26 | split=-1, 27 | schemaType="string", 28 | ignoreKeys=["queryId", "@path", "@method"], 29 | ) 30 | 31 | return { 32 | "openapi": [], 33 | "schemas": [], 34 | "other": [], 35 | "request": { 36 | key: [ 37 | SetResponsesHeader(), 38 | AddPathQueryIdOnParameters(split=-1), 39 | getParamHook, 40 | ] 41 | for key in ["default", "user", "users", "user-list", "tweet"] 42 | } 43 | | { 44 | key: [ 45 | SetResponsesHeader(), 46 | AddPathQueryIdOnParameters(split=-1), 47 | AddParametersOnBody( 48 | split=-1, 49 | schemaType=None, 50 | contentType="application/json", 51 | ignoreKeys=["@path", "@method"], 52 | ), 53 | ] 54 | for key in ["post"] 55 | } 56 | | { 57 | key: [ 58 | SetResponsesHeader(), 59 | AddParametersOnParameters(split=2, schemaType=None), 60 | ] 61 | for key in ["v1.1-get", "v2.0-get"] 62 | } 63 | | { 64 | key: [ 65 | SetResponsesHeader(), 66 | AddParametersOnBody( 67 | split=2, 68 | schemaType=None, 69 | contentType="application/x-www-form-urlencoded", 70 | ignoreKeys=["@path", "@method"], 71 | ), 72 | ] 73 | for key in ["v1.1-post", "v2.0-post"] 74 | } 75 | | {"other": []}, 76 | } 77 | 78 | def main(self): 79 | return { 80 | "docs": self.hooks_generator(), 81 | "compatible": self.hooks_generator( 82 | queryParameterJson=False, 83 | ), 84 | } 85 | -------------------------------------------------------------------------------- /tools/generater.py: -------------------------------------------------------------------------------- 1 | # https://github.com/tsukumijima/KonomiTV/blob/master/server/misc/TwitterAPIQueryGenerator.py 2 | # https://github.com/tsukumijima/KonomiTV/blob/master/License.txt 3 | 4 | #!/usr/bin/env python3 5 | 6 | # Usage: poetry run python -m misc.TwitterAPIQueryGenerator 7 | 8 | import json 9 | import re 10 | import urllib.parse 11 | 12 | from rich import print 13 | from rich.rule import Rule 14 | 15 | 16 | def main(): 17 | print(Rule(characters="=")) 18 | print( 19 | "Chrome DevTools の Network タブで検索窓に「graphql」と入力し「表示されているものをすべてfetch としてコピー」したコードを`input.js`に貼り付けてください。" 20 | ) 21 | print("Enter を押すと続行します。") 22 | print(Rule(characters="=")) 23 | input() 24 | 25 | with open("./tools/input.js", "r") as f: 26 | fetch_code_raw = f.read() 27 | 28 | print(Rule(characters="=")) 29 | 30 | splited = fetch_code_raw.split("\n") 31 | fetch_code_list = [] 32 | code = "" 33 | for line in splited: 34 | if line.startswith("fetch("): 35 | if code: 36 | fetch_code_list.append(code) 37 | code = line 38 | else: 39 | code += line + "\n" 40 | fetch_code_list.append(code) 41 | 42 | for fetch_code in fetch_code_list: 43 | # query_idとendpointを抽出 44 | query_id_match = re.search(r'/i/api/graphql/([^/]+)/([^"?]+)', fetch_code) 45 | if not query_id_match: 46 | print("query_id と endpoint の抽出に失敗しました。") 47 | print(Rule(characters="=")) 48 | return 49 | query_id = query_id_match.group(1) 50 | endpoint = query_id_match.group(2) 51 | 52 | # リクエストメソッドを判定 53 | method_match = re.search(r'"method"\s*:\s*"(GET|POST)"', fetch_code) 54 | if not method_match: 55 | print("リクエストメソッドの判定に失敗しました。") 56 | print(Rule(characters="=")) 57 | return 58 | method = method_match.group(1) 59 | 60 | if method == "POST": 61 | # POST リクエストの場合、fetch() コードの第二引数にある {} で囲まれたオブジェクトを正規表現で抽出したものを JSON としてパース 62 | body_match = re.search(r'"body"\s*:\s*"({.*})"', fetch_code, re.DOTALL) 63 | if not body_match: 64 | print("body の抽出に失敗しました。") 65 | print(Rule(characters="=")) 66 | return 67 | body_json_str = body_match.group(1).replace("\\", "") 68 | body_json = json.loads(body_json_str) 69 | features = body_json.get("features", None) 70 | variables = body_json.get("variables", None) 71 | fieldToggles = body_json.get("fieldToggles", None) 72 | else: 73 | # GET リクエストの場合、まず URL を抽出 74 | url_match = re.search(r'"(https?://[^"]+)"', fetch_code) 75 | if not url_match: 76 | print("URL の抽出に失敗しました。") 77 | print(Rule(characters="=")) 78 | return 79 | url = url_match.group(1) 80 | 81 | # URL をパースして query string を取得 82 | parsed_url = urllib.parse.urlparse(url) 83 | query_string = parsed_url.query 84 | 85 | # query string を dict 形式にパース 86 | query_dict = urllib.parse.parse_qs(query_string) 87 | 88 | # features を取得 89 | features_json_str = query_dict.get("features", [None])[0] 90 | if features_json_str is None: 91 | features = None 92 | else: 93 | try: 94 | features = json.loads(features_json_str) 95 | except json.JSONDecodeError: 96 | features = None 97 | print( 98 | "features の JSON パースに失敗しました。features は None として続行します。" 99 | ) 100 | 101 | variables_json_str = query_dict.get("variables", [None])[0] 102 | if variables_json_str is None: 103 | variables = None 104 | else: 105 | try: 106 | variables = json.loads(variables_json_str) 107 | except json.JSONDecodeError: 108 | variables = None 109 | print( 110 | "variables の JSON パースに失敗しました。variables は None として続行します。" 111 | ) 112 | 113 | fieldToggles_json_str = query_dict.get("fieldToggles", [None])[0] 114 | if fieldToggles_json_str is None: 115 | fieldToggles = None 116 | else: 117 | try: 118 | fieldToggles = json.loads(fieldToggles_json_str) 119 | except json.JSONDecodeError: 120 | fieldToggles = None 121 | print( 122 | "fieldToggles の JSON パースに失敗しました。fieldToggles は None として続行します。" 123 | ) 124 | 125 | with open("./src/config/placeholder.json", "r") as f: 126 | placeholder = json.load(f) 127 | 128 | def check(a, b, msg): 129 | if isinstance(a, dict) and isinstance(b, dict): 130 | for k in {*a.keys(), *b.keys()}: 131 | if k not in b: 132 | print(f"{msg} key: {k} が存在しません。") 133 | elif k not in a: 134 | print(f"{msg} key: {k} が存在しません。") 135 | else: 136 | check(a[k], b[k], msg) 137 | 138 | check( 139 | variables, 140 | placeholder.get(endpoint, {}).get("variables", {}), 141 | f"{endpoint} の variables が不一致です。", 142 | ) 143 | 144 | with open("./src/config/placeholder.json", "w") as f: 145 | placeholder[endpoint] = placeholder.get(endpoint, {}) 146 | placeholder[endpoint]["@path"] = f"/i/api/graphql/{query_id}/{endpoint}" 147 | placeholder[endpoint]["@method"] = method 148 | placeholder[endpoint]["queryId"] = query_id 149 | if features: 150 | placeholder[endpoint]["features"] = features 151 | if fieldToggles: 152 | placeholder[endpoint]["fieldToggles"] = fieldToggles 153 | json.dump(placeholder, f, indent=4) 154 | 155 | 156 | if __name__ == "__main__": 157 | main() 158 | -------------------------------------------------------------------------------- /tools/hooks.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import yaml 4 | 5 | 6 | class HookBase: 7 | PLACEHOLDER: dict 8 | 9 | def __init__(self): 10 | with open("src/config/placeholder.json", mode="r", encoding="utf-8") as f: 11 | self.PLACEHOLDER = json.load(f) 12 | 13 | def placeholder_to_yaml(self, obj, default=False, example=False) -> dict: 14 | def mine(x): 15 | return self.placeholder_to_yaml(x, default=default, example=example) 16 | 17 | def fn(x: str): 18 | return x[:-1] if x.endswith("?") else x 19 | 20 | if isinstance(obj, dict): 21 | ignore_req = [k[:-1] for k in obj.keys() if k.endswith("?")] 22 | req = [k for k in obj.keys() if fn(k) not in ignore_req] 23 | properties = {fn(k): mine(v) for k, v in obj.items()} 24 | value = { 25 | "type": "object", 26 | "properties": properties, 27 | } 28 | value.update({"required": [i for i in req]} if len(req) > 0 else {}) 29 | value.update({"default": properties} if default else {}) 30 | value.update({"example": properties} if example else {}) 31 | return value 32 | elif isinstance(obj, list) and len(obj) > 0: 33 | properties = mine(obj[0]) 34 | value = { 35 | "type": "array", 36 | "items": properties, 37 | } 38 | value.update({"default": [properties]} if default else {}) 39 | value.update({"example": [properties]} if example else {}) 40 | return value 41 | elif isinstance(obj, list) and len(obj) == 0: 42 | value = { 43 | "type": "array", 44 | "items": {"type": "object"}, 45 | } 46 | value.update({"default": []} if default else {}) 47 | value.update({"example": []} if example else {}) 48 | return value 49 | elif isinstance(obj, str): 50 | return {"type": "string", "example": obj, "default": obj} 51 | elif isinstance(obj, bool): 52 | return {"type": "boolean", "example": obj, "default": obj} 53 | elif isinstance(obj, int): 54 | return {"type": "integer", "example": obj, "default": obj} 55 | 56 | def load_component(self, name: str) -> dict: 57 | with open(f"src/config/component/{name}.yaml", mode="r", encoding="utf-8") as f: 58 | return yaml.safe_load(f) 59 | 60 | def load_placeholder(self) -> dict: 61 | with open("src/config/placeholder.json", mode="r", encoding="utf-8") as f: 62 | return yaml.safe_load(f) 63 | 64 | # def load_user_agent(self) -> str: 65 | # user_agent = urllib3.PoolManager().request( 66 | # "GET", 67 | # "https://raw.githubusercontent.com/fa0311/latest-user-agent/main/output.json", 68 | # ) 69 | # return json.loads(user_agent.data)["chrome-fetch"] 70 | 71 | 72 | # HookBase extends 73 | 74 | 75 | class OpenapiHookBase(HookBase): 76 | def hook(self, value: dict) -> dict: 77 | return value 78 | 79 | 80 | class OtherHookBase(HookBase): 81 | def hook(self) -> tuple[str, dict]: 82 | return "", {} 83 | 84 | 85 | class SchemasHookBase(HookBase): 86 | def hook(self, value: dict) -> dict: 87 | return value 88 | 89 | 90 | class RequestHookBase(HookBase): 91 | split: int 92 | path_name: str 93 | 94 | def __init__(self, split=1): 95 | super().__init__() 96 | self.split = split 97 | 98 | def hook(self, path: str, value: dict) -> tuple[str, dict]: 99 | value["parameters"] = value.get("parameters", []) 100 | self.path_name = "/".join(path.split("/")[self.split :]) 101 | return path, value 102 | 103 | 104 | # OpenapiHookBase extends 105 | 106 | 107 | # class AddSecuritySchemesOnSecuritySchemes(OpenapiHookBase): 108 | # def hook(self, value: dict): 109 | # value = super().hook(value) 110 | # component = self.load_component("security_schemes") 111 | # param = component["components"]["securitySchemes"] 112 | # value["components"]["securitySchemes"].update(param) 113 | # value["security"].extend(component["security"]) 114 | # return value 115 | 116 | 117 | # class SetUserAgentOnSecuritySchemes(OpenapiHookBase): 118 | # def hook(self, value: dict): 119 | # value = super().hook(value) 120 | # param = value["components"]["securitySchemes"] 121 | # param["UserAgent"]["description"] = self.load_user_agent() 122 | # return value 123 | 124 | 125 | # SchemasHookBase extends 126 | 127 | 128 | # class RemoveDiscriminator(SchemasHookBase): 129 | # def hook(self, value: dict): 130 | # if value.get("discriminator") is not None: 131 | # del value["discriminator"] 132 | # return value 133 | 134 | 135 | class SchemasCheck(SchemasHookBase): 136 | def hook(self, value: dict): 137 | if value.get("allOf") is not None: 138 | print("allOf is used") 139 | if value.get("type") is None: 140 | print("Type is None") 141 | return value 142 | 143 | 144 | class RequiredCheck(SchemasHookBase): 145 | def hook(self, value: dict): 146 | required = value.get("required", []) 147 | 148 | for key, property in value.get("properties", {}).items(): 149 | if key in required and property.get("default") is not None: 150 | print(f"{key} is required and has default value") 151 | d = property.get("default") is None and property.get("nullable", False) 152 | if property not in required and d: 153 | print(f"{key} is not required and has no default value") 154 | 155 | return value 156 | 157 | 158 | # RequestHookBase extends 159 | 160 | 161 | # class AddSecuritySchemesOnHeader(RequestHookBase): 162 | # def hook(self, path: str, value: dict): 163 | # path, value = super().hook(path, value) 164 | # component = self.load_component("security_schemes") 165 | # param = component["paths"]["/parameters"]["get"]["parameters"] 166 | # value["parameters"].extend(param) 167 | # return path, value 168 | 169 | 170 | # class SetUserAgentOnHeader(RequestHookBase): 171 | # def hook(self, path: str, value: dict): 172 | # path, value = super().hook(path, value) 173 | # component = self.load_component("security_schemes") 174 | # param = component["paths"]["/parameters"]["get"]["parameters"] 175 | # value["parameters"].extend(param) 176 | # return path, value 177 | 178 | 179 | class ReplaceQueryIdPlaceholder(RequestHookBase): 180 | def hook(self, path: str, value: dict): 181 | path, value = super().hook(path, value) 182 | new = self.PLACEHOLDER[self.path_name]["queryId"] 183 | return path.replace(r"{pathQueryId}", new), value 184 | 185 | 186 | class SetResponsesHeader(RequestHookBase): 187 | suffix: str 188 | 189 | def __init__(self): 190 | super().__init__() 191 | 192 | def hook(self, path: str, value: dict): 193 | path, value = super().hook(path, value) 194 | component = self.load_component("response_header") 195 | value["responses"]["200"]["headers"] = component["components"]["headers"] 196 | return path, value 197 | 198 | 199 | class AddPathQueryIdOnParameters(RequestHookBase): 200 | def __init__(self, split: str = 1): 201 | super().__init__(split=split) 202 | 203 | def hook(self, path: str, value: dict): 204 | path, value = super().hook(path, value) 205 | data = self.PLACEHOLDER[self.path_name] 206 | value["parameters"].append( 207 | { 208 | "in": "path", 209 | "name": "pathQueryId", 210 | "required": True, 211 | "schema": { 212 | "type": "string", 213 | "default": data["queryId"], 214 | "example": data["queryId"], 215 | }, 216 | } 217 | ) 218 | return path, value 219 | 220 | 221 | # class RemoveErrorHandle(RequestHookBase): 222 | # def hook(self, path: str, value: dict): 223 | # content = value["responses"]["200"]["content"]["application/json"] 224 | # content["schema"] = content["schema"]["oneOf"][0] 225 | # return path, value 226 | 227 | 228 | # OnParameters 229 | 230 | 231 | class AddParametersOnParameters(RequestHookBase): 232 | schemaType: str | None 233 | ignoreKeys: list[str] 234 | 235 | def __init__( 236 | self, 237 | split: str = 1, 238 | schemaType: str | None = None, 239 | ignoreKeys: list[str] | None = None, 240 | ): 241 | super().__init__(split=split) 242 | self.schemaType = schemaType 243 | self.ignoreKeys = ignoreKeys or [] 244 | 245 | def hook(self, path: str, value: dict): 246 | path, value = super().hook(path, value) 247 | data = self.PLACEHOLDER[self.path_name] 248 | data = {key: data[key] for key in data.keys() if key not in self.ignoreKeys} 249 | 250 | for key in data.keys(): 251 | if self.schemaType == "string": 252 | example = ( 253 | data[key] if isinstance(data[key], str) else json.dumps(data[key]) 254 | ) 255 | schema = { 256 | "type": "string", 257 | "default": example, 258 | "example": example, 259 | } 260 | elif self.schemaType == "object": 261 | example = ( 262 | data[key] if isinstance(data[key], str) else json.dumps(data[key]) 263 | ) 264 | schema = { 265 | "type": "object", 266 | "default": example, 267 | "example": example, 268 | } 269 | else: 270 | schema = self.placeholder_to_yaml(data[key]) 271 | value["parameters"].append( 272 | { 273 | "name": key, 274 | "in": "query", 275 | "required": True, 276 | "schema": schema, 277 | } 278 | ) 279 | return path, value 280 | 281 | 282 | # OnBody 283 | 284 | 285 | class AddParametersOnBody(RequestHookBase): 286 | schemaType: str | None 287 | contentType: str | None 288 | ignoreKeys: list[str] 289 | 290 | def __init__( 291 | self, 292 | split: str = 1, 293 | contentType: str = "application/json", 294 | schemaType: str | None = None, 295 | ignoreKeys: list[str] | None = None, 296 | ): 297 | super().__init__(split) 298 | self.schemaType = schemaType 299 | self.contentType = contentType 300 | self.ignoreKeys = ignoreKeys or [] 301 | 302 | def hook(self, path: str, value: dict): 303 | path, value = super().hook(path, value) 304 | data: dict[str, dict] = self.PLACEHOLDER[self.path_name] 305 | data = {k: v for k, v in data.items() if k not in self.ignoreKeys} 306 | 307 | if self.schemaType == "string": 308 | example = data if isinstance(data, str) else json.dumps(data) 309 | schema = { 310 | "type": "string", 311 | "default": example, 312 | "example": example, 313 | } 314 | elif self.schemaType == "object": 315 | example = data if isinstance(data, str) else json.dumps(data) 316 | schema = { 317 | "type": "object", 318 | "default": example, 319 | "example": example, 320 | } 321 | else: 322 | schema = { 323 | "properties": {k: self.placeholder_to_yaml(v) for k, v in data.items()}, 324 | "required": [k for k in data.keys()], 325 | } 326 | value["requestBody"] = { 327 | "description": "body", 328 | "required": True, 329 | "content": { 330 | self.contentType: { 331 | "schema": schema, 332 | } 333 | }, 334 | } 335 | return path, value 336 | 337 | 338 | # OnContent 339 | 340 | 341 | class AddParametersOnContent(RequestHookBase): 342 | contentType: str 343 | ignoreKeys: list[str] 344 | 345 | def __init__( 346 | self, 347 | split: str = 1, 348 | contentType: str = "application/json", 349 | ignoreKeys: list[str] | None = None, 350 | ): 351 | super().__init__(split=split) 352 | self.contentType = contentType 353 | self.ignoreKeys = ignoreKeys or [] 354 | 355 | def hook(self, path: str, value: dict): 356 | path, value = super().hook(path, value) 357 | data = self.PLACEHOLDER[self.path_name] 358 | data = {key: data[key] for key in data.keys() if key not in self.ignoreKeys} 359 | 360 | for key in data.keys(): 361 | value["parameters"].append( 362 | { 363 | "name": key, 364 | "in": "query", 365 | "required": True, 366 | "content": { 367 | self.contentType: { 368 | "schema": self.placeholder_to_yaml(data[key]), 369 | } 370 | }, 371 | } 372 | ) 373 | return path, value 374 | -------------------------------------------------------------------------------- /tools/login.py: -------------------------------------------------------------------------------- 1 | import json 2 | import base64 3 | from tweepy_authlib import CookieSessionUserHandler 4 | 5 | auth_handler = CookieSessionUserHandler( 6 | screen_name=input("screen_name: "), 7 | password=input("password: "), 8 | ) 9 | 10 | 11 | cookies = auth_handler.get_cookies() 12 | 13 | data = json.dumps(cookies.get_dict()) 14 | print(base64.b64encode(data.encode("utf-8")).decode("utf-8")) 15 | 16 | with open("cookie.json", "w") as f: 17 | f.write(data) 18 | --------------------------------------------------------------------------------