@pachca/cli 2026.8.3 → 2026.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +14 -2
- package/dist/client.js +23 -8
- package/dist/commands/chats/list.js +2 -2
- package/dist/commands/chats/request-export.js +7 -8
- package/dist/commands/drafts/create.js +98 -0
- package/dist/commands/drafts/delete.js +45 -0
- package/dist/commands/drafts/get.js +30 -0
- package/dist/commands/drafts/list.js +107 -0
- package/dist/commands/drafts/update.js +66 -0
- package/dist/commands/files/get.js +43 -0
- package/dist/commands/group-tags/list.js +1 -1
- package/dist/commands/members/leave.js +1 -1
- package/dist/commands/messages/create.js +3 -3
- package/dist/commands/profile/update-status.js +1 -1
- package/dist/commands/reactions/add.js +2 -2
- package/dist/commands/reactions/remove.js +2 -2
- package/dist/commands/search/list-chats.js +1 -0
- package/dist/commands/security/list.js +3 -3
- package/dist/commands/tasks/create.js +1 -1
- package/dist/commands/tasks/update.js +2 -2
- package/dist/commands/threads/list.js +2 -2
- package/dist/commands/users/update-status.js +1 -1
- package/dist/commands/views/submit-response.js +77 -0
- package/dist/data/alternatives.json +9 -2
- package/dist/data/changelog.json +61 -0
- package/dist/data/endpoints.json +1 -1
- package/dist/data/workflows.json +33 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026.9.1 (16 сентября 2026)
|
|
4
|
+
|
|
5
|
+
- **Изменено**: Повтор после `429` ждёт не меньше срока из `Retry-After`: прежний потолок в 60 секунд убран. Пауза дольше минуты не повторяется — команда сразу возвращает ошибку с заголовком, потому что повтор внутри паузы её удваивает
|
|
6
|
+
- **Исправлено** (api): `api <метод> --spec` и `--docs` показывают развёрнутые описания методов: кого метод пускает, что меняется в пространстве после вызова и чем он отличается от соседнего
|
|
7
|
+
- **Добавлено** (drafts): Новая группа команд — черновики и отложенные сообщения: `create`, `get`, `list`, `update`, `delete`. Отложенное задаётся флагом `--schedule`, отменяется удалением. Токенам ботов недоступны
|
|
8
|
+
- **Добавлено** (messages get): У вложений появилось поле `duration_ms` — длительность голосового, аудио- и видеофайла, и тип `video`
|
|
9
|
+
- **Исправлено** (chats request-export): Флаг `--webhook-url` больше не обязателен: без него о готовности узнает только заказавший, из письма и уведомления
|
|
10
|
+
- **Добавлено** (files get): Новая команда — скачивание вложения, у которого нет прямой ссылки: зашифрованного или конфиденциального. Файл сохраняется по пути из обязательного флага `--save`. Остальные вложения забираются по `url` из сообщения
|
|
11
|
+
|
|
12
|
+
## 2026.9.0 (9 сентября 2026)
|
|
13
|
+
|
|
14
|
+
- **Добавлено** (views submit-response): Ответ на отправку формы для бота без адреса исходящего вебхука: `view_id` и `submit_id` берутся из события `view_submit` в журнале событий
|
|
15
|
+
- **Добавлено** (security list): Флаг `--event-key` принимает значение `exchange_disabled` — отключена интеграция с Exchange
|
|
16
|
+
- **Исправлено** (api): `api POST /messages --spec` и `api PUT /messages/{id} --spec`: у прикрепляемых файлов показаны пределы длины — `key` до 1000 символов и `name` до 255
|
|
17
|
+
- **Исправлено** (api): `api POST /views/open --spec` и `--docs`: у блока `time` начальное значение принимается и в формате `HH:mm:ss`, секунды отбрасываются
|
|
18
|
+
|
|
3
19
|
## 2026.8.3 (28 августа 2026)
|
|
4
20
|
|
|
5
21
|
- **Добавлено** (security list): Флаг `--event-key` принимает значение `user_2fa_disabled` — сотрудник отключил себе двухфакторную аутентификацию
|
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ pachca guide # список всех сценариев
|
|
|
110
110
|
|---------|---------|
|
|
111
111
|
| `pachca members add-group-tags` | Добавление тегов |
|
|
112
112
|
| `pachca members remove-group-tag` | Исключение тега |
|
|
113
|
-
| `pachca members leave` | Выход из
|
|
113
|
+
| `pachca members leave` | Выход из чата |
|
|
114
114
|
| `pachca members list` | Список участников чата |
|
|
115
115
|
| `pachca members add` | Добавление пользователей |
|
|
116
116
|
| `pachca members remove` | Исключение пользователя |
|
|
@@ -127,14 +127,25 @@ pachca guide # список всех сценариев
|
|
|
127
127
|
| Команда | Описание |
|
|
128
128
|
|---------|---------|
|
|
129
129
|
| `pachca files direct-url` | Загрузка файла |
|
|
130
|
+
| `pachca files get` | Скачивание файла |
|
|
130
131
|
| `pachca files uploads` | Получение подписи, ключа и других параметров |
|
|
131
132
|
|
|
133
|
+
### drafts
|
|
134
|
+
|
|
135
|
+
| Команда | Описание |
|
|
136
|
+
|---------|---------|
|
|
137
|
+
| `pachca drafts create` | Новый черновик |
|
|
138
|
+
| `pachca drafts list` | Список черновиков |
|
|
139
|
+
| `pachca drafts get` | Информация о черновике |
|
|
140
|
+
| `pachca drafts update` | Редактирование черновика |
|
|
141
|
+
| `pachca drafts delete` | Удаление черновика |
|
|
142
|
+
|
|
132
143
|
### group-tags
|
|
133
144
|
|
|
134
145
|
| Команда | Описание |
|
|
135
146
|
|---------|---------|
|
|
136
147
|
| `pachca group-tags create` | Новый тег |
|
|
137
|
-
| `pachca group-tags list` | Список тегов
|
|
148
|
+
| `pachca group-tags list` | Список тегов |
|
|
138
149
|
| `pachca group-tags get` | Информация о теге |
|
|
139
150
|
| `pachca group-tags update` | Редактирование тега |
|
|
140
151
|
| `pachca group-tags delete` | Удаление тега |
|
|
@@ -231,6 +242,7 @@ pachca guide # список всех сценариев
|
|
|
231
242
|
| Команда | Описание |
|
|
232
243
|
|---------|---------|
|
|
233
244
|
| `pachca views open` | Открытие представления |
|
|
245
|
+
| `pachca views submit-response` | Ответ на отправку формы |
|
|
234
246
|
|
|
235
247
|
<!-- AUTO:COMMANDS:END -->
|
|
236
248
|
|
package/dist/client.js
CHANGED
|
@@ -84,12 +84,12 @@ const MAX_RETRY_AFTER_MS = 60_000;
|
|
|
84
84
|
*/
|
|
85
85
|
function parseRetryAfter(header) {
|
|
86
86
|
if (!header)
|
|
87
|
-
return
|
|
87
|
+
return null;
|
|
88
88
|
const seconds = Number.parseInt(header, 10);
|
|
89
89
|
const ms = Number.isNaN(seconds) ? Date.parse(header) - Date.now() : seconds * 1000;
|
|
90
90
|
if (Number.isNaN(ms))
|
|
91
|
-
return
|
|
92
|
-
return Math.
|
|
91
|
+
return null;
|
|
92
|
+
return Math.max(ms, 0);
|
|
93
93
|
}
|
|
94
94
|
export async function request(opts, clientFlags) {
|
|
95
95
|
const url = buildUrl(opts.path, opts.query);
|
|
@@ -160,12 +160,22 @@ export async function request(opts, clientFlags) {
|
|
|
160
160
|
// Handle rate limiting (429). Safe for any method: a throttled request
|
|
161
161
|
// was rejected before it was processed, so it cannot have been committed.
|
|
162
162
|
if (response.status === 429 && !noRetry && attempt < MAX_RETRIES) {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
const retryAfterMs = parseRetryAfter(response.headers.get('retry-after'));
|
|
164
|
+
// Дольше минуты ждут только суточный предел чата и часовой бан. Раньше мы
|
|
165
|
+
// зажимали ожидание минутой и повторяли — а повтор внутри паузы её удваивает,
|
|
166
|
+
// так что три попытки превращали час в восемь. Теперь такой ответ уходит
|
|
167
|
+
// вызывающему как есть: в нём Retry-After, и когда вернуться, решает он.
|
|
168
|
+
const waitsTooLong = retryAfterMs !== null && retryAfterMs > MAX_RETRY_AFTER_MS;
|
|
169
|
+
if (!waitsTooLong) {
|
|
170
|
+
// Retry-After — это минимум ожидания, а не оценка: ждать меньше нельзя.
|
|
171
|
+
const base = retryAfterMs ?? 1000;
|
|
172
|
+
const waitMs = Math.round(base * (1 + Math.random() * 0.25));
|
|
173
|
+
if (clientFlags?.verbose) {
|
|
174
|
+
process.stderr.write(`${ansis.dim(`⏳ Rate limited, retrying in ${waitMs}ms...`)}\n`);
|
|
175
|
+
}
|
|
176
|
+
await sleep(waitMs);
|
|
177
|
+
continue;
|
|
166
178
|
}
|
|
167
|
-
await sleep(waitMs);
|
|
168
|
-
continue;
|
|
169
179
|
}
|
|
170
180
|
// Handle 503 with backoff. Unlike 429, a 503 from a proxy or a draining
|
|
171
181
|
// load balancer says nothing about whether the backend already committed
|
|
@@ -185,6 +195,11 @@ export async function request(opts, clientFlags) {
|
|
|
185
195
|
if (response.status === 204 || response.headers.get('content-length') === '0') {
|
|
186
196
|
data = null;
|
|
187
197
|
}
|
|
198
|
+
else if (opts.isBinary && response.ok) {
|
|
199
|
+
// Тело файла нельзя читать как текст: любой байт вне UTF-8 портится
|
|
200
|
+
// при декодировании, и сохранённый файл оказывается битым.
|
|
201
|
+
data = Buffer.from(await response.arrayBuffer());
|
|
202
|
+
}
|
|
188
203
|
else if (contentType.includes('application/json')) {
|
|
189
204
|
data = await response.json();
|
|
190
205
|
}
|
|
@@ -36,10 +36,10 @@ export default class ChatsList extends BaseCommand {
|
|
|
36
36
|
allowNo: true,
|
|
37
37
|
}),
|
|
38
38
|
'last-message-at-after': Flags.string({
|
|
39
|
-
description: "Фильтрация по времени создания последнего
|
|
39
|
+
description: "Фильтрация по времени создания последнего сообщения: вернутся чаты, где оно позже указанного. Значение в ISO-8601, смещение вида `+03:00` учитывается, время без смещения считается UTC.",
|
|
40
40
|
}),
|
|
41
41
|
'last-message-at-before': Flags.string({
|
|
42
|
-
description: "Фильтрация по времени создания последнего
|
|
42
|
+
description: "Фильтрация по времени создания последнего сообщения: вернутся чаты, где оно раньше указанного. Значение в ISO-8601, смещение вида `+03:00` учитывается, время без смещения считается UTC.",
|
|
43
43
|
}),
|
|
44
44
|
'personal': Flags.boolean({
|
|
45
45
|
description: "Фильтрация по личным и групповым чатам. Если параметр не указан, возвращаются любые чаты.",
|
|
@@ -12,18 +12,18 @@ export default class ChatsRequestExport extends BaseCommand {
|
|
|
12
12
|
static plan = "corporation";
|
|
13
13
|
static apiMethod = "POST";
|
|
14
14
|
static apiPath = "/chats/exports";
|
|
15
|
-
static requiredFlags = ["start-at", "end-at"
|
|
15
|
+
static requiredFlags = ["start-at", "end-at"];
|
|
16
16
|
static args = {};
|
|
17
17
|
static flags = {
|
|
18
18
|
...BaseCommand.baseFlags,
|
|
19
19
|
'start-at': Flags.string({
|
|
20
|
-
description: "Дата начала для экспорта
|
|
20
|
+
description: "Дата начала для экспорта в формате YYYY-MM-DD. Если прислать дату со временем, время молча отбрасывается — экспорт всегда идёт по календарным дням.",
|
|
21
21
|
}),
|
|
22
22
|
'end-at': Flags.string({
|
|
23
|
-
description: "Дата окончания для экспорта
|
|
23
|
+
description: "Дата окончания для экспорта в формате YYYY-MM-DD, включительно. Если прислать дату со временем, время молча отбрасывается.",
|
|
24
24
|
}),
|
|
25
25
|
'webhook-url': Flags.string({
|
|
26
|
-
description: "Адрес, на который
|
|
26
|
+
description: "Адрес, на который придёт вебхук с номером готовой выгрузки. Без него узнать номер неоткуда: заказавший получит письмо и уведомление, а вызывающая программа — ничего.",
|
|
27
27
|
}),
|
|
28
28
|
'chat-ids': Flags.string({
|
|
29
29
|
description: "Массив идентификаторов чатов. Указывается, если нужно получить сообщения только некоторых чатов.",
|
|
@@ -37,9 +37,8 @@ export default class ChatsRequestExport extends BaseCommand {
|
|
|
37
37
|
const { args, flags } = await this.parse(ChatsRequestExport);
|
|
38
38
|
this.parsedFlags = flags;
|
|
39
39
|
const missingRequired = [
|
|
40
|
-
{ flag: 'start-at', label: "Дата начала для экспорта
|
|
41
|
-
{ flag: 'end-at', label: "Дата окончания для экспорта
|
|
42
|
-
{ flag: 'webhook-url', label: "Адрес, на который будет отправлен вебхук по завершению экспорта", type: 'string' },
|
|
40
|
+
{ flag: 'start-at', label: "Дата начала для экспорта в формате YYYY-MM-DD. Если прислать дату со временем, время молча отбрасывается — экспорт всегда идёт по календарным дням.", type: 'string' },
|
|
41
|
+
{ flag: 'end-at', label: "Дата окончания для экспорта в формате YYYY-MM-DD, включительно. Если прислать дату со временем, время молча отбрасывается.", type: 'string' },
|
|
43
42
|
].filter((f) => flags[f.flag] === undefined || flags[f.flag] === null);
|
|
44
43
|
if (missingRequired.length > 0) {
|
|
45
44
|
if (this.isInteractive()) {
|
|
@@ -61,7 +60,7 @@ export default class ChatsRequestExport extends BaseCommand {
|
|
|
61
60
|
}
|
|
62
61
|
}
|
|
63
62
|
else {
|
|
64
|
-
this.validationError(missingRequired.map((f) => ({ message: `Обязательный флаг --${f.flag} не передан`, flag: f.flag })), { hint: "Обязательные: --start-at <string>, --end-at <string
|
|
63
|
+
this.validationError(missingRequired.map((f) => ({ message: `Обязательный флаг --${f.flag} не передан`, flag: f.flag })), { hint: "Обязательные: --start-at <string>, --end-at <string>. pachca introspect chats request-export" });
|
|
65
64
|
}
|
|
66
65
|
}
|
|
67
66
|
const body = {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Auto-generated from openapi.yaml — DO NOT EDIT
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { BaseCommand } from '../../base-command.js';
|
|
4
|
+
import * as clack from '@clack/prompts';
|
|
5
|
+
export default class DraftsCreate extends BaseCommand {
|
|
6
|
+
static description = "Новый черновик";
|
|
7
|
+
static examples = [
|
|
8
|
+
"Отправить сообщение позже или по расписанию — Создай отложенное сообщение: `schedule.start_date` — когда отправить, `schedule.repetition.interval` — как повторять (`once`, если один раз):\n $ pachca drafts create"
|
|
9
|
+
];
|
|
10
|
+
static scope = "drafts:write";
|
|
11
|
+
static apiMethod = "POST";
|
|
12
|
+
static apiPath = "/drafts";
|
|
13
|
+
static defaultColumns = ["id", "content", "created_at", "entity_type", "entity_id"];
|
|
14
|
+
static requiredFlags = ["entity-type", "entity-id"];
|
|
15
|
+
static args = {};
|
|
16
|
+
static flags = {
|
|
17
|
+
...BaseCommand.baseFlags,
|
|
18
|
+
'entity-type': Flags.string({
|
|
19
|
+
description: "Куда пишется черновик: в беседу или канал, в тред либо в личную переписку с сотрудником",
|
|
20
|
+
}),
|
|
21
|
+
'entity-id': Flags.integer({
|
|
22
|
+
description: "Идентификатор того, что названо в `entity_type`: беседы или канала, треда либо сотрудника (pachca chats list | pachca users list)",
|
|
23
|
+
}),
|
|
24
|
+
'content': Flags.string({
|
|
25
|
+
description: "Текст черновика. Можно не передавать, если есть вложения, но пустым черновик быть не может.",
|
|
26
|
+
}),
|
|
27
|
+
'parent-message-id': Flags.integer({
|
|
28
|
+
description: "Идентификатор сообщения, ответом на которое уйдёт черновик. Сообщение должно быть в том же чате.",
|
|
29
|
+
}),
|
|
30
|
+
'files': Flags.string({
|
|
31
|
+
description: "Прикрепляемые файлы, не больше десяти",
|
|
32
|
+
}),
|
|
33
|
+
'schedule': Flags.string({
|
|
34
|
+
description: "Расписание отправки. С ним создаётся отложенное сообщение, без него — обычный черновик.",
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
async run() {
|
|
38
|
+
const { args, flags } = await this.parse(DraftsCreate);
|
|
39
|
+
this.parsedFlags = flags;
|
|
40
|
+
// Read from stdin if --content not provided and stdin is not TTY
|
|
41
|
+
if (!flags['content'] && !process.stdin.isTTY) {
|
|
42
|
+
const chunks = [];
|
|
43
|
+
for await (const chunk of process.stdin) {
|
|
44
|
+
chunks.push(chunk);
|
|
45
|
+
}
|
|
46
|
+
flags['content'] = Buffer.concat(chunks).toString('utf-8').trimEnd();
|
|
47
|
+
}
|
|
48
|
+
const missingRequired = [
|
|
49
|
+
{ flag: 'entity-type', label: "Куда пишется черновик: в беседу или канал, в тред либо в личную переписку с сотрудником", type: 'string' },
|
|
50
|
+
{ flag: 'entity-id', label: "Идентификатор того, что названо в `entity_type`: беседы или канала, треда либо сотрудника", type: 'integer' },
|
|
51
|
+
].filter((f) => flags[f.flag] === undefined || flags[f.flag] === null);
|
|
52
|
+
if (missingRequired.length > 0) {
|
|
53
|
+
if (this.isInteractive()) {
|
|
54
|
+
for (const field of missingRequired) {
|
|
55
|
+
const value = await clack.text({ message: field.label, validate: (v) => v.length === 0 ? 'Обязательное поле' : undefined });
|
|
56
|
+
if (clack.isCancel(value)) {
|
|
57
|
+
process.stderr.write('Отменено.\n');
|
|
58
|
+
this.exit(0);
|
|
59
|
+
}
|
|
60
|
+
if (field.type === 'integer') {
|
|
61
|
+
flags[field.flag] = Number.parseInt(value, 10);
|
|
62
|
+
}
|
|
63
|
+
else if (field.type === 'boolean') {
|
|
64
|
+
flags[field.flag] = value === 'true';
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
flags[field.flag] = value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
this.validationError(missingRequired.map((f) => ({ message: `Обязательный флаг --${f.flag} не передан`, flag: f.flag })), { hint: "Обязательные: --entity-type <string>, --entity-id <integer>. pachca introspect drafts create" });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const body = { draft: {
|
|
76
|
+
entity_type: flags['entity-type'],
|
|
77
|
+
entity_id: flags['entity-id'],
|
|
78
|
+
content: flags['content'],
|
|
79
|
+
parent_message_id: flags['parent-message-id'],
|
|
80
|
+
files: flags['files'] ? this.parseJSON(flags['files'], 'files') : undefined,
|
|
81
|
+
schedule: flags['schedule'] ? this.parseJSON(flags['schedule'], 'schedule') : undefined,
|
|
82
|
+
} };
|
|
83
|
+
// Clean undefined fields
|
|
84
|
+
const inner = body['draft'];
|
|
85
|
+
for (const [k, v] of Object.entries(inner)) {
|
|
86
|
+
if (v === undefined)
|
|
87
|
+
delete inner[k];
|
|
88
|
+
}
|
|
89
|
+
const { data } = await this.apiRequest({
|
|
90
|
+
method: 'POST',
|
|
91
|
+
path: '/drafts',
|
|
92
|
+
body,
|
|
93
|
+
});
|
|
94
|
+
const responseBody = (data ?? {});
|
|
95
|
+
const result = responseBody.data ?? responseBody;
|
|
96
|
+
this.output(result);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Auto-generated from openapi.yaml — DO NOT EDIT
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import { BaseCommand } from '../../base-command.js';
|
|
4
|
+
import * as clack from '@clack/prompts';
|
|
5
|
+
export default class DraftsDelete extends BaseCommand {
|
|
6
|
+
static description = "Удаление черновика";
|
|
7
|
+
static examples = [
|
|
8
|
+
"Отправить сообщение позже или по расписанию — Чтобы отменить отправку — удали отложенное сообщение:\n $ pachca drafts delete"
|
|
9
|
+
];
|
|
10
|
+
static scope = "drafts:write";
|
|
11
|
+
static apiMethod = "DELETE";
|
|
12
|
+
static apiPath = "/drafts/{id}";
|
|
13
|
+
static args = {
|
|
14
|
+
id: Args.integer({
|
|
15
|
+
description: "Идентификатор черновика",
|
|
16
|
+
required: true,
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
static flags = {
|
|
20
|
+
...BaseCommand.baseFlags,
|
|
21
|
+
force: Flags.boolean({
|
|
22
|
+
description: 'Пропустить подтверждение',
|
|
23
|
+
default: false,
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
async run() {
|
|
27
|
+
const { args, flags } = await this.parse(DraftsDelete);
|
|
28
|
+
this.parsedFlags = flags;
|
|
29
|
+
if (!flags.force) {
|
|
30
|
+
if (!this.isInteractive()) {
|
|
31
|
+
this.validationError([{ message: 'Деструктивная операция требует флага --force', flag: 'force' }], { type: 'PACHCA_DESTRUCTIVE_OP_ERROR', hint: "pachca drafts delete <id> --force" });
|
|
32
|
+
}
|
|
33
|
+
const confirm = await clack.confirm({ message: 'Вы уверены?' });
|
|
34
|
+
if (clack.isCancel(confirm) || !confirm) {
|
|
35
|
+
process.stderr.write('Отменено.\n');
|
|
36
|
+
this.exit(0);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const { data } = await this.apiRequest({
|
|
40
|
+
method: 'DELETE',
|
|
41
|
+
path: `/drafts/${args.id}`,
|
|
42
|
+
});
|
|
43
|
+
this.success('Удалено');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Auto-generated from openapi.yaml — DO NOT EDIT
|
|
2
|
+
import { Args } from '@oclif/core';
|
|
3
|
+
import { BaseCommand } from '../../base-command.js';
|
|
4
|
+
export default class DraftsGet extends BaseCommand {
|
|
5
|
+
static description = "Информация о черновике";
|
|
6
|
+
static scope = "drafts:read";
|
|
7
|
+
static apiMethod = "GET";
|
|
8
|
+
static apiPath = "/drafts/{id}";
|
|
9
|
+
static defaultColumns = ["id", "content", "created_at", "entity_type", "entity_id"];
|
|
10
|
+
static args = {
|
|
11
|
+
id: Args.integer({
|
|
12
|
+
description: "Идентификатор черновика",
|
|
13
|
+
required: true,
|
|
14
|
+
}),
|
|
15
|
+
};
|
|
16
|
+
static flags = {
|
|
17
|
+
...BaseCommand.baseFlags,
|
|
18
|
+
};
|
|
19
|
+
async run() {
|
|
20
|
+
const { args, flags } = await this.parse(DraftsGet);
|
|
21
|
+
this.parsedFlags = flags;
|
|
22
|
+
const { data } = await this.apiRequest({
|
|
23
|
+
method: 'GET',
|
|
24
|
+
path: `/drafts/${args.id}`,
|
|
25
|
+
});
|
|
26
|
+
const responseBody = (data ?? {});
|
|
27
|
+
const result = responseBody.data ?? responseBody;
|
|
28
|
+
this.output(result);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Auto-generated from openapi.yaml — DO NOT EDIT
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { BaseCommand } from '../../base-command.js';
|
|
4
|
+
export default class DraftsList extends BaseCommand {
|
|
5
|
+
static description = "Список черновиков";
|
|
6
|
+
static scope = "drafts:read";
|
|
7
|
+
static apiMethod = "GET";
|
|
8
|
+
static apiPath = "/drafts";
|
|
9
|
+
static defaultColumns = ["id", "content", "created_at", "entity_type", "entity_id"];
|
|
10
|
+
static args = {};
|
|
11
|
+
static flags = {
|
|
12
|
+
...BaseCommand.baseFlags,
|
|
13
|
+
'type': Flags.string({
|
|
14
|
+
description: "Что возвращать: `regular` — обычные черновики, `scheduled` — отложенные сообщения, `all` — и те и другие",
|
|
15
|
+
options: ["regular", "scheduled", "all"],
|
|
16
|
+
default: "regular",
|
|
17
|
+
}),
|
|
18
|
+
'entity-type': Flags.string({
|
|
19
|
+
description: "Тип чата для фильтра по одному чату. Указывается вместе с `entity_id`: если прислать только один из двух, ответ будет `422`.",
|
|
20
|
+
options: ["discussion", "thread", "user"],
|
|
21
|
+
}),
|
|
22
|
+
'entity-id': Flags.integer({
|
|
23
|
+
description: "Идентификатор того, что названо в `entity_type`. Указывается вместе с ним.",
|
|
24
|
+
}),
|
|
25
|
+
limit: Flags.integer({
|
|
26
|
+
description: 'Количество результатов на страницу',
|
|
27
|
+
}),
|
|
28
|
+
cursor: Flags.string({
|
|
29
|
+
description: 'Курсор для следующей страницы',
|
|
30
|
+
}),
|
|
31
|
+
all: Flags.boolean({
|
|
32
|
+
description: 'Загрузить все страницы автоматически',
|
|
33
|
+
default: false,
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
async run() {
|
|
37
|
+
const { args, flags } = await this.parse(DraftsList);
|
|
38
|
+
this.parsedFlags = flags;
|
|
39
|
+
if (flags.all) {
|
|
40
|
+
// Auto-pagination
|
|
41
|
+
const allData = [];
|
|
42
|
+
let nextCursor = undefined;
|
|
43
|
+
let pages = 0;
|
|
44
|
+
const seenCursors = new Set();
|
|
45
|
+
while (pages < 500) {
|
|
46
|
+
const query = {
|
|
47
|
+
type: flags['type'],
|
|
48
|
+
'entity_type': flags['entity-type'],
|
|
49
|
+
'entity_id': flags['entity-id'],
|
|
50
|
+
limit: flags.limit,
|
|
51
|
+
cursor: nextCursor,
|
|
52
|
+
};
|
|
53
|
+
const response = await this.apiRequest({ method: 'GET', path: '/drafts', query });
|
|
54
|
+
const body = response.data;
|
|
55
|
+
const items = body.data;
|
|
56
|
+
if (items)
|
|
57
|
+
allData.push(...items);
|
|
58
|
+
const meta = body.meta;
|
|
59
|
+
const paginate = meta?.paginate;
|
|
60
|
+
nextCursor = paginate?.next_page;
|
|
61
|
+
pages++;
|
|
62
|
+
// Условие конца: списочные методы — has_next === false; методы поиска и /users?query= (без has_next) — пустой data
|
|
63
|
+
const hasNext = paginate?.has_next;
|
|
64
|
+
if (typeof hasNext === 'boolean') {
|
|
65
|
+
if (!hasNext)
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
else if (!items || items.length === 0) {
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
if (process.stderr.isTTY) {
|
|
72
|
+
const total = paginate?.total;
|
|
73
|
+
const progress = total ? `${allData.length} / ${total}` : String(allData.length);
|
|
74
|
+
process.stderr.write(`\r Загружено: ${progress}...`);
|
|
75
|
+
}
|
|
76
|
+
if (!nextCursor)
|
|
77
|
+
break;
|
|
78
|
+
if (seenCursors.has(nextCursor)) {
|
|
79
|
+
process.stderr.write('\n⚠ Обнаружен цикл пагинации, остановка.\n');
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
seenCursors.add(nextCursor);
|
|
83
|
+
}
|
|
84
|
+
if (pages >= 500) {
|
|
85
|
+
process.stderr.write('\n⚠ Достигнут лимит 500 страниц.\n');
|
|
86
|
+
}
|
|
87
|
+
if (process.stderr.isTTY)
|
|
88
|
+
process.stderr.write('\n');
|
|
89
|
+
this.output(allData);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const { data } = await this.apiRequest({
|
|
93
|
+
method: 'GET',
|
|
94
|
+
path: '/drafts',
|
|
95
|
+
query: {
|
|
96
|
+
type: flags['type'],
|
|
97
|
+
'entity_type': flags['entity-type'],
|
|
98
|
+
'entity_id': flags['entity-id'],
|
|
99
|
+
limit: flags.limit,
|
|
100
|
+
cursor: flags.cursor,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
const responseBody = (data ?? {});
|
|
104
|
+
const items = responseBody.data ?? responseBody;
|
|
105
|
+
this.output(items);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Auto-generated from openapi.yaml — DO NOT EDIT
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import { BaseCommand } from '../../base-command.js';
|
|
4
|
+
export default class DraftsUpdate extends BaseCommand {
|
|
5
|
+
static description = "Редактирование черновика";
|
|
6
|
+
static scope = "drafts:write";
|
|
7
|
+
static apiMethod = "PUT";
|
|
8
|
+
static apiPath = "/drafts/{id}";
|
|
9
|
+
static defaultColumns = ["id", "content", "created_at", "entity_type", "entity_id"];
|
|
10
|
+
static args = {
|
|
11
|
+
id: Args.integer({
|
|
12
|
+
description: "Идентификатор черновика",
|
|
13
|
+
required: true,
|
|
14
|
+
}),
|
|
15
|
+
};
|
|
16
|
+
static flags = {
|
|
17
|
+
...BaseCommand.baseFlags,
|
|
18
|
+
'content': Flags.string({
|
|
19
|
+
description: "Текст черновика",
|
|
20
|
+
}),
|
|
21
|
+
'parent-message-id': Flags.integer({
|
|
22
|
+
description: "Идентификатор сообщения, ответом на которое уйдёт черновик. `null` снимает ответ.",
|
|
23
|
+
}),
|
|
24
|
+
'files': Flags.string({
|
|
25
|
+
description: "Прикрепляемые файлы, не больше десяти. Переданный массив заменяет прежний состав: пустой снимает все вложения, а чтобы сохранить уже прикреплённый файл, передайте его с `id`. Без этого поля вложения остаются как были.",
|
|
26
|
+
}),
|
|
27
|
+
'schedule': Flags.string({
|
|
28
|
+
description: "Расписание отправки. У обычного черновика оно превращает его в отложенное сообщение. У отложенного расписание можно изменить, но не снять: `null` в ответ даст `422` с кодом `draft_type_change_forbidden`.",
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
async run() {
|
|
32
|
+
const { args, flags } = await this.parse(DraftsUpdate);
|
|
33
|
+
this.parsedFlags = flags;
|
|
34
|
+
// Read from stdin if --content not provided and stdin is not TTY
|
|
35
|
+
if (!flags['content'] && !process.stdin.isTTY) {
|
|
36
|
+
const chunks = [];
|
|
37
|
+
for await (const chunk of process.stdin) {
|
|
38
|
+
chunks.push(chunk);
|
|
39
|
+
}
|
|
40
|
+
flags['content'] = Buffer.concat(chunks).toString('utf-8').trimEnd();
|
|
41
|
+
}
|
|
42
|
+
const body = { draft: {
|
|
43
|
+
content: flags['content'],
|
|
44
|
+
parent_message_id: flags['parent-message-id'],
|
|
45
|
+
files: flags['files'] ? this.parseJSON(flags['files'], 'files') : undefined,
|
|
46
|
+
schedule: flags['schedule'] ? this.parseJSON(flags['schedule'], 'schedule') : undefined,
|
|
47
|
+
} };
|
|
48
|
+
// Clean undefined fields
|
|
49
|
+
const inner = body['draft'];
|
|
50
|
+
for (const [k, v] of Object.entries(inner)) {
|
|
51
|
+
if (v === undefined)
|
|
52
|
+
delete inner[k];
|
|
53
|
+
}
|
|
54
|
+
if (Object.keys(inner).length === 0) {
|
|
55
|
+
this.validationError([{ message: 'Не указаны поля для обновления' }], { type: 'PACHCA_USAGE_ERROR' });
|
|
56
|
+
}
|
|
57
|
+
const { data } = await this.apiRequest({
|
|
58
|
+
method: 'PUT',
|
|
59
|
+
path: `/drafts/${args.id}`,
|
|
60
|
+
body,
|
|
61
|
+
});
|
|
62
|
+
const responseBody = (data ?? {});
|
|
63
|
+
const result = responseBody.data ?? responseBody;
|
|
64
|
+
this.output(result);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Auto-generated from openapi.yaml — DO NOT EDIT
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import { BaseCommand } from '../../base-command.js';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import { formatSize } from '../../utils.js';
|
|
6
|
+
export default class FilesGet extends BaseCommand {
|
|
7
|
+
static description = "Скачивание файла";
|
|
8
|
+
static scope = "files:read";
|
|
9
|
+
static apiMethod = "GET";
|
|
10
|
+
static apiPath = "/files/{id}";
|
|
11
|
+
static args = {
|
|
12
|
+
id: Args.integer({
|
|
13
|
+
description: "Идентификатор файла из поля `id` вложения",
|
|
14
|
+
required: true,
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
static flags = {
|
|
18
|
+
...BaseCommand.baseFlags,
|
|
19
|
+
'target': Flags.string({
|
|
20
|
+
description: "Что отдать вместо исходного файла: `pdf_preview` — документ, переведённый в PDF, `pdf_first_page` — первая страница картинкой, `thumb` — уменьшенная копия изображения, `image` — изображение как есть. Без параметра приходит исходный файл. Вариант, которого у этого файла нет, приводит к `400`.",
|
|
21
|
+
options: ["pdf_preview", "pdf_first_page", "thumb", "image"],
|
|
22
|
+
}),
|
|
23
|
+
save: Flags.string({
|
|
24
|
+
description: 'Путь для сохранения файла',
|
|
25
|
+
required: true,
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
async run() {
|
|
29
|
+
const { args, flags } = await this.parse(FilesGet);
|
|
30
|
+
this.parsedFlags = flags;
|
|
31
|
+
const { data } = await this.apiRequest({
|
|
32
|
+
method: 'GET',
|
|
33
|
+
path: `/files/${args.id}`,
|
|
34
|
+
query: {
|
|
35
|
+
target: flags['target'],
|
|
36
|
+
},
|
|
37
|
+
isBinary: true,
|
|
38
|
+
});
|
|
39
|
+
const fileBody = Buffer.isBuffer(data) ? data : Buffer.from(String(data ?? ''));
|
|
40
|
+
fs.writeFileSync(flags.save, fileBody);
|
|
41
|
+
this.success(`Сохранено: ${flags.save} (${formatSize(fileBody.length)})`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Flags } from '@oclif/core';
|
|
3
3
|
import { BaseCommand } from '../../base-command.js';
|
|
4
4
|
export default class GroupTagsList extends BaseCommand {
|
|
5
|
-
static description = "Список тегов
|
|
5
|
+
static description = "Список тегов";
|
|
6
6
|
static examples = [
|
|
7
7
|
"Получить всех сотрудников тега/департамента — Найди тег по названию, возьми `id`:\n $ pachca group-tags list"
|
|
8
8
|
];
|
|
@@ -3,7 +3,7 @@ import { Args, Flags } from '@oclif/core';
|
|
|
3
3
|
import { BaseCommand } from '../../base-command.js';
|
|
4
4
|
import * as clack from '@clack/prompts';
|
|
5
5
|
export default class MembersLeave extends BaseCommand {
|
|
6
|
-
static description = "Выход из
|
|
6
|
+
static description = "Выход из чата";
|
|
7
7
|
static examples = [
|
|
8
8
|
"Архивация и управление чатом — Покинуть чат:\n $ pachca members leave"
|
|
9
9
|
];
|
|
@@ -21,7 +21,7 @@ export default class MessagesCreate extends BaseCommand {
|
|
|
21
21
|
description: "Тип сущности. Регистр значения не учитывается. Значение вне списка возвращает `400` с кодом `invalid`.",
|
|
22
22
|
}),
|
|
23
23
|
'entity-id': Flags.integer({
|
|
24
|
-
description: "Идентификатор
|
|
24
|
+
description: "Идентификатор того, что названо в `entity_type`: беседы или канала, треда либо сотрудника. (pachca chats list | pachca users list)",
|
|
25
25
|
}),
|
|
26
26
|
'content': Flags.string({
|
|
27
27
|
description: "Текст сообщения. Поддерживает упоминания: `@nickname` или `<@user_id>` (будет автоматически преобразовано в `@nickname`).",
|
|
@@ -33,7 +33,7 @@ export default class MessagesCreate extends BaseCommand {
|
|
|
33
33
|
description: "Массив строк, каждая из которых представлена массивом кнопок. Максимум 100 кнопок у сообщения, до 8 кнопок в строке.",
|
|
34
34
|
}),
|
|
35
35
|
'parent-message-id': Flags.integer({
|
|
36
|
-
description: "Идентификатор
|
|
36
|
+
description: "Идентификатор сообщения, на которое отвечаете. Ответ появится прямо в ленте чата, а не в треде: для треда отправляйте сообщение с `entity_type: \"thread\"`.",
|
|
37
37
|
}),
|
|
38
38
|
'display-avatar-url': Flags.string({
|
|
39
39
|
description: "Ссылка на специальную аватарку отправителя для этого сообщения. Использование этого поля возможно только с access_token бота. (макс. 255 символов)",
|
|
@@ -62,7 +62,7 @@ export default class MessagesCreate extends BaseCommand {
|
|
|
62
62
|
flags['content'] = Buffer.concat(chunks).toString('utf-8').trimEnd();
|
|
63
63
|
}
|
|
64
64
|
const missingRequired = [
|
|
65
|
-
{ flag: 'entity-id', label: "Идентификатор
|
|
65
|
+
{ flag: 'entity-id', label: "Идентификатор того, что названо в `entity_type`: беседы или канала, треда либо сотрудника.", type: 'integer' },
|
|
66
66
|
{ flag: 'content', label: "Текст сообщения. Поддерживает упоминания: `@nickname` или `<@user_id>` (будет автоматически преобразовано в `@nickname`).", type: 'string' },
|
|
67
67
|
].filter((f) => flags[f.flag] === undefined || flags[f.flag] === null);
|
|
68
68
|
if (missingRequired.length > 0) {
|
|
@@ -22,7 +22,7 @@ export default class ProfileUpdateStatus extends BaseCommand {
|
|
|
22
22
|
description: "Текст статуса (макс. 50 символов)",
|
|
23
23
|
}),
|
|
24
24
|
'expires-at': Flags.string({
|
|
25
|
-
description: "Срок жизни статуса (ISO-8601
|
|
25
|
+
description: "Срок жизни статуса (ISO-8601). Смещение вида `+03:00` учитывается, время без смещения считается UTC. Сюда идёт срок, названный словами: «до трёх» это значение поля, а не часть подписи.",
|
|
26
26
|
}),
|
|
27
27
|
'is-away': Flags.boolean({
|
|
28
28
|
description: "Режим «Нет на месте»",
|