Skip to content
Open
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
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2907,7 +2907,7 @@ COMMANDS
ably push channels Manage push notification channel subscriptions
ably push config Manage push notification configuration (APNs, FCM)
ably push devices Manage push notification device registrations
ably push publish Publish a push notification to a device or client
ably push publish Publish a push notification to a device, client, or channel
```

_See code: [src/commands/push/index.ts](https://github.com/ably/ably-cli/blob/v0.17.0/src/commands/push/index.ts)_
Expand Down Expand Up @@ -3470,19 +3470,22 @@ _See code: [src/commands/push/devices/save.ts](https://github.com/ably/ably-cli/

## `ably push publish`

Publish a push notification to a device or client
Publish a push notification to a device, client, or channel

```
USAGE
$ ably push publish [-v] [--json | --pretty-json] [--device-id <value> | --client-id <value> | --recipient
<value>] [--title <value>] [--body <value>] [--sound <value>] [--icon <value>] [--badge <value>] [--data <value>]
[--collapse-key <value>] [--ttl <value>] [--payload <value>] [--apns <value>] [--fcm <value>] [--web <value>]
<value>] [--channel <value>] [--title <value>] [--body <value>] [--sound <value>] [--icon <value>] [--badge <value>]
[--data <value>] [--collapse-key <value>] [--ttl <value>] [--payload <value>] [--apns <value>] [--fcm <value>]
[--web <value>]

FLAGS
-v, --verbose Output verbose logs
--apns=<value> APNs-specific override as JSON
--badge=<value> Notification badge count
--body=<value> Notification body
--channel=<value> Target channel name (publishes push notification via the channel using extras.push;
ignored if --device-id, --client-id, or --recipient is also provided)
--client-id=<value> Target client ID
--collapse-key=<value> Collapse key for notification grouping
--data=<value> Custom data payload as JSON
Expand All @@ -3499,13 +3502,15 @@ FLAGS
--web=<value> Web push-specific override as JSON

DESCRIPTION
Publish a push notification to a device or client
Publish a push notification to a device, client, or channel

EXAMPLES
$ ably push publish --device-id device-123 --title Hello --body World

$ ably push publish --client-id client-1 --title Hello --body World

$ ably push publish --channel my-channel --title Hello --body World

$ ably push publish --device-id device-123 --payload '{"notification":{"title":"Hello","body":"World"}}'

$ ably push publish --recipient '{"transportType":"apns","deviceToken":"token123"}' --title Hello --body World
Expand Down
151 changes: 123 additions & 28 deletions src/commands/push/batch-publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@ import { BaseFlags } from "../../types/cli.js";
import {
formatCountLabel,
formatProgress,
formatResource,
formatSuccess,
formatWarning,
} from "../../utils/output.js";
import { promptForConfirmation } from "../../utils/prompt-confirmation.js";

export default class PushBatchPublish extends AblyBaseCommand {
static override description =
"Publish push notifications to multiple recipients in a batch";

static override examples = [
'<%= config.bin %> <%= command.id %> --payload \'[{"recipient":{"deviceId":"dev1"},"payload":{"notification":{"title":"Hello","body":"World"}}}]\'',
'<%= config.bin %> <%= command.id %> --payload \'[{"channel":"my-channel","payload":{"notification":{"title":"Hello","body":"World"}}}]\'',
"<%= config.bin %> <%= command.id %> --payload @batch.json",
"cat batch.json | <%= config.bin %> <%= command.id %> --payload -",
"<%= config.bin %> <%= command.id %> --payload @batch.json --json",
Expand All @@ -28,6 +32,10 @@ export default class PushBatchPublish extends AblyBaseCommand {
description: "Batch payload as JSON array, @filepath, or - for stdin",
required: true,
}),
force: Flags.boolean({
char: "f",
description: "Skip confirmation prompt when publishing to channels",
}),
};

async run(): Promise<void> {
Expand Down Expand Up @@ -97,11 +105,31 @@ export default class PushBatchPublish extends AblyBaseCommand {
);
}

type RecipientItem = {
recipient: Record<string, unknown>;
payload: Record<string, unknown>;
};
type ChannelItem = { channel: string; payload: Record<string, unknown> };

const recipientItems: RecipientItem[] = [];
const channelItemsList: ChannelItem[] = [];

for (const [index, item] of batchPayload.entries()) {
const entry = item as Record<string, unknown>;
if (!entry.recipient) {

if (entry.recipient && entry.channel) {
// Both present — channel is ignored, warn the user
const msg = `Item at index ${index}: "channel" is ignored when "recipient" is also provided.`;
if (this.shouldOutputJson(flags)) {
this.logJsonStatus("warning", msg, flags);
} else {
this.log(formatWarning(msg));
}
}

if (!entry.recipient && !entry.channel) {
this.fail(
`Item at index ${index} is missing required "recipient" field`,
`Item at index ${index} is missing required "recipient" or "channel" field`,
flags as BaseFlags,
"pushBatchPublish",
);
Expand All @@ -117,6 +145,39 @@ export default class PushBatchPublish extends AblyBaseCommand {
"pushBatchPublish",
);
}

if (entry.recipient) {
recipientItems.push({
recipient: entry.recipient as Record<string, unknown>,
payload: itemPayload!,
});
} else {
channelItemsList.push({
channel: entry.channel as string,
payload: itemPayload!,
});
}
}

// Prompt for confirmation when publishing to channels (non-JSON, non-force mode only)
if (
channelItemsList.length > 0 &&
!this.shouldOutputJson(flags) &&
!flags.force
) {
const uniqueChannels = [
...new Set(channelItemsList.map((i) => i.channel)),
];
const channelList = uniqueChannels
.map((c) => formatResource(c))
.join(", ");
const confirmed = await promptForConfirmation(
`This will publish push notifications to ${formatCountLabel(channelItemsList.length, "item")} targeting ${channelList}. Continue?`,
);
if (!confirmed) {
this.log("Batch publish cancelled.");
return;
}
}

if (!this.shouldOutputJson(flags)) {
Expand All @@ -127,50 +188,84 @@ export default class PushBatchPublish extends AblyBaseCommand {
);
}

const response = await rest.request(
"post",
"/push/batch/publish",
2,
null,
batchPayload,
);

// Parse response items for success/failure counts
const items = (response.items ?? []) as Record<string, unknown>[];
const failed = items.filter(
(item) => item.error || (item.statusCode && item.statusCode !== 200),
);
const succeeded =
items.length > 0 ? items.length - failed.length : batchPayload.length;
let succeeded = 0;
const failedItems: { index: number; error: string }[] = [];

// Publish recipient-based items via /push/batch/publish
if (recipientItems.length > 0) {
const response = await rest.request(
"post",
"/push/batch/publish",
2,
null,
recipientItems,
);
const responseItems = (response.items ?? []) as Record<
string,
unknown
>[];
for (const [i, result] of responseItems.entries()) {
if (
result.error ||
(result.statusCode && result.statusCode !== 200)
) {
const err = result.error as Record<string, unknown> | undefined;
failedItems.push({
index: i,
error: String(err?.message ?? "Unknown error"),
});
} else {
succeeded++;
}
}
if (responseItems.length === 0) {
succeeded += recipientItems.length;
}
}

// Publish channel-based items via channel extras.push
for (const [i, item] of channelItemsList.entries()) {
try {
await rest.channels
.get(item.channel)
.publish({ extras: { push: item.payload } });
succeeded++;
} catch (error) {
failedItems.push({
index: recipientItems.length + i,
error: error instanceof Error ? error.message : String(error),
});
}
}

const total = batchPayload.length;
const failedCount = failedItems.length;

if (this.shouldOutputJson(flags)) {
this.logJsonResult(
{
published: true,
total: batchPayload.length,
total,
succeeded,
failed: failed.length,
...(failed.length > 0 ? { failedItems: failed } : {}),
failed: failedCount,
...(failedCount > 0 ? { failedItems } : {}),
},
flags,
);
} else {
if (failed.length > 0) {
if (failedCount > 0) {
this.log(
formatSuccess(
`Batch published: ${succeeded} succeeded, ${failed.length} failed out of ${formatCountLabel(batchPayload.length, "notification")}.`,
`Batch published: ${succeeded} succeeded, ${failedCount} failed out of ${formatCountLabel(total, "notification")}.`,
),
);
for (const item of failed) {
const error = item.error as Record<string, unknown> | undefined;
const message = error?.message ?? "Unknown error";
const code = error?.code ? ` (code: ${error.code})` : "";
this.logToStderr(` Failed: ${message}${code}`);
for (const item of failedItems) {
this.logToStderr(` Failed (index ${item.index}): ${item.error}`);
}
} else {
this.log(
formatSuccess(
`Batch of ${formatCountLabel(batchPayload.length, "notification")} published.`,
`Batch of ${formatCountLabel(total, "notification")} published.`,
),
);
}
Expand Down
Loading
Loading