diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2509fdf..71a6437 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,14 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' pull_request: branches-ignore: - 'stl-preview-head/**' diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 098c0d1..b0cac61 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -18,5 +18,5 @@ jobs: run: | bash ./bin/check-release-environment env: - PACKAGIST_USERNAME: ${{ secrets.BEEPER_DESKTOP_PACKAGIST_USERNAME || secrets.PACKAGIST_USERNAME }} - PACKAGIST_SAFE_KEY: ${{ secrets.BEEPER_DESKTOP_PACKAGIST_SAFE_KEY || secrets.PACKAGIST_SAFE_KEY }} + PACKAGIST_USERNAME: ${{ secrets.BEEPER_PACKAGIST_USERNAME || secrets.PACKAGIST_USERNAME }} + PACKAGIST_SAFE_KEY: ${{ secrets.BEEPER_PACKAGIST_SAFE_KEY || secrets.PACKAGIST_SAFE_KEY }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1332969..3d2ac0b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.0.1" + ".": "0.1.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 8ec4701..ec75571 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-774bb08472b6bb14c280fe5b767925675516b5c8ccc0b89b5abd7ac7bc30fe5a.yml -openapi_spec_hash: ddd1ce1f334b45206ac008b0f5296842 -config_hash: b5ac0c1579dfe6257bcdb84cfd1002fc +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-356444646dafe352d3ef7c2e01aedf030197a5519b41cf2c3fd8be2571456b43.yml +openapi_spec_hash: 4840f003552e8b48eb8e689b59a819ef +config_hash: 05ebdec072113f63395372504da98192 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5f88220 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,46 @@ +# Changelog + +## 0.1.0 (2026-05-01) + +Full Changelog: [v0.0.1...v0.1.0](https://github.com/beeper/desktop-api-php/compare/v0.0.1...v0.1.0) + +### Features + +* **api:** add network, bridge fields to accounts ([8b25044](https://github.com/beeper/desktop-api-php/commit/8b250446d8d538fc1027c9df483f07034049e302)) +* **api:** api update ([e098615](https://github.com/beeper/desktop-api-php/commit/e0986159d91fbd76d8647de9e2f7e2f1aedb071f)) +* **api:** api update ([fe6b606](https://github.com/beeper/desktop-api-php/commit/fe6b606f1e40643b6f81a68bfbf467653d070a6a)) +* **api:** api update ([f813329](https://github.com/beeper/desktop-api-php/commit/f8133290552840e3723a0f67371a4481039c5ac6)) +* **api:** api update ([1c754b3](https://github.com/beeper/desktop-api-php/commit/1c754b319cf35db0357cd97d9713119b78f70fc5)) +* **api:** manual updates ([46dbc09](https://github.com/beeper/desktop-api-php/commit/46dbc095defeac9aa95e41cd668e883fcd20bea9)) +* **api:** update via SDK Studio ([1c4d0e7](https://github.com/beeper/desktop-api-php/commit/1c4d0e7cb265c3bfcca18b886ec84d25eb7154d2)) +* **api:** update via SDK Studio ([e372296](https://github.com/beeper/desktop-api-php/commit/e372296e9885f6bb33819a22d29e92ac725395f1)) +* support setting headers via env ([91d9d0e](https://github.com/beeper/desktop-api-php/commit/91d9d0e7e5e35f90aa52187308f366ceb438080b)) + + +### Bug Fixes + +* **client:** properly generate file params ([44ea2d6](https://github.com/beeper/desktop-api-php/commit/44ea2d6f7210b88dd61b764242e7a68254f07efb)) +* **client:** resolve serialization issue with unions and enums ([7d947c2](https://github.com/beeper/desktop-api-php/commit/7d947c2190514f5039947136a1e795e69069d42b)) +* populate enum-typed properties with enum instances ([8c3b6fc](https://github.com/beeper/desktop-api-php/commit/8c3b6fc3be27e61cfb5556fab51f29bff5eae2e6)) +* revert enum parsing change that lead to unconditional failure ([82c146a](https://github.com/beeper/desktop-api-php/commit/82c146af6213f707f29059eed0d9061aa0555c4b)) + + +### Chores + +* **internal:** codegen related update ([297da59](https://github.com/beeper/desktop-api-php/commit/297da598fb4ba18fec57a63e461e4316f3d6e641)) +* **internal:** tweak CI branches ([d3967d0](https://github.com/beeper/desktop-api-php/commit/d3967d0433e63e905f0d9699da709a7544ec12db)) +* **internal:** update multipart form array serialization ([05282bb](https://github.com/beeper/desktop-api-php/commit/05282bbbaad46dceff167998b75e34a778f42a24)) +* **internal:** upgrade phpunit ([958db57](https://github.com/beeper/desktop-api-php/commit/958db5719fcca4f33c8b9c4796e34b8a3c5a8d91)) +* **test:** do not count install time for mock server timeout ([344cf76](https://github.com/beeper/desktop-api-php/commit/344cf76b4dc4c254d6c380f035e8b6d2d86fb138)) +* **tests:** bump steady to v0.19.4 ([5ead500](https://github.com/beeper/desktop-api-php/commit/5ead500c3d450d125dee608bc6ec0054e6928e15)) +* **tests:** bump steady to v0.19.5 ([1987ab0](https://github.com/beeper/desktop-api-php/commit/1987ab0fcc91f0bebfdf55a94fd5b6c59f3658e3)) +* **tests:** bump steady to v0.19.6 ([5299bea](https://github.com/beeper/desktop-api-php/commit/5299bea39412364752a5a21800bc9e5837cbce81)) +* **tests:** bump steady to v0.19.7 ([356dd2b](https://github.com/beeper/desktop-api-php/commit/356dd2b76fe8549a90e60013ad72e3fd5683e3cd)) +* **tests:** bump steady to v0.20.1 ([9a809f7](https://github.com/beeper/desktop-api-php/commit/9a809f782c368dbed079b96555a27f76f39a9b42)) +* **tests:** bump steady to v0.20.2 ([71daf35](https://github.com/beeper/desktop-api-php/commit/71daf3588b2f471df63c4070b121e8c5587e5727)) +* **tests:** bump steady to v0.22.1 ([192de54](https://github.com/beeper/desktop-api-php/commit/192de547707b52e2d5f60984af9d2c153bef2060)) + + +### Refactors + +* **tests:** switch from prism to steady ([b2a0994](https://github.com/beeper/desktop-api-php/commit/b2a099467ced88b63d36ae05aa4db7da5b5c64c8)) diff --git a/README.md b/README.md index b5d91b8..e90f056 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,36 @@ $client = new Client(requestOptions: ['maxRetries' => 0]); $result = $client->accounts->list(requestOptions: ['maxRetries' => 5]); ``` +### File uploads + +Request parameters that correspond to file uploads can be passed as a resource returned by `fopen()`, a string of file contents, or a `FileParam` instance. + +```php +assets->upload( + file: FileParam::fromString($contents, filename: '/path/to/file', contentType: '…'), +); + +// Pass in only a string (where applicable) +$response = $client->assets->upload(file: '…'); + +// Pass an open resource: +$fd = fopen('/path/to/file', 'r'); +try { + $response = $client->assets->upload( + file: FileParam::fromResource($fd, filename: '/path/to/file', contentType: '…'), + ); +} finally { + fclose($fd); +} +``` + ## Advanced concepts ### Making custom or undocumented requests diff --git a/composer.lock b/composer.lock index 3f6c209..7a5e63d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5fc63f7c84d94b42416689723c547f69", + "content-hash": "ffa287ea8babf60e021f37e62c6c207a", "packages": [ { "name": "php-http/discovery", @@ -194,16 +194,16 @@ "packages-dev": [ { "name": "brianium/paratest", - "version": "v7.8.4", + "version": "v7.8.5", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "130a9bf0e269ee5f5b320108f794ad03e275cad4" + "reference": "9b324c8fc319cf9728b581c7a90e1c8f6361c5e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/130a9bf0e269ee5f5b320108f794ad03e275cad4", - "reference": "130a9bf0e269ee5f5b320108f794ad03e275cad4", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/9b324c8fc319cf9728b581c7a90e1c8f6361c5e5", + "reference": "9b324c8fc319cf9728b581c7a90e1c8f6361c5e5", "shasum": "" }, "require": { @@ -211,27 +211,27 @@ "ext-pcre": "*", "ext-reflection": "*", "ext-simplexml": "*", - "fidry/cpu-core-counter": "^1.2.0", + "fidry/cpu-core-counter": "^1.3.0", "jean85/pretty-package-versions": "^2.1.1", - "php": "~8.2.0 || ~8.3.0 || ~8.4.0", - "phpunit/php-code-coverage": "^11.0.10", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "phpunit/php-code-coverage": "^11.0.12", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-timer": "^7.0.1", - "phpunit/phpunit": "^11.5.24", + "phpunit/phpunit": "^11.5.46", "sebastian/environment": "^7.2.1", - "symfony/console": "^6.4.22 || ^7.3.0", - "symfony/process": "^6.4.20 || ^7.3.0" + "symfony/console": "^6.4.22 || ^7.3.4 || ^8.0.3", + "symfony/process": "^6.4.20 || ^7.3.4 || ^8.0.3" }, "require-dev": { "doctrine/coding-standard": "^12.0.0", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^2.1.17", + "phpstan/phpstan": "^2.1.33", "phpstan/phpstan-deprecation-rules": "^2.0.3", - "phpstan/phpstan-phpunit": "^2.0.6", - "phpstan/phpstan-strict-rules": "^2.0.4", - "squizlabs/php_codesniffer": "^3.13.2", - "symfony/filesystem": "^6.4.13 || ^7.3.0" + "phpstan/phpstan-phpunit": "^2.0.11", + "phpstan/phpstan-strict-rules": "^2.0.7", + "squizlabs/php_codesniffer": "^3.13.5", + "symfony/filesystem": "^6.4.13 || ^7.3.2 || ^8.0.1" }, "bin": [ "bin/paratest", @@ -271,7 +271,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.8.4" + "source": "https://github.com/paratestphp/paratest/tree/v7.8.5" }, "funding": [ { @@ -283,7 +283,7 @@ "type": "paypal" } ], - "time": "2025-06-23T06:07:21+00:00" + "time": "2026-01-08T08:02:38+00:00" }, { "name": "clue/ndjson-react", @@ -1412,38 +1412,38 @@ }, { "name": "pestphp/pest", - "version": "v3.8.4", + "version": "v3.8.5", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "72cf695554420e21858cda831d5db193db102574" + "reference": "7796630eafcfd1c02660cecdde3bc6984fbf01f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/72cf695554420e21858cda831d5db193db102574", - "reference": "72cf695554420e21858cda831d5db193db102574", + "url": "https://api.github.com/repos/pestphp/pest/zipball/7796630eafcfd1c02660cecdde3bc6984fbf01f4", + "reference": "7796630eafcfd1c02660cecdde3bc6984fbf01f4", "shasum": "" }, "require": { - "brianium/paratest": "^7.8.4", - "nunomaduro/collision": "^8.8.2", - "nunomaduro/termwind": "^2.3.1", + "brianium/paratest": "^7.8.5", + "nunomaduro/collision": "^8.8.3", + "nunomaduro/termwind": "^2.3.3", "pestphp/pest-plugin": "^3.0.0", "pestphp/pest-plugin-arch": "^3.1.1", "pestphp/pest-plugin-mutate": "^3.0.5", "php": "^8.2.0", - "phpunit/phpunit": "^11.5.33" + "phpunit/phpunit": "^11.5.50" }, "conflict": { "filp/whoops": "<2.16.0", - "phpunit/phpunit": ">11.5.33", + "phpunit/phpunit": ">11.5.50", "sebastian/exporter": "<6.0.0", "webmozart/assert": "<1.11.0" }, "require-dev": { "pestphp/pest-dev-tools": "^3.4.0", "pestphp/pest-plugin-type-coverage": "^3.6.1", - "symfony/process": "^7.3.0" + "symfony/process": "^7.4.4" }, "bin": [ "bin/pest" @@ -1508,7 +1508,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v3.8.4" + "source": "https://github.com/pestphp/pest/tree/v3.8.5" }, "funding": [ { @@ -1520,7 +1520,7 @@ "type": "github" } ], - "time": "2025-08-20T19:12:42+00:00" + "time": "2026-01-28T01:33:45+00:00" }, { "name": "pestphp/pest-plugin", @@ -2627,28 +2627,28 @@ }, { "name": "phpunit/php-file-iterator", - "version": "5.1.0", + "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", - "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -2676,15 +2676,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" } ], - "time": "2024-08-27T05:02:59+00:00" + "time": "2026-02-02T13:52:54+00:00" }, { "name": "phpunit/php-invoker", @@ -2872,16 +2884,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.33", + "version": "11.5.50", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "5965e9ff57546cb9137c0ff6aa78cb7442b05cf6" + "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5965e9ff57546cb9137c0ff6aa78cb7442b05cf6", - "reference": "5965e9ff57546cb9137c0ff6aa78cb7442b05cf6", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fdfc727f0fcacfeb8fcb30c7e5da173125b58be3", + "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3", "shasum": "" }, "require": { @@ -2895,17 +2907,17 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.10", + "phpunit/php-code-coverage": "^11.0.12", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", "phpunit/php-timer": "^7.0.1", "sebastian/cli-parser": "^3.0.2", "sebastian/code-unit": "^3.0.3", - "sebastian/comparator": "^6.3.2", + "sebastian/comparator": "^6.3.3", "sebastian/diff": "^6.0.2", "sebastian/environment": "^7.2.1", - "sebastian/exporter": "^6.3.0", + "sebastian/exporter": "^6.3.2", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", "sebastian/type": "^5.1.3", @@ -2953,7 +2965,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.33" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.50" }, "funding": [ { @@ -2977,7 +2989,7 @@ "type": "tidelift" } ], - "time": "2025-08-16T05:19:02+00:00" + "time": "2026-01-27T05:59:18+00:00" }, { "name": "psr/container", @@ -3936,16 +3948,16 @@ }, { "name": "sebastian/comparator", - "version": "6.3.2", + "version": "6.3.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8" + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8", - "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", "shasum": "" }, "require": { @@ -4004,7 +4016,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2" + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" }, "funding": [ { @@ -4024,7 +4036,7 @@ "type": "tidelift" } ], - "time": "2025-08-10T08:07:46+00:00" + "time": "2026-01-24T09:26:40+00:00" }, { "name": "sebastian/complexity", @@ -6668,5 +6680,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/scripts/mock b/scripts/mock index 0b28f6e..9c7c439 100755 --- a/scripts/mock +++ b/scripts/mock @@ -19,23 +19,34 @@ fi echo "==> Starting mock server with URL ${URL}" -# Run prism mock on the given spec +# Run steady mock on the given spec if [ "$1" == "--daemon" ]; then - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & + # Pre-install the package so the download doesn't eat into the startup timeout + npm exec --package=@stdy/cli@0.22.1 -- steady --version - # Wait for server to come online + npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + + # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" - while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + attempts=0 + while ! curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1; do + if ! kill -0 $! 2>/dev/null; then + echo + cat .stdy.log + exit 1 + fi + attempts=$((attempts + 1)) + if [ "$attempts" -ge 300 ]; then + echo + echo "Timed out waiting for Steady server to start" + cat .stdy.log + exit 1 + fi echo -n "." sleep 0.1 done - if grep -q "✖ fatal" ".prism.log"; then - cat .prism.log - exit 1 - fi - echo else - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" + npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 4b777e0..bef4dad 100755 --- a/scripts/test +++ b/scripts/test @@ -9,8 +9,8 @@ GREEN='\033[0;32m' YELLOW='\033[0;33m' NC='\033[0m' # No Color -function prism_is_running() { - curl --silent "http://localhost:4010" >/dev/null 2>&1 +function steady_is_running() { + curl --silent "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1 } kill_server_on_port() { @@ -25,7 +25,7 @@ function is_overriding_api_base_url() { [ -n "$TEST_API_BASE_URL" ] } -if ! is_overriding_api_base_url && ! prism_is_running ; then +if ! is_overriding_api_base_url && ! steady_is_running ; then # When we exit this script, make sure to kill the background mock server process trap 'kill_server_on_port 4010' EXIT @@ -36,19 +36,19 @@ fi if is_overriding_api_base_url ; then echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" echo -elif ! prism_is_running ; then - echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" +elif ! steady_is_running ; then + echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Steady server" echo -e "running against your OpenAPI spec." echo echo -e "To run the server, pass in the path or url of your OpenAPI" - echo -e "spec to the prism command:" + echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.22.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 else - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" + echo -e "${GREEN}✔ Mock steady server is running with your OpenAPI spec${NC}" echo fi diff --git a/src/Accounts/Account.php b/src/Accounts/Account.php index 270cb3e..8bb48cd 100644 --- a/src/Accounts/Account.php +++ b/src/Accounts/Account.php @@ -4,6 +4,8 @@ namespace BeeperDesktop\Accounts; +use BeeperDesktop\Accounts\Account\Bridge; +use BeeperDesktop\Core\Attributes\Optional; use BeeperDesktop\Core\Attributes\Required; use BeeperDesktop\Core\Concerns\SdkModel; use BeeperDesktop\Core\Contracts\BaseModel; @@ -12,9 +14,15 @@ /** * A chat account added to Beeper. * + * @phpstan-import-type BridgeShape from \BeeperDesktop\Accounts\Account\Bridge * @phpstan-import-type UserShape from \BeeperDesktop\User * - * @phpstan-type AccountShape = array{accountID: string, user: User|UserShape} + * @phpstan-type AccountShape = array{ + * accountID: string, + * bridge: Bridge|BridgeShape, + * user: User|UserShape, + * network?: string|null, + * } */ final class Account implements BaseModel { @@ -27,24 +35,36 @@ final class Account implements BaseModel #[Required] public string $accountID; + /** + * Bridge metadata for the account. Available in Beeper Desktop v4.2.789+. + */ + #[Required] + public Bridge $bridge; + /** * User the account belongs to. */ #[Required] public User $user; + /** + * Human-friendly network name for the account. Omitted when the network is unknown. + */ + #[Optional] + public ?string $network; + /** * `new Account()` is missing required properties by the API. * * To enforce required parameters use * ``` - * Account::with(accountID: ..., user: ...) + * Account::with(accountID: ..., bridge: ..., user: ...) * ``` * * Otherwise ensure the following setters are called * * ``` - * (new Account)->withAccountID(...)->withUser(...) + * (new Account)->withAccountID(...)->withBridge(...)->withUser(...) * ``` */ public function __construct() @@ -57,15 +77,23 @@ public function __construct() * * You must use named parameters to construct any parameters with a default value. * + * @param Bridge|BridgeShape $bridge * @param User|UserShape $user */ - public static function with(string $accountID, User|array $user): self - { + public static function with( + string $accountID, + Bridge|array $bridge, + User|array $user, + ?string $network = null, + ): self { $self = new self; $self['accountID'] = $accountID; + $self['bridge'] = $bridge; $self['user'] = $user; + null !== $network && $self['network'] = $network; + return $self; } @@ -80,6 +108,19 @@ public function withAccountID(string $accountID): self return $self; } + /** + * Bridge metadata for the account. Available in Beeper Desktop v4.2.789+. + * + * @param Bridge|BridgeShape $bridge + */ + public function withBridge(Bridge|array $bridge): self + { + $self = clone $this; + $self['bridge'] = $bridge; + + return $self; + } + /** * User the account belongs to. * @@ -92,4 +133,15 @@ public function withUser(User|array $user): self return $self; } + + /** + * Human-friendly network name for the account. Omitted when the network is unknown. + */ + public function withNetwork(string $network): self + { + $self = clone $this; + $self['network'] = $network; + + return $self; + } } diff --git a/src/Accounts/Account/Bridge.php b/src/Accounts/Account/Bridge.php new file mode 100644 index 0000000..2304404 --- /dev/null +++ b/src/Accounts/Account/Bridge.php @@ -0,0 +1,118 @@ +, type: string + * } + */ +final class Bridge implements BaseModel +{ + /** @use SdkModel */ + use SdkModel; + + /** + * Bridge instance identifier. Available in Beeper Desktop v4.2.789+. + */ + #[Required] + public string $id; + + /** + * Bridge provider for the account. Available in Beeper Desktop v4.2.789+. + * + * @var value-of $provider + */ + #[Required(enum: Provider::class)] + public string $provider; + + /** + * Bridge type. Available in Beeper Desktop v4.2.789+. + */ + #[Required] + public string $type; + + /** + * `new Bridge()` is missing required properties by the API. + * + * To enforce required parameters use + * ``` + * Bridge::with(id: ..., provider: ..., type: ...) + * ``` + * + * Otherwise ensure the following setters are called + * + * ``` + * (new Bridge)->withID(...)->withProvider(...)->withType(...) + * ``` + */ + public function __construct() + { + $this->initialize(); + } + + /** + * Construct an instance from the required parameters. + * + * You must use named parameters to construct any parameters with a default value. + * + * @param Provider|value-of $provider + */ + public static function with( + string $id, + Provider|string $provider, + string $type + ): self { + $self = new self; + + $self['id'] = $id; + $self['provider'] = $provider; + $self['type'] = $type; + + return $self; + } + + /** + * Bridge instance identifier. Available in Beeper Desktop v4.2.789+. + */ + public function withID(string $id): self + { + $self = clone $this; + $self['id'] = $id; + + return $self; + } + + /** + * Bridge provider for the account. Available in Beeper Desktop v4.2.789+. + * + * @param Provider|value-of $provider + */ + public function withProvider(Provider|string $provider): self + { + $self = clone $this; + $self['provider'] = $provider; + + return $self; + } + + /** + * Bridge type. Available in Beeper Desktop v4.2.789+. + */ + public function withType(string $type): self + { + $self = clone $this; + $self['type'] = $type; + + return $self; + } +} diff --git a/src/Accounts/Account/Bridge/Provider.php b/src/Accounts/Account/Bridge/Provider.php new file mode 100644 index 0000000..fcf152d --- /dev/null +++ b/src/Accounts/Account/Bridge/Provider.php @@ -0,0 +1,19 @@ +, + * participantIDs?: list|null, + * title?: string|null, + * type?: null|Type|value-of, + * user?: null|User|UserShape, * } */ final class ChatCreateParams implements BaseModel @@ -27,21 +37,72 @@ final class ChatCreateParams implements BaseModel use SdkModel; use SdkParams; + /** + * Account to create or start the chat on. + */ #[Required] - public Chat $chat; + public string $accountID; + + /** + * Only used for mode='start'. Whether invite-based DM creation is allowed when required by the platform. + */ + #[Optional] + public ?bool $allowInvite; + + /** + * Optional first message content if the platform requires it to create the chat. + */ + #[Optional] + public ?string $messageText; + + /** + * Operation mode. Use 'start' to resolve a user/contact and start a direct chat. Omit or set 'create' to create a chat directly. + * + * @var value-of|null $mode + */ + #[Optional(enum: Mode::class)] + public ?string $mode; + + /** + * Required for create mode. Provide exactly one user ID for 'single' chats and one or more for 'group' chats. + * + * @var list|null $participantIDs + */ + #[Optional(list: 'string')] + public ?array $participantIDs; + + /** + * Optional title for group chats; ignored for single chats on most networks. + */ + #[Optional] + public ?string $title; + + /** + * Required for create mode. 'single' creates a direct message chat; 'group' creates a group chat. + * + * @var value-of|null $type + */ + #[Optional(enum: Type::class)] + public ?string $type; + + /** + * Required for mode='start'. Merged user-like contact payload used to resolve the best identifier. + */ + #[Optional] + public ?User $user; /** * `new ChatCreateParams()` is missing required properties by the API. * * To enforce required parameters use * ``` - * ChatCreateParams::with(chat: ...) + * ChatCreateParams::with(accountID: ...) * ``` * * Otherwise ensure the following setters are called * * ``` - * (new ChatCreateParams)->withChat(...) + * (new ChatCreateParams)->withAccountID(...) * ``` */ public function __construct() @@ -54,26 +115,128 @@ public function __construct() * * You must use named parameters to construct any parameters with a default value. * - * @param Chat|ChatShape $chat + * @param Mode|value-of|null $mode + * @param list|null $participantIDs + * @param Type|value-of|null $type + * @param User|UserShape|null $user */ public static function with( - Chat|array $chat + string $accountID, + ?bool $allowInvite = null, + ?string $messageText = null, + Mode|string|null $mode = null, + ?array $participantIDs = null, + ?string $title = null, + Type|string|null $type = null, + User|array|null $user = null, ): self { $self = new self; - $self['chat'] = $chat; + $self['accountID'] = $accountID; + + null !== $allowInvite && $self['allowInvite'] = $allowInvite; + null !== $messageText && $self['messageText'] = $messageText; + null !== $mode && $self['mode'] = $mode; + null !== $participantIDs && $self['participantIDs'] = $participantIDs; + null !== $title && $self['title'] = $title; + null !== $type && $self['type'] = $type; + null !== $user && $self['user'] = $user; return $self; } /** - * @param Chat|ChatShape $chat + * Account to create or start the chat on. */ - public function withChat( - Chat|array $chat - ): self { + public function withAccountID(string $accountID): self + { + $self = clone $this; + $self['accountID'] = $accountID; + + return $self; + } + + /** + * Only used for mode='start'. Whether invite-based DM creation is allowed when required by the platform. + */ + public function withAllowInvite(bool $allowInvite): self + { + $self = clone $this; + $self['allowInvite'] = $allowInvite; + + return $self; + } + + /** + * Optional first message content if the platform requires it to create the chat. + */ + public function withMessageText(string $messageText): self + { + $self = clone $this; + $self['messageText'] = $messageText; + + return $self; + } + + /** + * Operation mode. Use 'start' to resolve a user/contact and start a direct chat. Omit or set 'create' to create a chat directly. + * + * @param Mode|value-of $mode + */ + public function withMode(Mode|string $mode): self + { + $self = clone $this; + $self['mode'] = $mode; + + return $self; + } + + /** + * Required for create mode. Provide exactly one user ID for 'single' chats and one or more for 'group' chats. + * + * @param list $participantIDs + */ + public function withParticipantIDs(array $participantIDs): self + { + $self = clone $this; + $self['participantIDs'] = $participantIDs; + + return $self; + } + + /** + * Optional title for group chats; ignored for single chats on most networks. + */ + public function withTitle(string $title): self + { + $self = clone $this; + $self['title'] = $title; + + return $self; + } + + /** + * Required for create mode. 'single' creates a direct message chat; 'group' creates a group chat. + * + * @param Type|value-of $type + */ + public function withType(Type|string $type): self + { + $self = clone $this; + $self['type'] = $type; + + return $self; + } + + /** + * Required for mode='start'. Merged user-like contact payload used to resolve the best identifier. + * + * @param User|UserShape $user + */ + public function withUser(User|array $user): self + { $self = clone $this; - $self['chat'] = $chat; + $self['user'] = $user; return $self; } diff --git a/src/Chats/ChatCreateParams/Chat.php b/src/Chats/ChatCreateParams/Chat.php deleted file mode 100644 index d1d63c2..0000000 --- a/src/Chats/ChatCreateParams/Chat.php +++ /dev/null @@ -1,237 +0,0 @@ -, - * participantIDs?: list|null, - * title?: string|null, - * type?: null|Type|value-of, - * user?: null|User|UserShape, - * } - */ -final class Chat implements BaseModel -{ - /** @use SdkModel */ - use SdkModel; - - /** - * Account to create or start the chat on. - */ - #[Required] - public string $accountID; - - /** - * Whether invite-based DM creation is allowed when required by the platform. Used for mode='start'. - */ - #[Optional] - public ?bool $allowInvite; - - /** - * Optional first message content if the platform requires it to create the chat. - */ - #[Optional] - public ?string $messageText; - - /** - * Operation mode. Defaults to 'create' when omitted. - * - * @var value-of|null $mode - */ - #[Optional(enum: Mode::class)] - public ?string $mode; - - /** - * Required when mode='create'. User IDs to include in the new chat. - * - * @var list|null $participantIDs - */ - #[Optional(list: 'string')] - public ?array $participantIDs; - - /** - * Optional title for group chats when mode='create'; ignored for single chats on most platforms. - */ - #[Optional] - public ?string $title; - - /** - * Required when mode='create'. 'single' requires exactly one participantID; 'group' supports multiple participants and optional title. - * - * @var value-of|null $type - */ - #[Optional(enum: Type::class)] - public ?string $type; - - /** - * Required when mode='start'. Merged user-like contact payload used to resolve the best identifier. - */ - #[Optional] - public ?User $user; - - /** - * `new Chat()` is missing required properties by the API. - * - * To enforce required parameters use - * ``` - * Chat::with(accountID: ...) - * ``` - * - * Otherwise ensure the following setters are called - * - * ``` - * (new Chat)->withAccountID(...) - * ``` - */ - public function __construct() - { - $this->initialize(); - } - - /** - * Construct an instance from the required parameters. - * - * You must use named parameters to construct any parameters with a default value. - * - * @param Mode|value-of|null $mode - * @param list|null $participantIDs - * @param Type|value-of|null $type - * @param User|UserShape|null $user - */ - public static function with( - string $accountID, - ?bool $allowInvite = null, - ?string $messageText = null, - Mode|string|null $mode = null, - ?array $participantIDs = null, - ?string $title = null, - Type|string|null $type = null, - User|array|null $user = null, - ): self { - $self = new self; - - $self['accountID'] = $accountID; - - null !== $allowInvite && $self['allowInvite'] = $allowInvite; - null !== $messageText && $self['messageText'] = $messageText; - null !== $mode && $self['mode'] = $mode; - null !== $participantIDs && $self['participantIDs'] = $participantIDs; - null !== $title && $self['title'] = $title; - null !== $type && $self['type'] = $type; - null !== $user && $self['user'] = $user; - - return $self; - } - - /** - * Account to create or start the chat on. - */ - public function withAccountID(string $accountID): self - { - $self = clone $this; - $self['accountID'] = $accountID; - - return $self; - } - - /** - * Whether invite-based DM creation is allowed when required by the platform. Used for mode='start'. - */ - public function withAllowInvite(bool $allowInvite): self - { - $self = clone $this; - $self['allowInvite'] = $allowInvite; - - return $self; - } - - /** - * Optional first message content if the platform requires it to create the chat. - */ - public function withMessageText(string $messageText): self - { - $self = clone $this; - $self['messageText'] = $messageText; - - return $self; - } - - /** - * Operation mode. Defaults to 'create' when omitted. - * - * @param Mode|value-of $mode - */ - public function withMode(Mode|string $mode): self - { - $self = clone $this; - $self['mode'] = $mode; - - return $self; - } - - /** - * Required when mode='create'. User IDs to include in the new chat. - * - * @param list $participantIDs - */ - public function withParticipantIDs(array $participantIDs): self - { - $self = clone $this; - $self['participantIDs'] = $participantIDs; - - return $self; - } - - /** - * Optional title for group chats when mode='create'; ignored for single chats on most platforms. - */ - public function withTitle(string $title): self - { - $self = clone $this; - $self['title'] = $title; - - return $self; - } - - /** - * Required when mode='create'. 'single' requires exactly one participantID; 'group' supports multiple participants and optional title. - * - * @param Type|value-of $type - */ - public function withType(Type|string $type): self - { - $self = clone $this; - $self['type'] = $type; - - return $self; - } - - /** - * Required when mode='start'. Merged user-like contact payload used to resolve the best identifier. - * - * @param User|UserShape $user - */ - public function withUser(User|array $user): self - { - $self = clone $this; - $self['user'] = $user; - - return $self; - } -} diff --git a/src/Chats/ChatCreateParams/Chat/Mode.php b/src/Chats/ChatCreateParams/Chat/Mode.php deleted file mode 100644 index ce0dabd..0000000 --- a/src/Chats/ChatCreateParams/Chat/Mode.php +++ /dev/null @@ -1,15 +0,0 @@ - $headers */ + $headers = [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'User-Agent' => sprintf('beeperdesktop/PHP %s', VERSION), + 'X-Stainless-Lang' => 'php', + 'X-Stainless-Package-Version' => '0.0.1', + 'X-Stainless-Arch' => Util::machtype(), + 'X-Stainless-OS' => Util::ostype(), + 'X-Stainless-Runtime' => php_sapi_name(), + 'X-Stainless-Runtime-Version' => phpversion(), + ]; + + $customHeadersEnv = Util::getenv('BEEPER_CUSTOM_HEADERS'); + if (null !== $customHeadersEnv) { + foreach (explode("\n", $customHeadersEnv) as $line) { + $colon = strpos($line, ':'); + if (false !== $colon) { + $headers[trim(substr($line, 0, $colon))] = trim(substr($line, $colon + 1)); + } + } + } + parent::__construct( - headers: [ - 'Content-Type' => 'application/json', - 'Accept' => 'application/json', - 'User-Agent' => sprintf('beeperdesktop/PHP %s', VERSION), - 'X-Stainless-Lang' => 'php', - 'X-Stainless-Package-Version' => '0.0.1', - 'X-Stainless-Arch' => Util::machtype(), - 'X-Stainless-OS' => Util::ostype(), - 'X-Stainless-Runtime' => php_sapi_name(), - 'X-Stainless-Runtime-Version' => phpversion(), - ], + headers: $headers, baseUrl: $baseUrl, options: $options ); @@ -159,8 +170,18 @@ public function search( return $this->beeperDesktopClientService->search($query, $requestOptions); } + /** + * @param array{bearerAuth?: bool} $security + * + * @return array + */ + protected function authHeaders(array $security): array + { + return [...($security['bearerAuth'] ?? false) ? $this->bearerAuth() : []]; + } + /** @return array */ - protected function authHeaders(): array + protected function bearerAuth(): array { return $this->accessToken ? [ 'Authorization' => "Bearer {$this->accessToken}", @@ -174,6 +195,7 @@ protected function authHeaders(): array * @param array $query * @param array|null> $headers * @param RequestOpts|null $opts + * @param array{bearerAuth?: bool}|null $security * * @return array{NormalizedRequest, RequestOptions} */ @@ -184,14 +206,19 @@ protected function buildRequest( array $headers, mixed $body, RequestOptions|array|null $opts, + ?array $security = null, ): array { return parent::buildRequest( method: $method, path: $path, query: $query, - headers: [...$this->authHeaders(), ...$headers], + headers: [ + ...$this->authHeaders(security: ($security ?? ['bearerAuth' => true])), + ...$headers, + ], body: $body, opts: $opts, + security: $security, ); } } diff --git a/src/Core/Attributes/Required.php b/src/Core/Attributes/Required.php index bcb6957..d861eb6 100644 --- a/src/Core/Attributes/Required.php +++ b/src/Core/Attributes/Required.php @@ -25,9 +25,6 @@ class Required public readonly bool $nullable; - /** @var array */ - private static array $enumConverters = []; - /** * @param class-string|Converter|string|null $type * @param class-string<\BackedEnum>|Converter|null $enum @@ -52,7 +49,7 @@ public function __construct( $type ??= new MapOf($map); } if (null !== $enum) { - $type ??= $enum instanceof Converter ? $enum : self::enumConverter($enum); + $type ??= $enum instanceof Converter ? $enum : EnumOf::fromBackedEnum($enum); } $this->apiName = $apiName; @@ -60,16 +57,4 @@ public function __construct( $this->optional = false; $this->nullable = $nullable; } - - /** @property class-string<\BackedEnum> $enum */ - private static function enumConverter(string $enum): Converter - { - if (!isset(self::$enumConverters[$enum])) { - // @phpstan-ignore-next-line argument.type - $converter = new EnumOf(array_column($enum::cases(), column_key: 'value')); - self::$enumConverters[$enum] = $converter; - } - - return self::$enumConverters[$enum]; - } } diff --git a/src/Core/BaseClient.php b/src/Core/BaseClient.php index 14b35ea..bed6a0d 100644 --- a/src/Core/BaseClient.php +++ b/src/Core/BaseClient.php @@ -55,6 +55,7 @@ public function __construct( * @param string|int|list|null $unwrap * @param class-string>|null $page * @param class-string>|null $stream + * @param array{bearerAuth?: bool}|null $security * @param RequestOptions|array|null $options * * @return BaseResponse @@ -69,6 +70,7 @@ public function request( string|Converter|ConverterSource|null $convert = null, ?string $page = null, ?string $stream = null, + ?array $security = null, RequestOptions|array|null $options = [], ): BaseResponse { [$req, $opts] = $this->buildRequest( @@ -79,6 +81,7 @@ public function request( // @phpstan-ignore argument.type headers: $headers, body: $body, + security: $security, // @phpstan-ignore argument.type opts: $options, ); @@ -113,6 +116,7 @@ protected function generateIdempotencyKey(): string * @param array $query * @param array|null> $headers * @param RequestOpts|null $opts + * @param array{bearerAuth?: bool}|null $security * * @return array{NormalizedRequest, RequestOptions} */ @@ -123,6 +127,7 @@ protected function buildRequest( array $headers, mixed $body, RequestOptions|array|null $opts, + ?array $security = null, ): array { $options = RequestOptions::parse($this->options, $opts); diff --git a/src/Core/Conversion.php b/src/Core/Conversion.php index 494ec69..84a6f56 100644 --- a/src/Core/Conversion.php +++ b/src/Core/Conversion.php @@ -8,6 +8,7 @@ use BeeperDesktop\Core\Conversion\Contracts\Converter; use BeeperDesktop\Core\Conversion\Contracts\ConverterSource; use BeeperDesktop\Core\Conversion\DumpState; +use BeeperDesktop\Core\Conversion\EnumOf; /** * @internal @@ -21,6 +22,10 @@ public static function dump_unknown(mixed $value, DumpState $state): mixed } if (is_object($value)) { + if ($value instanceof FileParam) { + return $value; + } + if (is_a($value, class: ConverterSource::class)) { return $value::converter()->dump($value, state: $state); } @@ -61,6 +66,13 @@ public static function coerce(Converter|ConverterSource|string $target, mixed $v return $target->coerce($value, state: $state); } + // BackedEnum class-name targets: wrap in EnumOf so enum values are scored + // against the enum's cases. Without this, tryConvert's default case scores + // any class-name target as `no`, even when the value is a valid enum member. + if (is_a($target, class: \BackedEnum::class, allow_string: true)) { + return EnumOf::fromBackedEnum($target)->coerce($value, state: $state); + } + return self::tryConvert($target, value: $value, state: $state); } @@ -74,6 +86,13 @@ public static function dump(Converter|ConverterSource|string $target, mixed $val return $target::converter()->dump($value, state: $state); } + // BackedEnum class-name targets: wrap in EnumOf so enum values are scored + // against the enum's cases. Without this, tryConvert's default case scores + // any class-name target as `no`, even when the value is a valid enum member. + if (is_a($target, class: \BackedEnum::class, allow_string: true)) { + return EnumOf::fromBackedEnum($target)->dump($value, state: $state); + } + self::tryConvert($target, value: $value, state: $state); return self::dump_unknown($value, state: $state); @@ -170,6 +189,37 @@ private static function tryConvert(Converter|ConverterSource|string $target, mix return $value; + case 'DateTimeInterface': + case 'DateTimeImmutable': + if (is_string($value)) { + try { + ++$state->maybe; + + return new \DateTimeImmutable($value); + } catch (\Exception) { + --$state->maybe; + } + } + + ++$state->no; + + return $value; + + case 'DateTime': + if (is_string($value)) { + try { + ++$state->maybe; + + return new \DateTime($value); + } catch (\Exception) { + --$state->maybe; + } + } + + ++$state->no; + + return $value; + default: ++$state->no; diff --git a/src/Core/Conversion/EnumOf.php b/src/Core/Conversion/EnumOf.php index a4d791b..b44c92c 100644 --- a/src/Core/Conversion/EnumOf.php +++ b/src/Core/Conversion/EnumOf.php @@ -14,6 +14,9 @@ final class EnumOf implements Converter { private readonly string $type; + /** @var array, self> */ + private static array $cache = []; + /** * @param list $members */ @@ -26,6 +29,13 @@ public function __construct(private readonly array $members) $this->type = $type; } + /** @param class-string<\BackedEnum> $enum */ + public static function fromBackedEnum(string $enum): self + { + // @phpstan-ignore-next-line argument.type + return self::$cache[$enum] ??= new self(array_column($enum::cases(), column_key: 'value')); + } + public function coerce(mixed $value, CoerceState $state): mixed { $this->tally($value, state: $state); @@ -42,9 +52,10 @@ public function dump(mixed $value, DumpState $state): mixed private function tally(mixed $value, CoerceState|DumpState $state): void { - if (in_array($value, haystack: $this->members, strict: true)) { + $needle = $value instanceof \BackedEnum ? $value->value : $value; + if (in_array($needle, haystack: $this->members, strict: true)) { ++$state->yes; - } elseif ($this->type === gettype($value)) { + } elseif ($this->type === gettype($needle)) { ++$state->maybe; } else { ++$state->no; diff --git a/src/Core/FileParam.php b/src/Core/FileParam.php new file mode 100644 index 0000000..eb8db4a --- /dev/null +++ b/src/Core/FileParam.php @@ -0,0 +1,63 @@ +files->upload(file: FileParam::fromResource(fopen('data.csv', 'r'))); + * + * // From a string: + * $client->files->upload(file: FileParam::fromString('csv data...', 'data.csv')); + * ``` + */ +final class FileParam +{ + public const DEFAULT_CONTENT_TYPE = 'application/octet-stream'; + + /** + * @param resource|string $data the file content as a resource or string + */ + private function __construct( + public readonly mixed $data, + public readonly string $filename, + public readonly string $contentType = self::DEFAULT_CONTENT_TYPE, + ) {} + + /** + * Create a FileParam from an open resource (e.g. from fopen()). + * + * @param resource $resource an open file resource + * @param string|null $filename Override the filename. Defaults to the resource URI basename. + * @param string $contentType override the content type + */ + public static function fromResource(mixed $resource, ?string $filename = null, string $contentType = self::DEFAULT_CONTENT_TYPE): self + { + if (!is_resource($resource)) { + throw new \InvalidArgumentException('Expected a resource, got '.get_debug_type($resource)); + } + + if (null === $filename) { + $meta = stream_get_meta_data($resource); + $filename = basename($meta['uri'] ?? 'upload'); + } + + return new self($resource, filename: $filename, contentType: $contentType); + } + + /** + * Create a FileParam from a string. + * + * @param string $content the file content + * @param string $filename the filename for the Content-Disposition header + * @param string $contentType override the content type + */ + public static function fromString(string $content, string $filename, string $contentType = self::DEFAULT_CONTENT_TYPE): self + { + return new self($content, filename: $filename, contentType: $contentType); + } +} diff --git a/src/Core/Util.php b/src/Core/Util.php index 35a9a0b..b385aff 100644 --- a/src/Core/Util.php +++ b/src/Core/Util.php @@ -283,7 +283,7 @@ public static function withSetBody( if (preg_match('/^multipart\/form-data/', $contentType)) { [$boundary, $gen] = self::encodeMultipartStreaming($body); - $encoded = implode('', iterator_to_array($gen)); + $encoded = implode('', iterator_to_array($gen, preserve_keys: false)); $stream = $factory->createStream($encoded); /** @var RequestInterface */ @@ -447,11 +447,18 @@ private static function writeMultipartContent( ): \Generator { $contentLine = "Content-Type: %s\r\n\r\n"; - if (is_resource($val)) { - yield sprintf($contentLine, $contentType ?? 'application/octet-stream'); - while (!feof($val)) { - if ($read = fread($val, length: self::BUF_SIZE)) { - yield $read; + if ($val instanceof FileParam) { + $ct = $val->contentType ?? $contentType; + + yield sprintf($contentLine, $ct); + $data = $val->data; + if (is_string($data)) { + yield $data; + } else { // resource + while (!feof($data)) { + if ($read = fread($data, length: self::BUF_SIZE)) { + yield $read; + } } } } elseif (is_string($val) || is_numeric($val) || is_bool($val)) { @@ -483,17 +490,48 @@ private static function writeMultipartChunk( yield 'Content-Disposition: form-data'; if (!is_null($key)) { - $name = rawurlencode(self::strVal($key)); + $name = str_replace(['"', "\r", "\n"], replace: '', subject: $key); yield "; name=\"{$name}\""; } + // File uploads require a filename in the Content-Disposition header, + // e.g. `Content-Disposition: form-data; name="file"; filename="data.csv"` + // Without this, many servers will reject the upload with a 400. + if ($val instanceof FileParam) { + $filename = str_replace(['"', "\r", "\n"], replace: '', subject: $val->filename); + + yield "; filename=\"{$filename}\""; + } + yield "\r\n"; foreach (self::writeMultipartContent($val, closing: $closing) as $chunk) { yield $chunk; } } + /** + * Expands list arrays into separate multipart parts, applying the configured array key format. + * + * @param list $closing + * + * @return \Generator + */ + private static function writeMultipartField( + string $boundary, + ?string $key, + mixed $val, + array &$closing + ): \Generator { + if (is_array($val) && array_is_list($val)) { + foreach ($val as $item) { + yield from self::writeMultipartField(boundary: $boundary, key: $key, val: $item, closing: $closing); + } + } else { + yield from self::writeMultipartChunk(boundary: $boundary, key: $key, val: $val, closing: $closing); + } + } + /** * @param bool|int|float|string|resource|\Traversable|array|null $body * @@ -508,14 +546,10 @@ private static function encodeMultipartStreaming(mixed $body): array try { if (is_array($body) || is_object($body)) { foreach ((array) $body as $key => $val) { - foreach (static::writeMultipartChunk(boundary: $boundary, key: $key, val: $val, closing: $closing) as $chunk) { - yield $chunk; - } + yield from static::writeMultipartField(boundary: $boundary, key: $key, val: $val, closing: $closing); } } else { - foreach (static::writeMultipartChunk(boundary: $boundary, key: null, val: $body, closing: $closing) as $chunk) { - yield $chunk; - } + yield from static::writeMultipartField(boundary: $boundary, key: null, val: $body, closing: $closing); } yield "--{$boundary}--\r\n"; diff --git a/src/CursorNoLimit.php b/src/CursorNoLimit.php index 5ea1b0c..9398fb2 100644 --- a/src/CursorNoLimit.php +++ b/src/CursorNoLimit.php @@ -2,132 +2,122 @@ namespace BeeperDesktop; -use BeeperDesktop\Core\Conversion; use BeeperDesktop\Core\Attributes\Optional; use BeeperDesktop\Core\Concerns\SdkModel; use BeeperDesktop\Core\Concerns\SdkPage; use BeeperDesktop\Core\Contracts\BaseModel; use BeeperDesktop\Core\Contracts\BasePage; -use BeeperDesktop\Core\Conversion\ListOf; +use BeeperDesktop\Core\Conversion; use BeeperDesktop\Core\Conversion\Contracts\Converter; use BeeperDesktop\Core\Conversion\Contracts\ConverterSource; +use BeeperDesktop\Core\Conversion\ListOf; use Psr\Http\Message\ResponseInterface; /** - * - * @phpstan-type CursorNoLimitShape = array{ - * items?: list>|null, - * hasMore?: bool|null, - * oldestCursor?: string|null, - * newestCursor?: string|null, - * } - * @template TItem - * @implements BasePage - * + * @phpstan-type CursorNoLimitShape = array{ + * items?: list>|null, + * hasMore?: bool|null, + * oldestCursor?: string|null, + * newestCursor?: string|null, + * } + * + * @template TItem + * + * @implements BasePage */ final class CursorNoLimit implements BaseModel, BasePage { - /** @use SdkModel */ - use SdkModel; - - /** @use SdkPage */ - use SdkPage; - - /** @var list|null $items */ - #[Optional(list: 'mixed')] - public ?array $items; - - /** @var bool|null $hasMore */ - #[Optional] - public ?bool $hasMore; - - /** @var string|null $oldestCursor */ - #[Optional(nullable: true)] - public ?string $oldestCursor; - - /** @var string|null $newestCursor */ - #[Optional(nullable: true)] - public ?string $newestCursor; - - /** @return list */ - function getItems(): array { - // @phpstan-ignore-next-line return.type - return $this->offsetGet('items') ?? []; - } - - /** - * @internal - * - * @return array{ - * array{ - * method: string, - * path: string, - * query: array, - * headers: array>, - * body: mixed, - * }, - * RequestOptions, - * }|null - */ - function nextRequest(): ?array { - if (!($this->hasMore ?? null)||!count($this->getItems())) { - return null; - + /** @use SdkModel */ + use SdkModel; + + /** @use SdkPage */ + use SdkPage; + + /** @var list|null $items */ + #[Optional(list: 'mixed')] + public ?array $items; + + #[Optional] + public ?bool $hasMore; + + #[Optional(nullable: true)] + public ?string $oldestCursor; + + #[Optional(nullable: true)] + public ?string $newestCursor; + + /** + * @internal + * + * @param array{ + * method: string, + * path: string, + * query: array, + * headers: array|null>, + * body: mixed, + * } $requestInfo + */ + public function __construct( + private string|Converter|ConverterSource $convert, + private Client $client, + private array $requestInfo, + private RequestOptions $options, + private ResponseInterface $response, + private mixed $parsedBody, + ) { + $this->initialize(); + + if (!is_array($this->parsedBody)) { + return; + } + + // @phpstan-ignore-next-line argument.type + self::__unserialize($this->parsedBody); + + if (is_array($items = $this->offsetGet('items'))) { + $parsed = Conversion::coerce(new ListOf($convert), value: $items); + // @phpstan-ignore-next-line + $this->offsetSet('items', value: $parsed); + } } - if (!($prev = $this->newestCursor ?? null)&&!($next = $this - ->oldestCursor ?? null)) { - return null; - + /** @return list */ + public function getItems(): array + { + // @phpstan-ignore-next-line return.type + return $this->offsetGet('items') ?? []; } - $nextRequest = array_merge_recursive( - $this->requestInfo, - ['query' => empty($prev) ? ['cursor' => $next] : [=> $prev]], - ); - - // @phpstan-ignore-next-line return.type - return [$nextRequest, $this->options]; - } - - /** - * @internal - * - * @param string|Converter|ConverterSource $convert - * @param Client $client - * @param array{ - * method: string, - * path: string, - * query: array, - * headers: array>, - * body: mixed, - * } $requestInfo - * @param RequestOptions $options - * @param mixed $parsedBody - */ - function __construct( - private string|Converter|ConverterSource $convert, - private Client $client, - private array $requestInfo, - private RequestOptions $options, - private ResponseInterface $response, - private mixed $parsedBody, - ) { - $this->initialize(); - - if (!is_array($this->parsedBody)) { - return; - - } - - // @phpstan-ignore-next-line argument.type - self::__unserialize($this->parsedBody); - - if (is_array($items = $this->offsetGet('items'))) { - $parsed = Conversion::coerce(new ListOf($convert), value: $items); - // @phpstan-ignore-next-line - $this->offsetSet('items', value: $parsed); - + /** + * @internal + * + * @return array{ + * array{ + * method: string, + * path: string, + * query: array, + * headers: array|null>, + * body: mixed, + * }, + * RequestOptions, + * }|null + */ + public function nextRequest(): ?array + { + if (!($this->hasMore ?? null) || !count($this->getItems())) { + return null; + } + + if (!($next = $this->oldestCursor ?? null)) { + return null; + } + + $nextRequest = array_merge_recursive( + $this->requestInfo, + ['query' => ['cursor' => $next]] + ); + + // @phpstan-ignore-next-line return.type + return [$nextRequest, $this->options]; } - } -} \ No newline at end of file +} diff --git a/src/CursorSearch.php b/src/CursorSearch.php index 9d46758..248cae5 100644 --- a/src/CursorSearch.php +++ b/src/CursorSearch.php @@ -2,132 +2,122 @@ namespace BeeperDesktop; -use BeeperDesktop\Core\Conversion; use BeeperDesktop\Core\Attributes\Optional; use BeeperDesktop\Core\Concerns\SdkModel; use BeeperDesktop\Core\Concerns\SdkPage; use BeeperDesktop\Core\Contracts\BaseModel; use BeeperDesktop\Core\Contracts\BasePage; -use BeeperDesktop\Core\Conversion\ListOf; +use BeeperDesktop\Core\Conversion; use BeeperDesktop\Core\Conversion\Contracts\Converter; use BeeperDesktop\Core\Conversion\Contracts\ConverterSource; +use BeeperDesktop\Core\Conversion\ListOf; use Psr\Http\Message\ResponseInterface; /** - * - * @phpstan-type CursorSearchShape = array{ - * items?: list>|null, - * hasMore?: bool|null, - * oldestCursor?: string|null, - * newestCursor?: string|null, - * } - * @template TItem - * @implements BasePage - * + * @phpstan-type CursorSearchShape = array{ + * items?: list>|null, + * hasMore?: bool|null, + * oldestCursor?: string|null, + * newestCursor?: string|null, + * } + * + * @template TItem + * + * @implements BasePage */ final class CursorSearch implements BaseModel, BasePage { - /** @use SdkModel */ - use SdkModel; - - /** @use SdkPage */ - use SdkPage; - - /** @var list|null $items */ - #[Optional(list: 'mixed')] - public ?array $items; - - /** @var bool|null $hasMore */ - #[Optional] - public ?bool $hasMore; - - /** @var string|null $oldestCursor */ - #[Optional(nullable: true)] - public ?string $oldestCursor; - - /** @var string|null $newestCursor */ - #[Optional(nullable: true)] - public ?string $newestCursor; - - /** @return list */ - function getItems(): array { - // @phpstan-ignore-next-line return.type - return $this->offsetGet('items') ?? []; - } - - /** - * @internal - * - * @return array{ - * array{ - * method: string, - * path: string, - * query: array, - * headers: array>, - * body: mixed, - * }, - * RequestOptions, - * }|null - */ - function nextRequest(): ?array { - if (!($this->hasMore ?? null)||!count($this->getItems())) { - return null; - + /** @use SdkModel */ + use SdkModel; + + /** @use SdkPage */ + use SdkPage; + + /** @var list|null $items */ + #[Optional(list: 'mixed')] + public ?array $items; + + #[Optional] + public ?bool $hasMore; + + #[Optional(nullable: true)] + public ?string $oldestCursor; + + #[Optional(nullable: true)] + public ?string $newestCursor; + + /** + * @internal + * + * @param array{ + * method: string, + * path: string, + * query: array, + * headers: array|null>, + * body: mixed, + * } $requestInfo + */ + public function __construct( + private string|Converter|ConverterSource $convert, + private Client $client, + private array $requestInfo, + private RequestOptions $options, + private ResponseInterface $response, + private mixed $parsedBody, + ) { + $this->initialize(); + + if (!is_array($this->parsedBody)) { + return; + } + + // @phpstan-ignore-next-line argument.type + self::__unserialize($this->parsedBody); + + if (is_array($items = $this->offsetGet('items'))) { + $parsed = Conversion::coerce(new ListOf($convert), value: $items); + // @phpstan-ignore-next-line + $this->offsetSet('items', value: $parsed); + } } - if (!($prev = $this->newestCursor ?? null)&&!($next = $this - ->oldestCursor ?? null)) { - return null; - + /** @return list */ + public function getItems(): array + { + // @phpstan-ignore-next-line return.type + return $this->offsetGet('items') ?? []; } - $nextRequest = array_merge_recursive( - $this->requestInfo, - ['query' => empty($prev) ? ['cursor' => $next] : [=> $prev]], - ); - - // @phpstan-ignore-next-line return.type - return [$nextRequest, $this->options]; - } - - /** - * @internal - * - * @param string|Converter|ConverterSource $convert - * @param Client $client - * @param array{ - * method: string, - * path: string, - * query: array, - * headers: array>, - * body: mixed, - * } $requestInfo - * @param RequestOptions $options - * @param mixed $parsedBody - */ - function __construct( - private string|Converter|ConverterSource $convert, - private Client $client, - private array $requestInfo, - private RequestOptions $options, - private ResponseInterface $response, - private mixed $parsedBody, - ) { - $this->initialize(); - - if (!is_array($this->parsedBody)) { - return; - - } - - // @phpstan-ignore-next-line argument.type - self::__unserialize($this->parsedBody); - - if (is_array($items = $this->offsetGet('items'))) { - $parsed = Conversion::coerce(new ListOf($convert), value: $items); - // @phpstan-ignore-next-line - $this->offsetSet('items', value: $parsed); - + /** + * @internal + * + * @return array{ + * array{ + * method: string, + * path: string, + * query: array, + * headers: array|null>, + * body: mixed, + * }, + * RequestOptions, + * }|null + */ + public function nextRequest(): ?array + { + if (!($this->hasMore ?? null) || !count($this->getItems())) { + return null; + } + + if (!($next = $this->oldestCursor ?? null)) { + return null; + } + + $nextRequest = array_merge_recursive( + $this->requestInfo, + ['query' => ['cursor' => $next]] + ); + + // @phpstan-ignore-next-line return.type + return [$nextRequest, $this->options]; } - } -} \ No newline at end of file +} diff --git a/src/CursorSortKey.php b/src/CursorSortKey.php deleted file mode 100644 index 7cdfa0c..0000000 --- a/src/CursorSortKey.php +++ /dev/null @@ -1,113 +0,0 @@ -|null, hasMore?: bool|null - * } - * - * @template TItem - * - * @implements BasePage - */ -final class CursorSortKey implements BaseModel, BasePage -{ - /** @use SdkModel */ - use SdkModel; - - /** @use SdkPage */ - use SdkPage; - - /** @var list|null $items */ - #[Optional(list: 'mixed')] - public ?array $items; - - #[Optional] - public ?bool $hasMore; - - /** - * @internal - * - * @param array{ - * method: string, - * path: string, - * query: array, - * headers: array|null>, - * body: mixed, - * } $requestInfo - */ - public function __construct( - private string|Converter|ConverterSource $convert, - private Client $client, - private array $requestInfo, - private RequestOptions $options, - private ResponseInterface $response, - private mixed $parsedBody, - ) { - $this->initialize(); - - if (!is_array($this->parsedBody)) { - return; - } - - // @phpstan-ignore-next-line argument.type - self::__unserialize($this->parsedBody); - - if (is_array($items = $this->offsetGet('items'))) { - $parsed = Conversion::coerce(new ListOf($convert), value: $items); - // @phpstan-ignore-next-line - $this->offsetSet('items', value: $parsed); - } - } - - /** @return list */ - public function getItems(): array - { - // @phpstan-ignore-next-line return.type - return $this->offsetGet('items') ?? []; - } - - /** - * @internal - * - * @return array{ - * array{ - * method: string, - * path: string, - * query: array, - * headers: array|null>, - * body: mixed, - * }, - * RequestOptions, - * }|null - */ - public function nextRequest(): ?array - { - $items = $this->getItems(); - if (!($this - ->hasMore ?? null) || !count($items) || !($key = array_key_last($items))) { - return null; - } - - $nextRequest = array_merge_recursive( - $this->requestInfo, - ['query' => ['sortKey' => $items[$key]]] - ); - - // @phpstan-ignore-next-line return.type - return [$nextRequest, $this->options]; - } -} diff --git a/src/CursorSortKey/Item.php b/src/CursorSortKey/Item.php deleted file mode 100644 index f3fe595..0000000 --- a/src/CursorSortKey/Item.php +++ /dev/null @@ -1,48 +0,0 @@ - */ - use SdkModel; - - #[Optional] - public ?string $sortKey; - - public function __construct() - { - $this->initialize(); - } - - /** - * Construct an instance from the required parameters. - * - * You must use named parameters to construct any parameters with a default value. - */ - public static function with(?string $sortKey = null): self - { - $self = new self; - - null !== $sortKey && $self['sortKey'] = $sortKey; - - return $self; - } - - public function withSortKey(string $sortKey): self - { - $self = clone $this; - $self['sortKey'] = $sortKey; - - return $self; - } -} diff --git a/src/Messages/MessageSearchParams.php b/src/Messages/MessageSearchParams.php index e9ffe25..706245f 100644 --- a/src/Messages/MessageSearchParams.php +++ b/src/Messages/MessageSearchParams.php @@ -11,10 +11,9 @@ use BeeperDesktop\Messages\MessageSearchParams\ChatType; use BeeperDesktop\Messages\MessageSearchParams\Direction; use BeeperDesktop\Messages\MessageSearchParams\MediaType; -use BeeperDesktop\Messages\MessageSearchParams\Sender; /** - * Search messages across chats using Beeper's message index. + * Search messages across chats. * * @see BeeperDesktop\Services\MessagesService::search() * @@ -31,7 +30,7 @@ * limit?: int|null, * mediaTypes?: list>|null, * query?: string|null, - * sender?: string|null|Sender|value-of, + * sender?: string|null, * } */ final class MessageSearchParams implements BaseModel @@ -124,10 +123,8 @@ final class MessageSearchParams implements BaseModel /** * Filter by sender: 'me' (messages sent by the authenticated user), 'others' (messages sent by others), or a specific user ID string (user.id). - * - * @var string|value-of|null $sender */ - #[Optional(enum: Sender::class)] + #[Optional] public ?string $sender; public function __construct() @@ -145,7 +142,6 @@ public function __construct() * @param ChatType|value-of|null $chatType * @param Direction|value-of|null $direction * @param list>|null $mediaTypes - * @param string|Sender|value-of|null $sender */ public static function with( ?array $accountIDs = null, @@ -160,7 +156,7 @@ public static function with( ?int $limit = null, ?array $mediaTypes = null, ?string $query = null, - Sender|string|null $sender = null, + ?string $sender = null, ): self { $self = new self; @@ -325,10 +321,8 @@ public function withQuery(string $query): self /** * Filter by sender: 'me' (messages sent by the authenticated user), 'others' (messages sent by others), or a specific user ID string (user.id). - * - * @param string|Sender|value-of $sender */ - public function withSender(Sender|string $sender): self + public function withSender(string $sender): self { $self = clone $this; $self['sender'] = $sender; diff --git a/src/Messages/MessageSearchParams/Sender.php b/src/Messages/MessageSearchParams/Sender.php deleted file mode 100644 index e58a8ab..0000000 --- a/src/Messages/MessageSearchParams/Sender.php +++ /dev/null @@ -1,15 +0,0 @@ -|AssetServeParams $params * @param RequestOpts|null $requestOptions * - * @return BaseResponse + * @return BaseResponse * * @throws APIException */ diff --git a/src/ServiceContracts/ChatsContract.php b/src/ServiceContracts/ChatsContract.php index e86488b..55837ea 100644 --- a/src/ServiceContracts/ChatsContract.php +++ b/src/ServiceContracts/ChatsContract.php @@ -4,20 +4,22 @@ namespace BeeperDesktop\ServiceContracts; -use BeeperDesktop\Chats\ChatCreateParams\Chat; +use BeeperDesktop\Chats\Chat; +use BeeperDesktop\Chats\ChatCreateParams\Mode; +use BeeperDesktop\Chats\ChatCreateParams\Type; +use BeeperDesktop\Chats\ChatCreateParams\User; use BeeperDesktop\Chats\ChatListParams\Direction; use BeeperDesktop\Chats\ChatListResponse; use BeeperDesktop\Chats\ChatNewResponse; use BeeperDesktop\Chats\ChatSearchParams\Inbox; use BeeperDesktop\Chats\ChatSearchParams\Scope; -use BeeperDesktop\Chats\ChatSearchParams\Type; use BeeperDesktop\Core\Exceptions\APIException; use BeeperDesktop\CursorNoLimit; use BeeperDesktop\CursorSearch; use BeeperDesktop\RequestOptions; /** - * @phpstan-import-type ChatShape from \BeeperDesktop\Chats\ChatCreateParams\Chat + * @phpstan-import-type UserShape from \BeeperDesktop\Chats\ChatCreateParams\User * @phpstan-import-type RequestOpts from \BeeperDesktop\RequestOptions */ interface ChatsContract @@ -25,14 +27,28 @@ interface ChatsContract /** * @api * - * @param Chat|ChatShape $chat + * @param string $accountID account to create or start the chat on + * @param bool $allowInvite Only used for mode='start'. Whether invite-based DM creation is allowed when required by the platform. + * @param string $messageText optional first message content if the platform requires it to create the chat + * @param Mode|value-of $mode Operation mode. Use 'start' to resolve a user/contact and start a direct chat. Omit or set 'create' to create a chat directly. + * @param list $participantIDs Required for create mode. Provide exactly one user ID for 'single' chats and one or more for 'group' chats. + * @param string $title optional title for group chats; ignored for single chats on most networks + * @param Type|value-of $type Required for create mode. 'single' creates a direct message chat; 'group' creates a group chat. + * @param User|UserShape $user Required for mode='start'. Merged user-like contact payload used to resolve the best identifier. * @param RequestOpts|null $requestOptions * * @throws APIException */ public function create( - Chat|array $chat, - RequestOptions|array|null $requestOptions = null + string $accountID, + bool $allowInvite = true, + ?string $messageText = null, + Mode|string|null $mode = null, + ?array $participantIDs = null, + ?string $title = null, + Type|string|null $type = null, + User|array|null $user = null, + RequestOptions|array|null $requestOptions = null, ): ChatNewResponse; /** @@ -48,7 +64,7 @@ public function retrieve( string $chatID, ?int $maxParticipantCount = -1, RequestOptions|array|null $requestOptions = null, - ): \BeeperDesktop\Chats\Chat; + ): Chat; /** * @api @@ -97,11 +113,11 @@ public function archive( * @param int $limit Set the maximum number of chats to retrieve. Valid range: 1-200, default is 50 * @param string $query Literal token search (non-semantic). Use single words users type (e.g., "dinner"). When multiple words provided, ALL must match. Case-insensitive. * @param Scope|value-of $scope search scope: 'titles' matches title + network; 'participants' matches participant names - * @param Type|value-of $type Specify the type of chats to retrieve: use "single" for direct messages, "group" for group chats, or "any" to get all types + * @param \BeeperDesktop\Chats\ChatSearchParams\Type|value-of<\BeeperDesktop\Chats\ChatSearchParams\Type> $type Specify the type of chats to retrieve: use "single" for direct messages, "group" for group chats, or "any" to get all types * @param bool|null $unreadOnly Set to true to only retrieve chats that have unread messages * @param RequestOpts|null $requestOptions * - * @return CursorSearch<\BeeperDesktop\Chats\Chat> + * @return CursorSearch * * @throws APIException */ @@ -116,7 +132,7 @@ public function search( int $limit = 50, ?string $query = null, Scope|string $scope = 'titles', - Type|string $type = 'any', + \BeeperDesktop\Chats\ChatSearchParams\Type|string $type = 'any', ?bool $unreadOnly = null, RequestOptions|array|null $requestOptions = null, ): CursorSearch; diff --git a/src/ServiceContracts/MessagesContract.php b/src/ServiceContracts/MessagesContract.php index 886634d..66c79a4 100644 --- a/src/ServiceContracts/MessagesContract.php +++ b/src/ServiceContracts/MessagesContract.php @@ -5,13 +5,12 @@ namespace BeeperDesktop\ServiceContracts; use BeeperDesktop\Core\Exceptions\APIException; +use BeeperDesktop\CursorNoLimit; use BeeperDesktop\CursorSearch; -use BeeperDesktop\CursorSortKey; use BeeperDesktop\Message; use BeeperDesktop\Messages\MessageListParams\Direction; use BeeperDesktop\Messages\MessageSearchParams\ChatType; use BeeperDesktop\Messages\MessageSearchParams\MediaType; -use BeeperDesktop\Messages\MessageSearchParams\Sender; use BeeperDesktop\Messages\MessageSendParams\Attachment; use BeeperDesktop\Messages\MessageSendResponse; use BeeperDesktop\Messages\MessageUpdateResponse; @@ -48,7 +47,7 @@ public function update( * @param Direction|value-of $direction Pagination direction used with 'cursor': 'before' fetches older results, 'after' fetches newer results. Defaults to 'before' when only 'cursor' is provided. * @param RequestOpts|null $requestOptions * - * @return CursorSortKey + * @return CursorNoLimit * * @throws APIException */ @@ -57,7 +56,7 @@ public function list( ?string $cursor = null, Direction|string|null $direction = null, RequestOptions|array|null $requestOptions = null, - ): CursorSortKey; + ): CursorNoLimit; /** * @api @@ -74,7 +73,7 @@ public function list( * @param int $limit maximum number of messages to return * @param list> $mediaTypes Filter messages by media types. Use ['any'] for any media type, or specify exact types like ['video', 'image']. Omit for no media filtering. * @param string $query Literal word search (non-semantic). Finds messages containing these EXACT words in any order. Use single words users actually type, not concepts or phrases. Example: use "dinner" not "dinner plans", use "sick" not "health issues". If omitted, returns results filtered only by other parameters. - * @param string|Sender|value-of $sender Filter by sender: 'me' (messages sent by the authenticated user), 'others' (messages sent by others), or a specific user ID string (user.id). + * @param string $sender Filter by sender: 'me' (messages sent by the authenticated user), 'others' (messages sent by others), or a specific user ID string (user.id). * @param RequestOpts|null $requestOptions * * @return CursorSearch @@ -94,7 +93,7 @@ public function search( int $limit = 20, ?array $mediaTypes = null, ?string $query = null, - Sender|string|null $sender = null, + ?string $sender = null, RequestOptions|array|null $requestOptions = null, ): CursorSearch; diff --git a/src/ServiceContracts/MessagesRawContract.php b/src/ServiceContracts/MessagesRawContract.php index 0afdc79..7a1c531 100644 --- a/src/ServiceContracts/MessagesRawContract.php +++ b/src/ServiceContracts/MessagesRawContract.php @@ -6,8 +6,8 @@ use BeeperDesktop\Core\Contracts\BaseResponse; use BeeperDesktop\Core\Exceptions\APIException; +use BeeperDesktop\CursorNoLimit; use BeeperDesktop\CursorSearch; -use BeeperDesktop\CursorSortKey; use BeeperDesktop\Message; use BeeperDesktop\Messages\MessageListParams; use BeeperDesktop\Messages\MessageSearchParams; @@ -46,7 +46,7 @@ public function update( * @param array|MessageListParams $params * @param RequestOpts|null $requestOptions * - * @return BaseResponse> + * @return BaseResponse> * * @throws APIException */ diff --git a/src/Services/AssetsRawService.php b/src/Services/AssetsRawService.php index 051bf2a..94db5e4 100644 --- a/src/Services/AssetsRawService.php +++ b/src/Services/AssetsRawService.php @@ -14,6 +14,7 @@ use BeeperDesktop\Client; use BeeperDesktop\Core\Contracts\BaseResponse; use BeeperDesktop\Core\Exceptions\APIException; +use BeeperDesktop\Core\FileParam; use BeeperDesktop\RequestOptions; use BeeperDesktop\ServiceContracts\AssetsRawContract; @@ -69,7 +70,7 @@ public function download( * @param array{url: string}|AssetServeParams $params * @param RequestOpts|null $requestOptions * - * @return BaseResponse + * @return BaseResponse * * @throws APIException */ @@ -87,8 +88,9 @@ public function serve( method: 'get', path: 'v1/assets/serve', query: $parsed, + headers: ['Accept' => 'application/octet-stream'], options: $options, - convert: null, + convert: 'string', ); } @@ -98,7 +100,7 @@ public function serve( * Upload a file to a temporary location using multipart/form-data. Returns an uploadID that can be referenced when sending messages with attachments. * * @param array{ - * file: string, fileName?: string, mimeType?: string + * file: string|FileParam, fileName?: string, mimeType?: string * }|AssetUploadParams $params * @param RequestOpts|null $requestOptions * diff --git a/src/Services/AssetsService.php b/src/Services/AssetsService.php index 6d77b41..95dfa91 100644 --- a/src/Services/AssetsService.php +++ b/src/Services/AssetsService.php @@ -9,6 +9,7 @@ use BeeperDesktop\Assets\AssetUploadResponse; use BeeperDesktop\Client; use BeeperDesktop\Core\Exceptions\APIException; +use BeeperDesktop\Core\FileParam; use BeeperDesktop\Core\Util; use BeeperDesktop\RequestOptions; use BeeperDesktop\ServiceContracts\AssetsContract; @@ -68,7 +69,7 @@ public function download( public function serve( string $url, RequestOptions|array|null $requestOptions = null - ): mixed { + ): string { $params = Util::removeNulls(['url' => $url]); // @phpstan-ignore-next-line argument.type @@ -82,7 +83,7 @@ public function serve( * * Upload a file to a temporary location using multipart/form-data. Returns an uploadID that can be referenced when sending messages with attachments. * - * @param string $file the file to upload (max 500 MB) + * @param string|FileParam $file the file to upload (max 500 MB) * @param string $fileName Original filename. Defaults to the uploaded file name if omitted * @param string $mimeType MIME type. Auto-detected from magic bytes if omitted * @param RequestOpts|null $requestOptions @@ -90,7 +91,7 @@ public function serve( * @throws APIException */ public function upload( - string $file, + string|FileParam $file, ?string $fileName = null, ?string $mimeType = null, RequestOptions|array|null $requestOptions = null, diff --git a/src/Services/BeeperDesktopClientRawService.php b/src/Services/BeeperDesktopClientRawService.php index e40a60d..ee0acfe 100644 --- a/src/Services/BeeperDesktopClientRawService.php +++ b/src/Services/BeeperDesktopClientRawService.php @@ -15,6 +15,8 @@ use BeeperDesktop\ServiceContracts\BeeperDesktopClientRawContract; /** + * Control the Beeper Desktop application. + * * @phpstan-import-type RequestOpts from \BeeperDesktop\RequestOptions */ final class BeeperDesktopClientRawService implements BeeperDesktopClientRawContract diff --git a/src/Services/BeeperDesktopClientService.php b/src/Services/BeeperDesktopClientService.php index 9f2272f..0d44a9a 100644 --- a/src/Services/BeeperDesktopClientService.php +++ b/src/Services/BeeperDesktopClientService.php @@ -13,6 +13,8 @@ use BeeperDesktop\ServiceContracts\BeeperDesktopClientContract; /** + * Control the Beeper Desktop application. + * * @phpstan-import-type RequestOpts from \BeeperDesktop\RequestOptions */ final class BeeperDesktopClientService implements BeeperDesktopClientContract diff --git a/src/Services/Chats/Messages/ReactionsRawService.php b/src/Services/Chats/Messages/ReactionsRawService.php index 80a8750..dcfb515 100644 --- a/src/Services/Chats/Messages/ReactionsRawService.php +++ b/src/Services/Chats/Messages/ReactionsRawService.php @@ -30,7 +30,7 @@ public function __construct(private Client $client) {} /** * @api * - * Remove the authenticated user's reaction from an existing message. + * Remove the reaction added by the authenticated user from an existing message. * * @param string $messageID Path param: ID of the message to remove a reaction from * @param array{chatID: string, reactionKey: string}|ReactionDeleteParams $params diff --git a/src/Services/Chats/Messages/ReactionsService.php b/src/Services/Chats/Messages/ReactionsService.php index 83d410b..06fd937 100644 --- a/src/Services/Chats/Messages/ReactionsService.php +++ b/src/Services/Chats/Messages/ReactionsService.php @@ -35,7 +35,7 @@ public function __construct(private Client $client) /** * @api * - * Remove the authenticated user's reaction from an existing message. + * Remove the reaction added by the authenticated user from an existing message. * * @param string $messageID Path param: ID of the message to remove a reaction from * @param string $chatID path param: Unique identifier of the chat diff --git a/src/Services/ChatsRawService.php b/src/Services/ChatsRawService.php index 1df2645..3140e67 100644 --- a/src/Services/ChatsRawService.php +++ b/src/Services/ChatsRawService.php @@ -4,9 +4,12 @@ namespace BeeperDesktop\Services; +use BeeperDesktop\Chats\Chat; use BeeperDesktop\Chats\ChatArchiveParams; use BeeperDesktop\Chats\ChatCreateParams; -use BeeperDesktop\Chats\ChatCreateParams\Chat; +use BeeperDesktop\Chats\ChatCreateParams\Mode; +use BeeperDesktop\Chats\ChatCreateParams\Type; +use BeeperDesktop\Chats\ChatCreateParams\User; use BeeperDesktop\Chats\ChatListParams; use BeeperDesktop\Chats\ChatListParams\Direction; use BeeperDesktop\Chats\ChatListResponse; @@ -15,7 +18,6 @@ use BeeperDesktop\Chats\ChatSearchParams; use BeeperDesktop\Chats\ChatSearchParams\Inbox; use BeeperDesktop\Chats\ChatSearchParams\Scope; -use BeeperDesktop\Chats\ChatSearchParams\Type; use BeeperDesktop\Client; use BeeperDesktop\Core\Contracts\BaseResponse; use BeeperDesktop\Core\Exceptions\APIException; @@ -27,7 +29,7 @@ /** * Manage chats. * - * @phpstan-import-type ChatShape from \BeeperDesktop\Chats\ChatCreateParams\Chat + * @phpstan-import-type UserShape from \BeeperDesktop\Chats\ChatCreateParams\User * @phpstan-import-type RequestOpts from \BeeperDesktop\RequestOptions */ final class ChatsRawService implements ChatsRawContract @@ -41,9 +43,18 @@ public function __construct(private Client $client) {} /** * @api * - * Create a single/group chat (mode='create') or start a direct chat from merged user data (mode='start'). + * Create a direct or group chat with mode="create", or use mode="start" to resolve a contact and open a direct chat. * - * @param array{chat: Chat|ChatShape}|ChatCreateParams $params + * @param array{ + * accountID: string, + * allowInvite?: bool, + * messageText?: string, + * mode?: Mode|value-of, + * participantIDs?: list, + * title?: string, + * type?: Type|value-of, + * user?: User|UserShape, + * }|ChatCreateParams $params * @param RequestOpts|null $requestOptions * * @return BaseResponse @@ -63,7 +74,7 @@ public function create( return $this->client->request( method: 'post', path: 'v1/chats', - body: (object) $parsed['chat'], + body: (object) $parsed, options: $options, convert: ChatNewResponse::class, ); @@ -78,7 +89,7 @@ public function create( * @param array{maxParticipantCount?: int|null}|ChatRetrieveParams $params * @param RequestOpts|null $requestOptions * - * @return BaseResponse<\BeeperDesktop\Chats\Chat> + * @return BaseResponse * * @throws APIException */ @@ -98,7 +109,7 @@ public function retrieve( path: ['v1/chats/%1$s', $chatID], query: $parsed, options: $options, - convert: \BeeperDesktop\Chats\Chat::class, + convert: Chat::class, ); } @@ -174,7 +185,7 @@ public function archive( /** * @api * - * Search chats by title/network or participants using Beeper Desktop's renderer algorithm. + * Search chats by title, network, or participant names. * * @param array{ * accountIDs?: list, @@ -187,12 +198,12 @@ public function archive( * limit?: int, * query?: string, * scope?: Scope|value-of, - * type?: Type|value-of, + * type?: ChatSearchParams\Type|value-of, * unreadOnly?: bool|null, * }|ChatSearchParams $params * @param RequestOpts|null $requestOptions * - * @return BaseResponse> + * @return BaseResponse> * * @throws APIException */ @@ -211,7 +222,7 @@ public function search( path: 'v1/chats/search', query: $parsed, options: $options, - convert: \BeeperDesktop\Chats\Chat::class, + convert: Chat::class, page: CursorSearch::class, ); } diff --git a/src/Services/ChatsService.php b/src/Services/ChatsService.php index 8ca0439..9f7169e 100644 --- a/src/Services/ChatsService.php +++ b/src/Services/ChatsService.php @@ -4,13 +4,15 @@ namespace BeeperDesktop\Services; -use BeeperDesktop\Chats\ChatCreateParams\Chat; +use BeeperDesktop\Chats\Chat; +use BeeperDesktop\Chats\ChatCreateParams\Mode; +use BeeperDesktop\Chats\ChatCreateParams\Type; +use BeeperDesktop\Chats\ChatCreateParams\User; use BeeperDesktop\Chats\ChatListParams\Direction; use BeeperDesktop\Chats\ChatListResponse; use BeeperDesktop\Chats\ChatNewResponse; use BeeperDesktop\Chats\ChatSearchParams\Inbox; use BeeperDesktop\Chats\ChatSearchParams\Scope; -use BeeperDesktop\Chats\ChatSearchParams\Type; use BeeperDesktop\Client; use BeeperDesktop\Core\Exceptions\APIException; use BeeperDesktop\Core\Util; @@ -24,7 +26,7 @@ /** * Manage chats. * - * @phpstan-import-type ChatShape from \BeeperDesktop\Chats\ChatCreateParams\Chat + * @phpstan-import-type UserShape from \BeeperDesktop\Chats\ChatCreateParams\User * @phpstan-import-type RequestOpts from \BeeperDesktop\RequestOptions */ final class ChatsService implements ChatsContract @@ -57,18 +59,43 @@ public function __construct(private Client $client) /** * @api * - * Create a single/group chat (mode='create') or start a direct chat from merged user data (mode='start'). + * Create a direct or group chat with mode="create", or use mode="start" to resolve a contact and open a direct chat. * - * @param Chat|ChatShape $chat + * @param string $accountID account to create or start the chat on + * @param bool $allowInvite Only used for mode='start'. Whether invite-based DM creation is allowed when required by the platform. + * @param string $messageText optional first message content if the platform requires it to create the chat + * @param Mode|value-of $mode Operation mode. Use 'start' to resolve a user/contact and start a direct chat. Omit or set 'create' to create a chat directly. + * @param list $participantIDs Required for create mode. Provide exactly one user ID for 'single' chats and one or more for 'group' chats. + * @param string $title optional title for group chats; ignored for single chats on most networks + * @param Type|value-of $type Required for create mode. 'single' creates a direct message chat; 'group' creates a group chat. + * @param User|UserShape $user Required for mode='start'. Merged user-like contact payload used to resolve the best identifier. * @param RequestOpts|null $requestOptions * * @throws APIException */ public function create( - Chat|array $chat, - RequestOptions|array|null $requestOptions = null + string $accountID, + bool $allowInvite = true, + ?string $messageText = null, + Mode|string|null $mode = null, + ?array $participantIDs = null, + ?string $title = null, + Type|string|null $type = null, + User|array|null $user = null, + RequestOptions|array|null $requestOptions = null, ): ChatNewResponse { - $params = Util::removeNulls(['chat' => $chat]); + $params = Util::removeNulls( + [ + 'accountID' => $accountID, + 'allowInvite' => $allowInvite, + 'messageText' => $messageText, + 'mode' => $mode, + 'participantIDs' => $participantIDs, + 'title' => $title, + 'type' => $type, + 'user' => $user, + ], + ); // @phpstan-ignore-next-line argument.type $response = $this->raw->create(params: $params, requestOptions: $requestOptions); @@ -91,7 +118,7 @@ public function retrieve( string $chatID, ?int $maxParticipantCount = -1, RequestOptions|array|null $requestOptions = null, - ): \BeeperDesktop\Chats\Chat { + ): Chat { $params = Util::removeNulls( ['maxParticipantCount' => $maxParticipantCount] ); @@ -163,7 +190,7 @@ public function archive( /** * @api * - * Search chats by title/network or participants using Beeper Desktop's renderer algorithm. + * Search chats by title, network, or participant names. * * @param list $accountIDs Provide an array of account IDs to filter chats from specific messaging accounts only * @param string $cursor Opaque pagination cursor; do not inspect. Use together with 'direction'. @@ -175,11 +202,11 @@ public function archive( * @param int $limit Set the maximum number of chats to retrieve. Valid range: 1-200, default is 50 * @param string $query Literal token search (non-semantic). Use single words users type (e.g., "dinner"). When multiple words provided, ALL must match. Case-insensitive. * @param Scope|value-of $scope search scope: 'titles' matches title + network; 'participants' matches participant names - * @param Type|value-of $type Specify the type of chats to retrieve: use "single" for direct messages, "group" for group chats, or "any" to get all types + * @param \BeeperDesktop\Chats\ChatSearchParams\Type|value-of<\BeeperDesktop\Chats\ChatSearchParams\Type> $type Specify the type of chats to retrieve: use "single" for direct messages, "group" for group chats, or "any" to get all types * @param bool|null $unreadOnly Set to true to only retrieve chats that have unread messages * @param RequestOpts|null $requestOptions * - * @return CursorSearch<\BeeperDesktop\Chats\Chat> + * @return CursorSearch * * @throws APIException */ @@ -194,7 +221,7 @@ public function search( int $limit = 50, ?string $query = null, Scope|string $scope = 'titles', - Type|string $type = 'any', + \BeeperDesktop\Chats\ChatSearchParams\Type|string $type = 'any', ?bool $unreadOnly = null, RequestOptions|array|null $requestOptions = null, ): CursorSearch { diff --git a/src/Services/InfoRawService.php b/src/Services/InfoRawService.php index 853a5b8..c5a4228 100644 --- a/src/Services/InfoRawService.php +++ b/src/Services/InfoRawService.php @@ -12,6 +12,8 @@ use BeeperDesktop\ServiceContracts\InfoRawContract; /** + * Control the Beeper Desktop application. + * * @phpstan-import-type RequestOpts from \BeeperDesktop\RequestOptions */ final class InfoRawService implements InfoRawContract @@ -42,6 +44,7 @@ public function retrieve( path: 'v1/info', options: $requestOptions, convert: InfoGetResponse::class, + security: [], ); } } diff --git a/src/Services/InfoService.php b/src/Services/InfoService.php index c38342e..20ccaf2 100644 --- a/src/Services/InfoService.php +++ b/src/Services/InfoService.php @@ -11,6 +11,8 @@ use BeeperDesktop\ServiceContracts\InfoContract; /** + * Control the Beeper Desktop application. + * * @phpstan-import-type RequestOpts from \BeeperDesktop\RequestOptions */ final class InfoService implements InfoContract diff --git a/src/Services/MessagesRawService.php b/src/Services/MessagesRawService.php index 58ac32b..0da852e 100644 --- a/src/Services/MessagesRawService.php +++ b/src/Services/MessagesRawService.php @@ -7,15 +7,14 @@ use BeeperDesktop\Client; use BeeperDesktop\Core\Contracts\BaseResponse; use BeeperDesktop\Core\Exceptions\APIException; +use BeeperDesktop\CursorNoLimit; use BeeperDesktop\CursorSearch; -use BeeperDesktop\CursorSortKey; use BeeperDesktop\Message; use BeeperDesktop\Messages\MessageListParams; use BeeperDesktop\Messages\MessageListParams\Direction; use BeeperDesktop\Messages\MessageSearchParams; use BeeperDesktop\Messages\MessageSearchParams\ChatType; use BeeperDesktop\Messages\MessageSearchParams\MediaType; -use BeeperDesktop\Messages\MessageSearchParams\Sender; use BeeperDesktop\Messages\MessageSendParams; use BeeperDesktop\Messages\MessageSendParams\Attachment; use BeeperDesktop\Messages\MessageSendResponse; @@ -84,7 +83,7 @@ public function update( * }|MessageListParams $params * @param RequestOpts|null $requestOptions * - * @return BaseResponse> + * @return BaseResponse> * * @throws APIException */ @@ -105,14 +104,14 @@ public function list( query: $parsed, options: $options, convert: Message::class, - page: CursorSortKey::class, + page: CursorNoLimit::class, ); } /** * @api * - * Search messages across chats using Beeper's message index + * Search messages across chats. * * @param array{ * accountIDs?: list, @@ -127,7 +126,7 @@ public function list( * limit?: int, * mediaTypes?: list>, * query?: string, - * sender?: string|Sender|value-of, + * sender?: string, * }|MessageSearchParams $params * @param RequestOpts|null $requestOptions * diff --git a/src/Services/MessagesService.php b/src/Services/MessagesService.php index 70f0386..2045b00 100644 --- a/src/Services/MessagesService.php +++ b/src/Services/MessagesService.php @@ -7,13 +7,12 @@ use BeeperDesktop\Client; use BeeperDesktop\Core\Exceptions\APIException; use BeeperDesktop\Core\Util; +use BeeperDesktop\CursorNoLimit; use BeeperDesktop\CursorSearch; -use BeeperDesktop\CursorSortKey; use BeeperDesktop\Message; use BeeperDesktop\Messages\MessageListParams\Direction; use BeeperDesktop\Messages\MessageSearchParams\ChatType; use BeeperDesktop\Messages\MessageSearchParams\MediaType; -use BeeperDesktop\Messages\MessageSearchParams\Sender; use BeeperDesktop\Messages\MessageSendParams\Attachment; use BeeperDesktop\Messages\MessageSendResponse; use BeeperDesktop\Messages\MessageUpdateResponse; @@ -77,7 +76,7 @@ public function update( * @param Direction|value-of $direction Pagination direction used with 'cursor': 'before' fetches older results, 'after' fetches newer results. Defaults to 'before' when only 'cursor' is provided. * @param RequestOpts|null $requestOptions * - * @return CursorSortKey + * @return CursorNoLimit * * @throws APIException */ @@ -86,7 +85,7 @@ public function list( ?string $cursor = null, Direction|string|null $direction = null, RequestOptions|array|null $requestOptions = null, - ): CursorSortKey { + ): CursorNoLimit { $params = Util::removeNulls( ['cursor' => $cursor, 'direction' => $direction] ); @@ -100,7 +99,7 @@ public function list( /** * @api * - * Search messages across chats using Beeper's message index + * Search messages across chats. * * @param list $accountIDs limit search to specific account IDs * @param list $chatIDs limit search to specific chat IDs @@ -114,7 +113,7 @@ public function list( * @param int $limit maximum number of messages to return * @param list> $mediaTypes Filter messages by media types. Use ['any'] for any media type, or specify exact types like ['video', 'image']. Omit for no media filtering. * @param string $query Literal word search (non-semantic). Finds messages containing these EXACT words in any order. Use single words users actually type, not concepts or phrases. Example: use "dinner" not "dinner plans", use "sick" not "health issues". If omitted, returns results filtered only by other parameters. - * @param string|Sender|value-of $sender Filter by sender: 'me' (messages sent by the authenticated user), 'others' (messages sent by others), or a specific user ID string (user.id). + * @param string $sender Filter by sender: 'me' (messages sent by the authenticated user), 'others' (messages sent by others), or a specific user ID string (user.id). * @param RequestOpts|null $requestOptions * * @return CursorSearch @@ -134,7 +133,7 @@ public function search( int $limit = 20, ?array $mediaTypes = null, ?string $query = null, - Sender|string|null $sender = null, + ?string $sender = null, RequestOptions|array|null $requestOptions = null, ): CursorSearch { $params = Util::removeNulls( diff --git a/src/Version.php b/src/Version.php index fa38b72..10c344d 100644 --- a/src/Version.php +++ b/src/Version.php @@ -5,5 +5,5 @@ namespace BeeperDesktop; // x-release-please-start-version -const VERSION = '0.0.1'; +const VERSION = '0.1.0'; // x-release-please-end diff --git a/tests/Services/AssetsTest.php b/tests/Services/AssetsTest.php index 4ae3576..6aaeb61 100644 --- a/tests/Services/AssetsTest.php +++ b/tests/Services/AssetsTest.php @@ -6,6 +6,7 @@ use BeeperDesktop\Assets\AssetUploadBase64Response; use BeeperDesktop\Assets\AssetUploadResponse; use BeeperDesktop\Client; +use BeeperDesktop\Core\FileParam; use BeeperDesktop\Core\Util; use PHPUnit\Framework\Attributes\CoversNothing; use PHPUnit\Framework\Attributes\Test; @@ -57,7 +58,7 @@ public function testServe(): void $result = $this->client->assets->serve(url: 'x'); // @phpstan-ignore-next-line method.alreadyNarrowedType - $this->assertNull($result); + $this->assertIsString($result); } #[Test] @@ -66,13 +67,15 @@ public function testServeWithOptionalParams(): void $result = $this->client->assets->serve(url: 'x'); // @phpstan-ignore-next-line method.alreadyNarrowedType - $this->assertNull($result); + $this->assertIsString($result); } #[Test] public function testUpload(): void { - $result = $this->client->assets->upload(file: 'file'); + $result = $this->client->assets->upload( + file: FileParam::fromString('Example data', filename: uniqid('file-upload-', true)), + ); // @phpstan-ignore-next-line method.alreadyNarrowedType $this->assertInstanceOf(AssetUploadResponse::class, $result); @@ -82,9 +85,9 @@ public function testUpload(): void public function testUploadWithOptionalParams(): void { $result = $this->client->assets->upload( - file: 'file', + file: FileParam::fromString('Example data', filename: uniqid('file-upload-', true)), fileName: 'fileName', - mimeType: 'mimeType' + mimeType: 'mimeType', ); // @phpstan-ignore-next-line method.alreadyNarrowedType diff --git a/tests/Services/ChatsTest.php b/tests/Services/ChatsTest.php index 71d8111..568e7d6 100644 --- a/tests/Services/ChatsTest.php +++ b/tests/Services/ChatsTest.php @@ -34,7 +34,7 @@ protected function setUp(): void #[Test] public function testCreate(): void { - $result = $this->client->chats->create(chat: ['accountID' => 'accountID']); + $result = $this->client->chats->create(accountID: 'accountID'); // @phpstan-ignore-next-line method.alreadyNarrowedType $this->assertInstanceOf(ChatNewResponse::class, $result); @@ -44,21 +44,19 @@ public function testCreate(): void public function testCreateWithOptionalParams(): void { $result = $this->client->chats->create( - chat: [ - 'accountID' => 'accountID', - 'allowInvite' => true, - 'messageText' => 'messageText', - 'mode' => 'create', - 'participantIDs' => ['string'], - 'title' => 'title', - 'type' => 'single', - 'user' => [ - 'id' => 'id', - 'email' => 'email', - 'fullName' => 'fullName', - 'phoneNumber' => 'phoneNumber', - 'username' => 'username', - ], + accountID: 'accountID', + allowInvite: true, + messageText: 'messageText', + mode: 'start', + participantIDs: ['string'], + title: 'title', + type: 'single', + user: [ + 'id' => 'id', + 'email' => 'email', + 'fullName' => 'fullName', + 'phoneNumber' => 'phoneNumber', + 'username' => 'username', ], ); diff --git a/tests/Services/MessagesTest.php b/tests/Services/MessagesTest.php index 0d1cab8..f0691c6 100644 --- a/tests/Services/MessagesTest.php +++ b/tests/Services/MessagesTest.php @@ -4,8 +4,8 @@ use BeeperDesktop\Client; use BeeperDesktop\Core\Util; +use BeeperDesktop\CursorNoLimit; use BeeperDesktop\CursorSearch; -use BeeperDesktop\CursorSortKey; use BeeperDesktop\Message; use BeeperDesktop\Messages\MessageSendResponse; use BeeperDesktop\Messages\MessageUpdateResponse; @@ -63,7 +63,7 @@ public function testList(): void $page = $this->client->messages->list('!NCdzlIaMjZUmvmvyHU:beeper.com'); // @phpstan-ignore-next-line method.alreadyNarrowedType - $this->assertInstanceOf(CursorSortKey::class, $page); + $this->assertInstanceOf(CursorNoLimit::class, $page); if ($item = $page->getItems()[0] ?? null) { // @phpstan-ignore-next-line method.alreadyNarrowedType