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
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ prerelease in `package.json`.
The version output includes both runtime components:

```text
polymorfa/0.1.0-dev.0 sdk/0.1.0-dev.20261001202146
polymorfa/0.1.0-dev.0 sdk/0.1.0-dev.20261001204803
```

The SDK component reports the version built into the installed SDK. Dev
Expand Down Expand Up @@ -477,6 +477,27 @@ polymorfa cloud credentials get --session number_123
polymorfa cloud credentials reauthorize --session number_123
```

`cloud group` commands (beta) manage WhatsApp groups on an Official API
Number. They need team enrollment and a Number whose WhatsApp Business Account
is an Official Business Account; otherwise the API returns
`whatsapp_groups_ineligible`. People join with the invite link and cannot be
added; a group holds at most 8 participants besides your business. `create`
returns a `requestId`; the group's conversation ID and invite link arrive in a
`group.update` event. Every change makes one network attempt; pass
`--idempotency-key` so a manual retry after an unknown outcome is answered by
the API instead of changing the group again.

```bash
polymorfa cloud group create --session number_123 --subject "Order 1042" --join-approval --idempotency-key grp-1042
polymorfa cloud group list --session number_123
polymorfa cloud group get GROUP_ID --session number_123
polymorfa cloud group invite-link GROUP_ID --session number_123
polymorfa cloud group join-requests list GROUP_ID --session number_123
polymorfa cloud group join-requests approve GROUP_ID --session number_123 --request JOIN_REQUEST_ID
polymorfa cloud group pin GROUP_ID --session number_123 --message MESSAGE_ID --days 7
polymorfa cloud group delete GROUP_ID --session number_123 --yes
```

Template commands use `messages:write` and the connected Meta business template
catalog. They are separate from project template drafts. Creation reads a JSON
object with `name`, `language`, `category`, and `components` from `--input`, or
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@modelcontextprotocol/server": "2.0.0",
"@napi-rs/keyring": "1.3.0",
"@oclif/core": "^4.13.5",
"@polymorfa/sdk": "0.1.0-dev.20261001202146",
"@polymorfa/sdk": "0.1.0-dev.20261001204803",
"jq-wasm": "3.0.0-jq-1.8.2",
"zod": "4.2.0"
},
Expand Down Expand Up @@ -314,6 +314,12 @@
},
"cloud:catalog": {
"description": "Read Meta product catalogs visible to a WhatsApp Business Account."
},
"cloud:group": {
"description": "Manage WhatsApp groups on an Official API Number (beta). Requires an Official Business Account."
},
"cloud:group:join-requests": {
"description": "Review join requests for Official groups that require approval."
}
}
},
Expand Down
49 changes: 49 additions & 0 deletions src/commands/cloud/group/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {Flags} from '@oclif/core'
import {BaseCommand} from '../../../base-command.js'
import {cloudResult, requestKey} from '../../../services/cloud.js'

export default class CloudGroupCreate extends BaseCommand {
static override description =
'Ask WhatsApp to create an Official group (beta). Requires groups:manage, team enrollment and an Official Business Account. Sends once; the group ID and invite link arrive in a group.update event carrying the returned requestId.'

static override flags = {
session: Flags.string({
description: 'Messaging Number ID.',
required: true,
}),
subject: Flags.string({
description: 'Group subject, 1 to 128 characters.',
required: true,
}),
description: Flags.string({
description: 'Group description, at most 2048 characters.',
}),
'join-approval': Flags.boolean({
description: 'Require approval for people who open the invite link.',
default: false,
}),
'idempotency-key': Flags.string({
description:
'Key that fences a manual retry: reuse it after an unknown outcome and the API answers instead of changing the group again.',
}),
}
async run(): Promise<void> {
const {flags} = await this.parse(CloudGroupCreate)
const client = await this.messagingClient(flags, {
serverCredentialOnly: 'Official API',
})
const response = await client.officialGroups.create(
flags.session,
{
subject: flags.subject,
...(flags.description === undefined
? {}
: {description: flags.description}),
...(flags['join-approval'] ? {joinApprovalRequired: true} : {}),
},
requestKey(flags['idempotency-key']),
)
const result = cloudResult(response)
this.writeValue(result, JSON.stringify(result.data, null, 2), flags)
}
}
47 changes: 47 additions & 0 deletions src/commands/cloud/group/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {Args, Flags} from '@oclif/core'
import {BaseCommand} from '../../../base-command.js'
import {cloudResult, requestKey} from '../../../services/cloud.js'
import {PolymorfaConfigurationError} from '@polymorfa/sdk'

export default class CloudGroupDelete extends BaseCommand {
static override description =
'Delete an Official group and remove every participant, including your business (beta). Requires groups:manage. Does not retry an uncertain write.'
static override args = {
group: Args.string({
description: 'Conversation ID of the group.',
required: true,
}),
}
static override flags = {
session: Flags.string({
description: 'Messaging Number ID.',
required: true,
}),
yes: Flags.boolean({
description: 'Confirm deleting the group for every participant.',
required: true,
}),
'idempotency-key': Flags.string({
description:
'Key that fences a manual retry: reuse it after an unknown outcome and the API answers instead of changing the group again.',
}),
}
async run(): Promise<void> {
const {args, flags} = await this.parse(CloudGroupDelete)
if (!flags.yes)
throw new PolymorfaConfigurationError(
'Pass --yes to delete the group.',
'yes',
)
const client = await this.messagingClient(flags, {
serverCredentialOnly: 'Official API',
})
const response = await client.officialGroups.delete(
flags.session,
args.group,
requestKey(flags['idempotency-key']),
)
const result = cloudResult(response)
this.writeValue(result, JSON.stringify(result.data, null, 2), flags)
}
}
32 changes: 32 additions & 0 deletions src/commands/cloud/group/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {Args, Flags} from '@oclif/core'
import {BaseCommand} from '../../../base-command.js'
import {cloudResult} from '../../../services/cloud.js'

export default class CloudGroupGet extends BaseCommand {
static override description =
'Read an Official group: subject, description, participants and settings (beta). Requires groups:read.'
static override args = {
group: Args.string({
description: 'Conversation ID of the group.',
required: true,
}),
}
static override flags = {
session: Flags.string({
description: 'Messaging Number ID.',
required: true,
}),
}
async run(): Promise<void> {
const {args, flags} = await this.parse(CloudGroupGet)
const client = await this.messagingClient(flags, {
serverCredentialOnly: 'Official API',
})
const response = await client.officialGroups.retrieve(
flags.session,
args.group,
)
const result = cloudResult(response)
this.writeValue(result, JSON.stringify(result.data, null, 2), flags)
}
}
50 changes: 50 additions & 0 deletions src/commands/cloud/group/invite-link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {Args, Flags} from '@oclif/core'
import {BaseCommand} from '../../../base-command.js'
import {cloudResult, requestKey} from '../../../services/cloud.js'
import {PolymorfaConfigurationError} from '@polymorfa/sdk'

export default class CloudGroupInviteLink extends BaseCommand {
static override description =
'Show an Official group invite link, or create a new one with --reset (beta). Resetting stops every earlier link. Requires groups:read, or groups:manage with --reset.'
static override args = {
group: Args.string({
description: 'Conversation ID of the group.',
required: true,
}),
}
static override flags = {
session: Flags.string({
description: 'Messaging Number ID.',
required: true,
}),
reset: Flags.boolean({
description:
'Create a new link; every earlier link stops working. Sends once.',
default: false,
}),
'idempotency-key': Flags.string({
description:
'Key that fences a manual retry: reuse it after an unknown outcome and the API answers instead of changing the group again.',
}),
}
async run(): Promise<void> {
const {args, flags} = await this.parse(CloudGroupInviteLink)
if (!flags.reset && flags['idempotency-key'] !== undefined)
throw new PolymorfaConfigurationError(
'--idempotency-key applies only with --reset.',
'idempotency-key',
)
const client = await this.messagingClient(flags, {
serverCredentialOnly: 'Official API',
})
const response = flags.reset
? await client.officialGroups.resetInviteLink(
flags.session,
args.group,
requestKey(flags['idempotency-key']),
)
: await client.officialGroups.getInviteLink(flags.session, args.group)
const result = cloudResult(response)
this.writeValue(result, JSON.stringify(result.data, null, 2), flags)
}
}
43 changes: 43 additions & 0 deletions src/commands/cloud/group/join-requests/approve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {Args, Flags} from '@oclif/core'
import {BaseCommand} from '../../../../base-command.js'
import {cloudResult, requestKey} from '../../../../services/cloud.js'

export default class CloudGroupJoinRequestsApprove extends BaseCommand {
static override description =
'Approve open join requests for an Official group (beta). Requires groups:manage. Sends once; requests WhatsApp could not approve are listed in failed.'
static override args = {
group: Args.string({
description: 'Conversation ID of the group.',
required: true,
}),
}
static override flags = {
session: Flags.string({
description: 'Messaging Number ID.',
required: true,
}),
request: Flags.string({
description: 'Join request ID. Repeat for several.',
required: true,
multiple: true,
}),
'idempotency-key': Flags.string({
description:
'Key that fences a manual retry: reuse it after an unknown outcome and the API answers instead of changing the group again.',
}),
}
async run(): Promise<void> {
const {args, flags} = await this.parse(CloudGroupJoinRequestsApprove)
const client = await this.messagingClient(flags, {
serverCredentialOnly: 'Official API',
})
const response = await client.officialGroups.approveJoinRequests(
flags.session,
args.group,
flags.request,
requestKey(flags['idempotency-key']),
)
const result = cloudResult(response)
this.writeValue(result, JSON.stringify(result.data, null, 2), flags)
}
}
43 changes: 43 additions & 0 deletions src/commands/cloud/group/join-requests/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {Args, Flags} from '@oclif/core'
import {BaseCommand} from '../../../../base-command.js'
import {cloudResult} from '../../../../services/cloud.js'

export default class CloudGroupJoinRequestsList extends BaseCommand {
static override description =
'List open join requests for an Official group that requires approval (beta). Requires groups:read.'
static override args = {
group: Args.string({
description: 'Conversation ID of the group.',
required: true,
}),
}
static override flags = {
session: Flags.string({
description: 'Messaging Number ID.',
required: true,
}),
before: Flags.string({
description: 'Cursor from a previous page (cursors.before).',
exclusive: ['after'],
}),
after: Flags.string({
description: 'Cursor from a previous page (cursors.after).',
}),
}
async run(): Promise<void> {
const {args, flags} = await this.parse(CloudGroupJoinRequestsList)
const client = await this.messagingClient(flags, {
serverCredentialOnly: 'Official API',
})
const response = await client.officialGroups.listJoinRequests(
flags.session,
args.group,
{
...(flags.before === undefined ? {} : {before: flags.before}),
...(flags.after === undefined ? {} : {after: flags.after}),
},
)
const result = cloudResult(response)
this.writeValue(result, JSON.stringify(result.data, null, 2), flags)
}
}
43 changes: 43 additions & 0 deletions src/commands/cloud/group/join-requests/reject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {Args, Flags} from '@oclif/core'
import {BaseCommand} from '../../../../base-command.js'
import {cloudResult, requestKey} from '../../../../services/cloud.js'

export default class CloudGroupJoinRequestsReject extends BaseCommand {
static override description =
'Reject open join requests for an Official group (beta). Requires groups:manage. Sends once; requests WhatsApp could not reject are listed in failed.'
static override args = {
group: Args.string({
description: 'Conversation ID of the group.',
required: true,
}),
}
static override flags = {
session: Flags.string({
description: 'Messaging Number ID.',
required: true,
}),
request: Flags.string({
description: 'Join request ID. Repeat for several.',
required: true,
multiple: true,
}),
'idempotency-key': Flags.string({
description:
'Key that fences a manual retry: reuse it after an unknown outcome and the API answers instead of changing the group again.',
}),
}
async run(): Promise<void> {
const {args, flags} = await this.parse(CloudGroupJoinRequestsReject)
const client = await this.messagingClient(flags, {
serverCredentialOnly: 'Official API',
})
const response = await client.officialGroups.rejectJoinRequests(
flags.session,
args.group,
flags.request,
requestKey(flags['idempotency-key']),
)
const result = cloudResult(response)
this.writeValue(result, JSON.stringify(result.data, null, 2), flags)
}
}
Loading