diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e3308d..1baf5c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: lint: timeout-minutes: 10 name: lint - runs-on: ${{ github.repository == 'stainless-sdks/blooio-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -41,7 +41,7 @@ jobs: permissions: contents: read id-token: write - runs-on: ${{ github.repository == 'stainless-sdks/blooio-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -78,7 +78,7 @@ jobs: test: timeout-minutes: 10 name: test - runs-on: ${{ github.repository == 'stainless-sdks/blooio-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d0ab664..2a8f4ff 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.2.0" + ".": "1.3.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 000c763..d4e4ced 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-bce90b321b6782841d7834e0e0689b1812ef1f10f8f2823cd9908185d9f306e6.yml -openapi_spec_hash: a90c7586ed5e83a938d72a286efa9f8e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-18cad1cfa869b6c0be9b0ef7573af1162034b9ae13a100a7378967f8633619f5.yml +openapi_spec_hash: ad3171218a23a9c40695fd30a67758fa config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/CHANGELOG.md b/CHANGELOG.md index ba8d78c..015d83a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## 1.3.0 (2026-09-03) + +Full Changelog: [v1.2.0...v1.3.0](https://github.com/Blooio/blooio-python-sdk/compare/v1.2.0...v1.3.0) + +### Features + +* **api:** api update ([b2d50ea](https://github.com/Blooio/blooio-python-sdk/commit/b2d50ea9ab22a13e8538ceff4193e5267992cc85)) +* **api:** api update ([d519459](https://github.com/Blooio/blooio-python-sdk/commit/d519459512a1adfa7a3caf17d534989f1769a0c2)) +* **api:** api update ([e423382](https://github.com/Blooio/blooio-python-sdk/commit/e4233820761194e86157825cb2fd1aa3925357da)) +* **api:** api update ([cf471a5](https://github.com/Blooio/blooio-python-sdk/commit/cf471a562375341931981795ed31394d650718c0)) +* **api:** api update ([c8e6827](https://github.com/Blooio/blooio-python-sdk/commit/c8e68277578cd19811353635d943a5589c705a30)) +* **api:** api update ([f725a2a](https://github.com/Blooio/blooio-python-sdk/commit/f725a2a9f09573cd3f72b6d0457a9eb807f2ddd9)) +* **api:** api update ([ef1df40](https://github.com/Blooio/blooio-python-sdk/commit/ef1df408e0298e430c14a26942564e5a77aa259c)) +* **api:** api update ([30e8dd1](https://github.com/Blooio/blooio-python-sdk/commit/30e8dd13b8e2ca5988573a2cc482c122ea1650ae)) +* **api:** api update ([f6caf8e](https://github.com/Blooio/blooio-python-sdk/commit/f6caf8e2ab1630818c57ab8142a5107cc534c57c)) +* **api:** api update ([b3ba795](https://github.com/Blooio/blooio-python-sdk/commit/b3ba7951d022e732ae0e3a458520ed3f42a49a5f)) +* **api:** api update ([d9b82ea](https://github.com/Blooio/blooio-python-sdk/commit/d9b82ea21659239202fb8a4d63366d54ea60fcbe)) +* **api:** api update ([2e075ad](https://github.com/Blooio/blooio-python-sdk/commit/2e075ad61159d021fc31bd6fda9f0152ea703a34)) +* **api:** api update ([178129e](https://github.com/Blooio/blooio-python-sdk/commit/178129ec16b48bb7be95b672071147fb1a4bf679)) +* **api:** api update ([03aecef](https://github.com/Blooio/blooio-python-sdk/commit/03aecefb518d40f36c37d71cfa7637cefb0575c4)) +* **api:** api update ([e4708cb](https://github.com/Blooio/blooio-python-sdk/commit/e4708cb01510e75961ac6a058308140cf69424ee)) +* **api:** api update ([bb8074b](https://github.com/Blooio/blooio-python-sdk/commit/bb8074b817bc550cba91e705ebbd710dee71caba)) +* **stlc:** configurable CI runner and private-production-repo support in workflow templates ([33ac236](https://github.com/Blooio/blooio-python-sdk/commit/33ac236b6e204193681f2db4822517a0de75b182)) + + +### Bug Fixes + +* **auth:** prioritize first auth header ([d692d48](https://github.com/Blooio/blooio-python-sdk/commit/d692d48a74bebaa8b86c6e645941c85ebab88c2e)) +* **internal:** resolve build failures ([12fb122](https://github.com/Blooio/blooio-python-sdk/commit/12fb122bc963ca30ba0d2767352d2f3fde733bfb)) + ## 1.2.0 (2026-05-14) Full Changelog: [v1.1.1...v1.2.0](https://github.com/Blooio/blooio-python-sdk/compare/v1.1.1...v1.2.0) diff --git a/api.md b/api.md index f00d67d..b0be732 100644 --- a/api.md +++ b/api.md @@ -90,15 +90,9 @@ Methods: # Facetime -Types: - -```python -from blooio.types import FacetimeInitiateCallResponse -``` - Methods: -- client.facetime.initiate_call(\*\*params) -> FacetimeInitiateCallResponse +- client.facetime.initiate_call(\*\*params) -> None # Groups @@ -127,19 +121,14 @@ Methods: Types: ```python -from blooio.types.groups import ( - GroupMember, - MemberListResponse, - MemberAddResponse, - MemberRemoveResponse, -) +from blooio.types.groups import GroupMember, MemberListResponse ``` Methods: - client.groups.members.list(group_id, \*\*params) -> MemberListResponse -- client.groups.members.add(group_id, \*\*params) -> MemberAddResponse -- client.groups.members.remove(contact_id, \*, group_id) -> MemberRemoveResponse +- client.groups.members.add(group_id, \*\*params) -> None +- client.groups.members.remove(contact_id, \*, group_id) -> None ## Icon diff --git a/pyproject.toml b/pyproject.toml index 4c6c403..4735d59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "blooio" -version = "1.2.0" +version = "1.3.0" description = "The official Python library for the blooio API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/scripts/lint b/scripts/lint index dea1dbb..ce63bbb 100755 --- a/scripts/lint +++ b/scripts/lint @@ -13,7 +13,7 @@ else fi echo "==> Running pyright" -uv run pyright +uv run pyright -p . echo "==> Running mypy" uv run mypy . diff --git a/src/blooio/_client.py b/src/blooio/_client.py index 9557526..5b0f012 100644 --- a/src/blooio/_client.py +++ b/src/blooio/_client.py @@ -185,9 +185,11 @@ def qs(self) -> Querystring: @override def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: - return { - **(self._bearer_auth if security.get("bearer_auth", False) else {}), - } + headers: dict[str, str] = {} + if security.get("bearer_auth", False): + for key, value in self._bearer_auth.items(): + headers.setdefault(key, value) + return headers @property def _bearer_auth(self) -> dict[str, str]: @@ -424,9 +426,11 @@ def qs(self) -> Querystring: @override def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: - return { - **(self._bearer_auth if security.get("bearer_auth", False) else {}), - } + headers: dict[str, str] = {} + if security.get("bearer_auth", False): + for key, value in self._bearer_auth.items(): + headers.setdefault(key, value) + return headers @property def _bearer_auth(self) -> dict[str, str]: diff --git a/src/blooio/_version.py b/src/blooio/_version.py index 2fd6bd0..b50668b 100644 --- a/src/blooio/_version.py +++ b/src/blooio/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "blooio" -__version__ = "1.2.0" # x-release-please-version +__version__ = "1.3.0" # x-release-please-version diff --git a/src/blooio/resources/chats/background.py b/src/blooio/resources/chats/background.py index a0c1121..e76ec76 100644 --- a/src/blooio/resources/chats/background.py +++ b/src/blooio/resources/chats/background.py @@ -25,7 +25,7 @@ class BackgroundResource(SyncAPIResource): - """Set, get, and remove conversation backgrounds""" + """View conversations and messages""" @cached_property def with_raw_response(self) -> BackgroundResourceWithRawResponse: @@ -132,12 +132,27 @@ def set( Works for both 1-on-1 and group chats. - The uploaded image is converted into a PosterKit-compatible archive and applied - to the iMessage conversation on the linked device. Supported formats: JPEG, PNG, - GIF, WebP, HEIC/HEIF. Maximum file size: 10 MB. + The request body must be `multipart/form-data` with a single `background` field + containing the **raw image file bytes** (not a URL or base64 string). Supported + formats: JPEG, PNG, GIF, WebP, HEIC/HEIF. Maximum file size: 10 MB. + + **Example with curl** — note the `@` prefix that tells curl to read the file + from disk: + + ```bash + curl -X PUT "https://api.blooio.com/v2/api/chats/%2B15551234567/background" \\ + -H "Authorization: Bearer YOUR_API_KEY" \\ + -F "background=@/path/to/image.jpg;type=image/jpeg" + ``` + + When the chat id is a phone number, percent-encode the leading `+` as `%2B` in + the URL path. Args: - background: The image file to set as the chat background + background: Binary image file upload (JPEG, PNG, GIF, WebP, HEIC/HEIF, max 10 MB). Send as a + file field in `multipart/form-data` — e.g. `-F "background=@/path/to/image.jpg"` + with curl, or a `File`/`Blob` appended to `FormData` in JavaScript. Do NOT send + a URL or base64 string. extra_headers: Send extra headers @@ -167,7 +182,7 @@ def set( class AsyncBackgroundResource(AsyncAPIResource): - """Set, get, and remove conversation backgrounds""" + """View conversations and messages""" @cached_property def with_raw_response(self) -> AsyncBackgroundResourceWithRawResponse: @@ -274,12 +289,27 @@ async def set( Works for both 1-on-1 and group chats. - The uploaded image is converted into a PosterKit-compatible archive and applied - to the iMessage conversation on the linked device. Supported formats: JPEG, PNG, - GIF, WebP, HEIC/HEIF. Maximum file size: 10 MB. + The request body must be `multipart/form-data` with a single `background` field + containing the **raw image file bytes** (not a URL or base64 string). Supported + formats: JPEG, PNG, GIF, WebP, HEIC/HEIF. Maximum file size: 10 MB. + + **Example with curl** — note the `@` prefix that tells curl to read the file + from disk: + + ```bash + curl -X PUT "https://api.blooio.com/v2/api/chats/%2B15551234567/background" \\ + -H "Authorization: Bearer YOUR_API_KEY" \\ + -F "background=@/path/to/image.jpg;type=image/jpeg" + ``` + + When the chat id is a phone number, percent-encode the leading `+` as `%2B` in + the URL path. Args: - background: The image file to set as the chat background + background: Binary image file upload (JPEG, PNG, GIF, WebP, HEIC/HEIF, max 10 MB). Send as a + file field in `multipart/form-data` — e.g. `-F "background=@/path/to/image.jpg"` + with curl, or a `File`/`Blob` appended to `FormData` in JavaScript. Do NOT send + a URL or base64 string. extra_headers: Send extra headers diff --git a/src/blooio/resources/chats/chats.py b/src/blooio/resources/chats/chats.py index 95f2bce..5c77eec 100644 --- a/src/blooio/resources/chats/chats.py +++ b/src/blooio/resources/chats/chats.py @@ -78,7 +78,7 @@ def typing(self) -> TypingResource: @cached_property def background(self) -> BackgroundResource: - """Set, get, and remove conversation backgrounds""" + """View conversations and messages""" return BackgroundResource(self._client) @cached_property @@ -152,9 +152,13 @@ def list( message. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. q: Search query (matches phone/email or contact name) @@ -239,6 +243,10 @@ def share_contact_card( will be piggybacked onto the next outgoing message (text or attachment) sent to this chat. This is idempotent — calling it multiple times is harmless. + ⚠️ **Plan requirement:** Contact card sharing is only available on **Dedicated + Commercial** and **Dedicated Enterprise** plans. Numbers on other plans receive + a `403`. + Args: extra_headers: Send extra headers @@ -279,7 +287,7 @@ def typing(self) -> AsyncTypingResource: @cached_property def background(self) -> AsyncBackgroundResource: - """Set, get, and remove conversation backgrounds""" + """View conversations and messages""" return AsyncBackgroundResource(self._client) @cached_property @@ -353,9 +361,13 @@ async def list( message. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. q: Search query (matches phone/email or contact name) @@ -440,6 +452,10 @@ async def share_contact_card( will be piggybacked onto the next outgoing message (text or attachment) sent to this chat. This is idempotent — calling it multiple times is harmless. + ⚠️ **Plan requirement:** Contact card sharing is only available on **Dedicated + Commercial** and **Dedicated Enterprise** plans. Numbers on other plans receive + a `403`. + Args: extra_headers: Send extra headers @@ -496,7 +512,7 @@ def typing(self) -> TypingResourceWithRawResponse: @cached_property def background(self) -> BackgroundResourceWithRawResponse: - """Set, get, and remove conversation backgrounds""" + """View conversations and messages""" return BackgroundResourceWithRawResponse(self._chats.background) @@ -536,7 +552,7 @@ def typing(self) -> AsyncTypingResourceWithRawResponse: @cached_property def background(self) -> AsyncBackgroundResourceWithRawResponse: - """Set, get, and remove conversation backgrounds""" + """View conversations and messages""" return AsyncBackgroundResourceWithRawResponse(self._chats.background) @@ -576,7 +592,7 @@ def typing(self) -> TypingResourceWithStreamingResponse: @cached_property def background(self) -> BackgroundResourceWithStreamingResponse: - """Set, get, and remove conversation backgrounds""" + """View conversations and messages""" return BackgroundResourceWithStreamingResponse(self._chats.background) @@ -616,5 +632,5 @@ def typing(self) -> AsyncTypingResourceWithStreamingResponse: @cached_property def background(self) -> AsyncBackgroundResourceWithStreamingResponse: - """Set, get, and remove conversation backgrounds""" + """View conversations and messages""" return AsyncBackgroundResourceWithStreamingResponse(self._chats.background) diff --git a/src/blooio/resources/chats/messages.py b/src/blooio/resources/chats/messages.py index 8f844d3..1db03e7 100644 --- a/src/blooio/resources/chats/messages.py +++ b/src/blooio/resources/chats/messages.py @@ -105,12 +105,20 @@ def list( """ List all messages in a conversation with optional filtering. + A conversation must already exist: this returns `404` for an address the + organization has never exchanged a message with, rather than an empty list. Use + `GET /chats` to enumerate the conversations that do exist. + Args: direction: Filter by message direction - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. since: Only messages sent after this timestamp (ms) @@ -209,8 +217,6 @@ def react( (-1 for last message, -2 for second-to-last, etc.). When using relative indices, you can optionally filter by message direction (inbound/outbound only). - Emoji reactions require macOS 14 (Sonoma) or later on the device. - Args: reaction: The reaction to add or remove. Must be prefixed with `+` to add or `-` to remove. @@ -220,7 +226,7 @@ def react( `-question` **Emoji reactions:** Any emoji prefixed with `+` or `-` (e.g. `+😂`, `-😂`, - `+👍`, `-🔥`). Emoji reactions require macOS 14 (Sonoma) or later on the device. + `+👍`, `-🔥`). direction: Filter by message direction (only used when messageId is a relative index like -1, -2) @@ -275,9 +281,11 @@ def send( ] ] | Omit = omit, + format: Literal["plain", "markdown"] | Omit = omit, from_number: str | Omit = omit, link_preview: Optional[LinkPreviewParam] | Omit = omit, parts: Iterable[message_send_params.Part] | Omit = omit, + reply_to: Optional[message_send_params.ReplyTo] | Omit = omit, share_contact: bool | Omit = omit, text: Union[str, SequenceNotStr[str]] | Omit = omit, use_typing_indicator: bool | Omit = omit, @@ -304,8 +312,28 @@ def send( message is delivered without the animation. Effects are not supported in multipart (`parts`) mode. + **Threaded replies (iMessage inline reply):** set the optional `reply_to` field + to send the outgoing message as a reply to a specific earlier message. Two + shapes are accepted: `{ "message_id": "msg_…" }` references a Blooio-minted + message in the same chat (most common — the message*id returned by an earlier + send or surfaced on a `message.received` webhook), or + `{ "guid": "…", "part_index": 0 }` references the raw iMessage GUID for the rare + case where the parent wasn't recorded by Blooio. The reply must target the same + chat and the same from-number as the new send, and the parent must be no older + than 30 days (the iMessage on-device retention horizon). Reply support is + iMessage-only and is rejected on Twilio, dashboard-Twilio, and hybrid send + paths; it's also rejected on multi-message fan-outs (`text` array or per-part + URL-balloon batch). See the `400` responses for the full set of + `reply_target*\\**` error codes. + Args: - attachments: Array of attachment URLs or objects with url/name + attachments: Array of attachment URLs or objects with url/name. + + **Voice memos:** a single audio file (`.mp3`, `.m4a`, `.wav`, `.aac`, `.opus`, + `.ogg`) is automatically sent as a voice memo (the native waveform/scrubber + bubble), not a plain audio-file attachment — no extra field is needed. A voice + memo is a standalone bubble, so it cannot be combined with `text` or any other + attachment; send the voice memo and the text as two separate messages. effect: Optional. Attach an iMessage send-with-effect to the outgoing message. @@ -340,6 +368,45 @@ def send( - When `text` is an array, every message in the array is sent with the same effect. + format: How to interpret `text` (and each `parts[].text`). Defaults to `plain`, which + sends the string exactly as given. + + With `markdown`, four constructs are parsed and delivered as real iMessage rich + text — the recipient sees styled text, not delimiters: + + | Construct | Syntax | + | ------------- | ------------------------ | + | Bold | `**bold**` or `__bold__` | + | Italic | `*italic*` or `_italic_` | + | Underline | `++underline++` | + | Strikethrough | `~~strike~~` | + + They nest freely (`**bold and _italic_**`). Everything else Markdown can express + — headings, lists, links, code spans, blockquotes, images — is NOT styling + iMessage can carry, so it is passed through as literal characters: + `[Blooio](https://blooio.com)` is delivered with its brackets and URL intact, + and `# Heading` keeps its `#`. Escape a delimiter with a backslash + (`\\**not italic\\**`) to send it literally. + + The styling travels in the message's attributed body, so the stored `text` and + the `text` returned on reads and webhooks is always the plain string the + recipient sees, with the delimiters removed. The Markdown itself comes back as + `formatted_text`, re-serialized into a normalized spelling rather than echoed + verbatim (`__bold__` returns as `**bold**`). + + Only valid on Blooio iMessage channels — + `400 format_unsupported_for_channel_type` on any other channel type, since no + other channel type has a rich-text equivalent and would otherwise deliver your + delimiters as literal text. Rich text also requires the message to be delivered + over iMessage: a Blooio send that falls back to SMS arrives as unstyled plain + text (the `text` string), because SMS cannot carry styling. + + Applies to a text send and to `parts`. Rejected with `400 invalid_content` when + combined with `attachments` — a media caption is not a styled bubble, so send + the media and the styled text as two messages — when set without `text` or + `parts`, when the Markdown source exceeds 20000 characters, or when it compiles + to more than 256 distinct formatting ranges. + from_number: E.164 phone number to send from. For Twilio API keys, this is optional — if omitted, the first assigned Twilio number is auto-selected. For Blooio (iMessage) API keys, this selects a specific number from your pool. Must be a @@ -363,8 +430,17 @@ def send( `text` being a single http(s) URL. Response contains `message_ids[]` + `count` instead of `message_id`. + reply_to: Inline-reply target on `POST /chats/{chatId}/messages`. Pass either `message_id` + (preferred — references a Blooio-minted message) or `guid` (raw iMessage GUID, + useful for replying to messages received before the row was minted in Blooio). + The new send is dispatched to Lava with the resolved `selectedMessageGuid` + + `partIndex`, which iMessage renders as an inline reply on the recipient's + device. + share_contact: If true, the contact card (Name & Photo) will be shared with this message. The - contact card is piggybacked onto the outgoing message. Defaults to false. + contact card is piggybacked onto the outgoing message. Defaults to false. ⚠️ + Only available on **Dedicated Commercial** and **Dedicated Enterprise** plans — + other plans receive a `403`. text: Message text. Can be a single string or array of strings (each becomes a separate message) @@ -388,9 +464,11 @@ def send( { "attachments": attachments, "effect": effect, + "format": format, "from_number": from_number, "link_preview": link_preview, "parts": parts, + "reply_to": reply_to, "share_contact": share_contact, "text": text, "use_typing_indicator": use_typing_indicator, @@ -480,12 +558,20 @@ async def list( """ List all messages in a conversation with optional filtering. + A conversation must already exist: this returns `404` for an address the + organization has never exchanged a message with, rather than an empty list. Use + `GET /chats` to enumerate the conversations that do exist. + Args: direction: Filter by message direction - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. since: Only messages sent after this timestamp (ms) @@ -584,8 +670,6 @@ async def react( (-1 for last message, -2 for second-to-last, etc.). When using relative indices, you can optionally filter by message direction (inbound/outbound only). - Emoji reactions require macOS 14 (Sonoma) or later on the device. - Args: reaction: The reaction to add or remove. Must be prefixed with `+` to add or `-` to remove. @@ -595,7 +679,7 @@ async def react( `-question` **Emoji reactions:** Any emoji prefixed with `+` or `-` (e.g. `+😂`, `-😂`, - `+👍`, `-🔥`). Emoji reactions require macOS 14 (Sonoma) or later on the device. + `+👍`, `-🔥`). direction: Filter by message direction (only used when messageId is a relative index like -1, -2) @@ -650,9 +734,11 @@ async def send( ] ] | Omit = omit, + format: Literal["plain", "markdown"] | Omit = omit, from_number: str | Omit = omit, link_preview: Optional[LinkPreviewParam] | Omit = omit, parts: Iterable[message_send_params.Part] | Omit = omit, + reply_to: Optional[message_send_params.ReplyTo] | Omit = omit, share_contact: bool | Omit = omit, text: Union[str, SequenceNotStr[str]] | Omit = omit, use_typing_indicator: bool | Omit = omit, @@ -679,8 +765,28 @@ async def send( message is delivered without the animation. Effects are not supported in multipart (`parts`) mode. + **Threaded replies (iMessage inline reply):** set the optional `reply_to` field + to send the outgoing message as a reply to a specific earlier message. Two + shapes are accepted: `{ "message_id": "msg_…" }` references a Blooio-minted + message in the same chat (most common — the message*id returned by an earlier + send or surfaced on a `message.received` webhook), or + `{ "guid": "…", "part_index": 0 }` references the raw iMessage GUID for the rare + case where the parent wasn't recorded by Blooio. The reply must target the same + chat and the same from-number as the new send, and the parent must be no older + than 30 days (the iMessage on-device retention horizon). Reply support is + iMessage-only and is rejected on Twilio, dashboard-Twilio, and hybrid send + paths; it's also rejected on multi-message fan-outs (`text` array or per-part + URL-balloon batch). See the `400` responses for the full set of + `reply_target*\\**` error codes. + Args: - attachments: Array of attachment URLs or objects with url/name + attachments: Array of attachment URLs or objects with url/name. + + **Voice memos:** a single audio file (`.mp3`, `.m4a`, `.wav`, `.aac`, `.opus`, + `.ogg`) is automatically sent as a voice memo (the native waveform/scrubber + bubble), not a plain audio-file attachment — no extra field is needed. A voice + memo is a standalone bubble, so it cannot be combined with `text` or any other + attachment; send the voice memo and the text as two separate messages. effect: Optional. Attach an iMessage send-with-effect to the outgoing message. @@ -715,6 +821,45 @@ async def send( - When `text` is an array, every message in the array is sent with the same effect. + format: How to interpret `text` (and each `parts[].text`). Defaults to `plain`, which + sends the string exactly as given. + + With `markdown`, four constructs are parsed and delivered as real iMessage rich + text — the recipient sees styled text, not delimiters: + + | Construct | Syntax | + | ------------- | ------------------------ | + | Bold | `**bold**` or `__bold__` | + | Italic | `*italic*` or `_italic_` | + | Underline | `++underline++` | + | Strikethrough | `~~strike~~` | + + They nest freely (`**bold and _italic_**`). Everything else Markdown can express + — headings, lists, links, code spans, blockquotes, images — is NOT styling + iMessage can carry, so it is passed through as literal characters: + `[Blooio](https://blooio.com)` is delivered with its brackets and URL intact, + and `# Heading` keeps its `#`. Escape a delimiter with a backslash + (`\\**not italic\\**`) to send it literally. + + The styling travels in the message's attributed body, so the stored `text` and + the `text` returned on reads and webhooks is always the plain string the + recipient sees, with the delimiters removed. The Markdown itself comes back as + `formatted_text`, re-serialized into a normalized spelling rather than echoed + verbatim (`__bold__` returns as `**bold**`). + + Only valid on Blooio iMessage channels — + `400 format_unsupported_for_channel_type` on any other channel type, since no + other channel type has a rich-text equivalent and would otherwise deliver your + delimiters as literal text. Rich text also requires the message to be delivered + over iMessage: a Blooio send that falls back to SMS arrives as unstyled plain + text (the `text` string), because SMS cannot carry styling. + + Applies to a text send and to `parts`. Rejected with `400 invalid_content` when + combined with `attachments` — a media caption is not a styled bubble, so send + the media and the styled text as two messages — when set without `text` or + `parts`, when the Markdown source exceeds 20000 characters, or when it compiles + to more than 256 distinct formatting ranges. + from_number: E.164 phone number to send from. For Twilio API keys, this is optional — if omitted, the first assigned Twilio number is auto-selected. For Blooio (iMessage) API keys, this selects a specific number from your pool. Must be a @@ -738,8 +883,17 @@ async def send( `text` being a single http(s) URL. Response contains `message_ids[]` + `count` instead of `message_id`. + reply_to: Inline-reply target on `POST /chats/{chatId}/messages`. Pass either `message_id` + (preferred — references a Blooio-minted message) or `guid` (raw iMessage GUID, + useful for replying to messages received before the row was minted in Blooio). + The new send is dispatched to Lava with the resolved `selectedMessageGuid` + + `partIndex`, which iMessage renders as an inline reply on the recipient's + device. + share_contact: If true, the contact card (Name & Photo) will be shared with this message. The - contact card is piggybacked onto the outgoing message. Defaults to false. + contact card is piggybacked onto the outgoing message. Defaults to false. ⚠️ + Only available on **Dedicated Commercial** and **Dedicated Enterprise** plans — + other plans receive a `403`. text: Message text. Can be a single string or array of strings (each becomes a separate message) @@ -763,9 +917,11 @@ async def send( { "attachments": attachments, "effect": effect, + "format": format, "from_number": from_number, "link_preview": link_preview, "parts": parts, + "reply_to": reply_to, "share_contact": share_contact, "text": text, "use_typing_indicator": use_typing_indicator, diff --git a/src/blooio/resources/chats/typing.py b/src/blooio/resources/chats/typing.py index f9576e2..17a4b37 100644 --- a/src/blooio/resources/chats/typing.py +++ b/src/blooio/resources/chats/typing.py @@ -56,7 +56,9 @@ def start( """Start the typing indicator for a chat. The indicator shows the recipient that - you are typing. + you are typing. Works for both 1:1 chats (pass a phone number or email as + `chatId`) and group chats (pass the group ID, e.g. `grp_...`); in a group every + participant sees the indicator. **RCS limitation:** typing indicators are only delivered for iMessage chats — the RCS protocol does not carry composing state. Calls against RCS-routed chats @@ -92,8 +94,11 @@ def stop( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> TypingResponse: - """ - Stop the typing indicator for a chat. + """Stop the typing indicator for a chat. + + Works for both 1:1 chats (pass a phone + number or email as `chatId`) and group chats (pass the group ID, e.g. + `grp_...`). **RCS limitation:** typing indicators are only delivered for iMessage chats — the RCS protocol does not carry composing state. Calls against RCS-routed chats @@ -155,7 +160,9 @@ async def start( """Start the typing indicator for a chat. The indicator shows the recipient that - you are typing. + you are typing. Works for both 1:1 chats (pass a phone number or email as + `chatId`) and group chats (pass the group ID, e.g. `grp_...`); in a group every + participant sees the indicator. **RCS limitation:** typing indicators are only delivered for iMessage chats — the RCS protocol does not carry composing state. Calls against RCS-routed chats @@ -191,8 +198,11 @@ async def stop( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> TypingResponse: - """ - Stop the typing indicator for a chat. + """Stop the typing indicator for a chat. + + Works for both 1:1 chats (pass a phone + number or email as `chatId`) and group chats (pass the group ID, e.g. + `grp_...`). **RCS limitation:** typing indicators are only delivered for iMessage chats — the RCS protocol does not carry composing state. Calls against RCS-routed chats diff --git a/src/blooio/resources/contacts/contacts.py b/src/blooio/resources/contacts/contacts.py index 3eed99f..12b2c26 100644 --- a/src/blooio/resources/contacts/contacts.py +++ b/src/blooio/resources/contacts/contacts.py @@ -193,9 +193,13 @@ def list( List all contacts for the organization with optional search and pagination. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. q: Search query (matches identifier or name) @@ -454,9 +458,13 @@ async def list( List all contacts for the organization with optional search and pagination. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. q: Search query (matches identifier or name) diff --git a/src/blooio/resources/facetime.py b/src/blooio/resources/facetime.py index ad8ac80..5d4528f 100644 --- a/src/blooio/resources/facetime.py +++ b/src/blooio/resources/facetime.py @@ -5,7 +5,7 @@ import httpx from ..types import facetime_initiate_call_params -from .._types import Body, Query, Headers, NotGiven, not_given +from .._types import Body, Query, Headers, NoneType, NotGiven, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -16,7 +16,6 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.facetime_initiate_call_response import FacetimeInitiateCallResponse __all__ = ["FacetimeResource", "AsyncFacetimeResource"] @@ -53,7 +52,7 @@ def initiate_call( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> FacetimeInitiateCallResponse: + ) -> None: """ **Coming Soon** -- This endpoint is temporarily disabled while we stabilize the FaceTime call flow. @@ -73,13 +72,14 @@ def initiate_call( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( "/facetime/calls", body=maybe_transform({"handle": handle}, facetime_initiate_call_params.FacetimeInitiateCallParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=FacetimeInitiateCallResponse, + cast_to=NoneType, ) @@ -115,7 +115,7 @@ async def initiate_call( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> FacetimeInitiateCallResponse: + ) -> None: """ **Coming Soon** -- This endpoint is temporarily disabled while we stabilize the FaceTime call flow. @@ -135,6 +135,7 @@ async def initiate_call( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( "/facetime/calls", body=await async_maybe_transform( @@ -143,7 +144,7 @@ async def initiate_call( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=FacetimeInitiateCallResponse, + cast_to=NoneType, ) diff --git a/src/blooio/resources/groups/groups.py b/src/blooio/resources/groups/groups.py index 5eb4504..bb993e8 100644 --- a/src/blooio/resources/groups/groups.py +++ b/src/blooio/resources/groups/groups.py @@ -228,9 +228,13 @@ def list( List all groups for the organization with optional search and pagination. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. q: Search query (matches group name) @@ -485,9 +489,13 @@ async def list( List all groups for the organization with optional search and pagination. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. q: Search query (matches group name) diff --git a/src/blooio/resources/groups/members.py b/src/blooio/resources/groups/members.py index c14ef2b..1c5802f 100644 --- a/src/blooio/resources/groups/members.py +++ b/src/blooio/resources/groups/members.py @@ -4,7 +4,7 @@ import httpx -from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -16,9 +16,7 @@ ) from ..._base_client import make_request_options from ...types.groups import member_add_params, member_list_params -from ...types.groups.member_add_response import MemberAddResponse from ...types.groups.member_list_response import MemberListResponse -from ...types.groups.member_remove_response import MemberRemoveResponse __all__ = ["MembersResource", "AsyncMembersResource"] @@ -62,9 +60,13 @@ def list( List all members of a group. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. extra_headers: Send extra headers @@ -105,7 +107,7 @@ def add( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MemberAddResponse: + ) -> None: """ ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize this feature. @@ -126,13 +128,14 @@ def add( """ if not group_id: raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( path_template("/groups/{group_id}/members", group_id=group_id), body=maybe_transform({"contact_id": contact_id}, member_add_params.MemberAddParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=MemberAddResponse, + cast_to=NoneType, ) def remove( @@ -146,7 +149,7 @@ def remove( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MemberRemoveResponse: + ) -> None: """ ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize this feature. @@ -168,12 +171,13 @@ def remove( raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") if not contact_id: raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( path_template("/groups/{group_id}/members/{contact_id}", group_id=group_id, contact_id=contact_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=MemberRemoveResponse, + cast_to=NoneType, ) @@ -216,9 +220,13 @@ async def list( List all members of a group. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. extra_headers: Send extra headers @@ -259,7 +267,7 @@ async def add( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MemberAddResponse: + ) -> None: """ ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize this feature. @@ -280,13 +288,14 @@ async def add( """ if not group_id: raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( path_template("/groups/{group_id}/members", group_id=group_id), body=await async_maybe_transform({"contact_id": contact_id}, member_add_params.MemberAddParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=MemberAddResponse, + cast_to=NoneType, ) async def remove( @@ -300,7 +309,7 @@ async def remove( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MemberRemoveResponse: + ) -> None: """ ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize this feature. @@ -322,12 +331,13 @@ async def remove( raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") if not contact_id: raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( path_template("/groups/{group_id}/members/{contact_id}", group_id=group_id, contact_id=contact_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=MemberRemoveResponse, + cast_to=NoneType, ) diff --git a/src/blooio/resources/me/numbers/contact_card.py b/src/blooio/resources/me/numbers/contact_card.py index fa47ab0..7231a43 100644 --- a/src/blooio/resources/me/numbers/contact_card.py +++ b/src/blooio/resources/me/numbers/contact_card.py @@ -100,6 +100,10 @@ def update( Update the personal contact card (Name & Photo) for the specified phone number. All fields are optional — only provided fields are updated. + ⚠️ **Plan requirement:** Setting the `first_name`, `last_name`, or `avatar` is + only available on **Dedicated Commercial** and **Dedicated Enterprise** plans. + Numbers on other plans receive a `403`. + Args: avatar: Profile photo as base64-encoded JPEG/PNG @@ -213,6 +217,10 @@ async def update( Update the personal contact card (Name & Photo) for the specified phone number. All fields are optional — only provided fields are updated. + ⚠️ **Plan requirement:** Setting the `first_name`, `last_name`, or `avatar` is + only available on **Dedicated Commercial** and **Dedicated Enterprise** plans. + Numbers on other plans receive a `403`. + Args: avatar: Profile photo as base64-encoded JPEG/PNG diff --git a/src/blooio/resources/webhooks/logs.py b/src/blooio/resources/webhooks/logs.py index c45621e..69e8f89 100644 --- a/src/blooio/resources/webhooks/logs.py +++ b/src/blooio/resources/webhooks/logs.py @@ -67,13 +67,17 @@ def list( List delivery logs for a specific webhook. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. max_status: Maximum HTTP status code min_status: Minimum HTTP status code - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. sort: Sort order by attempted time @@ -191,13 +195,17 @@ async def list( List delivery logs for a specific webhook. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. max_status: Maximum HTTP status code min_status: Minimum HTTP status code - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. sort: Sort order by attempted time diff --git a/src/blooio/resources/webhooks/webhooks.py b/src/blooio/resources/webhooks/webhooks.py index 471a3a5..82431d5 100644 --- a/src/blooio/resources/webhooks/webhooks.py +++ b/src/blooio/resources/webhooks/webhooks.py @@ -2,6 +2,7 @@ from __future__ import annotations +import typing_extensions from typing_extensions import Literal import httpx @@ -74,12 +75,12 @@ def with_streaming_response(self) -> WebhooksResourceWithStreamingResponse: """ return WebhooksResourceWithStreamingResponse(self) + @typing_extensions.deprecated("deprecated") def create( self, *, webhook_url: str, valid_until: int | Omit = omit, - webhook_type: Literal["message", "status", "all"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -87,15 +88,19 @@ def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> WebhookCreateResponse: - """ - Create a new webhook subscription. + """Registration through this endpoint is closed and returns 410. - Args: - webhook_url: URL to receive webhook events + Use POST + /v4/webhooks to create new subscriptions. Existing webhooks keep working and can + still be listed, updated, and deleted here. Re-posting the URL of a webhook that + already exists still returns 200 with that webhook, so idempotent provisioning + scripts continue to work unchanged. - valid_until: Expiration timestamp (-1 for no expiration) + Args: + webhook_url: URL of an existing webhook, for the idempotent 200 response. A URL that does not + already exist returns 410. - webhook_type: Type of events to receive + valid_until: Ignored. Retained so existing request bodies stay valid. extra_headers: Send extra headers @@ -111,7 +116,6 @@ def create( { "webhook_url": webhook_url, "valid_until": valid_until, - "webhook_type": webhook_type, }, webhook_create_params.WebhookCreateParams, ), @@ -289,12 +293,12 @@ def with_streaming_response(self) -> AsyncWebhooksResourceWithStreamingResponse: """ return AsyncWebhooksResourceWithStreamingResponse(self) + @typing_extensions.deprecated("deprecated") async def create( self, *, webhook_url: str, valid_until: int | Omit = omit, - webhook_type: Literal["message", "status", "all"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -302,15 +306,19 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> WebhookCreateResponse: - """ - Create a new webhook subscription. + """Registration through this endpoint is closed and returns 410. - Args: - webhook_url: URL to receive webhook events + Use POST + /v4/webhooks to create new subscriptions. Existing webhooks keep working and can + still be listed, updated, and deleted here. Re-posting the URL of a webhook that + already exists still returns 200 with that webhook, so idempotent provisioning + scripts continue to work unchanged. - valid_until: Expiration timestamp (-1 for no expiration) + Args: + webhook_url: URL of an existing webhook, for the idempotent 200 response. A URL that does not + already exist returns 410. - webhook_type: Type of events to receive + valid_until: Ignored. Retained so existing request bodies stay valid. extra_headers: Send extra headers @@ -326,7 +334,6 @@ async def create( { "webhook_url": webhook_url, "valid_until": valid_until, - "webhook_type": webhook_type, }, webhook_create_params.WebhookCreateParams, ), @@ -476,8 +483,10 @@ class WebhooksResourceWithRawResponse: def __init__(self, webhooks: WebhooksResource) -> None: self._webhooks = webhooks - self.create = to_raw_response_wrapper( - webhooks.create, + self.create = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + webhooks.create, # pyright: ignore[reportDeprecated], + ) ) self.retrieve = to_raw_response_wrapper( webhooks.retrieve, @@ -507,8 +516,10 @@ class AsyncWebhooksResourceWithRawResponse: def __init__(self, webhooks: AsyncWebhooksResource) -> None: self._webhooks = webhooks - self.create = async_to_raw_response_wrapper( - webhooks.create, + self.create = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + webhooks.create, # pyright: ignore[reportDeprecated], + ) ) self.retrieve = async_to_raw_response_wrapper( webhooks.retrieve, @@ -538,8 +549,10 @@ class WebhooksResourceWithStreamingResponse: def __init__(self, webhooks: WebhooksResource) -> None: self._webhooks = webhooks - self.create = to_streamed_response_wrapper( - webhooks.create, + self.create = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + webhooks.create, # pyright: ignore[reportDeprecated], + ) ) self.retrieve = to_streamed_response_wrapper( webhooks.retrieve, @@ -569,8 +582,10 @@ class AsyncWebhooksResourceWithStreamingResponse: def __init__(self, webhooks: AsyncWebhooksResource) -> None: self._webhooks = webhooks - self.create = async_to_streamed_response_wrapper( - webhooks.create, + self.create = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + webhooks.create, # pyright: ignore[reportDeprecated], + ) ) self.retrieve = async_to_streamed_response_wrapper( webhooks.retrieve, diff --git a/src/blooio/types/__init__.py b/src/blooio/types/__init__.py index dad7cd9..b326c61 100644 --- a/src/blooio/types/__init__.py +++ b/src/blooio/types/__init__.py @@ -30,7 +30,6 @@ from .webhook_delete_response import WebhookDeleteResponse as WebhookDeleteResponse from .chat_mark_as_read_response import ChatMarkAsReadResponse as ChatMarkAsReadResponse from .facetime_initiate_call_params import FacetimeInitiateCallParams as FacetimeInitiateCallParams -from .facetime_initiate_call_response import FacetimeInitiateCallResponse as FacetimeInitiateCallResponse from .chat_share_contact_card_response import ChatShareContactCardResponse as ChatShareContactCardResponse from .phone_number_batch_create_params import PhoneNumberBatchCreateParams as PhoneNumberBatchCreateParams from .phone_number_batch_create_response import PhoneNumberBatchCreateResponse as PhoneNumberBatchCreateResponse diff --git a/src/blooio/types/chat_list_params.py b/src/blooio/types/chat_list_params.py index 3a6dbab..c51e343 100644 --- a/src/blooio/types/chat_list_params.py +++ b/src/blooio/types/chat_list_params.py @@ -9,10 +9,18 @@ class ChatListParams(TypedDict, total=False): limit: int - """Maximum number of items to return (1-200)""" + """Maximum number of items to return in a single response. + + Must be between 1 and 200; defaults to 50. Use together with `offset` to page + through large result sets. + """ offset: int - """Number of items to skip""" + """Number of items to skip before returning results. + + Combine with `limit` for page-based pagination (e.g. `offset=50&limit=50` + returns the second page). Defaults to 0. + """ q: str """Search query (matches phone/email or contact name)""" diff --git a/src/blooio/types/chat_list_response.py b/src/blooio/types/chat_list_response.py index 6ec4a82..ebc1194 100644 --- a/src/blooio/types/chat_list_response.py +++ b/src/blooio/types/chat_list_response.py @@ -24,6 +24,12 @@ class Chat(BaseModel): id: Optional[str] = None """Chat identifier (phone number, email, or group ID)""" + background_id: Optional[str] = None + """Identifier for the active chat background""" + + background_url: Optional[str] = None + """Public URL of the chat background image (if one has been set via the API)""" + contact: Optional[ChatContact] = None """Contact info (only for non-group chats)""" diff --git a/src/blooio/types/chat_retrieve_response.py b/src/blooio/types/chat_retrieve_response.py index 14f98a5..676d1f1 100644 --- a/src/blooio/types/chat_retrieve_response.py +++ b/src/blooio/types/chat_retrieve_response.py @@ -23,6 +23,12 @@ class ChatRetrieveResponse(BaseModel): id: Optional[str] = None """Chat identifier (phone number, email, or group ID)""" + background_id: Optional[str] = None + """Identifier for the active chat background""" + + background_url: Optional[str] = None + """Public URL of the chat background image (if one has been set via the API)""" + contact: Optional[Contact] = None """Contact info (only for non-group chats)""" diff --git a/src/blooio/types/chats/background_set_params.py b/src/blooio/types/chats/background_set_params.py index 315fc10..5820f62 100644 --- a/src/blooio/types/chats/background_set_params.py +++ b/src/blooio/types/chats/background_set_params.py @@ -11,4 +11,9 @@ class BackgroundSetParams(TypedDict, total=False): background: Required[FileTypes] - """The image file to set as the chat background""" + """Binary image file upload (JPEG, PNG, GIF, WebP, HEIC/HEIF, max 10 MB). + + Send as a file field in `multipart/form-data` — e.g. + `-F "background=@/path/to/image.jpg"` with curl, or a `File`/`Blob` appended to + `FormData` in JavaScript. Do NOT send a URL or base64 string. + """ diff --git a/src/blooio/types/chats/chat_background_response.py b/src/blooio/types/chats/chat_background_response.py index 48c1348..26ffacd 100644 --- a/src/blooio/types/chats/chat_background_response.py +++ b/src/blooio/types/chats/chat_background_response.py @@ -13,6 +13,14 @@ class ChatBackgroundResponse(BaseModel): background_id: Optional[str] = None """Unique identifier for the current background, or null if none""" + background_url: Optional[str] = None + """Public URL of the persisted background image stored in R2. + + Returned after a successful PUT and on GET when a background has been set + through the API. May be null if persistence failed or the background was set + outside of the API. + """ + background_version: Optional[int] = None """Version number of the background (for cache invalidation)""" diff --git a/src/blooio/types/chats/message_get_status_response.py b/src/blooio/types/chats/message_get_status_response.py index da637b4..669d08f 100644 --- a/src/blooio/types/chats/message_get_status_response.py +++ b/src/blooio/types/chats/message_get_status_response.py @@ -17,11 +17,29 @@ class MessageGetStatusResponse(BaseModel): message_id: Optional[str] = None - protocol: Optional[Literal["imessage", "sms", "rcs", "non-imessage"]] = None + protocol: Optional[Literal["pending", "unknown", "imessage", "sms", "rcs"]] = None + """Transport used to carry the message; never null. + + `pending` = accepted and dispatched, wire service not resolved yet (settles + within seconds of send); `imessage` = delivered over iMessage (blue bubble); + `rcs` = delivered over RCS; `sms` = fell back to SMS/MMS (green bubble); + `unknown` = accepted by the carrier but the wire service could not be resolved + before the tracking window closed (see `error`). + """ status: Optional[ Literal["pending", "queued", "sent", "delivered", "failed", "cancellation_requested", "cancelled"] ] = None + """Delivery lifecycle state. + + `pending` = persisted and being prepared for dispatch; `queued` = accepted and + waiting to be handed to Apple/the carrier; `sent` = handed off to Apple/the + carrier (protocol resolution happens around here); `delivered` = a delivery + receipt was received; `failed` = could not be delivered (see `error`); + `cancellation_requested` = a cancel was requested for a still-queued message + (best-effort); `cancelled` = cancelled before dispatch. Inbound messages are + surfaced via webhooks with `received`; read receipts arrive as a `read` event. + """ time_delivered: Optional[int] = None diff --git a/src/blooio/types/chats/message_list_params.py b/src/blooio/types/chats/message_list_params.py index 0e35f39..6b2ff85 100644 --- a/src/blooio/types/chats/message_list_params.py +++ b/src/blooio/types/chats/message_list_params.py @@ -12,10 +12,18 @@ class MessageListParams(TypedDict, total=False): """Filter by message direction""" limit: int - """Maximum number of items to return (1-200)""" + """Maximum number of items to return in a single response. + + Must be between 1 and 200; defaults to 50. Use together with `offset` to page + through large result sets. + """ offset: int - """Number of items to skip""" + """Number of items to skip before returning results. + + Combine with `limit` for page-based pagination (e.g. `offset=50&limit=50` + returns the second page). Defaults to 0. + """ since: int """Only messages sent after this timestamp (ms)""" diff --git a/src/blooio/types/chats/message_list_response.py b/src/blooio/types/chats/message_list_response.py index 88001a7..0bd58ad 100644 --- a/src/blooio/types/chats/message_list_response.py +++ b/src/blooio/types/chats/message_list_response.py @@ -7,7 +7,31 @@ from ..._models import BaseModel from ..pagination import Pagination -__all__ = ["MessageListResponse", "Message"] +__all__ = ["MessageListResponse", "Message", "MessageReplyTo"] + + +class MessageReplyTo(BaseModel): + """Inline-reply parent reference. + + Identical shape on `message.received` webhooks and on every GET endpoint that returns a single message or a list of messages. + """ + + guid: Optional[str] = None + """The raw iMessage GUID of the parent. + + Always populated on real inline replies; the on-device record-of-truth + identifier that survives even when `message_id` cannot be resolved. + """ + + message_id: Optional[str] = None + """The Blooio `message_id` of the parent message. + + NULL when the parent isn't in our `messages` table (e.g., the original was sent + from outside Blooio's pipeline). + """ + + part_index: int + """Which part of the parent was replied to. 0 for the common single-part case.""" class Message(BaseModel): @@ -20,16 +44,51 @@ class Message(BaseModel): external_id: Optional[str] = None """Phone number or email of the contact, or group ID for group messages""" + formatted_text: Optional[str] = None + """Markdown for a rich-text (bold/italic/underline/strikethrough) message. + + Omitted entirely when the message carries no styling, so its presence is how you + detect rich text. + + Present in both directions: on an outbound send made with `format: "markdown"`, + and on an inbound iMessage whose sender styled their text — so styling a + customer applied in Messages arrives here even though your integration never + asked for it. + + Always a normalized re-serialization of the message's actual styling rather than + an echo of the source string: bold is spelled `**`, italic `*`, underline `++`, + strikethrough `~~`, and any character that would otherwise read as a delimiter + is backslash-escaped. Re-sending this value verbatim with `format: "markdown"` + reproduces the same styled message. Blooio iMessage only. This is the SAME field + delivered on the message webhooks, so a message reads identically via REST or + webhook. + """ + internal_id: Optional[str] = None """Organization phone number (from-number) used for this message""" message_id: Optional[str] = None - protocol: Optional[Literal["imessage", "sms", "rcs", "non-imessage"]] = None + protocol: Optional[Literal["pending", "unknown", "imessage", "sms", "rcs"]] = None + """Transport used to carry the message; never null. + + `pending` = accepted and dispatched, wire service not resolved yet (settles + within seconds of send); `imessage` = delivered over iMessage (blue bubble); + `rcs` = delivered over RCS; `sms` = fell back to SMS/MMS (green bubble); + `unknown` = accepted by the carrier but the wire service could not be resolved + before the tracking window closed (see `error`). + """ reactions: Optional[List[Reaction]] = None """Reactions on this message (tapbacks and emoji reactions)""" + reply_to: Optional[MessageReplyTo] = None + """Inline-reply parent reference. + + Identical shape on `message.received` webhooks and on every GET endpoint that + returns a single message or a list of messages. + """ + sender: Optional[str] = None """Sender's phone number or email for inbound group messages. @@ -39,6 +98,16 @@ class Message(BaseModel): status: Optional[ Literal["pending", "queued", "sent", "delivered", "failed", "cancellation_requested", "cancelled"] ] = None + """Delivery lifecycle state. + + `pending` = persisted and being prepared for dispatch; `queued` = accepted and + waiting to be handed to Apple/the carrier; `sent` = handed off to Apple/the + carrier (protocol resolution happens around here); `delivered` = a delivery + receipt was received; `failed` = could not be delivered (see `error`); + `cancellation_requested` = a cancel was requested for a still-queued message + (best-effort); `cancelled` = cancelled before dispatch. Inbound messages are + surfaced via webhooks with `received`; read receipts arrive as a `read` event. + """ text: Optional[str] = None diff --git a/src/blooio/types/chats/message_react_params.py b/src/blooio/types/chats/message_react_params.py index 9c6a6ed..5330e7c 100644 --- a/src/blooio/types/chats/message_react_params.py +++ b/src/blooio/types/chats/message_react_params.py @@ -22,7 +22,7 @@ class MessageReactParams(TypedDict, total=False): `-question` **Emoji reactions:** Any emoji prefixed with `+` or `-` (e.g. `+😂`, `-😂`, - `+👍`, `-🔥`). Emoji reactions require macOS 14 (Sonoma) or later on the device. + `+👍`, `-🔥`). """ direction: Literal["inbound", "outbound"] diff --git a/src/blooio/types/chats/message_retrieve_response.py b/src/blooio/types/chats/message_retrieve_response.py index a2c61ad..1299033 100644 --- a/src/blooio/types/chats/message_retrieve_response.py +++ b/src/blooio/types/chats/message_retrieve_response.py @@ -6,7 +6,7 @@ from .reaction import Reaction from ..._models import BaseModel -__all__ = ["MessageRetrieveResponse", "Contact"] +__all__ = ["MessageRetrieveResponse", "Contact", "ReplyTo"] class Contact(BaseModel): @@ -18,6 +18,30 @@ class Contact(BaseModel): name: Optional[str] = None +class ReplyTo(BaseModel): + """Inline-reply parent reference. + + Identical shape on `message.received` webhooks and on every GET endpoint that returns a single message or a list of messages. + """ + + guid: Optional[str] = None + """The raw iMessage GUID of the parent. + + Always populated on real inline replies; the on-device record-of-truth + identifier that survives even when `message_id` cannot be resolved. + """ + + message_id: Optional[str] = None + """The Blooio `message_id` of the parent message. + + NULL when the parent isn't in our `messages` table (e.g., the original was sent + from outside Blooio's pipeline). + """ + + part_index: int + """Which part of the parent was replied to. 0 for the common single-part case.""" + + class MessageRetrieveResponse(BaseModel): attachments: Optional[List[object]] = None @@ -29,16 +53,51 @@ class MessageRetrieveResponse(BaseModel): error: Optional[str] = None + formatted_text: Optional[str] = None + """Markdown for a rich-text (bold/italic/underline/strikethrough) message. + + Omitted entirely when the message carries no styling, so its presence is how you + detect rich text. + + Present in both directions: on an outbound send made with `format: "markdown"`, + and on an inbound iMessage whose sender styled their text — so styling a + customer applied in Messages arrives here even though your integration never + asked for it. + + Always a normalized re-serialization of the message's actual styling rather than + an echo of the source string: bold is spelled `**`, italic `*`, underline `++`, + strikethrough `~~`, and any character that would otherwise read as a delimiter + is backslash-escaped. Re-sending this value verbatim with `format: "markdown"` + reproduces the same styled message. Blooio iMessage only. This is the SAME field + delivered on the message webhooks, so a message reads identically via REST or + webhook. + """ + internal_id: Optional[str] = None """Organization phone number (from-number) used for this message""" message_id: Optional[str] = None - protocol: Optional[Literal["imessage", "sms", "rcs", "non-imessage"]] = None + protocol: Optional[Literal["pending", "unknown", "imessage", "sms", "rcs"]] = None + """Transport used to carry the message; never null. + + `pending` = accepted and dispatched, wire service not resolved yet (settles + within seconds of send); `imessage` = delivered over iMessage (blue bubble); + `rcs` = delivered over RCS; `sms` = fell back to SMS/MMS (green bubble); + `unknown` = accepted by the carrier but the wire service could not be resolved + before the tracking window closed (see `error`). + """ reactions: Optional[List[Reaction]] = None """Reactions on this message (tapbacks and emoji reactions)""" + reply_to: Optional[ReplyTo] = None + """Inline-reply parent reference. + + Identical shape on `message.received` webhooks and on every GET endpoint that + returns a single message or a list of messages. + """ + sender: Optional[str] = None """Sender's phone number or email for inbound group messages. @@ -48,6 +107,16 @@ class MessageRetrieveResponse(BaseModel): status: Optional[ Literal["pending", "queued", "sent", "delivered", "failed", "cancellation_requested", "cancelled"] ] = None + """Delivery lifecycle state. + + `pending` = persisted and being prepared for dispatch; `queued` = accepted and + waiting to be handed to Apple/the carrier; `sent` = handed off to Apple/the + carrier (protocol resolution happens around here); `delivered` = a delivery + receipt was received; `failed` = could not be delivered (see `error`); + `cancellation_requested` = a cancel was requested for a still-queued message + (best-effort); `cancelled` = cancelled before dispatch. Inbound messages are + surfaced via webhooks with `received`; read receipts arrive as a `read` event. + """ text: Optional[str] = None diff --git a/src/blooio/types/chats/message_send_params.py b/src/blooio/types/chats/message_send_params.py index b2b935b..7f7072a 100644 --- a/src/blooio/types/chats/message_send_params.py +++ b/src/blooio/types/chats/message_send_params.py @@ -9,12 +9,19 @@ from ..._utils import PropertyInfo from .link_preview_param import LinkPreviewParam -__all__ = ["MessageSendParams", "Attachment", "AttachmentUnionObjectVariant1", "Part"] +__all__ = ["MessageSendParams", "Attachment", "AttachmentUnionObjectVariant1", "Part", "ReplyTo"] class MessageSendParams(TypedDict, total=False): attachments: SequenceNotStr[Attachment] - """Array of attachment URLs or objects with url/name""" + """Array of attachment URLs or objects with url/name. + + **Voice memos:** a single audio file (`.mp3`, `.m4a`, `.wav`, `.aac`, `.opus`, + `.ogg`) is automatically sent as a voice memo (the native waveform/scrubber + bubble), not a plain audio-file attachment — no extra field is needed. A voice + memo is a standalone bubble, so it cannot be combined with `text` or any other + attachment; send the voice memo and the text as two separate messages. + """ effect: Optional[ Literal[ @@ -67,6 +74,48 @@ class MessageSendParams(TypedDict, total=False): effect. """ + format: Literal["plain", "markdown"] + """How to interpret `text` (and each `parts[].text`). + + Defaults to `plain`, which sends the string exactly as given. + + With `markdown`, four constructs are parsed and delivered as real iMessage rich + text — the recipient sees styled text, not delimiters: + + | Construct | Syntax | + | ------------- | ------------------------ | + | Bold | `**bold**` or `__bold__` | + | Italic | `*italic*` or `_italic_` | + | Underline | `++underline++` | + | Strikethrough | `~~strike~~` | + + They nest freely (`**bold and _italic_**`). Everything else Markdown can express + — headings, lists, links, code spans, blockquotes, images — is NOT styling + iMessage can carry, so it is passed through as literal characters: + `[Blooio](https://blooio.com)` is delivered with its brackets and URL intact, + and `# Heading` keeps its `#`. Escape a delimiter with a backslash + (`\\**not italic\\**`) to send it literally. + + The styling travels in the message's attributed body, so the stored `text` and + the `text` returned on reads and webhooks is always the plain string the + recipient sees, with the delimiters removed. The Markdown itself comes back as + `formatted_text`, re-serialized into a normalized spelling rather than echoed + verbatim (`__bold__` returns as `**bold**`). + + Only valid on Blooio iMessage channels — + `400 format_unsupported_for_channel_type` on any other channel type, since no + other channel type has a rich-text equivalent and would otherwise deliver your + delimiters as literal text. Rich text also requires the message to be delivered + over iMessage: a Blooio send that falls back to SMS arrives as unstyled plain + text (the `text` string), because SMS cannot carry styling. + + Applies to a text send and to `parts`. Rejected with `400 invalid_content` when + combined with `attachments` — a media caption is not a styled bubble, so send + the media and the styled text as two messages — when set without `text` or + `parts`, when the Markdown source exceeds 20000 characters, or when it compiles + to more than 256 distinct formatting ranges. + """ + from_number: str """E.164 phone number to send from. @@ -97,10 +146,22 @@ class MessageSendParams(TypedDict, total=False): `count` instead of `message_id`. """ + reply_to: Optional[ReplyTo] + """Inline-reply target on `POST /chats/{chatId}/messages`. + + Pass either `message_id` (preferred — references a Blooio-minted message) or + `guid` (raw iMessage GUID, useful for replying to messages received before the + row was minted in Blooio). The new send is dispatched to Lava with the resolved + `selectedMessageGuid` + `partIndex`, which iMessage renders as an inline reply + on the recipient's device. + """ + share_contact: bool """If true, the contact card (Name & Photo) will be shared with this message. - The contact card is piggybacked onto the outgoing message. Defaults to false. + The contact card is piggybacked onto the outgoing message. Defaults to false. ⚠️ + Only available on **Dedicated Commercial** and **Dedicated Enterprise** plans — + other plans receive a `403`. """ text: Union[str, SequenceNotStr[str]] @@ -149,3 +210,32 @@ class Part(TypedDict, total=False): url: str """URL to an attachment for this part. Mutually exclusive with 'text'.""" + + +class ReplyTo(TypedDict, total=False): + """Inline-reply target on `POST /chats/{chatId}/messages`. + + Pass either `message_id` (preferred — references a Blooio-minted message) or `guid` (raw iMessage GUID, useful for replying to messages received before the row was minted in Blooio). The new send is dispatched to Lava with the resolved `selectedMessageGuid` + `partIndex`, which iMessage renders as an inline reply on the recipient's device. + """ + + guid: str + """Raw iMessage GUID of the parent. + + When supplied without a `message_id`, Blooio attempts to look up the parent via + `provider_message_guid`; if the parent isn't in our table the send still + proceeds (Lava will thread on the device when possible) and the response carries + `parent_unresolved: true`. + """ + + message_id: str + """Blooio `message_id` of the parent. + + Must belong to the same chat, same from-number, and be no older than 30 days. + Returns 404 `reply_target_not_found` if unknown. + """ + + part_index: int + """Which part of the parent to reply to. + + Defaults to 0 (covers the 99% case of replying to a single-part text message). + """ diff --git a/src/blooio/types/chats/message_send_response.py b/src/blooio/types/chats/message_send_response.py index 385d693..13efb90 100644 --- a/src/blooio/types/chats/message_send_response.py +++ b/src/blooio/types/chats/message_send_response.py @@ -30,8 +30,21 @@ class MessageSendResponse(BaseModel): (URL-balloon batch mode). """ + parent_unresolved: Optional[bool] = None + """ + Present (and `true`) only when `reply_to.guid` was supplied without a + `message_id` and the GUID didn't map to any Blooio-minted row. The send still + proceeds and the device may still thread it; this flag signals that Blooio + couldn't link the new message to a known parent. + """ + participants: Optional[List[str]] = None """List of participants (present for multi-recipient)""" status: Optional[Literal["queued", "failed"]] = None - """Initial status of the message(s)""" + """Initial status of the message(s). + + `queued` = accepted for delivery (the normal 202 result); `failed` = rejected + before dispatch. Subsequent transitions (`sent` → `delivered`, or `failed`) are + reported via the status endpoint and `message.status` webhooks. + """ diff --git a/src/blooio/types/contact_list_params.py b/src/blooio/types/contact_list_params.py index 6c109ff..124dfe3 100644 --- a/src/blooio/types/contact_list_params.py +++ b/src/blooio/types/contact_list_params.py @@ -9,10 +9,18 @@ class ContactListParams(TypedDict, total=False): limit: int - """Maximum number of items to return (1-200)""" + """Maximum number of items to return in a single response. + + Must be between 1 and 200; defaults to 50. Use together with `offset` to page + through large result sets. + """ offset: int - """Number of items to skip""" + """Number of items to skip before returning results. + + Combine with `limit` for page-based pagination (e.g. `offset=50&limit=50` + returns the second page). Defaults to 0. + """ q: str """Search query (matches identifier or name)""" diff --git a/src/blooio/types/facetime_initiate_call_response.py b/src/blooio/types/facetime_initiate_call_response.py deleted file mode 100644 index 7b1d8f8..0000000 --- a/src/blooio/types/facetime_initiate_call_response.py +++ /dev/null @@ -1,17 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from .._models import BaseModel - -__all__ = ["FacetimeInitiateCallResponse"] - - -class FacetimeInitiateCallResponse(BaseModel): - handle: Optional[str] = None - """The handle that was called""" - - link: Optional[str] = None - """Shareable FaceTime link""" - - success: Optional[bool] = None diff --git a/src/blooio/types/group_list_params.py b/src/blooio/types/group_list_params.py index 7628e4e..2bb759d 100644 --- a/src/blooio/types/group_list_params.py +++ b/src/blooio/types/group_list_params.py @@ -9,10 +9,18 @@ class GroupListParams(TypedDict, total=False): limit: int - """Maximum number of items to return (1-200)""" + """Maximum number of items to return in a single response. + + Must be between 1 and 200; defaults to 50. Use together with `offset` to page + through large result sets. + """ offset: int - """Number of items to skip""" + """Number of items to skip before returning results. + + Combine with `limit` for page-based pagination (e.g. `offset=50&limit=50` + returns the second page). Defaults to 0. + """ q: str """Search query (matches group name)""" diff --git a/src/blooio/types/groups/__init__.py b/src/blooio/types/groups/__init__.py index caf4ab2..b1908ac 100644 --- a/src/blooio/types/groups/__init__.py +++ b/src/blooio/types/groups/__init__.py @@ -7,6 +7,4 @@ from .icon_set_params import IconSetParams as IconSetParams from .member_add_params import MemberAddParams as MemberAddParams from .member_list_params import MemberListParams as MemberListParams -from .member_add_response import MemberAddResponse as MemberAddResponse from .member_list_response import MemberListResponse as MemberListResponse -from .member_remove_response import MemberRemoveResponse as MemberRemoveResponse diff --git a/src/blooio/types/groups/member_add_response.py b/src/blooio/types/groups/member_add_response.py deleted file mode 100644 index 606e03e..0000000 --- a/src/blooio/types/groups/member_add_response.py +++ /dev/null @@ -1,14 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from ..._models import BaseModel -from .group_member import GroupMember - -__all__ = ["MemberAddResponse"] - - -class MemberAddResponse(BaseModel): - member: Optional[GroupMember] = None - - message: Optional[str] = None diff --git a/src/blooio/types/groups/member_list_params.py b/src/blooio/types/groups/member_list_params.py index 75b4ade..ed6fb90 100644 --- a/src/blooio/types/groups/member_list_params.py +++ b/src/blooio/types/groups/member_list_params.py @@ -9,7 +9,15 @@ class MemberListParams(TypedDict, total=False): limit: int - """Maximum number of items to return (1-200)""" + """Maximum number of items to return in a single response. + + Must be between 1 and 200; defaults to 50. Use together with `offset` to page + through large result sets. + """ offset: int - """Number of items to skip""" + """Number of items to skip before returning results. + + Combine with `limit` for page-based pagination (e.g. `offset=50&limit=50` + returns the second page). Defaults to 0. + """ diff --git a/src/blooio/types/groups/member_remove_response.py b/src/blooio/types/groups/member_remove_response.py deleted file mode 100644 index e1cfe9c..0000000 --- a/src/blooio/types/groups/member_remove_response.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from ..._models import BaseModel - -__all__ = ["MemberRemoveResponse"] - - -class MemberRemoveResponse(BaseModel): - removed_at: Optional[int] = None - - success: Optional[bool] = None diff --git a/src/blooio/types/webhook_create_params.py b/src/blooio/types/webhook_create_params.py index 99c36b1..2ee6ba3 100644 --- a/src/blooio/types/webhook_create_params.py +++ b/src/blooio/types/webhook_create_params.py @@ -2,17 +2,17 @@ from __future__ import annotations -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import Required, TypedDict __all__ = ["WebhookCreateParams"] class WebhookCreateParams(TypedDict, total=False): webhook_url: Required[str] - """URL to receive webhook events""" + """URL of an existing webhook, for the idempotent 200 response. - valid_until: int - """Expiration timestamp (-1 for no expiration)""" + A URL that does not already exist returns 410. + """ - webhook_type: Literal["message", "status", "all"] - """Type of events to receive""" + valid_until: int + """Ignored. Retained so existing request bodies stay valid.""" diff --git a/src/blooio/types/webhooks/log_list_params.py b/src/blooio/types/webhooks/log_list_params.py index d8aeaaa..c2eda44 100644 --- a/src/blooio/types/webhooks/log_list_params.py +++ b/src/blooio/types/webhooks/log_list_params.py @@ -9,7 +9,11 @@ class LogListParams(TypedDict, total=False): limit: int - """Maximum number of items to return (1-200)""" + """Maximum number of items to return in a single response. + + Must be between 1 and 200; defaults to 50. Use together with `offset` to page + through large result sets. + """ max_status: int """Maximum HTTP status code""" @@ -18,7 +22,11 @@ class LogListParams(TypedDict, total=False): """Minimum HTTP status code""" offset: int - """Number of items to skip""" + """Number of items to skip before returning results. + + Combine with `limit` for page-based pagination (e.g. `offset=50&limit=50` + returns the second page). Defaults to 0. + """ sort: Literal["asc", "desc"] """Sort order by attempted time""" diff --git a/src/blooio/types/webhooks/log_list_response.py b/src/blooio/types/webhooks/log_list_response.py index 386c9d6..a5f9a9b 100644 --- a/src/blooio/types/webhooks/log_list_response.py +++ b/src/blooio/types/webhooks/log_list_response.py @@ -39,6 +39,22 @@ class LogEventBody(BaseModel): attachments: Optional[List[LogEventBodyAttachment]] = None """Array of attachment objects""" + chat_guid: Optional[str] = None + """ + The device's own identifier for the group conversation this message arrived in + (only on `message.received` when is_group=true). Two group chats can hold the + same members and are then indistinguishable by `group_id` and `participants` + alone; `chat_guid` is what tells them apart. Matches the `chat_guid` on GET + /groups/{groupId}. + """ + + chat_name: Optional[str] = None + """ + The name the device reports for the conversation (only on `message.received` + when is_group=true). May differ from `group_name`, or be present when + `group_name` is null. + """ + delivered_at: Optional[int] = None """Timestamp when message was delivered (for message.delivered events)""" @@ -57,6 +73,26 @@ class LogEventBody(BaseModel): external_id: Optional[str] = None """Recipient identifier (phone number, email, or group ID)""" + formatted_text: Optional[str] = None + """Markdown for a rich-text (bold/italic/underline/strikethrough) message. + + Omitted entirely when the message carries no styling, so its presence is how you + detect rich text. + + Present in both directions: on an outbound send made with `format: "markdown"`, + and on an inbound iMessage whose sender styled their text — so styling a + customer applied in Messages arrives here even though your integration never + asked for it. + + Always a normalized re-serialization of the message's actual styling rather than + an echo of the source string: bold is spelled `**`, italic `*`, underline `++`, + strikethrough `~~`, and any character that would otherwise read as a delimiter + is backslash-escaped. Re-sending this value verbatim with `format: "markdown"` + reproduces the same styled message. Blooio iMessage only. This is the SAME field + delivered on the message webhooks, so a message reads identically via REST or + webhook. + """ + group_id: Optional[str] = None """Group ID (only present when is_group=true)""" @@ -73,10 +109,21 @@ class LogEventBody(BaseModel): """Unique message identifier""" participants: Optional[List[LogEventBodyParticipant]] = None - """Array of group participants (only present when is_group=true)""" + """Array of group participants (only present when is_group=true). + + One entry per person: a participant appears once even if Blooio holds more than + one identity for their number. + """ - protocol: Optional[Literal["imessage", "sms", "rcs", "non-imessage"]] = None - """Message protocol""" + protocol: Optional[Literal["pending", "unknown", "imessage", "sms", "rcs"]] = None + """Transport used to carry the message; never null. + + `pending` = accepted and dispatched, wire service not resolved yet (settles + within seconds of send); `imessage` = delivered over iMessage (blue bubble); + `rcs` = delivered over RCS; `sms` = fell back to SMS/MMS (green bubble); + `unknown` = accepted by the carrier but the wire service could not be resolved + before the tracking window closed (see `error`). + """ read_at: Optional[int] = None """Timestamp when message was read (for message.read events)""" @@ -88,7 +135,14 @@ class LogEventBody(BaseModel): """Timestamp when message was sent (for message.sent events)""" status: Optional[Literal["queued", "pending", "sent", "delivered", "failed", "read", "received"]] = None - """Message status""" + """Message status carried by the event. + + `queued` / `pending` = accepted, not yet handed off; `sent` = handed to + Apple/the carrier; `delivered` = a delivery receipt was received; `read` = a + read receipt was received (iMessage, when the recipient has read receipts on); + `failed` = delivery failed (see `error_code` / `error_message`); `received` = an + inbound message arrived. + """ text: Optional[str] = None """Message text content""" diff --git a/tests/api_resources/chats/test_messages.py b/tests/api_resources/chats/test_messages.py index 1ca5b92..4af40bf 100644 --- a/tests/api_resources/chats/test_messages.py +++ b/tests/api_resources/chats/test_messages.py @@ -266,6 +266,7 @@ def test_method_send_with_all_params(self, client: Blooio) -> None: chat_id="chatId", attachments=["string"], effect="slam", + format="plain", from_number="from_number", link_preview={ "image_url": "https://example.com", @@ -283,6 +284,11 @@ def test_method_send_with_all_params(self, client: Blooio) -> None: "url": "url", } ], + reply_to={ + "guid": "guid", + "message_id": "message_id", + "part_index": 0, + }, share_contact=True, text="string", use_typing_indicator=True, @@ -573,6 +579,7 @@ async def test_method_send_with_all_params(self, async_client: AsyncBlooio) -> N chat_id="chatId", attachments=["string"], effect="slam", + format="plain", from_number="from_number", link_preview={ "image_url": "https://example.com", @@ -590,6 +597,11 @@ async def test_method_send_with_all_params(self, async_client: AsyncBlooio) -> N "url": "url", } ], + reply_to={ + "guid": "guid", + "message_id": "message_id", + "part_index": 0, + }, share_contact=True, text="string", use_typing_indicator=True, diff --git a/tests/api_resources/groups/test_members.py b/tests/api_resources/groups/test_members.py index 8ccc6ba..fd7b097 100644 --- a/tests/api_resources/groups/test_members.py +++ b/tests/api_resources/groups/test_members.py @@ -9,11 +9,7 @@ from blooio import Blooio, AsyncBlooio from tests.utils import assert_matches_type -from blooio.types.groups import ( - MemberAddResponse, - MemberListResponse, - MemberRemoveResponse, -) +from blooio.types.groups import MemberListResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -80,7 +76,7 @@ def test_method_add(self, client: Blooio) -> None: group_id="grp_abc123def456", contact_id="+15551234567", ) - assert_matches_type(MemberAddResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -93,7 +89,7 @@ def test_raw_response_add(self, client: Blooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert_matches_type(MemberAddResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -106,7 +102,7 @@ def test_streaming_response_add(self, client: Blooio) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert_matches_type(MemberAddResponse, member, path=["response"]) + assert member is None assert cast(Any, response.is_closed) is True @@ -126,7 +122,7 @@ def test_method_remove(self, client: Blooio) -> None: contact_id="%2B15551234567", group_id="grp_abc123def456", ) - assert_matches_type(MemberRemoveResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -139,7 +135,7 @@ def test_raw_response_remove(self, client: Blooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert_matches_type(MemberRemoveResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -152,7 +148,7 @@ def test_streaming_response_remove(self, client: Blooio) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert_matches_type(MemberRemoveResponse, member, path=["response"]) + assert member is None assert cast(Any, response.is_closed) is True @@ -236,7 +232,7 @@ async def test_method_add(self, async_client: AsyncBlooio) -> None: group_id="grp_abc123def456", contact_id="+15551234567", ) - assert_matches_type(MemberAddResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -249,7 +245,7 @@ async def test_raw_response_add(self, async_client: AsyncBlooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert_matches_type(MemberAddResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -262,7 +258,7 @@ async def test_streaming_response_add(self, async_client: AsyncBlooio) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert_matches_type(MemberAddResponse, member, path=["response"]) + assert member is None assert cast(Any, response.is_closed) is True @@ -282,7 +278,7 @@ async def test_method_remove(self, async_client: AsyncBlooio) -> None: contact_id="%2B15551234567", group_id="grp_abc123def456", ) - assert_matches_type(MemberRemoveResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -295,7 +291,7 @@ async def test_raw_response_remove(self, async_client: AsyncBlooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert_matches_type(MemberRemoveResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -308,7 +304,7 @@ async def test_streaming_response_remove(self, async_client: AsyncBlooio) -> Non assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert_matches_type(MemberRemoveResponse, member, path=["response"]) + assert member is None assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_facetime.py b/tests/api_resources/test_facetime.py index 3611d91..9d5d2cd 100644 --- a/tests/api_resources/test_facetime.py +++ b/tests/api_resources/test_facetime.py @@ -8,8 +8,6 @@ import pytest from blooio import Blooio, AsyncBlooio -from tests.utils import assert_matches_type -from blooio.types import FacetimeInitiateCallResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -23,7 +21,7 @@ def test_method_initiate_call(self, client: Blooio) -> None: facetime = client.facetime.initiate_call( handle="+15551234567", ) - assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + assert facetime is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -35,7 +33,7 @@ def test_raw_response_initiate_call(self, client: Blooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" facetime = response.parse() - assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + assert facetime is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -47,7 +45,7 @@ def test_streaming_response_initiate_call(self, client: Blooio) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" facetime = response.parse() - assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + assert facetime is None assert cast(Any, response.is_closed) is True @@ -63,7 +61,7 @@ async def test_method_initiate_call(self, async_client: AsyncBlooio) -> None: facetime = await async_client.facetime.initiate_call( handle="+15551234567", ) - assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + assert facetime is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -75,7 +73,7 @@ async def test_raw_response_initiate_call(self, async_client: AsyncBlooio) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" facetime = await response.parse() - assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + assert facetime is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -87,6 +85,6 @@ async def test_streaming_response_initiate_call(self, async_client: AsyncBlooio) assert response.http_request.headers.get("X-Stainless-Lang") == "python" facetime = await response.parse() - assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + assert facetime is None assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_webhooks.py b/tests/api_resources/test_webhooks.py index a6595cc..92c3e78 100644 --- a/tests/api_resources/test_webhooks.py +++ b/tests/api_resources/test_webhooks.py @@ -16,6 +16,8 @@ WebhookDeleteResponse, ) +# pyright: reportDeprecated=false + base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -25,27 +27,31 @@ class TestWebhooks: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create(self, client: Blooio) -> None: - webhook = client.webhooks.create( - webhook_url="https://example.com/webhook", - ) + with pytest.warns(DeprecationWarning): + webhook = client.webhooks.create( + webhook_url="https://example.com/webhook", + ) + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_with_all_params(self, client: Blooio) -> None: - webhook = client.webhooks.create( - webhook_url="https://example.com/webhook", - valid_until=0, - webhook_type="message", - ) + with pytest.warns(DeprecationWarning): + webhook = client.webhooks.create( + webhook_url="https://example.com/webhook", + valid_until=0, + ) + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_create(self, client: Blooio) -> None: - response = client.webhooks.with_raw_response.create( - webhook_url="https://example.com/webhook", - ) + with pytest.warns(DeprecationWarning): + response = client.webhooks.with_raw_response.create( + webhook_url="https://example.com/webhook", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -55,14 +61,15 @@ def test_raw_response_create(self, client: Blooio) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_create(self, client: Blooio) -> None: - with client.webhooks.with_streaming_response.create( - webhook_url="https://example.com/webhook", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.webhooks.with_streaming_response.create( + webhook_url="https://example.com/webhook", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - webhook = response.parse() - assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) + webhook = response.parse() + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) assert cast(Any, response.is_closed) is True @@ -240,27 +247,31 @@ class TestAsyncWebhooks: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncBlooio) -> None: - webhook = await async_client.webhooks.create( - webhook_url="https://example.com/webhook", - ) + with pytest.warns(DeprecationWarning): + webhook = await async_client.webhooks.create( + webhook_url="https://example.com/webhook", + ) + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncBlooio) -> None: - webhook = await async_client.webhooks.create( - webhook_url="https://example.com/webhook", - valid_until=0, - webhook_type="message", - ) + with pytest.warns(DeprecationWarning): + webhook = await async_client.webhooks.create( + webhook_url="https://example.com/webhook", + valid_until=0, + ) + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncBlooio) -> None: - response = await async_client.webhooks.with_raw_response.create( - webhook_url="https://example.com/webhook", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.webhooks.with_raw_response.create( + webhook_url="https://example.com/webhook", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -270,14 +281,15 @@ async def test_raw_response_create(self, async_client: AsyncBlooio) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncBlooio) -> None: - async with async_client.webhooks.with_streaming_response.create( - webhook_url="https://example.com/webhook", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - webhook = await response.parse() - assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) + with pytest.warns(DeprecationWarning): + async with async_client.webhooks.with_streaming_response.create( + webhook_url="https://example.com/webhook", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = await response.parse() + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) assert cast(Any, response.is_closed) is True