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
85 changes: 82 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,32 @@ polymorfa group invite-link 750000000001 --session session_123

Add `--json` for complete resource data and response metadata. Group and
participant lists are unpaged because the source API does not expose cursors.
Mutating group commands remain under implementation.

Create and change groups with a server API key that has `groups:manage`:

```shell
polymorfa group create --session session_123 \
--name 'Launch team' \
--participant 750000000001 --participant 750000000002

polymorfa group participants add 750000000009 --session session_123 --participant 750000000003
polymorfa group participants promote 750000000009 --session session_123 --participant 750000000003
polymorfa group participants demote 750000000009 --session session_123 --participant 750000000003
polymorfa group participants remove 750000000009 --session session_123 --participant 750000000003 --yes

polymorfa group subject 750000000009 --session session_123 --value 'Launch crew'
polymorfa group description 750000000009 --session session_123 --value ''

polymorfa group settings 750000000009 --session session_123 \
--messaging admins --info-editing admins --member-add all --join-approval on
```

Participants are Polymorfa user IDs. Resolve phone numbers with
`polymorfa identity resolve` first. An empty `--value` clears the description.
`group settings` applies each requested setting with its own API call, in the
order info editing, messaging, member add, join approval. If one fails, the
error names the settings that were already applied. Removing participants
requires `--yes`.

## Messages

Expand Down Expand Up @@ -191,9 +216,63 @@ Every mutation accepts `--idempotency-key` for safe retry identity. Server API
keys require `messages:write`; client tokens remain subject to their live
recipient rules, allowed actions, and rate limits.

### Typed sends

Every `message send <kind>` command accepts the same `--session`, `--to`,
`--reply-to`, `--mention`, `--forwarded`, and `--idempotency-key` flags as
`message send text`:

```shell
# Media from a local file (sent inline as base64) or a public URL
polymorfa message send image --session session_123 --to +15551234567 --file ./photo.jpg --caption 'Today'
polymorfa message send video --session session_123 --to +15551234567 --url https://example.com/clip.mp4
polymorfa message send audio --session session_123 --to +15551234567 --file ./note.ogg --voice
polymorfa message send document --session session_123 --to +15551234567 --file ./invoice.pdf

polymorfa message send template --session session_123 --to +15551234567 \
--name order_update --language en_US --components components.json
polymorfa message send location --session session_123 --to +15551234567 \
--lat 51.5007 --long=-0.1246 --address 'Westminster'
polymorfa message send contact --session session_123 --to +15551234567 \
--name 'Ada Lovelace' --phone +15557654321
polymorfa message send poll --session session_123 --to 730000000001 \
--title 'Lunch?' --option Pizza --option Sushi --multi-select
polymorfa message send buttons --session session_123 --to +15551234567 \
--body 'Confirm your order?' --button reply:yes=Yes --button reply:no=No
polymorfa message send list --session session_123 --to +15551234567 \
--title 'Pick a slot' --button-text Choose \
--row 'Morning/am9=9:00' --row 'Afternoon/pm2=14:00|Near the office'
```

- Media commands take exactly one of `--file` or `--url`. Local files are
limited by `--max-bytes` (100 MiB by default) and the MIME type is inferred
from the extension unless `--mime-type` is set. `document` sends the local
file name unless `--filename` overrides it. `audio --voice` sends a
push-to-talk voice note.
- `template --components` reads a JSON array from a file or `-` for stdin.
- Pass negative coordinates as `--lat=-33.86` so they are not read as flags.
- `contact` builds a vCard 3.0 from `--name`, repeatable `--phone`, and
`--organization`, or sends the vCard file given by `--vcard`.
- `poll` takes 2 to 12 unique `--option` values.
- `buttons` takes repeatable `--button` values: `reply:<id>=<text>`,
`url:<text>=<url>`, `call:<text>=<+E.164>`, or `copy:<text>=<code>`.
- `list` takes repeatable `--row` values `[<section>/]<id>=<title>[|<description>]`.
Rows with the same section are grouped in order.

The API has no sticker send kind, so the CLI has no `message send sticker`.

### Edit and delete

```shell
polymorfa message edit 740000000001 --session session_123 --chat 730000000001 --body 'Corrected text'
polymorfa message delete 740000000001 --session session_123 --chat 730000000001 --yes
```

Edit and delete require `chats:manage`. Delete asks for confirmation; pass
`--yes` in scripts.

The API does not expose message history, list, or search operations. Those
commands are not synthesized by the CLI. Additional typed send kinds will be
added as focused subcommands instead of a generic JSON payload interface.
commands are not synthesized by the CLI.

## Media

Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@
"group": {
"description": "Inspect and manage Messaging groups."
},
"group:participants": {
"description": "List and change group participants and admins."
},
"key": {
"description": "Inspect and revoke organization API keys."
},
Expand Down
45 changes: 45 additions & 0 deletions src/commands/group/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {Flags} from '@oclif/core'

import {BaseCommand} from '../../base-command.js'
import {idempotencyOptions} from '../../input/message-send.js'
import {createGroup} from '../../services/groups.js'

export default class GroupCreate extends BaseCommand {
static override description = 'Create a group with initial participants.'

static override examples = [
'<%= config.bin %> group create --session session_123 --name "Launch team" --participant 750000000001 --participant 750000000002',
]

static override flags = {
'idempotency-key': Flags.string({
description: 'Safely identify retries of this creation.',
}),
name: Flags.string({description: 'Group subject.', required: true}),
participant: Flags.string({
description: 'Polymorfa user ID to add. Repeat for several participants.',
multiple: true,
required: true,
}),
session: Flags.string({
description: 'Messaging session ID.',
required: true,
}),
}

async run(): Promise<void> {
const {flags} = await this.parse(GroupCreate)
const result = await createGroup(
await this.messagingClient(flags, {serverApiKeyOnly: 'Groups'}),
flags.session,
flags.name,
flags.participant,
idempotencyOptions(flags['idempotency-key']),
)
this.writeValue(
result,
`Created group ${result.data.id} (${result.data.name}).`,
flags,
)
}
}
49 changes: 49 additions & 0 deletions src/commands/group/description.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {Args, Flags} from '@oclif/core'

import {BaseCommand} from '../../base-command.js'
import {idempotencyOptions} from '../../input/message-send.js'
import {setGroupText} from '../../services/groups.js'

export default class GroupDescription extends BaseCommand {
static override args = {
group: Args.string({description: 'Group identifier.', required: true}),
}

static override description =
'Replace a group description. Pass an empty value to clear it.'

static override examples = [
'<%= config.bin %> group description 750000000009 --session session_123 --value "Weekly launch coordination"',
]

static override flags = {
'idempotency-key': Flags.string({
description: 'Safely identify retries of this change.',
}),
session: Flags.string({
description: 'Messaging session ID.',
required: true,
}),
value: Flags.string({
description: 'New description. An empty string clears it.',
required: true,
}),
}

async run(): Promise<void> {
const {args, flags} = await this.parse(GroupDescription)
const result = await setGroupText(
await this.messagingClient(flags, {serverApiKeyOnly: 'Groups'}),
flags.session,
args.group,
'description',
flags.value,
idempotencyOptions(flags['idempotency-key']),
)
this.writeValue(
result,
`Updated the description of group ${args.group}.`,
flags,
)
}
}
53 changes: 53 additions & 0 deletions src/commands/group/participants/add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {Args, Flags} from '@oclif/core'

import {BaseCommand} from '../../../base-command.js'
import {idempotencyOptions} from '../../../input/message-send.js'
import {
changeGroupParticipants,
participantIds,
} from '../../../services/groups.js'

export default class GroupParticipantsAdd extends BaseCommand {
static override args = {
group: Args.string({description: 'Group identifier.', required: true}),
}

static override description = 'Add participants to a group.'

static override examples = [
'<%= config.bin %> group participants add 750000000009 --session session_123 --participant 750000000001',
]

static override flags = {
'idempotency-key': Flags.string({
description: 'Safely identify retries of this change.',
}),
participant: Flags.string({
description: 'Polymorfa user ID. Repeat for several participants.',
multiple: true,
required: true,
}),
session: Flags.string({
description: 'Messaging session ID.',
required: true,
}),
}

async run(): Promise<void> {
const {args, flags} = await this.parse(GroupParticipantsAdd)
const participants = participantIds(flags.participant)
const result = await changeGroupParticipants(
await this.messagingClient(flags, {serverApiKeyOnly: 'Groups'}),
flags.session,
args.group,
'add',
participants,
idempotencyOptions(flags['idempotency-key']),
)
this.writeValue(
result,
`Added ${participants.length} participant(s) in group ${args.group}.`,
flags,
)
}
}
53 changes: 53 additions & 0 deletions src/commands/group/participants/demote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {Args, Flags} from '@oclif/core'

import {BaseCommand} from '../../../base-command.js'
import {idempotencyOptions} from '../../../input/message-send.js'
import {
changeGroupParticipants,
participantIds,
} from '../../../services/groups.js'

export default class GroupParticipantsDemote extends BaseCommand {
static override args = {
group: Args.string({description: 'Group identifier.', required: true}),
}

static override description = 'Demote group admins to regular participants.'

static override examples = [
'<%= config.bin %> group participants demote 750000000009 --session session_123 --participant 750000000001',
]

static override flags = {
'idempotency-key': Flags.string({
description: 'Safely identify retries of this change.',
}),
participant: Flags.string({
description: 'Polymorfa user ID. Repeat for several participants.',
multiple: true,
required: true,
}),
session: Flags.string({
description: 'Messaging session ID.',
required: true,
}),
}

async run(): Promise<void> {
const {args, flags} = await this.parse(GroupParticipantsDemote)
const participants = participantIds(flags.participant)
const result = await changeGroupParticipants(
await this.messagingClient(flags, {serverApiKeyOnly: 'Groups'}),
flags.session,
args.group,
'demote',
participants,
idempotencyOptions(flags['idempotency-key']),
)
this.writeValue(
result,
`Demoted ${participants.length} participant(s) in group ${args.group}.`,
flags,
)
}
}
53 changes: 53 additions & 0 deletions src/commands/group/participants/promote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {Args, Flags} from '@oclif/core'

import {BaseCommand} from '../../../base-command.js'
import {idempotencyOptions} from '../../../input/message-send.js'
import {
changeGroupParticipants,
participantIds,
} from '../../../services/groups.js'

export default class GroupParticipantsPromote extends BaseCommand {
static override args = {
group: Args.string({description: 'Group identifier.', required: true}),
}

static override description = 'Promote participants to group admins.'

static override examples = [
'<%= config.bin %> group participants promote 750000000009 --session session_123 --participant 750000000001',
]

static override flags = {
'idempotency-key': Flags.string({
description: 'Safely identify retries of this change.',
}),
participant: Flags.string({
description: 'Polymorfa user ID. Repeat for several participants.',
multiple: true,
required: true,
}),
session: Flags.string({
description: 'Messaging session ID.',
required: true,
}),
}

async run(): Promise<void> {
const {args, flags} = await this.parse(GroupParticipantsPromote)
const participants = participantIds(flags.participant)
const result = await changeGroupParticipants(
await this.messagingClient(flags, {serverApiKeyOnly: 'Groups'}),
flags.session,
args.group,
'promote',
participants,
idempotencyOptions(flags['idempotency-key']),
)
this.writeValue(
result,
`Promoted ${participants.length} participant(s) in group ${args.group}.`,
flags,
)
}
}
Loading
Loading