Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,18 @@ product page. `MessagingClient.cloudCatalogs.listProducts` follows its WABA and
catalog path, `sessions:read` credential boundary, projected product fields,
and opaque cursor response. It does not establish live catalog access.

API PRs #333 and #385, merged at the pinned commit, add the Official API
message-media retrieval status and the stored-copy download route.
`MessagingClient.chats.downloadMessageMedia` and `downloadMessageMediaStream`
cover the route; `HistoryMessage.mediaRetrieval` types the status. This does
not establish deployed HMS beta access or SDK package publication.

The merged Campaigns failed-state follow-up updates the `campaign.failed`
reason example and the public archive/delete `409` descriptions. It changes no
operation fingerprint or SDK method mapping.

The API merge also incorporates two unrelated routes from API PRs #333 and
#332. Hosted-history media download is assigned to SDK PR #316; Messaging
#332. Hosted-history media download is covered by `MessagingClient.chats`; Messaging
campaign update is assigned to SDK PR #307. Both are intentional exclusions in
this Calls ledger. Existing hosted-history message reads now type the optional
`mediaRetrieval` status and remain covered.
Expand Down Expand Up @@ -205,9 +211,9 @@ signed events with these names still decode as unknown events.

| Status | Operations |
| ------------------- | ---------: |
| Covered | 405 |
| Covered | 406 |
| Missing | 0 |
| Excluded | 131 |
| Excluded | 130 |
| Partial | 0 |
| Changed fingerprint | 0 |
| Total | 536 |
Expand Down
4 changes: 2 additions & 2 deletions contracts/coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -587,8 +587,8 @@
"operationId": "getChatMessageMedia",
"fingerprint": "fff403df5eb56d8cfd73b930641f5ea070b39eb9a4e2d8bb05aa7f9992ca7065",
"typescript": {
"status": "excluded",
"reason": "Official API hosted-history media download is owned by SDK PR #316."
"status": "covered",
"method": "MessagingClient.chats.downloadMessageMedia"
}
},
{
Expand Down
14 changes: 11 additions & 3 deletions packages/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1178,14 +1178,22 @@ if (page.data.nextCursor) {
console.log(page.metadata.headers["polymorfa-data-region"]);
```

These four reads require an organization key or project token, a visible
These conversation and message reads require an organization key or project token, a visible
Number with hosted message storage enabled, team enrollment in
`messaging.history`, and `chats:read` or `messages:read` as appropriate. The
feature is an unreleased enrolled beta; an SDK method does not grant access.
Client tokens are refused before transport. A disabled HMS Number yields
`404 hms_not_enabled`; absent beta access yields `403 permission_denied`, and
an unavailable regional read yields `503 service_unavailable`. Media entries
carry an API download path, not a signed URL; downloading requires `media:read`.
an unavailable regional read yields `503 service_unavailable`.

For Official API media, `message.mediaRetrieval.state` reports whether a copy
is `pending`, `stored`, or in a final state without a copy. A `stored` message
has a media entry whose `url` is a message-scoped API path, not a signed URL.
Download that copy with `messaging.chats.downloadMessageMedia(session, conversation, message.id)`
or stream it with `downloadMessageMediaStream(session, conversation, message.id)`. These reads
require both `messages:read` and `media:read`; a missing copy returns 404.
`downloadMessageMedia` buffers the whole file, so prefer the stream for large
media. Linked-device media paths still use `messaging.media.download(mediaId)`.

Client tokens can call all five Messages operations only when the corresponding
live rule is enabled: `send_message` for send and star, `send_reaction` for
Expand Down
48 changes: 48 additions & 0 deletions packages/typescript/src/messaging/chats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
ListHistoryChatsParams,
ListHistoryMessagesParams,
} from "./history.js";
import type { MediaDownloadStream } from "./media.js";
import type {
DisappearingTimerRequest,
EditMessageRequest,
Expand Down Expand Up @@ -115,6 +116,53 @@ export class ChatsResource {
});
}

/** Downloads a stored Official API message copy. Requires `messages:read`, `media:read`, and enrolled HMS history access. */
downloadMessageMedia(
session: string,
conversation: string,
messageId: string,
options: RequestOptions = {},
): Promise<ApiResponse<ArrayBuffer>> {
this.assertHistoryCredential();
return this.transport.requestBinary({
method: "GET",
path: `${chatMessagePath(session, conversation, messageId)}/media`,
...options,
Comment thread
purpshell marked this conversation as resolved.
});
}

/** Streams a stored Official API message copy without buffering the file. */
async downloadMessageMediaStream(
session: string,
conversation: string,
messageId: string,
options: RequestOptions = {},
): Promise<MediaDownloadStream> {
this.assertHistoryCredential();
const response = await this.transport.requestStream({
method: "GET",
path: `${chatMessagePath(session, conversation, messageId)}/media`,
...options,
});
return Object.freeze({
body: response.body,
...(response.contentType === undefined
? {}
: { contentType: response.contentType }),
...(response.contentLength === undefined
? {}
: { contentLength: response.contentLength }),
...(response.filename === undefined
? {}
: { filename: response.filename }),
...(response.metadata.requestId === undefined
? {}
: { requestId: response.metadata.requestId }),
redirected: response.redirected,
metadata: response.metadata,
});
}

editMessage(
session: string,
chatId: string,
Expand Down
4 changes: 2 additions & 2 deletions packages/typescript/src/messaging/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ export interface HistoryMedia {
readonly id: string;
readonly mimeType: string;
readonly fileLength: number;
/** API path for stored bytes; this is not a signed URL. */
/** API download path. Cloud stored copies use `MessagingClient.chats.downloadMessageMedia`. */
readonly url: string;
}

/** Official API media storage status on a hosted history message. */
/** Official API media copy status. Other final states have no downloadable copy. */
export interface HistoryMediaRetrieval {
readonly state:
| "pending"
Expand Down
4 changes: 2 additions & 2 deletions packages/typescript/test/coverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,10 @@ describe("coverage checker", () => {
expect(result.report).toMatchObject({
sourceCommit: "29abb7b66a8e225af4df5cf354e9836ccb2ce04c",
total: 536,
covered: 405,
covered: 406,
partial: 0,
missing: 0,
excluded: 131,
excluded: 130,
changed: 0,
});
const resolutions = (result.report?.resolutions ?? []) as Array<{
Expand Down
44 changes: 44 additions & 0 deletions packages/typescript/test/history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,50 @@ describe("hosted message history", () => {
);
});

it("downloads a stored Official API copy by encoded message path", async () => {
const fetch = vi.fn<typeof globalThis.fetch>(
async () =>
new Response(Uint8Array.from([1, 2, 3]), {
headers: { "content-type": "image/jpeg", "content-length": "3" },
}),
);
const result = await client(fetch).chats.downloadMessageMedia(
"support/1",
"+14155550123",
"739182640518204",
);
expect(new Uint8Array(result.data)).toEqual(Uint8Array.from([1, 2, 3]));
const url = new URL(String(fetch.mock.calls[0]![0]));
expect(url.pathname).toBe(
"/messaging/support%2F1/chats/%2B14155550123/messages/739182640518204/media",
);

const stream = await client(fetch).chats.downloadMessageMediaStream(
"support",
"+14155550123",
"739182640518204",
);
expect(stream).toMatchObject({
contentType: "image/jpeg",
contentLength: 3,
});
expect(
new Uint8Array(await new Response(stream.body).arrayBuffer()),
).toEqual(Uint8Array.from([1, 2, 3]));
});

it("rejects client tokens before downloading stored media", async () => {
const fetch = vi.fn<typeof globalThis.fetch>();
const chats = client(fetch, "clientToken").chats;
expect(() =>
chats.downloadMessageMedia("support", "+14155550123", "1"),
).toThrow(PolymorfaConfigurationError);
await expect(
chats.downloadMessageMediaStream("support", "+14155550123", "1"),
).rejects.toBeInstanceOf(PolymorfaConfigurationError);
expect(fetch).not.toHaveBeenCalled();
});

it("rejects client tokens locally on all history reads", () => {
const fetch = vi.fn<typeof globalThis.fetch>();
const chats = client(fetch, "clientToken").chats;
Expand Down
Loading