├── .dockerignore ├── .editorconfig ├── .github └── workflows │ └── integration-test.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── fp-frontend-versions.txt ├── openapitools.json ├── package.json ├── pnpm-lock.yaml ├── src ├── async-schemas.json ├── floatplane-asyncapi-chat-specification.json ├── floatplane-asyncapi-frontend-specification.json └── floatplane-openapi-specification.json ├── static ├── SwaggerUI │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── index.html │ ├── oauth2-redirect.html │ ├── swagger-ui-bundle.js │ ├── swagger-ui-es-bundle-core.js │ ├── swagger-ui-es-bundle.js │ ├── swagger-ui-standalone-preset.js │ ├── swagger-ui.css │ └── swagger-ui.js ├── index.html ├── rapidoc-full.html └── rapidoc.html ├── tests ├── SchemaThesisTests │ ├── .gitignore │ ├── README.md │ ├── poetry.lock │ ├── pyproject.toml │ ├── schemathesistests │ │ └── __init__.py │ └── tests │ │ ├── __init__.py │ │ ├── test_AuthV3API.py │ │ └── test_Flow.py └── openapitools.json └── tools ├── find-nullables.js ├── fp-frontend-diff-2.sh ├── fp-frontend-diff.sh ├── fp-frontend-fetch-2.sh ├── fp-frontend-fetch.sh └── trim.js /.dockerignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /Docs 3 | openapitools.json 4 | /tools/Frontend 5 | /src/floatplane-openapi-specification-trimmed.json 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | indent_style = tab 7 | indent_size = 4 8 | 9 | [*.{diff,md}] 10 | trim_trailing_whitespace = false 11 | 12 | [package.json] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [.github/workflows/*.yml] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.github/workflows/integration-test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Floatplane API Integration Test 5 | 6 | on: 7 | workflow_dispatch: # Manual trigger 8 | schedule: 9 | - cron: '0 7 * * *' # Daily at midnight PST (7AM UTC) 10 | 11 | jobs: 12 | integration_test: 13 | 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | python-version: ["3.11"] 19 | poetry-version: ["1.4.0"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Setup Poetry 28 | uses: abatilo/actions-poetry@v2 29 | with: 30 | poetry-version: ${{ matrix.poetry-version }} 31 | - name: Install NPM Dependencies 32 | run: npm install 33 | - name: Install Poetry Dependencies 34 | working-directory: tests/SchemaThesisTests 35 | run: poetry install 36 | - name: Run Integration Tests 37 | env: 38 | SAILS_SID: ${{ secrets.INTEGRATION_TEST_SAILS_SID }} 39 | run: make test 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /Docs 3 | /tools/Frontend 4 | /src/floatplane-openapi-specification-trimmed.json 5 | **/.DS_Store 6 | 7 | .hypothesis/ 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "json.schemas": [ 3 | { 4 | "fileMatch": [ 5 | "/src/floatplane-openapi-specification.json" 6 | ], 7 | "url": "https://spec.openapis.org/oas/3.0/schema/2021-09-28" 8 | }, 9 | { 10 | "fileMatch": [ 11 | "/src/floatplane-asyncapi-chat-specification.json", 12 | "/src/floatplane-asyncapi-frontend-specification.json" 13 | ], 14 | "url": "https://raw.githubusercontent.com/asyncapi/spec-json-schemas/master/schemas/2.6.0.json" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [] 4 | } 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 as build 2 | RUN apt-get update && apt-get -y install openjdk-11-jre-headless chromium 3 | 4 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true 5 | ENV PUPPETEER_EXECUTABLE_PATH /usr/bin/chromium 6 | 7 | COPY ./package.json ./package-lock.json* / 8 | WORKDIR / 9 | RUN npm ci && npm cache clean --force 10 | 11 | COPY ./ / 12 | RUN make docs-all 13 | 14 | #Copy static files to Nginx 15 | FROM nginx:alpine 16 | COPY --from=build /Docs /usr/share/nginx/html 17 | 18 | WORKDIR /usr/share/nginx/html 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 James Linnell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # this tells Make to run 'make help' if the user runs 'make' 2 | # without this, Make would use the first target as the default 3 | .DEFAULT_GOAL := help 4 | 5 | # here we have a simple way of outputting documentation 6 | # the @-sign tells Make to not output the command before running it 7 | help: 8 | @echo 'Available commands:' 9 | @echo "clean validate test docs-all" 10 | @echo "docs-trimmed docs-full docs-async" 11 | 12 | # Section: Helpers and structural 13 | 14 | clean: 15 | rm -rf ./Docs 16 | rm -f src/floatplane-openapi-specification-trimmed.json 17 | trim: 18 | node tools/trim.js src/floatplane-openapi-specification.json src/floatplane-openapi-specification-trimmed.json 19 | validate: 20 | npx openapi-generator-cli validate -i src/floatplane-openapi-specification.json 21 | validate-trimmed: trim 22 | npx openapi-generator-cli validate -i src/floatplane-openapi-specification-trimmed.json 23 | docs-skeleton: 24 | mkdir -p Docs 25 | cp ./static/index.html ./Docs/index.html 26 | yaml: validate validate-trimmed docs-skeleton 27 | npx json2yaml src/floatplane-openapi-specification.json > Docs/floatplane-openapi-specification.yaml 28 | npx json2yaml src/floatplane-openapi-specification-trimmed.json > Docs/floatplane-openapi-specification-trimmed.yaml 29 | npx openapi-generator-cli validate -i Docs/floatplane-openapi-specification.yaml 30 | npx openapi-generator-cli validate -i Docs/floatplane-openapi-specification-trimmed.yaml 31 | test: validate trim 32 | cd tests/SchemaThesisTests; poetry run pytest -v -s 33 | 34 | # Section: Trimmed docs 35 | 36 | docs-oag-html2: docs-skeleton validate-trimmed 37 | npx openapi-generator-cli generate -i src/floatplane-openapi-specification-trimmed.json -o Docs/OAG-html2/ -g html2 38 | docs-oag-dynamic-html: docs-skeleton validate-trimmed 39 | npx openapi-generator-cli generate -i src/floatplane-openapi-specification-trimmed.json -o Docs/OAG-dynamic-html/ -g dynamic-html 40 | docs-redoc: docs-skeleton validate-trimmed 41 | mkdir -p Docs/Redoc 42 | npx redocly build-docs -o Docs/Redoc/redoc-static.html src/floatplane-openapi-specification-trimmed.json 43 | docs-rapidoc: docs-skeleton validate-trimmed 44 | mkdir -p Docs/Rapidoc 45 | cp ./static/rapidoc.html ./Docs/Rapidoc/rapidoc.html 46 | docs-reslate: docs-skeleton validate-trimmed 47 | mkdir -p Docs/ReSlateDocs 48 | cd Docs/ReSlateDocs; npx reslate init Docs/ReSlateDocs 49 | npx widdershins src/floatplane-openapi-specification-trimmed.json -o Docs/ReSlateDocs/index.md 50 | cd Docs/ReSlateDocs; npx reslate build 51 | rm -rf Docs/ReSlateDocs/site 52 | mv Docs/ReSlateDocs/_site Docs/ReSlateDocs/site 53 | docs-postman: docs-skeleton validate-trimmed 54 | mkdir -p Docs/Postman-v2.1 55 | npx openapi2postmanv2 --spec src/floatplane-openapi-specification-trimmed.json --output Docs/Postman-v2.1/floatplaneapi-postman2.1-collection.json --pretty -O folderStrategy=Tags,includeAuthInfoInExample=false 56 | docs-swaggerui: docs-skeleton validate-trimmed 57 | mkdir -p Docs/SwaggerUI 58 | cp static/SwaggerUI/* Docs/SwaggerUI 59 | sed -i.bak -e 's/floatplane-openapi-specification.json/floatplane-openapi-specification-trimmed.json/g' Docs/SwaggerUI/index.html 60 | docs-trimmed: docs-oag-html2 docs-oag-dynamic-html docs-redoc docs-rapidoc docs-reslate docs-postman docs-swaggerui 61 | @echo "docs-trimmed complete!" 62 | 63 | # Section: Full docs 64 | 65 | docs-oag-html2-full: docs-skeleton 66 | npx openapi-generator-cli generate -i src/floatplane-openapi-specification.json -o Docs/OAG-html2-full/ -g html2 67 | docs-oag-dynamic-html-full: 68 | npx openapi-generator-cli generate -i src/floatplane-openapi-specification.json -o Docs/OAG-dynamic-html-full/ -g dynamic-html 69 | docs-redoc-full: docs-skeleton 70 | mkdir -p Docs/Redoc-full 71 | npx redocly build-docs -o Docs/Redoc-full/redoc-static.html src/floatplane-openapi-specification.json 72 | docs-rapidoc-full: docs-skeleton 73 | mkdir -p Docs/Rapidoc-full 74 | cp ./static/rapidoc-full.html ./Docs/Rapidoc-full/rapidoc-full.html 75 | docs-reslate-full: docs-skeleton 76 | mkdir -p Docs/ReSlateDocs-full 77 | cd Docs/ReSlateDocs-full; npx reslate init Docs/ReSlateDocs-full 78 | npx widdershins src/floatplane-openapi-specification.json -o Docs/ReSlateDocs-full/index.md 79 | cd Docs/ReSlateDocs-full; npx reslate build 80 | rm -rf Docs/ReSlateDocs-full/site 81 | mv Docs/ReSlateDocs-full/_site Docs/ReSlateDocs-full/site 82 | docs-postman-full: docs-skeleton 83 | mkdir -p Docs/Postman-v2.1-full 84 | npx openapi2postmanv2 --spec src/floatplane-openapi-specification.json --output Docs/Postman-v2.1-full/floatplaneapi-postman2.1-collection.json --pretty -O folderStrategy=Tags,includeAuthInfoInExample=false 85 | docs-swaggerui-full: docs-skeleton 86 | mkdir -p Docs/SwaggerUI-full 87 | cp static/SwaggerUI/* Docs/SwaggerUI-full 88 | docs-full: validate docs-oag-html2-full docs-oag-dynamic-html-full docs-redoc-full docs-rapidoc-full docs-reslate-full docs-postman-full docs-swaggerui-full 89 | @echo "docs-full complete!" 90 | 91 | # Section: Async docs 92 | 93 | docs-async-frontend: docs-skeleton 94 | npx ag -o Docs/AsyncAPIFrontend src/floatplane-asyncapi-frontend-specification.json @asyncapi/html-template 95 | docs-async-chat: 96 | npx ag -o Docs/AsyncAPIChat src/floatplane-asyncapi-chat-specification.json @asyncapi/html-template 97 | docs-async: docs-async-frontend docs-async-chat 98 | @echo "docs-async complete!" 99 | 100 | # Section: Final docs 101 | 102 | docs-schemas: trim docs-skeleton 103 | cp ./src/floatplane-openapi-specification.json ./src/floatplane-openapi-specification-trimmed.json Docs/ 104 | cp ./src/floatplane-asyncapi-frontend-specification.json Docs/ 105 | cp ./src/floatplane-asyncapi-chat-specification.json Docs/ 106 | cp ./src/async-schemas.json Docs/ 107 | docs-all: clean validate docs-skeleton docs-trimmed docs-full docs-async docs-schemas yaml 108 | @echo "docs-all complete!" 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Floatplane API Specification 2 | 3 | Visit the pre-generated documentation, trimmed OpenAPI spec, and Postman collection at https://jamamp.github.io/FloatplaneAPIDocs/. 4 | 5 | Visit Floatplane at https://www.floatplane.com. 6 | 7 | --- 8 | 9 | This repository is an API specification of the video streaming service [Floatplane](https://www.floatplane.com) using in the [OpenAPI 3.0.3](https://swagger.io/specification/) specification for REST and [AsyncAPI 2.4.0](https://www.asyncapi.com/) specification for asynchronous events. The main files for this repository are `floatplane-openapi-specification.json`, `floatplane-asyncapi-chat-specification.json`, and `floatplane-asyncapi-frontend-specification.json`. Contained in them are definitions for all of the paths and channels in the Floatplane API, definitions of common models between the API endpoints, descriptions of authentication and authorization mechanisms, and more. 10 | 11 | This repository serves as an open and central source specification for the Floatplane API by the community, for purposes of tinkering and creating custom clients for Floatplane. At the time of writing, Floatplane has its main website, along with both Android and iOS applications. The main use case envisioned in the creation of this repository is to make it easier to create TV-first applications for, e.g., tvOS, Roku, Google TV, etc. 12 | 13 | ⚠️ NOTE ⚠️: **Please** set a custom `User-Agent` header in your library of choice that uniquely identifies *what* app the requests are coming from, and *what **version** of your app is sending the requests. This is very helpful to the Floatplane team, and is good practice to do in general. 14 | 15 | # Automatic Generation 16 | 17 | The main purpose of this repository is to enable automatic generation of documentation and client code libraries. 18 | 19 | ## OpenAPI/AsyncAPI & Code Generation 20 | 21 | The Floatplane API specification can be used to automatically generate client code of the Floatplane API in most major programming languages. It is best advised to use the [trimmed version](https://jamamp.github.io/FloatplaneAPIDocs/floatplane-openapi-specification-trimmed.json) of the REST API to only generate the endpoints that have been thoroughly documented. Various generators exist for different use cases. 22 | 23 | A notable open-source generator is [OpenAPI Generator](https://openapi-generator.tech/docs/generators) which supports 38 different languages, along with variations for different networking libraries in some languages. It additionally includes many configurations when generating clients. For AsyncAPI, the [AsyncAPI Generator](https://github.com/asyncapi/generator) is recommended. 24 | 25 | It would be best to keep your version of the specification, and a script to auto-generate the library with all of the correct configurations, in source control. Then, run the script to generate the code library or files, and keep those in source control as well in your project, or execute the script in your build scripts. 26 | 27 | ### Example 28 | 29 | ```sh 30 | openapi-generator openapi-generator generate -i floatplane-openapi-specification-trimmed.json -o Swift -g swift5 --library vapor 31 | ``` 32 | 33 | ```sh 34 | ag -o FloatplaneChatAPI floatplane-asyncapi-chat-specification.json @asyncapi/nodejs-template 35 | ``` 36 | 37 | ## OpenAPI/AsyncAPI & Documentation Generation 38 | 39 | The API specifications can also be used to generate documentation. Pre-generated renders of the documentation for this repository are available at https://jamamp.github.io/FloatplaneAPIDocs. There are a variety of renders available, including: 40 | - Swagger UI - https://github.com/swagger-api/swagger-ui 41 | - Redoc - https://redoc.ly/redoc 42 | - ReSlate & Widdershins - https://github.com/Mermade/reslate - https://github.com/Mermade/widdershins 43 | - RapiDoc - https://mrin9.github.io/RapiDoc/ 44 | - OpenAPI Generator - https://openapi-generator.tech/ 45 | - OpenAPI to Postman v2.1 Converter - https://github.com/postmanlabs/openapi-to-postman 46 | - AsyncAPI Generator - https://github.com/asyncapi/generator 47 | 48 | ### Example 49 | 50 | ```sh 51 | redoc-cli bundle -o Docs/Redoc/redoc-static.html floatplane-openapi-specification.json 52 | ``` 53 | 54 | ```sh 55 | ag -o Docs/AsyncAPIChat floatplane-asyncapi-chat-specification.json @asyncapi/html-template 56 | ``` 57 | 58 | --- 59 | 60 | ## Working on This Repository 61 | 62 | ### Documentation Generation 63 | 64 | In order to generate all of the documentation available at https://jamamp.github.io/FloatplaneAPIDocs automatically when testing changes to the OpenAPI file, 65 | 1. Clone this repository 66 | 2. Run `pnpm install` 67 | 1. This will install all of the necessary tooling 68 | 2. The OpenAPI Generator tooling requires Java to be installed, but will fail silently if it is not. 69 | 3. Note that for AsyncAPI, it depends on installing `puppeteer`. If working on an M1 Apple device, you may run into issues with this dependency. Following [this article](https://linguinecode.com/post/how-to-fix-m1-mac-puppeteer-chromium-arm64-bug) may help. 70 | 3. Make changes as necessary to `floatplane-openapi-specification.json` or the AsyncAPI specification files 71 | 4. Run `make docs-all`. This will: 72 | 1. Trim the spec into `floatplane-openapi-specification-trimmed.json` 73 | 2. Generate documentation for the trimmed spec into the `/Docs` folder 74 | 3. Generate documentation for the full spec into the `/Docs` folder 75 | 4. Copy over the spec and some other files from `/static` into the `/Docs` folder 76 | 77 | Then, open `/Docs/index.html` to view the changes. A Dockerfile is also available: 78 | 79 | ```sh 80 | docker build --tag fpapidocs:latest . 81 | ``` 82 | 83 | ### Analyze difference between Floatplane frontends 84 | 85 | The list of APIs was generated from the Floatplane frontend files, available at https://frontend.floatplane.com/{version}/*.js 86 | 87 | When a new version of the Floatplane frontend is released (which is done silently), we can analyze the differences between the files to find new endpoints being used. To do so more easily, some tools are included: 88 | 89 | 1. Clone this repository 90 | 2. Change directory into the `/tools` folder: `cd tools` 91 | 3. Fetch the frontend files for the **previous** version: `./fp-frontend-fetch-2.sh ` 92 | 1. E.g. `./fp-frontend-fetch-2.sh 4.0.12` 93 | 2. This assumes that `wget` is installed on the system 94 | 4. Fetch the frontend files for the **current** version: `./fp-frontend-fetch-2.sh ` 95 | 1. E.g. `./fp-frontend-fetch.sh 4.0.13` 96 | 5. Un-minify the files: `prettier --write Frontend/4.0.12 && prettier --write Frontend/4.0.13` 97 | 1. This assumes that [Prettier](https://prettier.io/) is installed on the system 98 | 6. Perform a quick diff to clean up the files and see which files differ: `./fp-frontend-diff-2.sh ` 99 | 1. E.g. `./fp-frontend-diff.sh 4.0.12 4.0.13` 100 | 2. The cleanup replaces references of the version numbers in, specifically, `main.js` with a common piece of text in order to avoid many false-positives in the resulting diffs. 101 | 7. Then, manually inspect the diff of the files listed to see what has changed. 102 | 103 | The file `fp-frontend-version.txt` is a collection of recent version changes that Floatplane has made, starting with `3.5.1`. This may be updated irregularly. 104 | 105 | ### Integration Testing 106 | 107 | After making changes to the `floatplane-openapi-specification.json`, run `make test` in order to run integration tests with the Floatplane API. This ensures that the specification and its models are aligned with the API correctly. 108 | 109 | Integration test run requirements: 110 | - The environment variable "sails.sid" needs to be set to the value of the `sails.sid` HTTP Cookie for authentication in order for integration tests to run. 111 | - Python 3 must be installed. 112 | 113 | Integration tests will test for: 114 | - Expected HTTP 200 responses for valid requests 115 | - Expected HTTP 400/401/403/404 responses for invalid requests 116 | - A strict match between the response data from FP and the response schema/model in the specification: 117 | - Case-sensitive property matching 118 | - Case-sensitive enumeration matching 119 | - No missing properties in the schema (if new properties begin appearing in FP response objects we want to begin tracking them) 120 | - No extraneous properties in schema (if a property isn't in the FP response then it shouldn't be in the schema) 121 | - Exact type matching (a string with a number in it should not be equivalent to a number) 122 | 123 | Write integration/unit tests in the `tests/SchemaThesisTests` Python/Poetry project. 124 | 125 | --- 126 | 127 | # Contributions 128 | 129 | Anyone is free to help 130 | - Document undocumented API endpoints 131 | - Add more information to already-documented endpoints 132 | - Fix issues with documentation 133 | - Restructure models 134 | - Add more pre-generated docs 135 | - Etc. 136 | 137 | # License 138 | 139 | MIT License. See the LICENSE file. 140 | -------------------------------------------------------------------------------- /fp-frontend-versions.txt: -------------------------------------------------------------------------------- 1 | 3.5.1 2 | 3.5.1-a 3 | 3.5.2 4 | 3.6.2 5 | 3.7.0 6 | 3.7.1 7 | 3.7.2 8 | 3.8.0 9 | 3.8.1 10 | 3.8.2 11 | 3.8.3 12 | 3.8.4 13 | 3.8.5 14 | 3.8.6 15 | 3.8.7 16 | 3.8.8 17 | 3.8.9 18 | 3.9.0 19 | 3.9.1 20 | 3.9.4 21 | 3.9.5 22 | 3.9.8 23 | 3.9.9 24 | 3.9.10 25 | 3.10.0-a 26 | 3.10.0-c 27 | 3.10.0-d 28 | 3.10.1-q 29 | 4.0.12 30 | 4.0.13 31 | -------------------------------------------------------------------------------- /openapitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json", 3 | "spaces": 4, 4 | "generator-cli": { 5 | "version": "6.5.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "floatplaneapi", 3 | "version": "4.0.13", 4 | "description": "Floatplane API Specification", 5 | "main": "floatplane-openapi-specification.json", 6 | "devDependencies": { 7 | "@asyncapi/generator": "^1.17.25", 8 | "@asyncapi/html-template": "^0.28.4", 9 | "@openapitools/openapi-generator-cli": "^2.20.2", 10 | "@redocly/cli": "^1.34.3", 11 | "json2yaml": "^1.1.0", 12 | "openapi-to-postmanv2": "^2.14.1", 13 | "reslate": "3.0.0-5", 14 | "widdershins": "4.0.1" 15 | }, 16 | "scripts": { 17 | "preinstall": "npx only-allow pnpm" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/Jman012/FloatplaneAPI.git" 22 | }, 23 | "author": "", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/Jman012/FloatplaneAPI/issues" 27 | }, 28 | "homepage": "https://github.com/Jman012/FloatplaneAPI#readme" 29 | } 30 | -------------------------------------------------------------------------------- /src/async-schemas.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "title": "FloatplaneAPIAsyncModels", 4 | "description": "This document serves as a collection of model schemas used in the associated AsyncAPI documents. The root level object is only here to reference everything within this document so that code generators (such as Quicktype) actually generate everything.", 5 | "type": "object", 6 | "properties": { 7 | "SailsHeaders": { 8 | "$ref": "#/definitions/SailsHeaders" 9 | }, 10 | "SailsStatusCode": { 11 | "$ref": "#/definitions/SailsStatusCode" 12 | }, 13 | "JoinLivestreamRadioFrequency": { 14 | "$ref": "#/definitions/JoinLivestreamRadioFrequency" 15 | }, 16 | "JoinedLivestreamRadioFrequency": { 17 | "$ref": "#/definitions/JoinedLivestreamRadioFrequency" 18 | }, 19 | "LeaveLivestreamRadioFrequency": { 20 | "$ref": "#/definitions/LeaveLivestreamRadioFrequency" 21 | }, 22 | "LeftLivestreamRadioFrequency": { 23 | "$ref": "#/definitions/LeftLivestreamRadioFrequency" 24 | }, 25 | "GetChatUserList": { 26 | "$ref": "#/definitions/GetChatUserList" 27 | }, 28 | "ChatUserList": { 29 | "$ref": "#/definitions/ChatUserList" 30 | }, 31 | "SendLivestreamRadioChatter": { 32 | "$ref": "#/definitions/SendLivestreamRadioChatter" 33 | }, 34 | "SentLivestreamRadioChatter": { 35 | "$ref": "#/definitions/SentLivestreamRadioChatter" 36 | }, 37 | "RadioChatter": { 38 | "$ref": "#/definitions/RadioChatter" 39 | }, 40 | "EmoteList": { 41 | "$ref": "#/definitions/EmoteList" 42 | }, 43 | "SailsConnect": { 44 | "$ref": "#/definitions/SailsConnect" 45 | }, 46 | "SailsConnected": { 47 | "$ref": "#/definitions/SailsConnected" 48 | }, 49 | "SailsDisonnect": { 50 | "$ref": "#/definitions/SailsDisconnect" 51 | }, 52 | "SailsDisonnected": { 53 | "$ref": "#/definitions/SailsDisconnected" 54 | }, 55 | "JoinLiveRoom": { 56 | "$ref": "#/definitions/JoinLiveRoom" 57 | }, 58 | "JoinedLiveRoom": { 59 | "$ref": "#/definitions/JoinedLiveRoom" 60 | }, 61 | "LeaveLiveRoom": { 62 | "$ref": "#/definitions/LeaveLiveRoom" 63 | }, 64 | "LeftLiveRoom": { 65 | "$ref": "#/definitions/LeftLiveRoom" 66 | }, 67 | "CreatorNotification": { 68 | "$ref": "#/definitions/CreatorNotification" 69 | }, 70 | "PostRelease": { 71 | "$ref": "#/definitions/PostRelease" 72 | }, 73 | "NotificationData": { 74 | "$ref": "#/definitions/NotificationData" 75 | }, 76 | "CreatorMenuUpdate": { 77 | "$ref": "#/definitions/CreatorMenuUpdate" 78 | }, 79 | "PollOpenClose": { 80 | "$ref": "#/definitions/PollOpenClose" 81 | }, 82 | "PollUpdateTally": { 83 | "$ref": "#/definitions/PollUpdateTally" 84 | }, 85 | "ImageModel": { 86 | "$ref": "#/definitions/ImageModel" 87 | }, 88 | "ChildImageModel": { 89 | "$ref": "#/definitions/ChildImageModel" 90 | } 91 | }, 92 | "definitions": { 93 | "SailsHeaders": { 94 | "type": "object", 95 | "description": "Headers by Sails are not being used by Floatplane. Not much is known of this structure at this time.", 96 | "additionalProperties": { 97 | "type": "string" 98 | } 99 | }, 100 | "SailsStatusCode": { 101 | "type": "integer", 102 | "description": "These are the same as HTTP response status codes.", 103 | "minimum": 200, 104 | "maximum": 599 105 | }, 106 | "JoinLivestreamRadioFrequency": { 107 | "type": "object", 108 | "description": "Join a livestream chat channel in order to receive chat messages (via the `radioChatter` event) from others in the room.", 109 | "properties": { 110 | "method": { 111 | "const": "get", 112 | "description": "This endpoint expects a GET." 113 | }, 114 | "headers": { 115 | "$ref": "#/definitions/SailsHeaders" 116 | }, 117 | "data": { 118 | "type": "object", 119 | "properties": { 120 | "channel": { 121 | "type": "string", 122 | "description": "Which livestream channel to join. Of the format `/live/{livestreamId}`. The `livestreamId` comes from the `liveStream` object on the creator's info in the REST API." 123 | }, 124 | "message": { 125 | "type": "null", 126 | "description": "When joining, this is usually `null`." 127 | } 128 | }, 129 | "additionalProperties": false, 130 | "required": [ 131 | "channel" 132 | ] 133 | }, 134 | "url": { 135 | "const": "/RadioMessage/joinLivestreamRadioFrequency", 136 | "description": "The required endpoint for this event." 137 | } 138 | }, 139 | "additionalProperties": false, 140 | "required": [ 141 | "method", 142 | "headers", 143 | "data", 144 | "url" 145 | ] 146 | }, 147 | "JoinedLivestreamRadioFrequency": { 148 | "type": "object", 149 | "description": "Indicates that the channel has been joined successfully, as well as sending the current emotes configured for the livestream.", 150 | "properties": { 151 | "body": { 152 | "type": "object", 153 | "properties": { 154 | "success": { 155 | "type": "boolean" 156 | }, 157 | "emotes": { 158 | "$ref": "#/definitions/EmoteList" 159 | } 160 | }, 161 | "required": [ 162 | "success" 163 | ] 164 | }, 165 | "headers": { 166 | "$ref": "#/definitions/SailsHeaders" 167 | }, 168 | "statusCode": { 169 | "$ref": "#/definitions/SailsStatusCode" 170 | } 171 | }, 172 | "additionalProperties": false, 173 | "required": [ 174 | "body", 175 | "headers", 176 | "statusCode" 177 | ] 178 | }, 179 | "LeaveLivestreamRadioFrequency": { 180 | "type": "object", 181 | "description": "Tells the server that this socket should no longer receive `radioChatter` events from the previously-joined channel.", 182 | "properties": { 183 | "method": { 184 | "const": "post", 185 | "description": "This endpoint expects a POST." 186 | }, 187 | "headers": { 188 | "$ref": "#/definitions/SailsHeaders" 189 | }, 190 | "data": { 191 | "type": "object", 192 | "properties": { 193 | "channel": { 194 | "type": "string", 195 | "description": "Which livestream channel to leave. Of the format `/live/{livestreamId}`." 196 | }, 197 | "message": { 198 | "type": "string", 199 | "description": "This message does not appear to be relayed to others in the chat." 200 | } 201 | }, 202 | "additionalProperties": false, 203 | "required": [ 204 | "channel", 205 | "message" 206 | ] 207 | }, 208 | "url": { 209 | "const": "/RadioMessage/leaveLivestreamRadioFrequency", 210 | "description": "The required endpoint for this event." 211 | } 212 | }, 213 | "additionalProperties": false, 214 | "required": [ 215 | "method", 216 | "headers", 217 | "data", 218 | "url" 219 | ] 220 | }, 221 | "LeftLivestreamRadioFrequency": { 222 | "type": "object", 223 | "properties": { 224 | "body": { 225 | "type": "object", 226 | "additionalProperties": { 227 | "type": "string" 228 | } 229 | }, 230 | "headers": { 231 | "$ref": "#/definitions/SailsHeaders" 232 | }, 233 | "statusCode": { 234 | "$ref": "#/definitions/SailsStatusCode" 235 | } 236 | }, 237 | "additionalProperties": false, 238 | "required": [ 239 | "body", 240 | "headers", 241 | "statusCode" 242 | ] 243 | }, 244 | "GetChatUserList": { 245 | "type": "object", 246 | "description": "Returns a list of users currently in the channel/livestream/chat room, in order to display a full list in the UI.", 247 | "properties": { 248 | "method": { 249 | "const": "get", 250 | "description": "This endpoint expects a GET." 251 | }, 252 | "headers": { 253 | "$ref": "#/definitions/SailsHeaders" 254 | }, 255 | "data": { 256 | "type": "object", 257 | "properties": { 258 | "channel": { 259 | "type": "string", 260 | "description": "Which livestream channel to query. Of the format `/live/{livestreamId}`." 261 | } 262 | }, 263 | "additionalProperties": false, 264 | "required": [ 265 | "channel" 266 | ] 267 | }, 268 | "url": { 269 | "const": "/RadioMessage/getChatUserList/", 270 | "description": "The required endpoint for this event." 271 | } 272 | }, 273 | "additionalProperties": false, 274 | "required": [ 275 | "method", 276 | "headers", 277 | "data", 278 | "url" 279 | ] 280 | }, 281 | "ChatUserList": { 282 | "type": "object", 283 | "properties": { 284 | "body": { 285 | "type": "object", 286 | "properties": { 287 | "success": { 288 | "type": "boolean" 289 | }, 290 | "pilots": { 291 | "type": "array", 292 | "items": { 293 | "type": "string" 294 | } 295 | }, 296 | "passengers": { 297 | "type": "array", 298 | "items": { 299 | "type": "string" 300 | } 301 | } 302 | }, 303 | "additionalProperties": false, 304 | "required": [ 305 | "success", 306 | "pilots", 307 | "passengers" 308 | ] 309 | }, 310 | "headers": { 311 | "$ref": "#/definitions/SailsHeaders" 312 | }, 313 | "statusCode": { 314 | "$ref": "#/definitions/SailsStatusCode" 315 | } 316 | }, 317 | "additionalProperties": false, 318 | "required": [ 319 | "body", 320 | "headers", 321 | "statusCode" 322 | ] 323 | }, 324 | "SendLivestreamRadioChatter": { 325 | "type": "object", 326 | "description": "Sends a chat message to the specified livestream channel for other users to see. Note that sending a chat message will both receive a Sails HTTP response as well as a `radioChatter` event from yourself.", 327 | "properties": { 328 | "method": { 329 | "const": "post", 330 | "description": "This endpoint expects a POST." 331 | }, 332 | "headers": { 333 | "$ref": "#/definitions/SailsHeaders" 334 | }, 335 | "data": { 336 | "type": "object", 337 | "properties": { 338 | "channel": { 339 | "type": "string", 340 | "description": "Which livestream channel to send a chat to. Of the format `/live/{livestreamId}`." 341 | }, 342 | "message": { 343 | "type": "string", 344 | "description": "Message contents. May contain emotes, a word surrounded by colons. In order to send a valid emote, it should be an emote code that is returned in the `JoinedLivestreamRadioFrequency` response." 345 | } 346 | }, 347 | "additionalProperties": false, 348 | "required": [ 349 | "channel", 350 | "message" 351 | ] 352 | }, 353 | "url": { 354 | "const": "/RadioMessage/sendLivestreamRadioChatter/", 355 | "description": "The required endpoint for this event." 356 | } 357 | }, 358 | "additionalProperties": false, 359 | "required": [ 360 | "method", 361 | "headers", 362 | "data", 363 | "url" 364 | ] 365 | }, 366 | "SentLivestreamRadioChatter": { 367 | "type": "object", 368 | "properties": { 369 | "body": { 370 | "$ref": "#/definitions/RadioChatter" 371 | }, 372 | "headers": { 373 | "$ref": "#/definitions/SailsHeaders" 374 | }, 375 | "statusCode": { 376 | "$ref": "#/definitions/SailsStatusCode" 377 | } 378 | }, 379 | "additionalProperties": false, 380 | "required": [ 381 | "body", 382 | "headers", 383 | "statusCode" 384 | ] 385 | }, 386 | "RadioChatter": { 387 | "type": "object", 388 | "properties": { 389 | "id": { 390 | "type": "string", 391 | "description": "Identifier of the chat message itself. Should be unique per radio chatter." 392 | }, 393 | "userGUID": { 394 | "type": "string", 395 | "description": "Identifier of the user sending the message." 396 | }, 397 | "username": { 398 | "type": "string", 399 | "description": "Display name of the user sending the message." 400 | }, 401 | "channel": { 402 | "type": "string", 403 | "description": "Which livestream the radio chatter is from. Of the format `/live/{livestreamId}`." 404 | }, 405 | "message": { 406 | "type": "string", 407 | "description": "Message contents. May contain emotes, a word surrounded by colons. If the emote is valid for the user, the emote code and image path are included in `emotes` below." 408 | }, 409 | "userType": { 410 | "type": "string", 411 | "enum": [ 412 | "Normal", 413 | "Moderator" 414 | ] 415 | }, 416 | "emotes": { 417 | "$ref": "#/definitions/EmoteList" 418 | }, 419 | "success": { 420 | "type": "boolean", 421 | "description": "Included in `radioChatter` events and is usually `true`, but mainly useful in `SentLivestreamRadioChatter` responses to indicate if sending the message was successful. An example of why it might not work is using an invalid emote, or some system problem." 422 | } 423 | }, 424 | "additionalProperties": false, 425 | "required": [ 426 | "id", 427 | "userGUID", 428 | "username", 429 | "channel", 430 | "message", 431 | "userType" 432 | ] 433 | }, 434 | "EmoteList": { 435 | "type": "array", 436 | "description": "When the user types this `code` in their message, surrounded by two colons (`:`), that portion of the message should be replaced with the `image` property in the UI.", 437 | "items": { 438 | "title": "Emote", 439 | "type": "object", 440 | "properties": { 441 | "code": { 442 | "type": "string" 443 | }, 444 | "image": { 445 | "type": "string" 446 | } 447 | }, 448 | "required": [ 449 | "code", 450 | "image" 451 | ] 452 | } 453 | }, 454 | "SailsConnect": { 455 | "type": "object", 456 | "description": "Connect to Floatplane (after a socket connection has been made) in order to receive sync events, such as new post notifications.", 457 | "properties": { 458 | "method": { 459 | "const": "post", 460 | "description": "This endpoint expects a POST." 461 | }, 462 | "headers": { 463 | "$ref": "#/definitions/SailsHeaders" 464 | }, 465 | "data": { 466 | "type": "object", 467 | "description": "No payload necessary.", 468 | "additionalProperties": false 469 | }, 470 | "url": { 471 | "const": "/api/v3/socket/connect", 472 | "description": "The required endpoint for this event." 473 | } 474 | }, 475 | "additionalProperties": false, 476 | "required": [ 477 | "method", 478 | "headers", 479 | "data", 480 | "url" 481 | ] 482 | }, 483 | "SailsConnected": { 484 | "type": "object", 485 | "description": "The response received from connecting to Floatplane for sync events. Once this is successfully received, sync events may appear on the socket asynchronously.", 486 | "properties": { 487 | "body": { 488 | "type": "object", 489 | "properties": { 490 | "message": { 491 | "type": "string" 492 | } 493 | } 494 | }, 495 | "headers": { 496 | "$ref": "#/definitions/SailsHeaders" 497 | }, 498 | "statusCode": { 499 | "$ref": "#/definitions/SailsStatusCode" 500 | } 501 | }, 502 | "additionalProperties": false, 503 | "required": [ 504 | "body", 505 | "headers", 506 | "statusCode" 507 | ] 508 | }, 509 | "SailsDisconnect": { 510 | "type": "object", 511 | "description": "Disconnect from Floatplane (after a socket connection has been made) in order to stop receiving sync events.", 512 | "properties": { 513 | "method": { 514 | "const": "post", 515 | "description": "This endpoint expects a POST." 516 | }, 517 | "headers": { 518 | "$ref": "#/definitions/SailsHeaders" 519 | }, 520 | "data": { 521 | "type": "object", 522 | "description": "No payload necessary.", 523 | "additionalProperties": false 524 | }, 525 | "url": { 526 | "const": "/api/v3/socket/disconnect", 527 | "description": "The required endpoint for this event." 528 | } 529 | }, 530 | "additionalProperties": false, 531 | "required": [ 532 | "method", 533 | "headers", 534 | "data", 535 | "url" 536 | ] 537 | }, 538 | "SailsDisconnected": { 539 | "type": "object", 540 | "description": "The response received after disconnecting from Floatplane sync events.", 541 | "properties": { 542 | "body": { 543 | "type": "object", 544 | "properties": { 545 | "message": { 546 | "type": "string" 547 | } 548 | } 549 | }, 550 | "headers": { 551 | "$ref": "#/definitions/SailsHeaders" 552 | }, 553 | "statusCode": { 554 | "$ref": "#/definitions/SailsStatusCode" 555 | } 556 | }, 557 | "additionalProperties": false, 558 | "required": [ 559 | "body", 560 | "headers", 561 | "statusCode" 562 | ] 563 | }, 564 | "JoinLiveRoom": { 565 | "type": "object", 566 | "description": "Connect to a creator's live poll room (after a socket connection has been made) in order to receive poll events, such as new polls, poll tally updates, and closed polls. While not on the chat socket, this should typically be connected to while watching a livestream, and disconnected when leaving a livestream.", 567 | "properties": { 568 | "method": { 569 | "const": "post", 570 | "description": "This endpoint expects a POST." 571 | }, 572 | "headers": { 573 | "$ref": "#/definitions/SailsHeaders" 574 | }, 575 | "data": { 576 | "type": "object", 577 | "properties": { 578 | "creatorId": { 579 | "type": "string", 580 | "description": "The id of the creator for which to join the live poll room." 581 | } 582 | }, 583 | "additionalProperties": false, 584 | "required": [ 585 | "creatorId" 586 | ] 587 | }, 588 | "url": { 589 | "const": "/api/v3/poll/live/joinroom", 590 | "description": "The required endpoint for this event." 591 | } 592 | }, 593 | "additionalProperties": false, 594 | "required": [ 595 | "method", 596 | "headers", 597 | "data", 598 | "url" 599 | ] 600 | }, 601 | "JoinedLiveRoom": { 602 | "type": "object", 603 | "description": "", 604 | "properties": { 605 | "body": { 606 | "type": "object", 607 | "properties": { 608 | "activePolls": { 609 | "type": "array", 610 | "items": { 611 | "$ref": "#/definitions/PollOpenClose" 612 | } 613 | } 614 | }, 615 | "additionalProperties": false, 616 | "required": [ 617 | "activePolls" 618 | ] 619 | }, 620 | "headers": { 621 | "$ref": "#/definitions/SailsHeaders" 622 | }, 623 | "statusCode": { 624 | "$ref": "#/definitions/SailsStatusCode" 625 | } 626 | }, 627 | "additionalProperties": false, 628 | "required": [ 629 | "body", 630 | "headers", 631 | "statusCode" 632 | ] 633 | }, 634 | "LeaveLiveRoom": { 635 | "type": "object", 636 | "description": "Leave a live poll room and no longer receive poll events from the creator on this socket connection.", 637 | "properties": { 638 | "method": { 639 | "const": "post", 640 | "description": "This endpoint expects a POST." 641 | }, 642 | "headers": { 643 | "$ref": "#/definitions/SailsHeaders" 644 | }, 645 | "data": { 646 | "type": "object", 647 | "properties": { 648 | "creatorId": { 649 | "type": "string", 650 | "description": "The id of the creator from which to leave the live poll room." 651 | } 652 | }, 653 | "additionalProperties": false, 654 | "required": [ 655 | "creatorId" 656 | ] 657 | }, 658 | "url": { 659 | "const": "/api/v3/poll/live/leaveroom", 660 | "description": "The required endpoint for this event." 661 | } 662 | }, 663 | "additionalProperties": false, 664 | "required": [ 665 | "method", 666 | "headers", 667 | "data", 668 | "url" 669 | ] 670 | }, 671 | "LeftLiveRoom": { 672 | "type": "object", 673 | "description": "Indicates that leaving the live poll room was successful.", 674 | "properties": { 675 | "body": { 676 | "type": "boolean" 677 | }, 678 | "headers": { 679 | "$ref": "#/definitions/SailsHeaders" 680 | }, 681 | "statusCode": { 682 | "$ref": "#/definitions/SailsStatusCode" 683 | } 684 | }, 685 | "additionalProperties": false, 686 | "required": [ 687 | "body", 688 | "headers", 689 | "statusCode" 690 | ] 691 | }, 692 | "CreatorNotification": { 693 | "type": "object", 694 | "description": "This event is sent usually for new post notifications, where `eventType` is `CONTENT_POST_RELEASE`, along with information on which creator released a new post, and information on the post itself.", 695 | "properties": { 696 | "event": { 697 | "const": "creatorNotification" 698 | }, 699 | "data": { 700 | "$ref": "#/definitions/NotificationData" 701 | } 702 | }, 703 | "additionalProperties": false, 704 | "required": [ 705 | "event", 706 | "data" 707 | ] 708 | }, 709 | "PostRelease": { 710 | "type": "object", 711 | "description": "This event is sent usually for new post notifications, where `eventType` is `CONTENT_POST_RELEASE`, along with information on which creator released a new post, and information on the post itself. This sync event type seems to be deprecated, as the Floatplane website uses the above `creatorNotification` instead of this `postRelease`. For `CONTENT_POST_RELEASE`, these two have the same schema.", 712 | "properties": { 713 | "event": { 714 | "const": "postRelease" 715 | }, 716 | "data": { 717 | "$ref": "#/definitions/NotificationData" 718 | } 719 | }, 720 | "additionalProperties": false, 721 | "required": [ 722 | "event", 723 | "data" 724 | ] 725 | }, 726 | "NotificationData": { 727 | "type": "object", 728 | "description": "Contains data necessary to both show the notifiction in a user interface as well as technical details on what is being notified. Currently, this is used for notifying about new posts being released and the beginning of livestreams. Not all fields are present for all kinds of event types (for instance, livestream notifications do not have `video` or `content` objects, among others.", 729 | "properties": { 730 | "id": { 731 | "type": "string", 732 | "description": "Usually of the format `{eventType}:{content}`." 733 | }, 734 | "eventType": { 735 | "type": "string", 736 | "enum": [ 737 | "CONTENT_POST_RELEASE", 738 | "CONTENT_LIVESTREAM_START" 739 | ], 740 | "description": "The `CONTENT_POST_RELEASE` enumeration indicates a new post has been released. The `CONTENT_LIVESTREAM_START` enumeration indicates that a livestream has been started by the creator. Other enumerations are unknown at this time." 741 | }, 742 | "title": { 743 | "type": "string", 744 | "description": "Notification title." 745 | }, 746 | "message": { 747 | "type": "string", 748 | "description": "Notification message/body." 749 | }, 750 | "creator": { 751 | "type": "string", 752 | "description": "The identifier of the creator the notification is from." 753 | }, 754 | "content": { 755 | "type": "string", 756 | "description": "Usually the id of the blog post, when `eventType` is `CONTENT_POST_RELEASE`." 757 | }, 758 | "icon": { 759 | "type": "string", 760 | "format": "uri" 761 | }, 762 | "thumbnail": { 763 | "type": "string", 764 | "format": "uri" 765 | }, 766 | "target": { 767 | "type": "object", 768 | "description": "If the `target.matchPortion` of the browser's current href matches the `target.match` variable via the `target.matchScheme`, and if `target.foregroundDiscardOnMatch`, then do not show this notification because the user has already seen it.", 769 | "properties": { 770 | "url": { 771 | "type": "string", 772 | "description": "Unused in Floatplane code." 773 | }, 774 | "matchScheme": { 775 | "type": "string", 776 | "description": "This is usually `contains`.", 777 | "enum": [ 778 | "contains", 779 | "startsWith", 780 | "endsWith", 781 | "equals" 782 | ] 783 | }, 784 | "match": { 785 | "type": "string" 786 | }, 787 | "foregroundDiscardOnMatch": { 788 | "type": "boolean" 789 | }, 790 | "matchPortion": { 791 | "type": "string", 792 | "description": "This is usually `path` instead of `url`.", 793 | "default": "path", 794 | "enum": [ 795 | "path", 796 | "url" 797 | ] 798 | } 799 | }, 800 | "required": [ 801 | "url", 802 | "matchScheme", 803 | "match", 804 | "foregroundDiscardOnMatch", 805 | "matchPortion" 806 | ] 807 | }, 808 | "foregroundVisible": { 809 | "type": "string", 810 | "enum": [ 811 | "yes", 812 | "no" 813 | ] 814 | }, 815 | "video": { 816 | "type": "object", 817 | "properties": { 818 | "creator": { 819 | "type": "string" 820 | }, 821 | "guid": { 822 | "type": "string" 823 | } 824 | }, 825 | "required": [ 826 | "creator", 827 | "guid" 828 | ] 829 | }, 830 | "post": { 831 | "type": "object", 832 | "properties": { 833 | "creator": { 834 | "type": "string" 835 | }, 836 | "guid": { 837 | "type": "string" 838 | }, 839 | "id": { 840 | "type": "string" 841 | }, 842 | "text": { 843 | "type": "string" 844 | }, 845 | "title": { 846 | "type": "string" 847 | } 848 | } 849 | } 850 | }, 851 | "required": [ 852 | "id", 853 | "eventType", 854 | "creator" 855 | ] 856 | }, 857 | "CreatorMenuUpdate": { 858 | "type": "object", 859 | "description": "Does not appear to be used in Floatplane code. This model is similar to ContentPostV3Response in the REST API, but without attachment details. Its purpose is to help dynamically insert a single post into the list of posts on the screen, instead of making the client re-pull the 20 latest posts.", 860 | "properties": { 861 | "event": { 862 | "const": "creatorMenuUpdate" 863 | }, 864 | "data": { 865 | "type": "object", 866 | "properties": { 867 | "id": { 868 | "type": "string" 869 | }, 870 | "guid": { 871 | "type": "string" 872 | }, 873 | "title": { 874 | "type": "string" 875 | }, 876 | "text": { 877 | "type": "string" 878 | }, 879 | "type": { 880 | "type": "string" 881 | }, 882 | "tags": { 883 | "type": "array", 884 | "items": { 885 | "type": "string" 886 | } 887 | }, 888 | "attachmentOrder": { 889 | "type": "array", 890 | "items": { 891 | "type": "string" 892 | } 893 | }, 894 | "metadata": { 895 | "type": "object", 896 | "properties": { 897 | "hasVideo": { 898 | "type": "boolean" 899 | }, 900 | "videoCount": { 901 | "type": "integer" 902 | }, 903 | "videoDuration": { 904 | "type": "integer" 905 | }, 906 | "hasAudio": { 907 | "type": "boolean" 908 | }, 909 | "audioCount": { 910 | "type": "integer" 911 | }, 912 | "audioDuration": { 913 | "type": "integer" 914 | }, 915 | "hasPicture": { 916 | "type": "boolean" 917 | }, 918 | "pictureCount": { 919 | "type": "integer" 920 | }, 921 | "hasGallery": { 922 | "type": "boolean" 923 | }, 924 | "galleryCount": { 925 | "type": "integer" 926 | }, 927 | "isFeatured": { 928 | "type": "boolean" 929 | } 930 | } 931 | }, 932 | "releaseDate": { 933 | "type": "string", 934 | "format": "date-time" 935 | }, 936 | "likes": { 937 | "type": "integer" 938 | }, 939 | "dislikes": { 940 | "type": "integer" 941 | }, 942 | "score": { 943 | "type": "integer" 944 | }, 945 | "comments": { 946 | "type": "integer" 947 | }, 948 | "creator": { 949 | "type": "string" 950 | }, 951 | "wasReleasedSilently": { 952 | "type": "boolean" 953 | }, 954 | "thumbnail": { 955 | "$ref": "#/definitions/ImageModel" 956 | } 957 | } 958 | } 959 | } 960 | }, 961 | "PollOpenClose": { 962 | "type": "object", 963 | "description": "This schema is used for both PollOpen and PollClose.", 964 | "properties": { 965 | "poll": { 966 | "type": "object", 967 | "properties": { 968 | "id": { 969 | "type": "string", 970 | "description": "A unique identifier for the poll that is being opened or closed. Subsequent pollUpdateTally events will correspond to this id." 971 | }, 972 | "type": { 973 | "type": "string", 974 | "description": "The type of poll that is being shown. So far, only `simple` is known as a type here." 975 | }, 976 | "creator": { 977 | "type": "string", 978 | "description": "The id of the creator that is opening the poll. Useful if multiple livestreams are happening at the same time, so the UI knows which poll to show." 979 | }, 980 | "title": { 981 | "type": "string", 982 | "description": "The main question of the poll being presented to the user." 983 | }, 984 | "options": { 985 | "type": "array", 986 | "description": "The options that the user can select in the poll.", 987 | "items": { 988 | "type": "string" 989 | } 990 | }, 991 | "startDate": { 992 | "type": "string", 993 | "format": "date-time", 994 | "description": "When the poll was first opened." 995 | }, 996 | "endDate": { 997 | "type": "string", 998 | "format": "date-time", 999 | "description": "For PollOpen events, this is the time in which the poll should automatically close. This is usually 60 seconds after `startDate`. For PollClose events which close a poll early, this is the time in which it was closed by the creator, and is usually before the `endDate` from the corresponding PollOpen event." 1000 | }, 1001 | "finalTallyApproximate": { 1002 | "type": "null", 1003 | "description": "Unknown so far." 1004 | }, 1005 | "finalTallyReal": { 1006 | "type": "null", 1007 | "description": "Unknown so far." 1008 | }, 1009 | "runningTally": { 1010 | "type": "object", 1011 | "properties": { 1012 | "tick": { 1013 | "type": "integer", 1014 | "description": "A consecutively incrementing integer specifying the timeline of poll updates. Use the latest event by `tick` to show latest results. For PollOpen, this is always 0. For PollClose, this is the same tick as the latest PollUpdateTally event." 1015 | }, 1016 | "counts": { 1017 | "type": "array", 1018 | "description": "A list of poll vote counts for each poll option. The order of these matches the order of `options` in the initial PollOpen event. For PollOpen, these are always 0. For PollClose, these reflect the same values as the latest PollUpdateTally event.", 1019 | "items": { 1020 | "type": "integer" 1021 | } 1022 | } 1023 | }, 1024 | "additionalProperties": false, 1025 | "required": [ 1026 | "tick", 1027 | "counts" 1028 | ] 1029 | } 1030 | }, 1031 | "additionalProperties": false, 1032 | "required": [ 1033 | "id", 1034 | "type", 1035 | "creator", 1036 | "title", 1037 | "options", 1038 | "startDate", 1039 | "endDate", 1040 | "finalTallyApproximate", 1041 | "finalTallyReal", 1042 | "runningTally" 1043 | ] 1044 | } 1045 | }, 1046 | "additionalProperties": false, 1047 | "required": [ 1048 | "poll" 1049 | ] 1050 | }, 1051 | "PollUpdateTally": { 1052 | "type": "object", 1053 | "properties": { 1054 | "tick": { 1055 | "type": "integer", 1056 | "description": "A consecutively incrementing integer specifying the timeline of poll updates. Use the latest event by `tick` to show latest results." 1057 | }, 1058 | "counts": { 1059 | "type": "array", 1060 | "description": "A list of poll vote counts for each poll option. The order of these matches the order of `options` in the initial PollOpen event.", 1061 | "items": { 1062 | "type": "integer" 1063 | } 1064 | }, 1065 | "pollId": { 1066 | "type": "string", 1067 | "description": "Which poll this update corresponds to." 1068 | } 1069 | }, 1070 | "additionalProperties": false, 1071 | "required": [ 1072 | "tick", 1073 | "counts", 1074 | "pollId" 1075 | ] 1076 | }, 1077 | "ImageModel": { 1078 | "type": "object", 1079 | "properties": { 1080 | "width": { 1081 | "type": "integer" 1082 | }, 1083 | "height": { 1084 | "type": "integer" 1085 | }, 1086 | "path": { 1087 | "type": "string", 1088 | "format": "uri" 1089 | }, 1090 | "size": { 1091 | "type": "integer" 1092 | }, 1093 | "childImages": { 1094 | "type": "array", 1095 | "items": { 1096 | "$ref": "#/definitions/ChildImageModel" 1097 | } 1098 | } 1099 | }, 1100 | "required": [ 1101 | "width", 1102 | "height", 1103 | "path" 1104 | ] 1105 | }, 1106 | "ChildImageModel": { 1107 | "type": "object", 1108 | "properties": { 1109 | "width": { 1110 | "type": "integer" 1111 | }, 1112 | "height": { 1113 | "type": "integer" 1114 | }, 1115 | "path": { 1116 | "type": "string", 1117 | "format": "uri" 1118 | } 1119 | }, 1120 | "required": [ 1121 | "width", 1122 | "height", 1123 | "path" 1124 | ] 1125 | } 1126 | } 1127 | } 1128 | -------------------------------------------------------------------------------- /src/floatplane-asyncapi-chat-specification.json: -------------------------------------------------------------------------------- 1 | { 2 | "asyncapi": "2.6.0", 3 | "id": "https://github.com/Jman012/FloatplaneAPIDocs", 4 | "defaultContentType": "application/json", 5 | "tags": [], 6 | "info": { 7 | "title": "Floatplane Async Chat API", 8 | "version": "4.0.13", 9 | "description": "Homepage: [https:\/\/jman012.github.io\/FloatplaneAPIDocs](https:\/\/jman012.github.io\/FloatplaneAPIDocs)\r\n\r\nThis document describes the asynchronous\/event-driven API layer of [https:\/\/www.floatplane.com](https:\/\/www.floatplane.com), a content creation and video streaming website created by Floatplane Media Inc. and Linus Media Group, where users can support their favorite creates via paid subscriptions in order to watch their video and livestream content in higher quality and other perks.\r\n\r\nThis API is specific to the chat\/livestream activities of the Floatplane website, which is responsible for sending and receiving chat message, user lists, emote lists, etc., and is meant for a connection to chat.floatplane.com. If you are looking for the frontend API layer of Floatplane (www.floatplane.com), please visit [this document](..\/AsyncAPIFrontend) instead.\r\n\r\n## Implementation Notes\r\n\r\nThis document is an AsyncAPI implementation on top of a Socket.IO connection. AsyncAPI does not have any specifics for Socket.IO, and so any automatic code-generation from this document may not work 100% out-of-the-box on a WebSocket connection. At best, it will provide all of the proper models needed for the different events and messages. But it may require some glue code to get it working properly with Sails. See the below section on the difference between Socket.IO, Sails, and Floatplane for this socket connection.\r\n\r\nIt is recommended for any client implementation to use a Socket.IO reference library implementation [in your language of choice](https:\/\/github.com\/orgs\/socketio\/repositories?q=socket.io-client&type=all&language=&sort=) with this document. A version of this document for Sails would be preferred, but there is only a single client reference implementation (JavaScript), and the Sails layer on top of Socket.IO is fairly straightforward.\r\n\r\n## Technology Notes\r\n\r\nFloatplane's backend primarily uses [Sails](https:\/\/sailsjs.com\/) and the website UI uses an Angular frontend along with Sails' socket connection for low-latency request\/response and event-driven architecture. Sails is an MVC framework for making Node.js websites and APIs. Sails' socket connection is built on three tiers of technology that should be understood when implementing this API:\r\n\r\n1. [Engine.IO](https:\/\/socket.io\/docs\/v4\/how-it-works\/#engineio) ([GitHub](https:\/\/github.com\/socketio\/engine.io))\r\n\t1. This layer is responsible for abstracting the socket connection for reliability. It primarily uses WebSockets as the communication channel, but will fall back to HTTP long-polling if WebSockets are not available.\r\n\t1. It provides a rather simple protocol on top of the below items, usually prefixing a WebSocket frame with a single byte indicating the packet type.\r\n\t1. Engine.IO does have its own connection mechanism, and it would be best to have this implemented by a library rather than by hand. While it has a reference [JavaScript\/Node.JS](https:\/\/github.com\/socketio\/engine.io-client) implementation and a [Java](https:\/\/github.com\/socketio\/engine.io-client-java) implementation, it is recommended to use a Socket.io library outlined below.\r\n\t1. On the wire, you'll see connection frames with the `0` prefix, pings\/pongs as a single byte (`2`\/`3`), or messages with the `4` prefix. If you are analyzing the WebSocket frames directly, it would be beneficial to familiarize yourself with the [Engine.IO protocol](https:\/\/github.com\/socketio\/engine.io-protocol).\r\n1. [Socket.IO](https:\/\/socket.io\/)\r\n\t1. This builds on Engine.IO by adding reconnection, packet buffering, acknowledgements (request\/response), broadcasting, and multiplexing (namespaces) features.\r\n\t1. Note that Floatplane is **using Socket.IO version v2**. Not the latest v4. This may affect which client Socket.IO library implementation you use, as not all latest-version client libraries support v2.\r\n\t1. It would be useful to learn how the [Socket.IO Protocol](https:\/\/github.com\/socketio\/socket.io-protocol) is structured. In short, events are prefixed with `2`, acknowledgements are prefixed with `3` (both are after the Engine.IO prefixes\/headers), and the data of the event is stored as a JSON-encoded array of items, where the first item is always a string identifying the event name, and optional subsequent items are the arguments to the event.\r\n1. [Sails Socket Client](https:\/\/sailsjs.com\/documentation\/reference\/web-sockets\/socket-client)\r\n\t1. The Sails socket client primarily adds HTTP request\/response emulation on top of Socket.IO. For instance, it adds HTTP verbs such as `GET`, `POST`, etc. onto the socket connection, and uses Socket.IO's acknowledgements in order to send back a response body.\r\n\t1. This is implemented by emitting Socket.IO events where the event name is the HTTP verb (e.g. `\"get\"`, `\"post\"`, etc.), and the first and only argument to the Socket.IO event is a data structure with Sails-specific fields: `method`, `headers`, `url`, and `data`. The `data` field is where the application-specific data is stored in the event.\r\n\t1. The Sails response is sent back to the client as a Socket.IO acknowledgement in a similar format with the `body`, `headers`, and `statusCode` fields as the first and only argument in the ack.\r\n\t1. There is a reference Sails client library for [JavaScript](https:\/\/github.com\/balderdashy\/sails.io.js), but there is no other official reference implementation.\r\n1. Floatplane\r\n\t1. The final layer is the application Floatplane itself, which is described in this document.\r\n\r\n### Over-the-Wire Examples\r\n\r\nThe following list shows some examples of what a raw WebSocket connection might entail, and splits up the data between the various layers. This section is mostly to help with debugging raw connections, and understanding the technical stack of Floatplane.\r\n\r\n1. C->S `GET wss:\/\/www.floatplane.com\/socket.io\/?__sails_io_sdk_version=0.13.8&__sails_io_sdk_platform=browser&__sails_io_sdk_language=javascript&EIO=3&transport=websocket`\r\n\t1. The client first connects directly to the WebSocket endpoint, and receives an `HTTP 101 Switching Protocols`.\r\n\t1. This is Engine.IO only.\r\n1. S->C `0{\"sid\":\"b1VcCLtZ1SUXiYEGAB49\",\"upgrades\":[],\"pingInterval\":25000,\"pingTimeout\":60000}`\r\n\t1. The WebSocket connection is established and the server sends the open packet.\r\n\t1. `0`: Engine.IO open header.\r\n\t1. `{\"sid\":\"b1VcCLtZ1SUXiYEGAB49\",\"upgrades\":[],\"pingInterval\":25000,\"pingTimeout\":60000}`: Engine.IO open payload.\r\n\t1. Note that if using a proper Engine.IO or Socket.IO implementation, you will not need to see or handle this in your code.\r\n\t1. This is Engine.IO only.\r\n1. S->C `40`\r\n\t1. The server automatically connects the client to the Socket.IO default namespace (`\/`).\r\n\t1. `4`: Engine.IO message header.\r\n\t1. `0`: Socket.IO connect header.\r\n\t1. Note that the namespace `\/` is implied but not included in this message due to the Socket.IO protocol marking is as optional. The non-encoded message for this in Socket.IO would look like `{\"type\": 0,\"nsp\": \"\/admin\",\"data\": {}}`.\r\n\t1. Note that if using a proper Socket.IO library implementation, you will not need to see or handle this in your code.\r\n\t1. This is Engine.IO and Socket.IO only. \r\n1. C->S: `420[\"post\",{\"method\":\"post\",\"headers\":{},\"data\":{},\"url\":\"\/api\/v3\/socket\/connect\"}]`\r\n\t1. The client posts to the socket connect URL in Sails, in order for Floatplane to connect and listen for events.\r\n\t1. `4`: Engine.IO message header.\r\n\t1. `20`: Socket.IO header.\r\n\t\t1. `2`: Marks that this message is an Event.\r\n\t\t1. `0`: Id parameter. Needed for the following acknowledgement.\r\n\t1. `[\"post\",{\"method\":\"post\",\"headers\":{},\"data\":{},\"url\":\"\/api\/v3\/socket\/connect\"}]`: Socket.IO event data.\r\n\t\t1. `\"post\"`: Socket.IO event name.\r\n\t\t\t1. This event name is reserved and used by Sails.\r\n\t\t1. `{\"method\":\"post\",\"headers\":{},\"data\":{},\"url\":\"\/api\/v3\/socket\/connect\"}`: Socket.IO event argument for the Sails `\"post\"` event.\r\n\t\t\t1. `\"method\":\"post,\"url\":\"\/api\/v3\/socket\/connect\"`: This Sails POST request is destined for the `\/api\/v3\/socket\/connect` endpoint.\r\n\t\t\t1. `\"data\":{}`: Sails request body.\r\n\t\t\t\t1. `{}`: No Floatplane data in the request.\r\n1. S->C: `430[{\"body\":{\"message\":\"User sync setup.\"},\"headers\":{},\"statusCode\":200}]`\r\n\t1. The Floatplane server responds to the POST request in Sails, telling the Floatplane client that it is connected.\r\n\t1. `4`: Engine.IO message header.\r\n\t1. `30`: Socket.IO header.\r\n\t\t1. `3`: Marks that this message is an Acknowledgement\r\n\t\t1. `0`: Id parameter. Marks that it is acknowledging event number 0.\r\n\t1. `[{\"body\":{\"message\":\"User sync setup.\"},\"headers\":{},\"statusCode\":200}]`: Socket.IO acknowledgement data.\r\n\t\t1. `{\"body\":{\"message\":\"User sync setup.\"},\"headers\":{},\"statusCode\":200}`: The only argument in the Socket.IO ack\r\n\t\t\t1. `\"statusCode\":200`: Sails HTTP response status code.\r\n\t\t\t1. `\"body\":{\"message\":\"User sync setup.\"}`: Sails HTTP response body.\r\n\t\t\t\t1. `{\"message\":\"User sync setup.\"}`: Floatplane connection response.\r\n1. S->C: `421[\"syncEvent\",{\"event\": \"creatorNotification\",\"data\": {\"id\": \"CONTENT_POST_RELEASE:yQDi4v3EMc\",\"eventType\": \"CONTENT_POST_RELEASE\",...}}]`\r\n\t1. The Floatplane server sends an event to the client about a new post notification.\r\n\t1. `4`: Engine.IO message header.\r\n\t1. `2`: Socket.IO event header.\r\n\t1. `[\"syncEvent\",{\"event\": \"creatorNotification\",\"data\": {\"id\": \"CONTENT_POST_RELEASE:yQDi4v3EMc\",\"eventType\": \"CONTENT_POST_RELEASE\",...}}]`: Socket.IO event data.\r\n\t\t1. `\"syncEvent\"`: Socket.IO event name.\r\n\t\t\t1. This name is used by the Floatplane application. The server is using the Sails socket to emit the event, but does not add anything to the request. It is basically emitting directly on the socket from Socket.IO.\r\n\t\t1. `{\"event\": \"creatorNotification\",\"data\": {\"id\": \"CONTENT_POST_RELEASE:yQDi4v3EMc\",\"eventType\": \"CONTENT_POST_RELEASE\",...}}`: Socket.IO event argument for the `\"syncEvent\"` event.\r\n\t\t\t1. This is structured entirely by Floatplane.\r\n\r\n## Document Organization\r\n\r\nThis document is primarily organized around the AsyncAPI blog post on Socket.IO ([Part 1](https:\/\/www.asyncapi.com\/blog\/socketio-part1), and [Part 2](https:\/\/www.asyncapi.com\/blog\/socketio-part2)). The extension [`x-ack`](https:\/\/www.asyncapi.com\/blog\/socketio-part2#message-ack-object) is used in order to model Socket.IO's acknowledgement feature that emulates the request\/response paradigm. The Socket.IO acknowledgement does not have an event name that goes over the wire, and instead directly references the initiating event via its unique identifier. As such, acknowledgements are not specified in the AsyncAPI's channel's publisher or subscriber operators.\r\n\r\nThis document also manually incorporates features of Sails on top of Socket.IO. It does so manually because the Sails abstraction is rather light, by basically specifying event names and models for data to pass through for requests and responses to virtual endpoints. Sails introduces the event names `\"get\"`, `\"post\"`, `\"put\"`, and other HTTP verbs, where the event argument (request) is JSON in the form of:\r\n\r\n```json\r\n{\r\n\t\"method\": \"get\",\r\n\t\"headers\": {},\r\n\t\"body\": {\r\n\t\t...\r\n\t},\r\n\t\"url\": \"\/api\/v3\/...\"\r\n}\r\n```\r\n\r\nAnd the acknowledgement\/response argument is JSON in the form of:\r\n\r\n```json\r\n{\r\n\t\"body\": {\r\n\t\t...\r\n\t},\r\n\t\"headers\": {},\r\n\t\"statusCode: 200\r\n}\r\n```\r\n\r\nWhere `body` in each is specific to Floatplane. The rest of the data structure emulates HTTP requests and responses. As such, this AsyncAPI document explicitly models these structures around the Floatplane-specific requests and responses. \r\n\r\nFinally, because Sails uses generic `\"get\"`, `\"post\"`, etc. event names for multiple types of actualized events on Socket.IO, a single AsyncAPI Operator is defined for each of `\"get\"` and `\"post\"`, and uses JSON Schema's `oneOf` feature for multiple kinds of `body` models, one for each request\/response.\r\n\r\nUseful links for AsyncAPI and WebSockets\/Socket.IO:\r\n- https:\/\/www.asyncapi.com\/blog\/websocket-part1\r\n- https:\/\/www.asyncapi.com\/blog\/websocket-part2\r\n- https:\/\/www.asyncapi.com\/blog\/socketio-part1\r\n- https:\/\/www.asyncapi.com\/blog\/socketio-part2\r\n\r\n\r\n## Socket.IO Connection Tips\r\n\r\nWhen configuring a Socket.IO connection for use with Floatplane, there are some particular configurations to perform in order to prevent issues.\r\n\r\n- Path: `\/socket.io`\r\n\t- Floatplane's preferred Socket.IO path is `\/socket.io`.\r\n\t- By default, some client libraries will use a path of `\/engine.io` and may result in an HTTP 404 if used.\r\n- Secure: `true`\r\n\t- Floatplane is HTTPS\/WSS only, and has HTTP\/WS disabled.\r\n\t- Some client libraries have TLS disabled by default.\r\n- Transports: `websocket` \/ Secure: `true`\r\n\t- Floatplane appears to have HTTP long-polling disabled with their Engine.IO configuration, and thus the only option available is to use WebSockets.\r\n\t- When connecting to Floatplane's sockets, client libraries typically try to default to HTTP and only upgrade to WebSockets afterward. This may not work correctly with Floatplane, so attempt to connect via WebSockets by default.\r\n- Query \/ ConnectParams\r\n\t- Set the query or connection parameters like so:\r\n\t\t- `__sails_io_sdk_version`: `0.13.8`\r\n\t\t- `__sails_io_sdk_platform`: {your platform}\r\n\t\t- `__sails_io_sdk_language`: {your language}\r\n\t- These are required for Sails to initialize properly. Floatplane's Sails version defaults to the version to `0.9.0` which will throw an error when performing an Sails-related events, because it thinks your code will be too old to handle it.\r\n- Headers:\r\n\t- `Origin: https:\/\/www.floatplane.com\"`\r\n\t\t- For security-related purposes, Floatplane will deny WebSocket connections from what it thinks are other websites. This is to prevent cross-site request forgery. \r\n\t\t- When implementing an application in a browser, this is not customizable. But from a regular application, this is needed in order for Floatplane to trust your connection.\r\n\t- Cookies:\r\n\t\t- Some client libraries in Socket.IO have a separate configuration for Cookies, while others require you to bundle it in the `extraHeaders` configuration.\r\n\t\t- `sails.sid`\r\n\t\t\t- The `sails.sid` cookie is not required to make a raw Socket.IO connection with Floatplane, but will be required for making most Sails get\/post requests.\r\n\t\t\t- Otherwise, the socket connection is largely useless.", 10 | "license": { 11 | "name": "MIT", 12 | "url": "https://github.com/Jman012/FloatplaneAPI/blob/main/LICENSE" 13 | }, 14 | "contact": { 15 | "name": "James Linnell", 16 | "url": "https://github.com/Jman012/FloatplaneAPI/", 17 | "email": "james.n.linnell@gmail.com" 18 | } 19 | }, 20 | "servers": { 21 | "chat": { 22 | "url": "chat.floatplane.com/socket.io/?__sails_io_sdk_version={sailsVersion}&__sails_io_sdk_platform={sailsPlatform}&__sails_io_sdk_language={sailsLanguage}&EIO=3&transport=websocket", 23 | "protocol": "wss", 24 | "description": "A client connects to Floatplane's asynchronous API via WebSockets with TLS (wss). The socket it connects to is goeverned by Sails, which requires specifying which version of Sails to use, as well as other parameters like the platform and language. The purpose of the `EIO` query parameter is unknown. If using a proper Sails client library, the parameters should be auto-filled for you, and simply connecting to `www.floatplane.com/socket.io/` should suffice.", 25 | "variables": { 26 | "sailsVersion": { 27 | "default": "0.13.8", 28 | "description": "The value `0.13.8` is the current value at the time of writing (2022-05-08). This gets updated every so often with updates to the Floatplane frontend. There may be compatibility issues if too old of a value is supplied (the Sails backend may reject the connection). It is important to keep this value as up-to-date as possible, in order to prevent rejected connection issues.", 29 | "examples": [ 30 | "0.13.8" 31 | ] 32 | }, 33 | "sailsPlatform": { 34 | "default": "browser", 35 | "description": "The value `browser` is the current value used by the Sails JS client library. It is not known what effect values other than those defined by Sails may have on the socket connection.", 36 | "examples": [ 37 | "browser", 38 | "node" 39 | ] 40 | }, 41 | "sailsLanguage": { 42 | "default": "javascript", 43 | "description": "The value `javascript` is the current value used by the Sails JS client library. It is not known what effect values other than `javascript` may have on the socket connection.", 44 | "examples": [ 45 | "javascript" 46 | ] 47 | } 48 | } 49 | } 50 | }, 51 | "channels": { 52 | "/": { 53 | "description": "Socket.IO and Sails groups messages and events into different namespaces, with `/` being the default namespace. Multiple kinds of messages can be sent in a single namespace, with Socket.IO having its own mechanisms to differentiate message types. Floatplane only uses the root Socket.IO namespace (`/`).", 54 | "publish": { 55 | "operationId": "rootPublish", 56 | "message": { 57 | "oneOf": [ 58 | { 59 | "$ref": "#/components/messages/SailsGet" 60 | }, 61 | { 62 | "$ref": "#/components/messages/SailsPost" 63 | } 64 | ] 65 | } 66 | }, 67 | "subscribe": { 68 | "operationId": "rootSubscribe", 69 | "message": { 70 | "oneOf": [ 71 | { 72 | "$ref": "#/components/messages/RadioChatter" 73 | } 74 | ] 75 | } 76 | }, 77 | "bindings": { 78 | "ws": { 79 | "method": "GET", 80 | "headers": { 81 | "description": "The `sails.sid` cookie must be sent upon connecting to the socket for authentication with the server. This is the authentication cookie from the Floatplane website used to identify the user.", 82 | "type": "object", 83 | "properties": { 84 | "Cookies": { 85 | "type": "string", 86 | "description": "Should contain a value for `sails.sid`." 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | }, 94 | "components": { 95 | "messages": { 96 | "SailsGet": { 97 | "messageId": "get", 98 | "name": "get", 99 | "title": "Sails HTTP GET", 100 | "summary": "HTTP GET via Sails socket connection", 101 | "description": "This one asynchronous Sails/Socket.IO event may contain different Floatplane events in the payload, and contain different Floatplane events in the response/acknowledgement (depending on the request).", 102 | "contentType": "application/json", 103 | "payload": { 104 | "oneOf": [ 105 | { 106 | "$ref": "async-schemas.json#/definitions/JoinLivestreamRadioFrequency" 107 | }, 108 | { 109 | "$ref": "async-schemas.json#/definitions/GetChatUserList" 110 | } 111 | ] 112 | }, 113 | "examples": [ 114 | { 115 | "name": "JoinLivestreamRadioFrequency", 116 | "summary": "Connecting to a Floatplane livestream via a Sails HTTP GET.", 117 | "payload": { 118 | "method": "get", 119 | "headers": {}, 120 | "data": { 121 | "channel": "/live/5c13f3c006f1be15e08e05c0" 122 | }, 123 | "url": "/RadioMessage/joinLivestreamRadioFrequency" 124 | } 125 | }, 126 | { 127 | "name": "GetChatUserList", 128 | "summary": "Retrieve the user list in a livestream chat via a Sails HTTP GET.", 129 | "payload": { 130 | "method": "get", 131 | "headers": {}, 132 | "data": { 133 | "channel": "/live/5c13f3c006f1be15e08e05c0" 134 | }, 135 | "url": "/RadioMessage/getChatUserList/" 136 | } 137 | } 138 | ], 139 | "x-ack": { 140 | "args": { 141 | "oneOf": [ 142 | { 143 | "$ref": "async-schemas.json#/definitions/JoinedLivestreamRadioFrequency" 144 | }, 145 | { 146 | "$ref": "async-schemas.json#/definitions/ChatUserList" 147 | } 148 | ] 149 | }, 150 | "examples": [ 151 | { 152 | "name": "JoinedLivestreamRadioFrequency", 153 | "summary": "Successful response from a `JoinLivestreamRadioFrequency` message.", 154 | "payload": { 155 | "body": { 156 | "success": true, 157 | "emotes": [ 158 | { 159 | "code": "meow", 160 | "image": "https://pbs.floatplanecdn.com/emotes/global/430846941512581_1549005125768.png" 161 | }, 162 | { 163 | "code": "wan", 164 | "image": "https://pbs.floatplanecdn.com/emotes/global/128262935123759_1549055965843.png" 165 | }, 166 | { 167 | "code": "soontm", 168 | "image": "https://pbs.floatplanecdn.com/emotes/global/424666970631704_1549055985375.png" 169 | } 170 | ] 171 | }, 172 | "headers": {}, 173 | "statusCode": 200 174 | } 175 | }, 176 | { 177 | "name": "ChatUserList", 178 | "summary": "Successful response from a `GetChatUserList` message.", 179 | "payload": { 180 | "body": { 181 | "success": true, 182 | "pilots": [], 183 | "passengers": [ 184 | "username1" 185 | ] 186 | }, 187 | "headers": {}, 188 | "statusCode": 200 189 | } 190 | } 191 | ] 192 | } 193 | }, 194 | "SailsPost": { 195 | "messageId": "post", 196 | "name": "post", 197 | "title": "Sails HTTP POST", 198 | "summary": "HTTP POST via Sails socket connection", 199 | "description": "This one asynchronous Sails/Socket.IO event may contain different Floatplane events in the payload, and contain different Floatplane events in the response/acknowledgement (depending on the request).", 200 | "contentType": "application/json", 201 | "payload": { 202 | "oneOf": [ 203 | { 204 | "$ref": "async-schemas.json#/definitions/SendLivestreamRadioChatter" 205 | }, 206 | { 207 | "$ref": "async-schemas.json#/definitions/LeaveLivestreamRadioFrequency" 208 | } 209 | ] 210 | }, 211 | "examples": [ 212 | { 213 | "name": "SendLivestreamRadioChatter", 214 | "summary": "Send a livestream chat message via a Sails HTTP POST.", 215 | "payload": { 216 | "method": "post", 217 | "headers": {}, 218 | "data": { 219 | "channel": "/live/5c13f3c006f1be15e08e05c0", 220 | "message": "test" 221 | }, 222 | "url": "/RadioMessage/sendLivestreamRadioChatter/" 223 | } 224 | }, 225 | { 226 | "name": "LeaveLivestreamRadioFrequency", 227 | "summary": "Leave a Floatplane livestream via a Sails HTTP POST.", 228 | "payload": { 229 | "method": "post", 230 | "headers": {}, 231 | "data": { 232 | "channel": "/live/5c13f3c006f1be15e08e05c0", 233 | "message": "bye!" 234 | }, 235 | "url": "/RadioMessage/leaveLivestreamRadioFrequency" 236 | } 237 | } 238 | ], 239 | "x-ack": { 240 | "args": { 241 | "oneOf": [ 242 | { 243 | "$ref": "async-schemas.json#/definitions/SentLivestreamRadioChatter" 244 | }, 245 | { 246 | "$ref": "async-schemas.json#/definitions/LeftLivestreamRadioFrequency" 247 | } 248 | ] 249 | }, 250 | "examples": [ 251 | { 252 | "name": "SentLivestreamRadioChatter", 253 | "summary": "Successful response from a `SendLivestreamRadioChatter` message.", 254 | "payload": { 255 | "body": { 256 | "id": "cl30pgqfr01x301eh212rdr8q", 257 | "userGUID": "5fb69b4f8573b6cd8cc7f3b0", 258 | "username": "jamamp", 259 | "channel": "/live/5c13f3c006f1be15e08e05c0", 260 | "message": "test", 261 | "userType": "Normal", 262 | "emotes": [], 263 | "success": true 264 | }, 265 | "headers": {}, 266 | "statusCode": 200 267 | } 268 | }, 269 | { 270 | "name": "LeftLivestreamRadioFrequency", 271 | "summary": "Response from a `LeaveLivestreamRadioFrequency` message. Note that Floatplane currently sends a 404 back, so this might be bugged on their end.", 272 | "payload": { 273 | "body": "Not Found", 274 | "headers": {}, 275 | "statusCode": 404 276 | } 277 | } 278 | ] 279 | } 280 | }, 281 | "RadioChatter": { 282 | "messageId": "radioChatter", 283 | "name": "radioChatter", 284 | "title": "Floatplane Livestream Radio Chatter", 285 | "summary": "A radio chatter event arrives from Floatplane after joining a livestream via `/RadioMessage/joinLivestreamRadioFrequency`.", 286 | "description": "These events are primarily incoming chats from others in the livestream chat room, specifying who sent the chat, the message of the chat, and other meta data.", 287 | "contentType": "application/json", 288 | "payload": { 289 | "$ref": "async-schemas.json#/definitions/RadioChatter" 290 | }, 291 | "examples": [ 292 | { 293 | "payload": { 294 | "id": "cl30pgqfr01x301eh212rdr8q", 295 | "userGUID": "5fb69b4f8573b6cd8cc7f3b0", 296 | "username": "jamamp", 297 | "channel": "/live/5c13f3c006f1be15e08e05c0", 298 | "message": "test", 299 | "userType": "Normal", 300 | "emotes": [], 301 | "success": true 302 | } 303 | } 304 | ] 305 | } 306 | } 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/floatplane-asyncapi-frontend-specification.json: -------------------------------------------------------------------------------- 1 | { 2 | "asyncapi": "2.6.0", 3 | "id": "https://github.com/Jman012/FloatplaneAPIDocs", 4 | "defaultContentType": "application/json", 5 | "tags": [], 6 | "info": { 7 | "title": "Floatplane Async Frontend API", 8 | "version": "4.0.13", 9 | "description": "Homepage: [https:\/\/jman012.github.io\/FloatplaneAPIDocs](https:\/\/jman012.github.io\/FloatplaneAPIDocs)\r\n\r\nThis document describes the asynchronous\/event-driven API layer of [https:\/\/www.floatplane.com](https:\/\/www.floatplane.com), a content creation and video streaming website created by Floatplane Media Inc. and Linus Media Group, where users can support their favorite creates via paid subscriptions in order to watch their video and livestream content in higher quality and other perks.\r\n\r\nThis API is specific to the frontend activities of the Floatplane website, which is responsible for pushing new-post notifications to the client, and is meant for a connection to www.floatplane.com. If you are looking for the chat\/livestream async API layer of Floatplane (chat.floatplane.com), please visit [this document](..\/AsyncAPIChat) instead.\r\n\r\n## Implementation Notes\r\n\r\nThis document is an AsyncAPI implementation on top of a Socket.IO connection. AsyncAPI does not have any specifics for Socket.IO, and so any automatic code-generation from this document may not work 100% out-of-the-box on a WebSocket connection. At best, it will provide all of the proper models needed for the different events and messages. But it may require some glue code to get it working properly with Sails. See the below section on the difference between Socket.IO, Sails, and Floatplane for this socket connection.\r\n\r\nIt is recommended for any client implementation to use a Socket.IO reference library implementation [in your language of choice](https:\/\/github.com\/orgs\/socketio\/repositories?q=socket.io-client&type=all&language=&sort=) with this document. A version of this document for Sails would be preferred, but there is only a single client reference implementation (JavaScript), and the Sails layer on top of Socket.IO is fairly straightforward.\r\n\r\n## Technology Notes\r\n\r\nFloatplane's backend primarily uses [Sails](https:\/\/sailsjs.com\/) and the website UI uses an Angular frontend along with Sails' socket connection for low-latency request\/response and event-driven architecture. Sails is an MVC framework for making Node.js websites and APIs. Sails' socket connection is built on three tiers of technology that should be understood when implementing this API:\r\n\r\n1. [Engine.IO](https:\/\/socket.io\/docs\/v4\/how-it-works\/#engineio) ([GitHub](https:\/\/github.com\/socketio\/engine.io))\r\n\t1. This layer is responsible for abstracting the socket connection for reliability. It primarily uses WebSockets as the communication channel, but will fall back to HTTP long-polling if WebSockets are not available.\r\n\t1. It provides a rather simple protocol on top of the below items, usually prefixing a WebSocket frame with a single byte indicating the packet type.\r\n\t1. Engine.IO does have its own connection mechanism, and it would be best to have this implemented by a library rather than by hand. While it has a reference [JavaScript\/Node.JS](https:\/\/github.com\/socketio\/engine.io-client) implementation and a [Java](https:\/\/github.com\/socketio\/engine.io-client-java) implementation, it is recommended to use a Socket.io library outlined below.\r\n\t1. On the wire, you'll see connection frames with the `0` prefix, pings\/pongs as a single byte (`2`\/`3`), or messages with the `4` prefix. If you are analyzing the WebSocket frames directly, it would be beneficial to familiarize yourself with the [Engine.IO protocol](https:\/\/github.com\/socketio\/engine.io-protocol).\r\n1. [Socket.IO](https:\/\/socket.io\/)\r\n\t1. This builds on Engine.IO by adding reconnection, packet buffering, acknowledgements (request\/response), broadcasting, and multiplexing (namespaces) features.\r\n\t1. Note that Floatplane is **using Socket.IO version v2**. Not the latest v4. This may affect which client Socket.IO library implementation you use, as not all latest-version client libraries support v2.\r\n\t1. It would be useful to learn how the [Socket.IO Protocol](https:\/\/github.com\/socketio\/socket.io-protocol) is structured. In short, events are prefixed with `2`, acknowledgements are prefixed with `3` (both are after the Engine.IO prefixes\/headers), and the data of the event is stored as a JSON-encoded array of items, where the first item is always a string identifying the event name, and optional subsequent items are the arguments to the event.\r\n1. [Sails Socket Client](https:\/\/sailsjs.com\/documentation\/reference\/web-sockets\/socket-client)\r\n\t1. The Sails socket client primarily adds HTTP request\/response emulation on top of Socket.IO. For instance, it adds HTTP verbs such as `GET`, `POST`, etc. onto the socket connection, and uses Socket.IO's acknowledgements in order to send back a response body.\r\n\t1. This is implemented by emitting Socket.IO events where the event name is the HTTP verb (e.g. `\"get\"`, `\"post\"`, etc.), and the first and only argument to the Socket.IO event is a data structure with Sails-specific fields: `method`, `headers`, `url`, and `data`. The `data` field is where the application-specific data is stored in the event.\r\n\t1. The Sails response is sent back to the client as a Socket.IO acknowledgement in a similar format with the `body`, `headers`, and `statusCode` fields as the first and only argument in the ack.\r\n\t1. There is a reference Sails client library for [JavaScript](https:\/\/github.com\/balderdashy\/sails.io.js), but there is no other official reference implementation.\r\n1. Floatplane\r\n\t1. The final layer is the application Floatplane itself, which is described in this document.\r\n\r\n### Over-the-Wire Examples\r\n\r\nThe following list shows some examples of what a raw WebSocket connection might entail, and splits up the data between the various layers. This section is mostly to help with debugging raw connections, and understanding the technical stack of Floatplane.\r\n\r\n1. C->S `GET wss:\/\/www.floatplane.com\/socket.io\/?__sails_io_sdk_version=0.13.8&__sails_io_sdk_platform=browser&__sails_io_sdk_language=javascript&EIO=3&transport=websocket`\r\n\t1. The client first connects directly to the WebSocket endpoint, and receives an `HTTP 101 Switching Protocols`.\r\n\t1. This is Engine.IO only.\r\n1. S->C `0{\"sid\":\"b1VcCLtZ1SUXiYEGAB49\",\"upgrades\":[],\"pingInterval\":25000,\"pingTimeout\":60000}`\r\n\t1. The WebSocket connection is established and the server sends the open packet.\r\n\t1. `0`: Engine.IO open header.\r\n\t1. `{\"sid\":\"b1VcCLtZ1SUXiYEGAB49\",\"upgrades\":[],\"pingInterval\":25000,\"pingTimeout\":60000}`: Engine.IO open payload.\r\n\t1. Note that if using a proper Engine.IO or Socket.IO implementation, you will not need to see or handle this in your code.\r\n\t1. This is Engine.IO only.\r\n1. S->C `40`\r\n\t1. The server automatically connects the client to the Socket.IO default namespace (`\/`).\r\n\t1. `4`: Engine.IO message header.\r\n\t1. `0`: Socket.IO connect header.\r\n\t1. Note that the namespace `\/` is implied but not included in this message due to the Socket.IO protocol marking is as optional. The non-encoded message for this in Socket.IO would look like `{\"type\": 0,\"nsp\": \"\/admin\",\"data\": {}}`.\r\n\t1. Note that if using a proper Socket.IO library implementation, you will not need to see or handle this in your code.\r\n\t1. This is Engine.IO and Socket.IO only. \r\n1. C->S: `420[\"post\",{\"method\":\"post\",\"headers\":{},\"data\":{},\"url\":\"\/api\/v3\/socket\/connect\"}]`\r\n\t1. The client posts to the socket connect URL in Sails, in order for Floatplane to connect and listen for events.\r\n\t1. `4`: Engine.IO message header.\r\n\t1. `20`: Socket.IO header.\r\n\t\t1. `2`: Marks that this message is an Event.\r\n\t\t1. `0`: Id parameter. Needed for the following acknowledgement.\r\n\t1. `[\"post\",{\"method\":\"post\",\"headers\":{},\"data\":{},\"url\":\"\/api\/v3\/socket\/connect\"}]`: Socket.IO event data.\r\n\t\t1. `\"post\"`: Socket.IO event name.\r\n\t\t\t1. This event name is reserved and used by Sails.\r\n\t\t1. `{\"method\":\"post\",\"headers\":{},\"data\":{},\"url\":\"\/api\/v3\/socket\/connect\"}`: Socket.IO event argument for the Sails `\"post\"` event.\r\n\t\t\t1. `\"method\":\"post,\"url\":\"\/api\/v3\/socket\/connect\"`: This Sails POST request is destined for the `\/api\/v3\/socket\/connect` endpoint.\r\n\t\t\t1. `\"data\":{}`: Sails request body.\r\n\t\t\t\t1. `{}`: No Floatplane data in the request.\r\n1. S->C: `430[{\"body\":{\"message\":\"User sync setup.\"},\"headers\":{},\"statusCode\":200}]`\r\n\t1. The Floatplane server responds to the POST request in Sails, telling the Floatplane client that it is connected.\r\n\t1. `4`: Engine.IO message header.\r\n\t1. `30`: Socket.IO header.\r\n\t\t1. `3`: Marks that this message is an Acknowledgement\r\n\t\t1. `0`: Id parameter. Marks that it is acknowledging event number 0.\r\n\t1. `[{\"body\":{\"message\":\"User sync setup.\"},\"headers\":{},\"statusCode\":200}]`: Socket.IO acknowledgement data.\r\n\t\t1. `{\"body\":{\"message\":\"User sync setup.\"},\"headers\":{},\"statusCode\":200}`: The only argument in the Socket.IO ack\r\n\t\t\t1. `\"statusCode\":200`: Sails HTTP response status code.\r\n\t\t\t1. `\"body\":{\"message\":\"User sync setup.\"}`: Sails HTTP response body.\r\n\t\t\t\t1. `{\"message\":\"User sync setup.\"}`: Floatplane connection response.\r\n1. S->C: `421[\"syncEvent\",{\"event\": \"creatorNotification\",\"data\": {\"id\": \"CONTENT_POST_RELEASE:yQDi4v3EMc\",\"eventType\": \"CONTENT_POST_RELEASE\",...}}]`\r\n\t1. The Floatplane server sends an event to the client about a new post notification.\r\n\t1. `4`: Engine.IO message header.\r\n\t1. `2`: Socket.IO event header.\r\n\t1. `[\"syncEvent\",{\"event\": \"creatorNotification\",\"data\": {\"id\": \"CONTENT_POST_RELEASE:yQDi4v3EMc\",\"eventType\": \"CONTENT_POST_RELEASE\",...}}]`: Socket.IO event data.\r\n\t\t1. `\"syncEvent\"`: Socket.IO event name.\r\n\t\t\t1. This name is used by the Floatplane application. The server is using the Sails socket to emit the event, but does not add anything to the request. It is basically emitting directly on the socket from Socket.IO.\r\n\t\t1. `{\"event\": \"creatorNotification\",\"data\": {\"id\": \"CONTENT_POST_RELEASE:yQDi4v3EMc\",\"eventType\": \"CONTENT_POST_RELEASE\",...}}`: Socket.IO event argument for the `\"syncEvent\"` event.\r\n\t\t\t1. This is structured entirely by Floatplane.\r\n\r\n## Document Organization\r\n\r\nThis document is primarily organized around the AsyncAPI blog post on Socket.IO ([Part 1](https:\/\/www.asyncapi.com\/blog\/socketio-part1), and [Part 2](https:\/\/www.asyncapi.com\/blog\/socketio-part2)). The extension [`x-ack`](https:\/\/www.asyncapi.com\/blog\/socketio-part2#message-ack-object) is used in order to model Socket.IO's acknowledgement feature that emulates the request\/response paradigm. The Socket.IO acknowledgement does not have an event name that goes over the wire, and instead directly references the initiating event via its unique identifier. As such, acknowledgements are not specified in the AsyncAPI's channel's publisher or subscriber operators.\r\n\r\nThis document also manually incorporates features of Sails on top of Socket.IO. It does so manually because the Sails abstraction is rather light, by basically specifying event names and models for data to pass through for requests and responses to virtual endpoints. Sails introduces the event names `\"get\"`, `\"post\"`, `\"put\"`, and other HTTP verbs, where the event argument (request) is JSON in the form of:\r\n\r\n```json\r\n{\r\n\t\"method\": \"get\",\r\n\t\"headers\": {},\r\n\t\"body\": {\r\n\t\t...\r\n\t},\r\n\t\"url\": \"\/api\/v3\/...\"\r\n}\r\n```\r\n\r\nAnd the acknowledgement\/response argument is JSON in the form of:\r\n\r\n```json\r\n{\r\n\t\"body\": {\r\n\t\t...\r\n\t},\r\n\t\"headers\": {},\r\n\t\"statusCode: 200\r\n}\r\n```\r\n\r\nWhere `body` in each is specific to Floatplane. The rest of the data structure emulates HTTP requests and responses. As such, this AsyncAPI document explicitly models these structures around the Floatplane-specific requests and responses. \r\n\r\nFinally, because Sails uses generic `\"get\"`, `\"post\"`, etc. event names for multiple types of actualized events on Socket.IO, a single AsyncAPI Operator is defined for each of `\"get\"` and `\"post\"`, and uses JSON Schema's `oneOf` feature for multiple kinds of `body` models, one for each request\/response.\r\n\r\nUseful links for AsyncAPI and WebSockets\/Socket.IO:\r\n- https:\/\/www.asyncapi.com\/blog\/websocket-part1\r\n- https:\/\/www.asyncapi.com\/blog\/websocket-part2\r\n- https:\/\/www.asyncapi.com\/blog\/socketio-part1\r\n- https:\/\/www.asyncapi.com\/blog\/socketio-part2\r\n\r\n\r\n## Socket.IO Connection Tips\r\n\r\nWhen configuring a Socket.IO connection for use with Floatplane, there are some particular configurations to perform in order to prevent issues.\r\n\r\n- Path: `\/socket.io`\r\n\t- Floatplane's preferred Socket.IO path is `\/socket.io`.\r\n\t- By default, some client libraries will use a path of `\/engine.io` and may result in an HTTP 404 if used.\r\n- Secure: `true`\r\n\t- Floatplane is HTTPS\/WSS only, and has HTTP\/WS disabled.\r\n\t- Some client libraries have TLS disabled by default.\r\n- Transports: `websocket` \/ Secure: `true`\r\n\t- Floatplane appears to have HTTP long-polling disabled with their Engine.IO configuration, and thus the only option available is to use WebSockets.\r\n\t- When connecting to Floatplane's sockets, client libraries typically try to default to HTTP and only upgrade to WebSockets afterward. This may not work correctly with Floatplane, so attempt to connect via WebSockets by default.\r\n- Query \/ ConnectParams\r\n\t- Set the query or connection parameters like so:\r\n\t\t- `__sails_io_sdk_version`: `0.13.8`\r\n\t\t- `__sails_io_sdk_platform`: {your platform}\r\n\t\t- `__sails_io_sdk_language`: {your language}\r\n\t- These are required for Sails to initialize properly. Floatplane's Sails version defaults to the version to `0.9.0` which will throw an error when performing an Sails-related events, because it thinks your code will be too old to handle it.\r\n- Headers:\r\n\t- `Origin: https:\/\/www.floatplane.com\"`\r\n\t\t- For security-related purposes, Floatplane will deny WebSocket connections from what it thinks are other websites. This is to prevent cross-site request forgery. \r\n\t\t- When implementing an application in a browser, this is not customizable. But from a regular application, this is needed in order for Floatplane to trust your connection.\r\n\t- Cookies:\r\n\t\t- Some client libraries in Socket.IO have a separate configuration for Cookies, while others require you to bundle it in the `extraHeaders` configuration.\r\n\t\t- `sails.sid`\r\n\t\t\t- The `sails.sid` cookie is not required to make a raw Socket.IO connection with Floatplane, but will be required for making most Sails get\/post requests.\r\n\t\t\t- Otherwise, the socket connection is largely useless.", 10 | "license": { 11 | "name": "MIT", 12 | "url": "https://github.com/Jman012/FloatplaneAPI/blob/main/LICENSE" 13 | }, 14 | "contact": { 15 | "name": "James Linnell", 16 | "url": "https://github.com/Jman012/FloatplaneAPI/", 17 | "email": "james.n.linnell@gmail.com" 18 | } 19 | }, 20 | "servers": { 21 | "website": { 22 | "url": "www.floatplane.com/socket.io/?__sails_io_sdk_version={sailsVersion}&__sails_io_sdk_platform={sailsPlatform}&__sails_io_sdk_language={sailsLanguage}&EIO=3&transport=websocket", 23 | "protocol": "wss", 24 | "description": "A client connects to Floatplane's asynchronous API via WebSockets with TLS (wss). The socket it connects to is goeverned by Sails, which requires specifying which version of Sails to use, as well as other parameters like the platform and language. The purpose of the `EIO` query parameter is unknown. If using a proper Sails client library, the parameters should be auto-filled for you, and simply connecting to `www.floatplane.com/socket.io/` should suffice.", 25 | "variables": { 26 | "sailsVersion": { 27 | "default": "0.13.8", 28 | "description": "The value `0.13.8` is the current value at the time of writing (2022-05-08). This gets updated every so often with updates to the Floatplane frontend. There may be compatibility issues if too old of a value is supplied (the Sails backend may reject the connection). It is important to keep this value as up-to-date as possible, in order to prevent rejected connection issues.", 29 | "examples": [ 30 | "0.13.8" 31 | ] 32 | }, 33 | "sailsPlatform": { 34 | "default": "browser", 35 | "description": "The value `browser` is the current value used by the Sails JS client library. It is not known what effect values other than those defined by Sails may have on the socket connection.", 36 | "examples": [ 37 | "browser", 38 | "node" 39 | ] 40 | }, 41 | "sailsLanguage": { 42 | "default": "javascript", 43 | "description": "The value `javascript` is the current value used by the Sails JS client library. It is not known what effect values other than `javascript` may have on the socket connection.", 44 | "examples": [ 45 | "javascript" 46 | ] 47 | } 48 | } 49 | } 50 | }, 51 | "channels": { 52 | "/": { 53 | "description": "Socket.IO and Sails groups messages and events into different namespaces, with `/` being the default namespace. Multiple kinds of messages can be sent in a single namespace, with Socket.IO having its own mechanisms to differentiate message types. Floatplane only uses the root Socket.IO namespace (`/`).", 54 | "publish": { 55 | "operationId": "rootPublish", 56 | "message": { 57 | "oneOf": [ 58 | { 59 | "$ref": "#/components/messages/SailsPost" 60 | } 61 | ] 62 | } 63 | }, 64 | "subscribe": { 65 | "operationId": "rootSubscribe", 66 | "message": { 67 | "oneOf": [ 68 | { 69 | "$ref": "#/components/messages/SyncEvent" 70 | }, 71 | { 72 | "$ref": "#/components/messages/PollOpen" 73 | }, 74 | { 75 | "$ref": "#/components/messages/PollClose" 76 | }, 77 | { 78 | "$ref": "#/components/messages/PollUpdateTally" 79 | } 80 | ] 81 | } 82 | }, 83 | "bindings": { 84 | "ws": { 85 | "method": "GET", 86 | "headers": { 87 | "description": "The `sails.sid` cookie must be sent upon connecting to the socket for authentication with the server. This is the authentication cookie from the Floatplane website used to identify the user.", 88 | "type": "object", 89 | "properties": { 90 | "Cookies": { 91 | "type": "string", 92 | "description": "Should contain a value for `sails.sid`." 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | }, 100 | "components": { 101 | "messages": { 102 | "SailsPost": { 103 | "messageId": "post", 104 | "name": "post", 105 | "title": "Sails HTTP POST", 106 | "summary": "HTTP POST via Sails socket connection", 107 | "description": "This one asynchronous Sails/Socket.IO event may contain different Floatplane events in the payload, and contain different Floatplane events in the response/acknowledgement (depending on the request).", 108 | "contentType": "application/json", 109 | "payload": { 110 | "oneOf": [ 111 | { 112 | "$ref": "async-schemas.json#/definitions/SailsConnect" 113 | }, 114 | { 115 | "$ref": "async-schemas.json#/definitions/SailsDisconnect" 116 | }, 117 | { 118 | "$ref": "async-schemas.json#/definitions/JoinLiveRoom" 119 | }, 120 | { 121 | "$ref": "async-schemas.json#/definitions/LeaveLiveRoom" 122 | } 123 | ] 124 | }, 125 | "examples": [ 126 | { 127 | "name": "SailsConnect", 128 | "summary": "Connecting to Floatplane sync events via a Sails HTTP POST.", 129 | "payload": { 130 | "method": "post", 131 | "headers": {}, 132 | "data": {}, 133 | "url": "/api/v3/socket/connect" 134 | } 135 | }, 136 | { 137 | "name": "SailsDisconnect", 138 | "summary": "Disconnect from Floatplane sync events via a Sails HTTP POST.", 139 | "payload": { 140 | "method": "post", 141 | "headers": {}, 142 | "data": {}, 143 | "url": "/api/v3/socket/disconnect" 144 | } 145 | }, 146 | { 147 | "name": "JoinLiveRoom", 148 | "summary": "Connect to a live poll room via a Sails HTTP POST.", 149 | "payload": { 150 | "method": "post", 151 | "headers": {}, 152 | "data": { 153 | "creatorId": "59f94c0bdd241b70349eb72b" 154 | }, 155 | "url": "/api/v3/poll/live/joinroom" 156 | } 157 | }, 158 | { 159 | "name": "LeaveLiveRoom", 160 | "summary": "Disconnect from a live poll room via a Sails HTTP POST.", 161 | "payload": { 162 | "method": "post", 163 | "headers": {}, 164 | "data": { 165 | "creatorId": "59f94c0bdd241b70349eb72b" 166 | }, 167 | "url": "/api/v3/poll/live/leaveroom" 168 | } 169 | } 170 | ], 171 | "x-ack": { 172 | "args": { 173 | "oneOf": [ 174 | { 175 | "$ref": "async-schemas.json#/definitions/SailsConnected" 176 | }, 177 | { 178 | "$ref": "async-schemas.json#/definitions/JoinedLiveRoom" 179 | }, 180 | { 181 | "$ref": "async-schemas.json#/definitions/LeftLiveRoom" 182 | } 183 | ] 184 | }, 185 | "examples": [ 186 | { 187 | "name": "SailsConnected", 188 | "summary": "Successful response from a `SailsConnect` message.", 189 | "payload": { 190 | "body": { 191 | "message": "User sync setup." 192 | }, 193 | "headers": {}, 194 | "statusCode": 200 195 | } 196 | }, 197 | { 198 | "name": "JoinedLiveRoom", 199 | "summary": "Successful response from a `JoinLiveRoom` message.", 200 | "payload": { 201 | "body": { 202 | "activePolls": [] 203 | }, 204 | "headers": {}, 205 | "statusCode": 200 206 | } 207 | }, 208 | { 209 | "name": "LeftLiveRoom", 210 | "summary": "Successful response from a `LeaveLiveRoom` message.", 211 | "payload": { 212 | "body": true, 213 | "headers": {}, 214 | "statusCode": 200 215 | } 216 | } 217 | ] 218 | } 219 | }, 220 | "SyncEvent": { 221 | "messageId": "syncEvent", 222 | "name": "syncEvent", 223 | "title": "Floatplane Sync Event", 224 | "summary": "A sync event arrives from the Floatplane server after registrating with the connect event (`/api/v3/socket/connect`).", 225 | "description": "The sync event is another wrapper over Socket.IO, where the actual event type is embedded within the Socket.IO event payload. Most kinds of sync events are notifications of new blog posts on Floatplane from a subscribed creator.", 226 | "contentType": "application/json", 227 | "payload": { 228 | "oneOf": [ 229 | { 230 | "$ref": "async-schemas.json#/definitions/CreatorNotification" 231 | }, 232 | { 233 | "$ref": "async-schemas.json#/definitions/PostRelease" 234 | }, 235 | { 236 | "$ref": "async-schemas.json#/definitions/CreatorMenuUpdate" 237 | } 238 | ] 239 | }, 240 | "examples": [ 241 | { 242 | "name": "CreatorNotification Sync Event: Content Post Release", 243 | "summary": "Floatplane sent a sync event for a content post release via the `creatorNotification` event.", 244 | "payload": { 245 | "event": "creatorNotification", 246 | "data": { 247 | "id": "CONTENT_POST_RELEASE:yQDi4v3EMc", 248 | "eventType": "CONTENT_POST_RELEASE", 249 | "title": "New post from LinusTechTips", 250 | "message": "Reacting to OLD Computer Magazines!", 251 | "creator": "59f94c0bdd241b70349eb72b", 252 | "content": "yQDi4v3EMc", 253 | "icon": "https://pbs.floatplane.com/creator_icons/59f94c0bdd241b70349eb72b/770551996990709_1551249357205.jpeg", 254 | "thumbnail": "https://pbs.floatplane.com/blogPost_thumbnails/yQDi4v3EMc/682332670704114_1651513307946.jpeg", 255 | "target": { 256 | "url": "/post/yQDi4v3EMc", 257 | "matchScheme": "contains", 258 | "match": "yQDi4v3EMc", 259 | "foregroundDiscardOnMatch": true, 260 | "matchPortion": "path" 261 | }, 262 | "foregroundVisible": "yes", 263 | "video": { 264 | "creator": "59f94c0bdd241b70349eb72b", 265 | "guid": "yQDi4v3EMc" 266 | }, 267 | "post": { 268 | "creator": "59f94c0bdd241b70349eb72b", 269 | "guid": "yQDi4v3EMc", 270 | "id": "yQDi4v3EMc", 271 | "text": "

Join us on a trip down memory lane where Linus looks at some of the greatest and latest PC tech circa 2004. These old issues of Maximum PC are filled with tons of hilarious takes, forgotten products, and beloved childhood favourites!

", 272 | "title": "Reacting to OLD Computer Magazines!" 273 | } 274 | } 275 | } 276 | }, 277 | { 278 | "name": "CreatorNotification Sync Event: Content Livestream Start", 279 | "summary": "Floatplane sent a sync event for a content livestream start via the `creatorNotification` event.", 280 | "payload": { 281 | "event": "creatorNotification", 282 | "data": { 283 | "id": "CONTENT_LIVESTREAM_START:91ecb0f8-541b-4c5b-b35f-05552df7f805", 284 | "eventType": "CONTENT_LIVESTREAM_START", 285 | "title": "LinusTechTips started a livestream", 286 | "message": "We're Finally Free - WAN Show May 13, 2022", 287 | "creator": "59f94c0bdd241b70349eb72b", 288 | "icon": "https://pbs.floatplane.com/creator_icons/59f94c0bdd241b70349eb72b/770551996990709_1551249357205.jpeg", 289 | "thumbnail": "https://pbs.floatplane.com/stream_thumbnails/5c13f3c006f1be15e08e05c0/481046370800602_1651880382456.jpeg", 290 | "target": { 291 | "url": "/channel/linustechtips/live/", 292 | "matchScheme": "contains", 293 | "match": "linustechtips", 294 | "foregroundDiscardOnMatch": true, 295 | "matchPortion": "path" 296 | }, 297 | "foregroundVisible": "yes" 298 | } 299 | } 300 | }, 301 | { 302 | "name": "PostRelease Sync Event: Content Post Release", 303 | "summary": "Floatplane sent a sync event for a content post release via the `postRelease` event.", 304 | "payload": { 305 | "event": "postRelease", 306 | "data": { 307 | "id": "CONTENT_POST_RELEASE:yQDi4v3EMc", 308 | "eventType": "CONTENT_POST_RELEASE", 309 | "title": "New post from LinusTechTips", 310 | "message": "Reacting to OLD Computer Magazines!", 311 | "creator": "59f94c0bdd241b70349eb72b", 312 | "content": "yQDi4v3EMc", 313 | "icon": "https://pbs.floatplane.com/creator_icons/59f94c0bdd241b70349eb72b/770551996990709_1551249357205.jpeg", 314 | "thumbnail": "https://pbs.floatplane.com/blogPost_thumbnails/yQDi4v3EMc/682332670704114_1651513307946.jpeg", 315 | "target": { 316 | "url": "/post/yQDi4v3EMc", 317 | "matchScheme": "contains", 318 | "match": "yQDi4v3EMc", 319 | "foregroundDiscardOnMatch": true, 320 | "matchPortion": "path" 321 | }, 322 | "foregroundVisible": "yes", 323 | "video": { 324 | "creator": "59f94c0bdd241b70349eb72b", 325 | "guid": "yQDi4v3EMc" 326 | }, 327 | "post": { 328 | "creator": "59f94c0bdd241b70349eb72b", 329 | "guid": "yQDi4v3EMc", 330 | "id": "yQDi4v3EMc", 331 | "text": "

Join us on a trip down memory lane where Linus looks at some of the greatest and latest PC tech circa 2004. These old issues of Maximum PC are filled with tons of hilarious takes, forgotten products, and beloved childhood favourites!

", 332 | "title": "Reacting to OLD Computer Magazines!" 333 | } 334 | } 335 | } 336 | }, 337 | { 338 | "name": "CreatorMenuUpdate Sync Event", 339 | "summary": "Floatplane sent a sync event for a creator menu update.", 340 | "payload": { 341 | "event": "creatorMenuUpdate", 342 | "data": { 343 | "id": "yQDi4v3EMc", 344 | "guid": "yQDi4v3EMc", 345 | "title": "Reacting to OLD Computer Magazines!", 346 | "text": "

Join us on a trip down memory lane where Linus looks at some of the greatest and latest PC tech circa 2004. These old issues of Maximum PC are filled with tons of hilarious takes, forgotten products, and beloved childhood favourites!

", 347 | "type": "blogPost", 348 | "tags": [], 349 | "attachmentOrder": [ 350 | "lb7QsgcEtc" 351 | ], 352 | "metadata": { 353 | "hasVideo": true, 354 | "videoCount": 1, 355 | "videoDuration": 1096, 356 | "hasAudio": false, 357 | "audioCount": 0, 358 | "audioDuration": 0, 359 | "hasPicture": false, 360 | "pictureCount": 0, 361 | "hasGallery": false, 362 | "galleryCount": 0, 363 | "isFeatured": false 364 | }, 365 | "releaseDate": "2022-05-02T19:44:00.043Z", 366 | "likes": 0, 367 | "dislikes": 0, 368 | "score": 0, 369 | "comments": 0, 370 | "creator": "59f94c0bdd241b70349eb72b", 371 | "wasReleasedSilently": false, 372 | "thumbnail": { 373 | "width": 1920, 374 | "height": 1080, 375 | "path": "https://pbs.floatplane.com/blogPost_thumbnails/yQDi4v3EMc/682332670704114_1651513307946.jpeg", 376 | "childImages": [ 377 | { 378 | "width": 400, 379 | "height": 225, 380 | "path": "https://pbs.floatplane.com/blogPost_thumbnails/yQDi4v3EMc/682332670704114_1651513307946_400x225.jpeg" 381 | }, 382 | { 383 | "width": 1200, 384 | "height": 675, 385 | "path": "https://pbs.floatplane.com/blogPost_thumbnails/yQDi4v3EMc/682332670704114_1651513307946_1200x675.jpeg" 386 | } 387 | ] 388 | } 389 | } 390 | } 391 | } 392 | ] 393 | }, 394 | "PollOpen": { 395 | "messageId": "pollOpen", 396 | "name": "pollOpen", 397 | "title": "Poll Opened", 398 | "summary": "A `pollOpen` event arrives when a creator has started a new poll for users to vote on.", 399 | "description": "This provides options for the user to choose from, when the poll opened, and when it will automatically close. If the `endDate` is reached, the UI should show the conclusion and hide the poll as there will be no corresponding PollClose event. The `startDate` and `endDate` can be used to show a timer in the UI. In most polls, only a single option is permitted to be voted on, and the user is only allowed one chance at a vote.\n\nIn order to vote on a poll, perform an HTTP POST to `/api/v3/poll/vote`. Look at the OpenAPI specification document for more information.", 400 | "contentType": "application/json", 401 | "payload": { 402 | "$ref": "async-schemas.json#/definitions/PollOpenClose" 403 | }, 404 | "examples": [ 405 | { 406 | "name": "PollOpen Example", 407 | "summary": "Opening a new poll with three options, with 60 seconds to answer.", 408 | "payload": { 409 | "poll": { 410 | "id": "62c8c1dd968bc0899bbb4b92", 411 | "type": "simple", 412 | "creator": "59f94c0bdd241b70349eb72b", 413 | "title": "When will WAN go live?", 414 | "options": [ 415 | "5:00", 416 | "5:30", 417 | "It's already live 👽" 418 | ], 419 | "startDate": "2022-07-08T23:46:37.429Z", 420 | "endDate": "2022-07-08T23:47:37.428Z", 421 | "finalTallyApproximate": null, 422 | "finalTallyReal": null, 423 | "runningTally": { 424 | "tick": 0, 425 | "counts": [ 426 | 0, 427 | 0, 428 | 0 429 | ] 430 | } 431 | } 432 | } 433 | } 434 | ] 435 | }, 436 | "PollClose": { 437 | "messageId": "pollClose", 438 | "name": "pollClose", 439 | "title": "Poll Closed", 440 | "summary": "A `pollClose` event arrives when a creator has ended a poll earlier than the expected `endDate`. Sometimes this happens right before starting a new poll, as it appears only one poll can be active at a time, per-creator. This payload will include the same details as the PollOpen event, but with an actual `endDate` as well as the final results of the tally.", 441 | "description": "", 442 | "contentType": "application/json", 443 | "payload": { 444 | "$ref": "async-schemas.json#/definitions/PollOpenClose" 445 | }, 446 | "examples": [ 447 | { 448 | "name": "PollClose Example", 449 | "summary": "Closing a two-option poll early with a final count of 38 to 48. It ended about 17 seconds into the poll.", 450 | "payload": { 451 | "poll": { 452 | "id": "62c8e33a9f58a79f5269bbd7", 453 | "type": "simple", 454 | "creator": "59f94c0bdd241b70349eb72b", 455 | "title": "Should they do a poll? ", 456 | "options": [ 457 | "Yes", 458 | "Different Yes" 459 | ], 460 | "startDate": "2022-07-09T02:08:58.784Z", 461 | "endDate": "2022-07-09T02:09:15.514Z", 462 | "finalTallyApproximate": null, 463 | "finalTallyReal": null, 464 | "runningTally": { 465 | "tick": 86, 466 | "counts": [ 467 | 38, 468 | 48 469 | ] 470 | } 471 | } 472 | } 473 | } 474 | ] 475 | }, 476 | "PollUpdateTally": { 477 | "messageId": "pollUpdateTally", 478 | "name": "pollUpdateTally", 479 | "title": "Poll Tally Updated", 480 | "summary": "A `pollUpdateTally` even will arrive upon every vote on the poll by a user.", 481 | "description": "The `tick` value will increment by one for each event, and one of the option counters should increment by one due to each vote. ", 482 | "contentType": "application/json", 483 | "payload": { 484 | "$ref": "async-schemas.json#/definitions/PollUpdateTally" 485 | }, 486 | "examples": [ 487 | { 488 | "name": "PollUpdateTally Example", 489 | "summary": "The first vote performed on a poll, voting for the third option.", 490 | "payload": { 491 | "tick": 1, 492 | "counts": [ 493 | 0, 494 | 0, 495 | 1 496 | ], 497 | "pollId": "62c8c1dd968bc0899bbb4b92" 498 | } 499 | } 500 | ] 501 | } 502 | } 503 | } 504 | } 505 | -------------------------------------------------------------------------------- /static/SwaggerUI/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamamp/FloatplaneAPI/396baa47d462197681dc649f2d2e26b56857f41b/static/SwaggerUI/favicon-16x16.png -------------------------------------------------------------------------------- /static/SwaggerUI/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamamp/FloatplaneAPI/396baa47d462197681dc649f2d2e26b56857f41b/static/SwaggerUI/favicon-32x32.png -------------------------------------------------------------------------------- /static/SwaggerUI/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 9 | 10 | 31 | 32 | 33 | 34 |
35 | 36 | 37 | 38 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /static/SwaggerUI/oauth2-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Swagger UI: OAuth2 Redirect 5 | 6 | 7 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Floatplane API Documentation 12 | 13 | 14 | 15 |
16 |
17 |
18 |

Floatplane API Documentation

19 |

Unofficial API documentation for content streaming service https://www.floatplane.com

21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |

29 | Source: https://github.com/Jman012/FloatplaneAPI/ 30 |

31 |

REST API

32 |

Trimmed Documentation

33 |

The trimmed version of the docs leaves out administrator, creator, moderator, and other APIs that are not used by regular users and can not be or have not yet been documented.

34 | 87 | 88 |

Full Documentation

89 |

The full versions of the documentation contain every API endpoint that is known, although most will be blank placeholders.

90 | 143 | 144 |

Asynchronous API

145 | 153 |

Frontend

154 |

This API is specific to the frontend activities of the Floatplane website, which is responsible for pushing new-post notifications to the client, and is meant for a connection to www.floatplane.com.

155 | 169 |

Chat

170 |

This API is specific to the chat/livestream activities of the Floatplane website, which is responsible for sending and receiving chat message, user lists, emote lists, etc., and is meant for a connection to chat.floatplane.com.

171 | 185 |
186 |
187 |
188 |
189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /static/rapidoc-full.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /static/rapidoc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/SchemaThesisTests/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /tests/SchemaThesisTests/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamamp/FloatplaneAPI/396baa47d462197681dc649f2d2e26b56857f41b/tests/SchemaThesisTests/README.md -------------------------------------------------------------------------------- /tests/SchemaThesisTests/poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "anyio" 5 | version = "3.6.2" 6 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 7 | category = "main" 8 | optional = false 9 | python-versions = ">=3.6.2" 10 | files = [ 11 | {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, 12 | {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, 13 | ] 14 | 15 | [package.dependencies] 16 | idna = ">=2.8" 17 | sniffio = ">=1.1" 18 | 19 | [package.extras] 20 | doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 21 | test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] 22 | trio = ["trio (>=0.16,<0.22)"] 23 | 24 | [[package]] 25 | name = "attrs" 26 | version = "22.1.0" 27 | description = "Classes Without Boilerplate" 28 | category = "main" 29 | optional = false 30 | python-versions = ">=3.5" 31 | files = [ 32 | {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, 33 | {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, 34 | ] 35 | 36 | [package.extras] 37 | dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] 38 | docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] 39 | tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] 40 | tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] 41 | 42 | [[package]] 43 | name = "backoff" 44 | version = "2.2.1" 45 | description = "Function decoration for backoff and retry" 46 | category = "main" 47 | optional = false 48 | python-versions = ">=3.7,<4.0" 49 | files = [ 50 | {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, 51 | {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, 52 | ] 53 | 54 | [[package]] 55 | name = "certifi" 56 | version = "2022.12.7" 57 | description = "Python package for providing Mozilla's CA Bundle." 58 | category = "main" 59 | optional = false 60 | python-versions = ">=3.6" 61 | files = [ 62 | {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, 63 | {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, 64 | ] 65 | 66 | [[package]] 67 | name = "charset-normalizer" 68 | version = "2.1.1" 69 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 70 | category = "main" 71 | optional = false 72 | python-versions = ">=3.6.0" 73 | files = [ 74 | {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, 75 | {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, 76 | ] 77 | 78 | [package.extras] 79 | unicode-backport = ["unicodedata2"] 80 | 81 | [[package]] 82 | name = "click" 83 | version = "8.1.3" 84 | description = "Composable command line interface toolkit" 85 | category = "main" 86 | optional = false 87 | python-versions = ">=3.7" 88 | files = [ 89 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 90 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 91 | ] 92 | 93 | [package.dependencies] 94 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 95 | 96 | [[package]] 97 | name = "colorama" 98 | version = "0.4.6" 99 | description = "Cross-platform colored terminal text." 100 | category = "main" 101 | optional = false 102 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 103 | files = [ 104 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 105 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 106 | ] 107 | 108 | [[package]] 109 | name = "curlify" 110 | version = "2.2.1" 111 | description = "Library to convert python requests object to curl command." 112 | category = "main" 113 | optional = false 114 | python-versions = "*" 115 | files = [ 116 | {file = "curlify-2.2.1.tar.gz", hash = "sha256:0d3f02e7235faf952de8ef45ef469845196d30632d5838bcd5aee217726ddd6d"}, 117 | ] 118 | 119 | [package.dependencies] 120 | requests = "*" 121 | 122 | [[package]] 123 | name = "graphql-core" 124 | version = "3.2.3" 125 | description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." 126 | category = "main" 127 | optional = false 128 | python-versions = ">=3.6,<4" 129 | files = [ 130 | {file = "graphql-core-3.2.3.tar.gz", hash = "sha256:06d2aad0ac723e35b1cb47885d3e5c45e956a53bc1b209a9fc5369007fe46676"}, 131 | {file = "graphql_core-3.2.3-py3-none-any.whl", hash = "sha256:5766780452bd5ec8ba133f8bf287dc92713e3868ddd83aee4faab9fc3e303dc3"}, 132 | ] 133 | 134 | [[package]] 135 | name = "h11" 136 | version = "0.14.0" 137 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 138 | category = "main" 139 | optional = false 140 | python-versions = ">=3.7" 141 | files = [ 142 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 143 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 144 | ] 145 | 146 | [[package]] 147 | name = "httpcore" 148 | version = "0.16.3" 149 | description = "A minimal low-level HTTP client." 150 | category = "main" 151 | optional = false 152 | python-versions = ">=3.7" 153 | files = [ 154 | {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, 155 | {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, 156 | ] 157 | 158 | [package.dependencies] 159 | anyio = ">=3.0,<5.0" 160 | certifi = "*" 161 | h11 = ">=0.13,<0.15" 162 | sniffio = ">=1.0.0,<2.0.0" 163 | 164 | [package.extras] 165 | http2 = ["h2 (>=3,<5)"] 166 | socks = ["socksio (>=1.0.0,<2.0.0)"] 167 | 168 | [[package]] 169 | name = "httpx" 170 | version = "0.23.3" 171 | description = "The next generation HTTP client." 172 | category = "main" 173 | optional = false 174 | python-versions = ">=3.7" 175 | files = [ 176 | {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, 177 | {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, 178 | ] 179 | 180 | [package.dependencies] 181 | certifi = "*" 182 | httpcore = ">=0.15.0,<0.17.0" 183 | rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} 184 | sniffio = "*" 185 | 186 | [package.extras] 187 | brotli = ["brotli", "brotlicffi"] 188 | cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] 189 | http2 = ["h2 (>=3,<5)"] 190 | socks = ["socksio (>=1.0.0,<2.0.0)"] 191 | 192 | [[package]] 193 | name = "hypothesis" 194 | version = "6.68.2" 195 | description = "A library for property-based testing" 196 | category = "main" 197 | optional = false 198 | python-versions = ">=3.7" 199 | files = [ 200 | {file = "hypothesis-6.68.2-py3-none-any.whl", hash = "sha256:2a41cc766cde52705895e54547374af89c617e8ec7bc4186cb7f03884a667d4e"}, 201 | {file = "hypothesis-6.68.2.tar.gz", hash = "sha256:a7eb2b0c9a18560d8197fe35047ceb58e7e8ab7623a3e5a82613f6a2cd71cffa"}, 202 | ] 203 | 204 | [package.dependencies] 205 | attrs = ">=19.2.0" 206 | sortedcontainers = ">=2.1.0,<3.0.0" 207 | 208 | [package.extras] 209 | all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "django (>=3.2)", "dpcontracts (>=0.4)", "importlib-metadata (>=3.6)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.9.0)", "pandas (>=1.0)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2022.7)"] 210 | cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] 211 | codemods = ["libcst (>=0.3.16)"] 212 | dateutil = ["python-dateutil (>=1.4)"] 213 | django = ["django (>=3.2)"] 214 | dpcontracts = ["dpcontracts (>=0.4)"] 215 | ghostwriter = ["black (>=19.10b0)"] 216 | lark = ["lark (>=0.10.1)"] 217 | numpy = ["numpy (>=1.9.0)"] 218 | pandas = ["pandas (>=1.0)"] 219 | pytest = ["pytest (>=4.6)"] 220 | pytz = ["pytz (>=2014.1)"] 221 | redis = ["redis (>=3.0.0)"] 222 | zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2022.7)"] 223 | 224 | [[package]] 225 | name = "hypothesis-graphql" 226 | version = "0.9.2" 227 | description = "Hypothesis strategies for GraphQL queries" 228 | category = "main" 229 | optional = false 230 | python-versions = ">=3.6,<4.0" 231 | files = [ 232 | {file = "hypothesis_graphql-0.9.2-py3-none-any.whl", hash = "sha256:3e85e49b072ff8ec02bbdb8ace62eddc57f141e2638938f9be205530c0ca2e49"}, 233 | {file = "hypothesis_graphql-0.9.2.tar.gz", hash = "sha256:3b3c242649f39d7d9bc00c548d8c6d4c592f86be95ad8bd1dc37e58a4f79cc06"}, 234 | ] 235 | 236 | [package.dependencies] 237 | attrs = ">20.3.0,<=22.1.0" 238 | graphql-core = ">=3.1.0,<3.3.0" 239 | hypothesis = ">=5.8.0,<7.0" 240 | 241 | [[package]] 242 | name = "hypothesis-jsonschema" 243 | version = "0.22.1" 244 | description = "Generate test data from JSON schemata with Hypothesis" 245 | category = "main" 246 | optional = false 247 | python-versions = ">=3.7" 248 | files = [ 249 | {file = "hypothesis-jsonschema-0.22.1.tar.gz", hash = "sha256:5dd7449009f323e408a9aa64afb4d18bd1f60ea2eabf5bf152a510da728b34f2"}, 250 | {file = "hypothesis_jsonschema-0.22.1-py3-none-any.whl", hash = "sha256:082968cb86a6aac2369627b08753cbf714c08054b1ebfce3588e3756e652cde6"}, 251 | ] 252 | 253 | [package.dependencies] 254 | hypothesis = ">=6.31.6" 255 | jsonschema = ">=4.0.0" 256 | 257 | [[package]] 258 | name = "idna" 259 | version = "3.4" 260 | description = "Internationalized Domain Names in Applications (IDNA)" 261 | category = "main" 262 | optional = false 263 | python-versions = ">=3.5" 264 | files = [ 265 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 266 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 267 | ] 268 | 269 | [[package]] 270 | name = "iniconfig" 271 | version = "2.0.0" 272 | description = "brain-dead simple config-ini parsing" 273 | category = "main" 274 | optional = false 275 | python-versions = ">=3.7" 276 | files = [ 277 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 278 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 279 | ] 280 | 281 | [[package]] 282 | name = "jsonschema" 283 | version = "4.17.3" 284 | description = "An implementation of JSON Schema validation for Python" 285 | category = "main" 286 | optional = false 287 | python-versions = ">=3.7" 288 | files = [ 289 | {file = "jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6"}, 290 | {file = "jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d"}, 291 | ] 292 | 293 | [package.dependencies] 294 | attrs = ">=17.4.0" 295 | pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" 296 | 297 | [package.extras] 298 | format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] 299 | format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] 300 | 301 | [[package]] 302 | name = "junit-xml" 303 | version = "1.9" 304 | description = "Creates JUnit XML test result documents that can be read by tools such as Jenkins" 305 | category = "main" 306 | optional = false 307 | python-versions = "*" 308 | files = [ 309 | {file = "junit-xml-1.9.tar.gz", hash = "sha256:de16a051990d4e25a3982b2dd9e89d671067548718866416faec14d9de56db9f"}, 310 | {file = "junit_xml-1.9-py2.py3-none-any.whl", hash = "sha256:ec5ca1a55aefdd76d28fcc0b135251d156c7106fa979686a4b48d62b761b4732"}, 311 | ] 312 | 313 | [package.dependencies] 314 | six = "*" 315 | 316 | [[package]] 317 | name = "markupsafe" 318 | version = "2.1.2" 319 | description = "Safely add untrusted strings to HTML/XML markup." 320 | category = "main" 321 | optional = false 322 | python-versions = ">=3.7" 323 | files = [ 324 | {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, 325 | {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, 326 | {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, 327 | {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, 328 | {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, 329 | {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, 330 | {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, 331 | {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, 332 | {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, 333 | {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, 334 | {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, 335 | {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, 336 | {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, 337 | {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, 338 | {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, 339 | {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, 340 | {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, 341 | {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, 342 | {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, 343 | {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, 344 | {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, 345 | {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, 346 | {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, 347 | {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, 348 | {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, 349 | {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, 350 | {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, 351 | {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, 352 | {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, 353 | {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, 354 | {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, 355 | {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, 356 | {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, 357 | {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, 358 | {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, 359 | {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, 360 | {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, 361 | {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, 362 | {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, 363 | {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, 364 | {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, 365 | {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, 366 | {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, 367 | {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, 368 | {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, 369 | {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, 370 | {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, 371 | {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, 372 | {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, 373 | {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, 374 | ] 375 | 376 | [[package]] 377 | name = "multidict" 378 | version = "6.0.4" 379 | description = "multidict implementation" 380 | category = "main" 381 | optional = false 382 | python-versions = ">=3.7" 383 | files = [ 384 | {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, 385 | {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, 386 | {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, 387 | {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, 388 | {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, 389 | {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, 390 | {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, 391 | {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, 392 | {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, 393 | {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, 394 | {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, 395 | {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, 396 | {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, 397 | {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, 398 | {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, 399 | {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, 400 | {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, 401 | {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, 402 | {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, 403 | {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, 404 | {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, 405 | {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, 406 | {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, 407 | {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, 408 | {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, 409 | {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, 410 | {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, 411 | {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, 412 | {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, 413 | {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, 414 | {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, 415 | {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, 416 | {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, 417 | {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, 418 | {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, 419 | {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, 420 | {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, 421 | {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, 422 | {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, 423 | {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, 424 | {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, 425 | {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, 426 | {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, 427 | {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, 428 | {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, 429 | {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, 430 | {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, 431 | {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, 432 | {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, 433 | {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, 434 | {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, 435 | {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, 436 | {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, 437 | {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, 438 | {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, 439 | {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, 440 | {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, 441 | {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, 442 | {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, 443 | {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, 444 | {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, 445 | {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, 446 | {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, 447 | {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, 448 | {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, 449 | {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, 450 | {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, 451 | {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, 452 | {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, 453 | {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, 454 | {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, 455 | {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, 456 | {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, 457 | {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, 458 | ] 459 | 460 | [[package]] 461 | name = "packaging" 462 | version = "23.0" 463 | description = "Core utilities for Python packages" 464 | category = "main" 465 | optional = false 466 | python-versions = ">=3.7" 467 | files = [ 468 | {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, 469 | {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, 470 | ] 471 | 472 | [[package]] 473 | name = "pluggy" 474 | version = "1.0.0" 475 | description = "plugin and hook calling mechanisms for python" 476 | category = "main" 477 | optional = false 478 | python-versions = ">=3.6" 479 | files = [ 480 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 481 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 482 | ] 483 | 484 | [package.extras] 485 | dev = ["pre-commit", "tox"] 486 | testing = ["pytest", "pytest-benchmark"] 487 | 488 | [[package]] 489 | name = "pyrsistent" 490 | version = "0.19.3" 491 | description = "Persistent/Functional/Immutable data structures" 492 | category = "main" 493 | optional = false 494 | python-versions = ">=3.7" 495 | files = [ 496 | {file = "pyrsistent-0.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a"}, 497 | {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64"}, 498 | {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf"}, 499 | {file = "pyrsistent-0.19.3-cp310-cp310-win32.whl", hash = "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a"}, 500 | {file = "pyrsistent-0.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da"}, 501 | {file = "pyrsistent-0.19.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9"}, 502 | {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393"}, 503 | {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19"}, 504 | {file = "pyrsistent-0.19.3-cp311-cp311-win32.whl", hash = "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3"}, 505 | {file = "pyrsistent-0.19.3-cp311-cp311-win_amd64.whl", hash = "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8"}, 506 | {file = "pyrsistent-0.19.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28"}, 507 | {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf"}, 508 | {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9"}, 509 | {file = "pyrsistent-0.19.3-cp37-cp37m-win32.whl", hash = "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1"}, 510 | {file = "pyrsistent-0.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b"}, 511 | {file = "pyrsistent-0.19.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8"}, 512 | {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a"}, 513 | {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c"}, 514 | {file = "pyrsistent-0.19.3-cp38-cp38-win32.whl", hash = "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c"}, 515 | {file = "pyrsistent-0.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7"}, 516 | {file = "pyrsistent-0.19.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc"}, 517 | {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2"}, 518 | {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3"}, 519 | {file = "pyrsistent-0.19.3-cp39-cp39-win32.whl", hash = "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2"}, 520 | {file = "pyrsistent-0.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98"}, 521 | {file = "pyrsistent-0.19.3-py3-none-any.whl", hash = "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64"}, 522 | {file = "pyrsistent-0.19.3.tar.gz", hash = "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440"}, 523 | ] 524 | 525 | [[package]] 526 | name = "pytest" 527 | version = "7.2.2" 528 | description = "pytest: simple powerful testing with Python" 529 | category = "main" 530 | optional = false 531 | python-versions = ">=3.7" 532 | files = [ 533 | {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"}, 534 | {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"}, 535 | ] 536 | 537 | [package.dependencies] 538 | attrs = ">=19.2.0" 539 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 540 | iniconfig = "*" 541 | packaging = "*" 542 | pluggy = ">=0.12,<2.0" 543 | 544 | [package.extras] 545 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 546 | 547 | [[package]] 548 | name = "pytest-dependency" 549 | version = "0.5.1" 550 | description = "Manage dependencies of tests" 551 | category = "main" 552 | optional = false 553 | python-versions = "*" 554 | files = [ 555 | {file = "pytest-dependency-0.5.1.tar.gz", hash = "sha256:c2a892906192663f85030a6ab91304e508e546cddfe557d692d61ec57a1d946b"}, 556 | ] 557 | 558 | [package.dependencies] 559 | pytest = ">=3.6.0" 560 | 561 | [[package]] 562 | name = "pytest-subtests" 563 | version = "0.7.0" 564 | description = "unittest subTest() support and subtests fixture" 565 | category = "main" 566 | optional = false 567 | python-versions = ">=3.6" 568 | files = [ 569 | {file = "pytest-subtests-0.7.0.tar.gz", hash = "sha256:95c44c77e3fbede9848bb88ca90b384815fcba8090ef9a9f55659ab163b1681c"}, 570 | {file = "pytest_subtests-0.7.0-py3-none-any.whl", hash = "sha256:2e3691caedea0c464fe96ffffd14bf872df1406b88d1930971dafe1966095bad"}, 571 | ] 572 | 573 | [package.dependencies] 574 | pytest = ">=7.0" 575 | 576 | [[package]] 577 | name = "pyyaml" 578 | version = "6.0" 579 | description = "YAML parser and emitter for Python" 580 | category = "main" 581 | optional = false 582 | python-versions = ">=3.6" 583 | files = [ 584 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 585 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, 586 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, 587 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, 588 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, 589 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, 590 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, 591 | {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, 592 | {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, 593 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, 594 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, 595 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, 596 | {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, 597 | {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, 598 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, 599 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, 600 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, 601 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, 602 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, 603 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, 604 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, 605 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, 606 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, 607 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, 608 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, 609 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, 610 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, 611 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, 612 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, 613 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, 614 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, 615 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, 616 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, 617 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, 618 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, 619 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, 620 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, 621 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, 622 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, 623 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, 624 | ] 625 | 626 | [[package]] 627 | name = "requests" 628 | version = "2.28.1" 629 | description = "Python HTTP for Humans." 630 | category = "main" 631 | optional = false 632 | python-versions = ">=3.7, <4" 633 | files = [ 634 | {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, 635 | {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, 636 | ] 637 | 638 | [package.dependencies] 639 | certifi = ">=2017.4.17" 640 | charset-normalizer = ">=2,<3" 641 | idna = ">=2.5,<4" 642 | urllib3 = ">=1.21.1,<1.27" 643 | 644 | [package.extras] 645 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 646 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 647 | 648 | [[package]] 649 | name = "rfc3986" 650 | version = "1.5.0" 651 | description = "Validating URI References per RFC 3986" 652 | category = "main" 653 | optional = false 654 | python-versions = "*" 655 | files = [ 656 | {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, 657 | {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, 658 | ] 659 | 660 | [package.dependencies] 661 | idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} 662 | 663 | [package.extras] 664 | idna2008 = ["idna"] 665 | 666 | [[package]] 667 | name = "schemathesis" 668 | version = "3.18.5" 669 | description = "Property-based testing framework for Open API and GraphQL based apps" 670 | category = "main" 671 | optional = false 672 | python-versions = ">=3.7" 673 | files = [ 674 | {file = "schemathesis-3.18.5-py3-none-any.whl", hash = "sha256:e221a080c22b3f988c880e395db75d48cb1420465240c44e032c38e89f59f512"}, 675 | {file = "schemathesis-3.18.5.tar.gz", hash = "sha256:56d0d292b0a36c781154c5f9eb3c8bd64d5396f76be9f3f9f3b3121952ffc901"}, 676 | ] 677 | 678 | [package.dependencies] 679 | attrs = ">=22.1" 680 | backoff = ">=2.1.2,<3.0" 681 | click = ">=7.0,<9.0" 682 | colorama = ">=0.4,<1.0" 683 | curlify = ">=2.2.1,<3.0" 684 | httpx = ">=0.22.0,<1.0" 685 | hypothesis = ">=6.13.3,<7" 686 | hypothesis-graphql = ">=0.9.0,<1" 687 | hypothesis-jsonschema = ">=0.22.1,<1" 688 | jsonschema = ">=4.3.2,<5.0" 689 | junit-xml = ">=1.9,<2.0" 690 | pytest = ">=4.6.4,<8" 691 | pytest-subtests = ">=0.2.1,<0.8.0" 692 | pyyaml = ">=5.1,<7.0" 693 | requests = ">=2.22,<=2.28.1" 694 | starlette = ">=0.13,<1" 695 | starlette-testclient = "0.2.0" 696 | tomli = ">=2.0.1,<3.0" 697 | tomli-w = ">=1.0.0,<2.0" 698 | typing-extensions = ">=3.7,<5" 699 | werkzeug = ">=0.16.0,<=3" 700 | yarl = ">=1.5,<2.0" 701 | 702 | [package.extras] 703 | cov = ["coverage-enable-subprocess", "coverage[toml] (>=5.3)"] 704 | dev = ["schemathesis[cov,docs,tests]"] 705 | docs = ["sphinx"] 706 | tests = ["aiohttp (>=3.8.3,<4.0)", "coverage (>=6)", "fastapi (>=0.86.0)", "flask (>=2.1.1,<3.0)", "pydantic (>=1.10.2)", "pytest-asyncio (>=0.18.0,<1.0)", "pytest-httpserver (>=1.0,<2.0)", "pytest-mock (>=3.7.0,<4.0)", "pytest-xdist (>=2.5,<3.0)", "strawberry-graphql[fastapi] (>=0.109.0)", "trustme (>=0.9.0,<1.0)"] 707 | 708 | [[package]] 709 | name = "six" 710 | version = "1.16.0" 711 | description = "Python 2 and 3 compatibility utilities" 712 | category = "main" 713 | optional = false 714 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 715 | files = [ 716 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 717 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 718 | ] 719 | 720 | [[package]] 721 | name = "sniffio" 722 | version = "1.3.0" 723 | description = "Sniff out which async library your code is running under" 724 | category = "main" 725 | optional = false 726 | python-versions = ">=3.7" 727 | files = [ 728 | {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, 729 | {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, 730 | ] 731 | 732 | [[package]] 733 | name = "sortedcontainers" 734 | version = "2.4.0" 735 | description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" 736 | category = "main" 737 | optional = false 738 | python-versions = "*" 739 | files = [ 740 | {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, 741 | {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, 742 | ] 743 | 744 | [[package]] 745 | name = "starlette" 746 | version = "0.26.0.post1" 747 | description = "The little ASGI library that shines." 748 | category = "main" 749 | optional = false 750 | python-versions = ">=3.7" 751 | files = [ 752 | {file = "starlette-0.26.0.post1-py3-none-any.whl", hash = "sha256:5b80b546ed60d43da45f80113c05ff9f4c44fae95ee884945958eba685c56253"}, 753 | {file = "starlette-0.26.0.post1.tar.gz", hash = "sha256:af0e54d08afed70fcbc53ae01e71c9c62c8ab038ff8cfd3f7477bf0f086b5ab4"}, 754 | ] 755 | 756 | [package.dependencies] 757 | anyio = ">=3.4.0,<5" 758 | 759 | [package.extras] 760 | full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] 761 | 762 | [[package]] 763 | name = "starlette-testclient" 764 | version = "0.2.0" 765 | description = "A backport of Starlette TestClient using requests! ⏪️" 766 | category = "main" 767 | optional = false 768 | python-versions = ">=3.7" 769 | files = [ 770 | {file = "starlette_testclient-0.2.0-py3-none-any.whl", hash = "sha256:dfbcceba46302d58bec086645c789032707a3bb0256d4cf0de66d40c13ded20e"}, 771 | {file = "starlette_testclient-0.2.0.tar.gz", hash = "sha256:3fb6681d1dc7e9ab6dc05b5ab455b822a03f37e7371316a828e2d8380a198a4a"}, 772 | ] 773 | 774 | [package.dependencies] 775 | requests = "*" 776 | starlette = "*" 777 | 778 | [[package]] 779 | name = "tomli" 780 | version = "2.0.1" 781 | description = "A lil' TOML parser" 782 | category = "main" 783 | optional = false 784 | python-versions = ">=3.7" 785 | files = [ 786 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 787 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 788 | ] 789 | 790 | [[package]] 791 | name = "tomli-w" 792 | version = "1.0.0" 793 | description = "A lil' TOML writer" 794 | category = "main" 795 | optional = false 796 | python-versions = ">=3.7" 797 | files = [ 798 | {file = "tomli_w-1.0.0-py3-none-any.whl", hash = "sha256:9f2a07e8be30a0729e533ec968016807069991ae2fd921a78d42f429ae5f4463"}, 799 | {file = "tomli_w-1.0.0.tar.gz", hash = "sha256:f463434305e0336248cac9c2dc8076b707d8a12d019dd349f5c1e382dd1ae1b9"}, 800 | ] 801 | 802 | [[package]] 803 | name = "typing-extensions" 804 | version = "4.5.0" 805 | description = "Backported and Experimental Type Hints for Python 3.7+" 806 | category = "main" 807 | optional = false 808 | python-versions = ">=3.7" 809 | files = [ 810 | {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, 811 | {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, 812 | ] 813 | 814 | [[package]] 815 | name = "urllib3" 816 | version = "1.26.15" 817 | description = "HTTP library with thread-safe connection pooling, file post, and more." 818 | category = "main" 819 | optional = false 820 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 821 | files = [ 822 | {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, 823 | {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, 824 | ] 825 | 826 | [package.extras] 827 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 828 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 829 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 830 | 831 | [[package]] 832 | name = "werkzeug" 833 | version = "2.2.3" 834 | description = "The comprehensive WSGI web application library." 835 | category = "main" 836 | optional = false 837 | python-versions = ">=3.7" 838 | files = [ 839 | {file = "Werkzeug-2.2.3-py3-none-any.whl", hash = "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612"}, 840 | {file = "Werkzeug-2.2.3.tar.gz", hash = "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe"}, 841 | ] 842 | 843 | [package.dependencies] 844 | MarkupSafe = ">=2.1.1" 845 | 846 | [package.extras] 847 | watchdog = ["watchdog"] 848 | 849 | [[package]] 850 | name = "yarl" 851 | version = "1.8.2" 852 | description = "Yet another URL library" 853 | category = "main" 854 | optional = false 855 | python-versions = ">=3.7" 856 | files = [ 857 | {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bb81f753c815f6b8e2ddd2eef3c855cf7da193b82396ac013c661aaa6cc6b0a5"}, 858 | {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:47d49ac96156f0928f002e2424299b2c91d9db73e08c4cd6742923a086f1c863"}, 859 | {file = "yarl-1.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3fc056e35fa6fba63248d93ff6e672c096f95f7836938241ebc8260e062832fe"}, 860 | {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58a3c13d1c3005dbbac5c9f0d3210b60220a65a999b1833aa46bd6677c69b08e"}, 861 | {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10b08293cda921157f1e7c2790999d903b3fd28cd5c208cf8826b3b508026996"}, 862 | {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de986979bbd87272fe557e0a8fcb66fd40ae2ddfe28a8b1ce4eae22681728fef"}, 863 | {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c4fcfa71e2c6a3cb568cf81aadc12768b9995323186a10827beccf5fa23d4f8"}, 864 | {file = "yarl-1.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae4d7ff1049f36accde9e1ef7301912a751e5bae0a9d142459646114c70ecba6"}, 865 | {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf071f797aec5b96abfc735ab97da9fd8f8768b43ce2abd85356a3127909d146"}, 866 | {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:74dece2bfc60f0f70907c34b857ee98f2c6dd0f75185db133770cd67300d505f"}, 867 | {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:df60a94d332158b444301c7f569659c926168e4d4aad2cfbf4bce0e8fb8be826"}, 868 | {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:63243b21c6e28ec2375f932a10ce7eda65139b5b854c0f6b82ed945ba526bff3"}, 869 | {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cfa2bbca929aa742b5084fd4663dd4b87c191c844326fcb21c3afd2d11497f80"}, 870 | {file = "yarl-1.8.2-cp310-cp310-win32.whl", hash = "sha256:b05df9ea7496df11b710081bd90ecc3a3db6adb4fee36f6a411e7bc91a18aa42"}, 871 | {file = "yarl-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:24ad1d10c9db1953291f56b5fe76203977f1ed05f82d09ec97acb623a7976574"}, 872 | {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2a1fca9588f360036242f379bfea2b8b44cae2721859b1c56d033adfd5893634"}, 873 | {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f37db05c6051eff17bc832914fe46869f8849de5b92dc4a3466cd63095d23dfd"}, 874 | {file = "yarl-1.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77e913b846a6b9c5f767b14dc1e759e5aff05502fe73079f6f4176359d832581"}, 875 | {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0978f29222e649c351b173da2b9b4665ad1feb8d1daa9d971eb90df08702668a"}, 876 | {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388a45dc77198b2460eac0aca1efd6a7c09e976ee768b0d5109173e521a19daf"}, 877 | {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2305517e332a862ef75be8fad3606ea10108662bc6fe08509d5ca99503ac2aee"}, 878 | {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42430ff511571940d51e75cf42f1e4dbdded477e71c1b7a17f4da76c1da8ea76"}, 879 | {file = "yarl-1.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3150078118f62371375e1e69b13b48288e44f6691c1069340081c3fd12c94d5b"}, 880 | {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c15163b6125db87c8f53c98baa5e785782078fbd2dbeaa04c6141935eb6dab7a"}, 881 | {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4d04acba75c72e6eb90745447d69f84e6c9056390f7a9724605ca9c56b4afcc6"}, 882 | {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e7fd20d6576c10306dea2d6a5765f46f0ac5d6f53436217913e952d19237efc4"}, 883 | {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:75c16b2a900b3536dfc7014905a128a2bea8fb01f9ee26d2d7d8db0a08e7cb2c"}, 884 | {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6d88056a04860a98341a0cf53e950e3ac9f4e51d1b6f61a53b0609df342cc8b2"}, 885 | {file = "yarl-1.8.2-cp311-cp311-win32.whl", hash = "sha256:fb742dcdd5eec9f26b61224c23baea46c9055cf16f62475e11b9b15dfd5c117b"}, 886 | {file = "yarl-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8c46d3d89902c393a1d1e243ac847e0442d0196bbd81aecc94fcebbc2fd5857c"}, 887 | {file = "yarl-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ceff9722e0df2e0a9e8a79c610842004fa54e5b309fe6d218e47cd52f791d7ef"}, 888 | {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f6b4aca43b602ba0f1459de647af954769919c4714706be36af670a5f44c9c1"}, 889 | {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1684a9bd9077e922300ecd48003ddae7a7474e0412bea38d4631443a91d61077"}, 890 | {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebb78745273e51b9832ef90c0898501006670d6e059f2cdb0e999494eb1450c2"}, 891 | {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3adeef150d528ded2a8e734ebf9ae2e658f4c49bf413f5f157a470e17a4a2e89"}, 892 | {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57a7c87927a468e5a1dc60c17caf9597161d66457a34273ab1760219953f7f4c"}, 893 | {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:efff27bd8cbe1f9bd127e7894942ccc20c857aa8b5a0327874f30201e5ce83d0"}, 894 | {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a783cd344113cb88c5ff7ca32f1f16532a6f2142185147822187913eb989f739"}, 895 | {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:705227dccbe96ab02c7cb2c43e1228e2826e7ead880bb19ec94ef279e9555b5b"}, 896 | {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:34c09b43bd538bf6c4b891ecce94b6fa4f1f10663a8d4ca589a079a5018f6ed7"}, 897 | {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a48f4f7fea9a51098b02209d90297ac324241bf37ff6be6d2b0149ab2bd51b37"}, 898 | {file = "yarl-1.8.2-cp37-cp37m-win32.whl", hash = "sha256:0414fd91ce0b763d4eadb4456795b307a71524dbacd015c657bb2a39db2eab89"}, 899 | {file = "yarl-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d881d152ae0007809c2c02e22aa534e702f12071e6b285e90945aa3c376463c5"}, 900 | {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5df5e3d04101c1e5c3b1d69710b0574171cc02fddc4b23d1b2813e75f35a30b1"}, 901 | {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7a66c506ec67eb3159eea5096acd05f5e788ceec7b96087d30c7d2865a243918"}, 902 | {file = "yarl-1.8.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2b4fa2606adf392051d990c3b3877d768771adc3faf2e117b9de7eb977741229"}, 903 | {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e21fb44e1eff06dd6ef971d4bdc611807d6bd3691223d9c01a18cec3677939e"}, 904 | {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93202666046d9edadfe9f2e7bf5e0782ea0d497b6d63da322e541665d65a044e"}, 905 | {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc77086ce244453e074e445104f0ecb27530d6fd3a46698e33f6c38951d5a0f1"}, 906 | {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dd68a92cab699a233641f5929a40f02a4ede8c009068ca8aa1fe87b8c20ae3"}, 907 | {file = "yarl-1.8.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b372aad2b5f81db66ee7ec085cbad72c4da660d994e8e590c997e9b01e44901"}, 908 | {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e6f3515aafe0209dd17fb9bdd3b4e892963370b3de781f53e1746a521fb39fc0"}, 909 | {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dfef7350ee369197106805e193d420b75467b6cceac646ea5ed3049fcc950a05"}, 910 | {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:728be34f70a190566d20aa13dc1f01dc44b6aa74580e10a3fb159691bc76909d"}, 911 | {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ff205b58dc2929191f68162633d5e10e8044398d7a45265f90a0f1d51f85f72c"}, 912 | {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf211dcad448a87a0d9047dc8282d7de59473ade7d7fdf22150b1d23859f946"}, 913 | {file = "yarl-1.8.2-cp38-cp38-win32.whl", hash = "sha256:272b4f1599f1b621bf2aabe4e5b54f39a933971f4e7c9aa311d6d7dc06965165"}, 914 | {file = "yarl-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:326dd1d3caf910cd26a26ccbfb84c03b608ba32499b5d6eeb09252c920bcbe4f"}, 915 | {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f8ca8ad414c85bbc50f49c0a106f951613dfa5f948ab69c10ce9b128d368baf8"}, 916 | {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:418857f837347e8aaef682679f41e36c24250097f9e2f315d39bae3a99a34cbf"}, 917 | {file = "yarl-1.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ae0eec05ab49e91a78700761777f284c2df119376e391db42c38ab46fd662b77"}, 918 | {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:009a028127e0a1755c38b03244c0bea9d5565630db9c4cf9572496e947137a87"}, 919 | {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3edac5d74bb3209c418805bda77f973117836e1de7c000e9755e572c1f7850d0"}, 920 | {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da65c3f263729e47351261351b8679c6429151ef9649bba08ef2528ff2c423b2"}, 921 | {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ef8fb25e52663a1c85d608f6dd72e19bd390e2ecaf29c17fb08f730226e3a08"}, 922 | {file = "yarl-1.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcd7bb1e5c45274af9a1dd7494d3c52b2be5e6bd8d7e49c612705fd45420b12d"}, 923 | {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44ceac0450e648de86da8e42674f9b7077d763ea80c8ceb9d1c3e41f0f0a9951"}, 924 | {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:97209cc91189b48e7cfe777237c04af8e7cc51eb369004e061809bcdf4e55220"}, 925 | {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:48dd18adcf98ea9cd721a25313aef49d70d413a999d7d89df44f469edfb38a06"}, 926 | {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e59399dda559688461762800d7fb34d9e8a6a7444fd76ec33220a926c8be1516"}, 927 | {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d617c241c8c3ad5c4e78a08429fa49e4b04bedfc507b34b4d8dceb83b4af3588"}, 928 | {file = "yarl-1.8.2-cp39-cp39-win32.whl", hash = "sha256:cb6d48d80a41f68de41212f3dfd1a9d9898d7841c8f7ce6696cf2fd9cb57ef83"}, 929 | {file = "yarl-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:6604711362f2dbf7160df21c416f81fac0de6dbcf0b5445a2ef25478ecc4c778"}, 930 | {file = "yarl-1.8.2.tar.gz", hash = "sha256:49d43402c6e3013ad0978602bf6bf5328535c48d192304b91b97a3c6790b1562"}, 931 | ] 932 | 933 | [package.dependencies] 934 | idna = ">=2.0" 935 | multidict = ">=4.0" 936 | 937 | [metadata] 938 | lock-version = "2.0" 939 | python-versions = "^3.11" 940 | content-hash = "3568925e0a85d27b610fb8190b252de1d9c6578d9f6afb15802af918d1fcf588" 941 | -------------------------------------------------------------------------------- /tests/SchemaThesisTests/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "schemathesistests" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["James Linnell "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.11" 10 | schemathesis = "^3.18.5" 11 | pytest-dependency = "^0.5.1" 12 | 13 | 14 | [build-system] 15 | requires = ["poetry-core"] 16 | build-backend = "poetry.core.masonry.api" 17 | -------------------------------------------------------------------------------- /tests/SchemaThesisTests/schemathesistests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamamp/FloatplaneAPI/396baa47d462197681dc649f2d2e26b56857f41b/tests/SchemaThesisTests/schemathesistests/__init__.py -------------------------------------------------------------------------------- /tests/SchemaThesisTests/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamamp/FloatplaneAPI/396baa47d462197681dc649f2d2e26b56857f41b/tests/SchemaThesisTests/tests/__init__.py -------------------------------------------------------------------------------- /tests/SchemaThesisTests/tests/test_AuthV3API.py: -------------------------------------------------------------------------------- 1 | import schemathesis 2 | 3 | schema = schemathesis.from_path("../../src/floatplane-openapi-specification-trimmed.json", base_url="https://floatplane.com") 4 | 5 | @schema.parametrize(endpoint="/api/v3/auth/captcha/info") 6 | def test_AuthAPIV3_CaptchaInfo(case): 7 | case.call_and_validate() 8 | -------------------------------------------------------------------------------- /tests/SchemaThesisTests/tests/test_Flow.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import schemathesis 3 | from urllib.parse import urljoin 4 | import os 5 | import pytest 6 | import time 7 | import random 8 | import json 9 | 10 | # https://stackoverflow.com/a/51026159/464870 11 | class FPSession(requests.Session): 12 | def __init__(self, base_url=None): 13 | super().__init__() 14 | self.base_url = base_url 15 | self.headers.update({ 16 | "User-Agent": "Floatplane API Docs Integration and Regression Automated Tests v0.1.0, CFNetwork", 17 | }) 18 | self.cookies.set("sails.sid", os.environ["SAILS_SID"]) 19 | 20 | def request(self, method, url, *args, **kwargs): 21 | joined_url = urljoin(self.base_url, url) 22 | return super().request(method, joined_url, *args, **kwargs) 23 | 24 | class TestFPAPIFlow(): 25 | schema = schemathesis.from_path("../../src/floatplane-openapi-specification-trimmed.json", base_url="https://floatplane.com") 26 | session = FPSession("https://floatplane.com") 27 | 28 | def getValidateAndAssert(self, path: str, status: list[int] = [requests.codes.ok], params: dict = None) -> requests.Response: 29 | time.sleep(1) 30 | response = self.session.get(path, params=params) 31 | print("Request GET " + path + " " + str(params) + ": " + str(response.status_code) + " and " + str(len(response.content)) + " bytes") 32 | # print("Response text: " + response.text) 33 | self.schema[path]["GET"].validate_response(response) 34 | assert response.status_code in status, response.text 35 | if response.status_code != requests.codes.ok: 36 | print("WARN: Response was not 200 OK. Instead: " + str(response.status_code)) 37 | return response 38 | 39 | """ 40 | First level of tests. 41 | Dependencies: None 42 | Retrieves: Subscriptions, top-level lists (list of creators), user information, etc. 43 | """ 44 | subscribedCreatorIds: set[str] = set() 45 | subscribedLivestreamIds: set[str] = set() 46 | creatorIds: set[str] = set() 47 | creatorUrlNames: set[str] = set() 48 | creatorOwnerIds: set[str] = set() 49 | 50 | @pytest.mark.dependency() 51 | def test_CreatorSubscriptionPlanV2(self): 52 | print() 53 | print("SubscriptionsV3 List User Subscriptions") 54 | response = self.getValidateAndAssert("/api/v3/user/subscriptions") 55 | self.subscribedCreatorIds.update([sub["creator"] for sub in response.json()]) 56 | 57 | @pytest.mark.dependency() 58 | def test_LoadCreators(self): 59 | print() 60 | print("CreatorV3 Get Creators") 61 | response = self.getValidateAndAssert("/api/v3/creator/list") 62 | self.creatorIds.update([creator["id"] for creator in response.json()]) 63 | self.creatorUrlNames.update([creator["urlname"] for creator in response.json()]) 64 | self.creatorOwnerIds.update([creator["owner"]["id"] for creator in response.json()]) 65 | self.subscribedLivestreamIds.update([creator["liveStream"]["id"] for creator in response.json() if creator["id"] in self.subscribedCreatorIds]) 66 | 67 | @pytest.mark.dependency() 68 | def test_EdgesV2(self): 69 | print() 70 | print("EdgesV2 Get Edges") 71 | response = self.getValidateAndAssert("/api/v2/edges") 72 | 73 | @pytest.mark.dependency() 74 | def test_PaymentsV2(self): 75 | print() 76 | print("PaymentsV2 List Payment Methods") 77 | response = self.getValidateAndAssert("/api/v2/payment/method/list") 78 | 79 | print("PaymentsV2 List Address") 80 | response = self.getValidateAndAssert("/api/v2/payment/address/list") 81 | 82 | print("PaymentsV2 List Invoices") 83 | response = self.getValidateAndAssert("/api/v2/payment/invoice/list") 84 | 85 | @pytest.mark.dependency() 86 | def test_FAQV2(self): 87 | print() 88 | print("FAQV2 Get FAQs") 89 | response = self.getValidateAndAssert("/api/v2/faq/list") 90 | 91 | @pytest.mark.dependency() 92 | def test_ConnectedAccountsV2(self): 93 | print() 94 | print("ConnectedAccountsV2 List Connections") 95 | response = self.getValidateAndAssert("/api/v2/connect/list") 96 | 97 | @pytest.mark.dependency() 98 | def test_UserV2BanStatus(self): 99 | print() 100 | print("V2 Get User Creator Ban Status") 101 | for creatorId in self.subscribedCreatorIds: 102 | response = self.getValidateAndAssert("/api/v2/user/ban/status", params={"creator": creatorId}) 103 | 104 | """ 105 | Second level of tests. 106 | Dependencies: Creator names/ids 107 | Retrieves: Creator information, Subscription Plan information 108 | """ 109 | 110 | @pytest.mark.dependency(depends=["TestFPAPIFlow::test_LoadCreators", "TestFPAPIFlow::test_CreatorSubscriptionPlanV2"]) 111 | def test_CreatorSubscriptionPlans(self): 112 | print() 113 | print("CreatorSubscriptionPlanV2 Get Creator Sub Info Public") 114 | for creatorId in self.creatorIds: 115 | response = self.getValidateAndAssert("/api/v2/plan/info", params={"creatorId": creatorId}) 116 | 117 | @pytest.mark.dependency(depends=["TestFPAPIFlow::test_LoadCreators", "TestFPAPIFlow::test_CreatorSubscriptionPlanV2"]) 118 | def test_CreatorV2GetInfo(self): 119 | print() 120 | print("CreatorV2 Get Info") 121 | for creatorId in self.creatorIds: 122 | response = self.getValidateAndAssert("/api/v2/creator/info", params={"creatorGUID[0]": creatorId}) 123 | 124 | @pytest.mark.dependency(depends=["TestFPAPIFlow::test_LoadCreators", "TestFPAPIFlow::test_CreatorSubscriptionPlanV2"]) 125 | def test_CreatorV2GetInfoByName(self): 126 | print() 127 | print("CreatorV2 Get Info By Name") 128 | for creatorUrlName in self.creatorUrlNames: 129 | response = self.getValidateAndAssert("/api/v2/creator/named", params={"creatorURL[0]": creatorUrlName}) 130 | assert len(response.json()) > 0 131 | 132 | @pytest.mark.dependency(depends=["TestFPAPIFlow::test_LoadCreators", "TestFPAPIFlow::test_CreatorSubscriptionPlanV2"]) 133 | def test_CreatorV3GetCreator(self): 134 | print() 135 | print("CreatorV3 Get Creator") 136 | for creatorId in self.creatorIds: 137 | response = self.getValidateAndAssert("/api/v3/creator/info", params={"id": creatorId}) 138 | 139 | @pytest.mark.dependency(depends=["TestFPAPIFlow::test_LoadCreators", "TestFPAPIFlow::test_CreatorSubscriptionPlanV2"]) 140 | def test_CreatorV3GetCreatoryName(self): 141 | print() 142 | print("CreatorV3 Get Creator by Name") 143 | for creatorUrlName in self.creatorUrlNames: 144 | response = self.getValidateAndAssert("/api/v3/creator/named", params={"creatorURL[0]": creatorUrlName}) 145 | assert len(response.json()) > 0 146 | 147 | @pytest.mark.dependency(depends=["TestFPAPIFlow::test_LoadCreators", "TestFPAPIFlow::test_CreatorSubscriptionPlanV2"]) 148 | def test_CreatorV3ListCreatorChannels(self): 149 | print() 150 | print("CreatorV3 List Creator Channels") 151 | for creatorId in self.creatorIds: 152 | response = self.getValidateAndAssert("/api/v3/creator/channels/list", params={"ids[0]": creatorId}) 153 | assert len(response.json()) > 0 154 | 155 | @pytest.mark.dependency(depends=["TestFPAPIFlow::test_LoadCreators", "TestFPAPIFlow::test_CreatorSubscriptionPlanV2"]) 156 | def test_ContentV3GetContentTags(self): 157 | print() 158 | print("V3 Get Content Tags") 159 | for creatorId in self.creatorIds: 160 | response = self.getValidateAndAssert("/api/v3/content/tags", params={"creatorIds[0]": creatorId}) 161 | 162 | """ 163 | Third level of tests. 164 | Dependencies: Creator names/ids 165 | Retrieves: Content Lists 166 | """ 167 | blogPostIds: set[(str, str)] = set() 168 | 169 | @pytest.mark.dependency(depends=["TestFPAPIFlow::test_LoadCreators", "TestFPAPIFlow::test_CreatorSubscriptionPlanV2"]) 170 | def test_ContentV3GetCreatorBlogPosts(self): 171 | print() 172 | print("V3 Get Creator Blog Posts") 173 | limit = 2 # Get 2 per page 174 | for creatorId in self.creatorIds: 175 | # Get latest and earliest 176 | for sort in ["DESC", "ASC"]: 177 | # Get various post type combinations 178 | for type in [{}, {"hasVideo": True}, {"hasAudio": True}, {"hasPicture": True}, {"hasText": True}]: 179 | # Get posts 180 | params = dict({"id": creatorId, "limit": limit, "sort": sort}, **type) 181 | response = self.getValidateAndAssert("/api/v3/content/creator", params=params) 182 | self.blogPostIds.update([(creatorId, blogPost["id"]) for blogPost in response.json()]) 183 | 184 | @pytest.mark.dependency(depends=["TestFPAPIFlow::test_LoadCreators", "TestFPAPIFlow::test_CreatorSubscriptionPlanV2"]) 185 | def test_ContentV3GetMultiCreatorBlogPosts(self): 186 | print() 187 | print("V3 Get Multi Creator Blog Posts") 188 | params = dict() 189 | for (i, creatorId) in enumerate(self.subscribedCreatorIds): 190 | params["ids[" + str(i) + "]"] = creatorId 191 | response = self.getValidateAndAssert("/api/v3/content/creator/list", params=params) 192 | 193 | params["fetchAfter"] = json.dumps(response.json()["lastElements"]) 194 | response = self.getValidateAndAssert("/api/v3/content/creator/list", params=params) 195 | 196 | """ 197 | Fourth level of tests. 198 | Dependencies: Content ids 199 | Retrieves: Content information, content comments 200 | """ 201 | videoAttachmentIds: set[(str, str, str)] = set() 202 | audioAttachmentIds: set[(str, str, str)] = set() 203 | pictureAttachmentIds: set[(str, str, str)] = set() 204 | 205 | @pytest.mark.dependency(depends=["TestFPAPIFlow::test_ContentV3GetCreatorBlogPosts"]) 206 | def test_ContentV3GetBlogPost(self): 207 | print() 208 | print("V3 Get Blog Post") 209 | for (creatorId, blogPostId) in self.blogPostIds: 210 | if creatorId not in self.subscribedCreatorIds: 211 | continue 212 | response = self.getValidateAndAssert("/api/v3/content/post", status=[requests.codes.ok, requests.codes.forbidden], params={"id": blogPostId}) 213 | self.videoAttachmentIds.update([(creatorId, blogPostId, x["id"]) for x in response.json()["videoAttachments"]]) 214 | self.audioAttachmentIds.update([(creatorId, blogPostId, x["id"]) for x in response.json()["audioAttachments"]]) 215 | self.pictureAttachmentIds.update([(creatorId, blogPostId, x["id"]) for x in response.json()["pictureAttachments"]]) 216 | 217 | @pytest.mark.dependency(depends=["TestFPAPIFlow::test_ContentV3GetCreatorBlogPosts"]) 218 | def test_ContentV3GetRelatedBlogPosts(self): 219 | print() 220 | print("V3 Get Related Blog Posts") 221 | for (creatorId, blogPostId) in self.blogPostIds: 222 | if creatorId not in self.subscribedCreatorIds: 223 | continue 224 | response = self.getValidateAndAssert("/api/v3/content/related", status=[requests.codes.ok, requests.codes.forbidden], params={"id": blogPostId}) 225 | 226 | """ 227 | Fourth level of tests. 228 | Dependencies: Content ids 229 | Retrieves: Attachment information 230 | """ 231 | 232 | @pytest.mark.dependency(depends=["TestFPAPIFlow::test_ContentV3GetBlogPost"]) 233 | def test_ContentV3GetVideoContent(self): 234 | print() 235 | print("V3 Get Video Content") 236 | for (creatorId, blogPostId, videoAttachmentId) in self.videoAttachmentIds: 237 | if creatorId not in self.subscribedCreatorIds: 238 | continue 239 | response = self.getValidateAndAssert("/api/v3/content/video", status=[requests.codes.ok, requests.codes.forbidden], params={"id": videoAttachmentId}) 240 | 241 | @pytest.mark.dependency(depends=["TestFPAPIFlow::test_ContentV3GetBlogPost"]) 242 | def test_ContentV3GetPictureContent(self): 243 | print() 244 | print("V3 Get Picture Content") 245 | for (creatorId, blogPostId, pictureAttachmentId) in self.pictureAttachmentIds: 246 | if creatorId not in self.subscribedCreatorIds: 247 | continue 248 | response = self.getValidateAndAssert("/api/v3/content/picture", status=[requests.codes.ok, requests.codes.forbidden], params={"id": pictureAttachmentId}) 249 | 250 | @pytest.mark.dependency(depends=["TestFPAPIFlow::test_ContentV3GetBlogPost"]) 251 | def test_DeliveryV3GetDeliveryInfo(self): 252 | print() 253 | print("V3 Get Delivery Info - On Demand") 254 | limit = 1 255 | sleepDuration = 35 # 35 seconds inbetween each. Current rate limit is 2 req per min 256 | for (creatorId, blogPostId, videoAttachmentId) in random.sample(list(self.videoAttachmentIds), limit): 257 | if creatorId not in self.subscribedCreatorIds: 258 | continue 259 | response = self.getValidateAndAssert("/api/v3/delivery/info", status=[requests.codes.ok, requests.codes.forbidden], params={"scenario": "onDemand", "entityId": videoAttachmentId}) 260 | time.sleep(sleepDuration) 261 | 262 | print("V3 Get Delivery Info - Download") 263 | for (creatorId, blogPostId, videoAttachmentId) in random.sample(list(self.videoAttachmentIds), limit): 264 | if creatorId not in self.subscribedCreatorIds: 265 | continue 266 | response = self.getValidateAndAssert("/api/v3/delivery/info", status=[requests.codes.ok, requests.codes.forbidden], params={"scenario": "download", "entityId": videoAttachmentId}) 267 | time.sleep(sleepDuration) 268 | 269 | print("V3 Get Delivery Info - Livestream") 270 | for liveStreamId in self.subscribedLivestreamIds: 271 | response = self.getValidateAndAssert("/api/v3/delivery/info", status=[requests.codes.ok, requests.codes.forbidden], params={"scenario": "live", "entityId": liveStreamId}) 272 | time.sleep(sleepDuration) 273 | 274 | """ 275 | Sixth level of tests. 276 | Dependencies: User ids/names 277 | Retrieves: User information 278 | """ 279 | 280 | @pytest.mark.dependency(depends=[]) 281 | def test_UserV2V3GetSelf(self): 282 | print() 283 | print("V3 Get Self") 284 | response = self.getValidateAndAssert("/api/v3/user/self") 285 | id = response.json()["id"] 286 | username = response.json()["username"] 287 | 288 | print() 289 | print("V3 Get User Notification List") 290 | response = self.getValidateAndAssert("/api/v3/user/notification/list") 291 | 292 | print() 293 | print("V3 Get External Activity") 294 | response = self.getValidateAndAssert("/api/v3/user/activity", params={"id": id}) 295 | 296 | print() 297 | print("V2 Get User Info") 298 | response = self.getValidateAndAssert("/api/v2/user/info", params={"id": id}) 299 | 300 | print() 301 | print("V2 Get User Info By Name") 302 | response = self.getValidateAndAssert("/api/v2/user/named", params={"username": username}) 303 | 304 | print() 305 | print("V2 Get User Security") 306 | response = self.getValidateAndAssert("/api/v2/user/security") 307 | -------------------------------------------------------------------------------- /tests/openapitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", 3 | "spaces": 4, 4 | "generator-cli": { 5 | "version": "6.0.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tools/find-nullables.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | function forObjectProperty(theObject, propertyName, doFunc, path) { 4 | Object.keys(theObject).forEach(el => { 5 | if (el == propertyName) { 6 | doFunc(theObject, path); 7 | } 8 | 9 | if (typeof theObject[el] === "object") { 10 | forObjectProperty(theObject[el], propertyName, doFunc, path + "." + el); 11 | } 12 | }); 13 | } 14 | 15 | function difference(setA, setB) { 16 | const _difference = new Set(setA); 17 | for (const elem of setB) { 18 | _difference.delete(elem); 19 | } 20 | return _difference; 21 | } 22 | 23 | fs.readFile("../floatplane-openapi-specification.json", "utf8", (err, data) => { 24 | if (err) { 25 | console.log("Could not read ../floatplane-openapi-specification.json"); 26 | } else { 27 | const spec = JSON.parse(data); 28 | 29 | forObjectProperty(spec.components.schemas, "properties", (schemaObject, path) => { 30 | let propertyKeys = Object.keys(schemaObject["properties"]); 31 | let requiredKeys = schemaObject["required"] || []; 32 | 33 | let leftoverKeys = difference(propertyKeys, requiredKeys); 34 | // console.log("======================"); 35 | // console.log(propertyKeys); 36 | // console.log(requiredKeys); 37 | // console.log(leftoverKeys); 38 | if (leftoverKeys.size > 0) { 39 | console.log(path, Array.from(leftoverKeys)); 40 | } 41 | }, "spec.components.schemas"); 42 | } 43 | }); 44 | 45 | -------------------------------------------------------------------------------- /tools/fp-frontend-diff-2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$1" ] || [ -z "$2" ] 4 | then 5 | echo "Usage: ./fp-frontend-diff.sh " 6 | exit 1 7 | fi 8 | 9 | before=${1//./\\.} 10 | after=${2//./\\.} 11 | sed -i.bak -e "s/$before/theversion/g" Frontend/$1/main.js 12 | sed -i.bak -e "s/$after/theversion/g" Frontend/$2/main.js 13 | 14 | paths=("/runtime.js" "/polyfills.js" "/scripts.js" "/main.js") 15 | for path in "${paths[@]}"; do 16 | diff -q Frontend/$1$path Frontend/$2$path 17 | done 18 | -------------------------------------------------------------------------------- /tools/fp-frontend-diff.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$1" ] || [ -z "$2" ] 4 | then 5 | echo "Usage: ./fp-frontend-diff.sh " 6 | exit 1 7 | fi 8 | 9 | before=${1//./\\.} 10 | after=${2//./\\.} 11 | sed -i.bak -e "s/$before/theversion/g" Frontend/$1/app.js 12 | sed -i.bak -e "s/$after/theversion/g" Frontend/$2/app.js 13 | 14 | paths=("/app.js" "/vendor.js" "/assets/js/dependencies.js" "/assets/js/video/shaka.mint.js" "/assets/js/video/video.min.js" "/assets/js/video/plugin/chromecast.min.js" "/assets/js/video/plugin/contrib-hls.mint.js" "/assets/js/video/plugin/contrib-shaka.es6.js" "/assets/js/video/plugin/hotkeys.min.js" "/assets/js/video/plugin/resolution-switcher.min.js" "/assets/js/video/plugin/thumbnails.js") 15 | for path in "${paths[@]}"; do 16 | diff -q Frontend/$1$path Frontend/$2$path 17 | done 18 | -------------------------------------------------------------------------------- /tools/fp-frontend-fetch-2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -z "$1" ] 3 | then 4 | echo "Usage: ./fp-frontend-fetch.sh " 5 | exit 1 6 | fi 7 | 8 | root="https://frontend.floatplane.com/" 9 | echo Downloading Floatplane frontend version $root$1 ... 10 | paths=("/runtime.js" "/polyfills.js" "/scripts.js" "/main.js") 11 | mkdir -p Frontend/$1 12 | for path in "${paths[@]}"; do 13 | echo "$root$1$path" 14 | wget "$root$1$path" -O "Frontend/$1$path" -q 15 | done 16 | 17 | echo "Done fetching frontend files for $1. Run 'prettier --write Frontend' to un-minify the files so they can be diffed, and then './fp-frontend-diff.sh $1 ' to get a quick diff summary." 18 | -------------------------------------------------------------------------------- /tools/fp-frontend-fetch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -z "$1" ] 3 | then 4 | echo "Usage: ./fp-frontend-fetch.sh " 5 | exit 1 6 | fi 7 | 8 | root="https://frontend.floatplane.com/" 9 | echo Downloading Floatplane frontend version $root$1 ... 10 | mkdir -p Frontend/$1/assets/js/video/plugin 11 | paths=("/app.js" "/vendor.js" "/assets/js/dependencies.js" "/assets/js/video/shaka.mint.js" "/assets/js/video/video.min.js" "/assets/js/video/plugin/chromecast.min.js" "/assets/js/video/plugin/contrib-hls.mint.js" "/assets/js/video/plugin/contrib-shaka.es6.js" "/assets/js/video/plugin/hotkeys.min.js" "/assets/js/video/plugin/resolution-switcher.min.js" "/assets/js/video/plugin/thumbnails.js") 12 | for path in "${paths[@]}"; do 13 | echo "$root$1$path" 14 | wget "$root$1$path" -O "Frontend/$1$path" -q 15 | done 16 | 17 | echo "Done fetching frontend files for $1. Run 'prettier --write Frontend' to un-minify the files so they can be diffed, and then './fp-frontend-diff.sh $1 ' to get a quick diff summary." 18 | -------------------------------------------------------------------------------- /tools/trim.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | // Nothing to trim for either Frontend nor Chat AsyncAPI files. 4 | let fromFile = process.argv[2]; 5 | let toFile = process.argv[3]; 6 | 7 | fs.readFile(fromFile, "utf8", (err, data) => { 8 | if (err) { 9 | console.log("Could not read " + fromFile); 10 | } else { 11 | const spec = JSON.parse(data); 12 | 13 | for (var path in spec.paths) { 14 | for (var method in spec.paths[path]) { 15 | if (spec.paths[path][method].description.indexOf("TODO") == 0) { 16 | console.log("Removed: " + path + " " + method); 17 | delete spec.paths[path][method]; 18 | } 19 | } 20 | } 21 | 22 | for (var path in spec.paths) { 23 | if (Object.keys(spec.paths[path]).length == 0) { 24 | delete spec.paths[path]; 25 | } 26 | } 27 | 28 | var tagsToRemove = []; 29 | for (var tag in spec.tags) { 30 | if (!Object.keys(spec.paths).flatMap(path => Object.keys(spec.paths[path]).map(operation => spec.paths[path][operation])).some(op => op.tags.some(t => t == spec.tags[tag].name))) { 31 | console.log("Removing tag " + spec.tags[tag].name); 32 | tagsToRemove.push(spec.tags[tag].name); 33 | } 34 | } 35 | for (var tagToRemove of tagsToRemove) { 36 | spec.tags = spec.tags.filter(tag => tag.name != tagToRemove); 37 | } 38 | 39 | fs.writeFile(toFile, JSON.stringify(spec, null, 4), "utf8", (err) => { 40 | if (err) { 41 | console.log("Cound not write to " + toFile); 42 | } else { 43 | console.log("Done"); 44 | } 45 | }); 46 | } 47 | }); 48 | --------------------------------------------------------------------------------