@pachca/cli 2026.10.4 → 2026.11.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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.11.1 (9 ноября 2026)
4
+
5
+ - **Изменено** (auth status): Показывает токен, с которым работают команды, в том числе из `--token` и `PACHCA_TOKEN`. Владельца, права и срок жизни такого токена команда спрашивает у сервера и называет сохранённый профиль, который не используется, пока задан `PACHCA_TOKEN`.
6
+ - **Изменено**: Подсказки к отказу `insufficient_scope` ведут к `pachca auth status` с тем же `--token` или `--profile`, что у команды, а для сохранённого профиля в `-o json` ещё и с `--remote`
7
+ - **Изменено** (auth login): Подсказка `pachca auth status` после входа называет профиль, если без него команда покажет другой токен, а при заданном `PACHCA_TOKEN` вход предупреждает, что команды используют переменную
8
+ - **Добавлено** (messages get, messages list, messages create, messages update, search list-messages): В ответах с сообщениями есть поля `source` и `application`: через что сотрудник отправил сообщение и какая программа
9
+ - **Изменено** (chats list, messages list): `--order` без `--sort` задаёт направление по `id`
10
+ - **Изменено** (bots create, bots update, bots update-webhook): В описании адреса вебхука коды отказа, а в описании шаблона проверка синтаксиса
11
+ - **Изменено** (users update): В описании чата для перевода в гости письмо о смене роли
12
+
13
+ ## 2026.11.0 (4 ноября 2026)
14
+
15
+ - **Изменено** (tasks create): Работает без `--kind` и не спрашивает тип в терминале: по умолчанию создаётся простое напоминание
16
+ - **Изменено** (reactions remove): Снимает реакцию по `--name` без `--code`
17
+ - **Добавлено** (profile update-status, users update-status): `--expires-at null` убирает срок статуса, а `--away-message null` убирает текст «Нет на месте»
18
+ - **Добавлено** (bots create, bots update, bots update-webhook, messages update, reactions add, users create, users update): Длина значений проверяется до запроса: `--outgoing-url`, `--challenge-key`, `--display-avatar-url`, `--display-name` и `--title` до 255 символов, а `--code` у `reactions add` до 16
19
+ - **Добавлено**: В выводе команд с сообщениями и черновиками есть поле `voice_content.transcript_status`: статус расшифровки голосового сообщения
20
+ - **Изменено** (api): Справка `--docs` и схема `--spec` показывают необязательными поля деталей журнала аудита, которых нет в записи без значения, а список скоупов без `files:write`, `webhooks:read` и `webhooks:write`
21
+ - **Изменено**: Справка команд следует уточнённым описаниям API: ник сотрудника и бота, пустой бот, события, команды и `--ignore-self-messages` бота, поиск сотрудников и ботов, длина поискового запроса, подпись сообщения бота, название чата, упоминания в черновике, текст «Нет на месте», смена почты, приостановка владельца, `--actor-id` у `pachca security list` и `--done-at` у `pachca tasks update`
22
+
3
23
  ## 2026.10.4 (31 октября 2026)
4
24
 
5
25
  - **Исправлено** (api): Справка `--docs` и схема `--spec` показывают `dlp_rule_id` в деталях события `dlp_violation_detected` строкой, как его отдаёт API
@@ -1,5 +1,5 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
- import { resolveToken, TokenNotFoundError, ProfileNotFoundError, getAuthMethod, getDefaults, profileFlag, } from './profiles.js';
2
+ import { resolveToken, TokenNotFoundError, ProfileNotFoundError, getAuthMethod, getDefaults, profileFlag, tokenSource, } from './profiles.js';
3
3
  import { ApiError, getExitCode, formatDryRun, request } from './client.js';
4
4
  import { outputData, outputError, outputSuccess } from './output.js';
5
5
  import { defaultOutputFormat, isInteractive } from './utils.js';
@@ -246,8 +246,21 @@ export class BaseCommand extends Command {
246
246
  reportScopeError(err) {
247
247
  const format = this.getOutputFormat();
248
248
  const scope = typeof err.details.scope === 'string' ? err.details.scope : undefined;
249
- if (format === 'json' || !process.stderr.isTTY) {
250
- outputError(err.details, format);
249
+ // `auth status` checks the token every command uses. A bare run finds PACHCA_TOKEN
250
+ // and the active profile by itself; a token or profile named on this command has
251
+ // to be named again. The token value is never echoed back.
252
+ const source = tokenSource({ token: this.parsedFlags.token, profile: this.parsedFlags.profile });
253
+ const human = format !== 'json' && process.stderr.isTTY;
254
+ const again = source.kind === 'flag'
255
+ ? human ? ' --token <токен>' : ' --token <token>'
256
+ : source.kind === 'profile' && source.via === '--profile'
257
+ ? ` --profile ${source.profileName}`
258
+ : '';
259
+ if (!human) {
260
+ // A saved profile keeps the scope list from login, which may still name the
261
+ // scope just refused: only the server's answer settles it.
262
+ const remote = source.kind === 'profile' ? ' --remote' : '';
263
+ outputError({ ...err.details, hint: `pachca auth status${remote}${again}` }, format);
251
264
  this.exit(3);
252
265
  }
253
266
  const resolved = this.tryResolveProfile();
@@ -255,7 +268,7 @@ export class BaseCommand extends Command {
255
268
  process.stderr.write(`✗ Не хватает права${scope ? ` ${scope}` : ''}.\n`);
256
269
  if (!profile) {
257
270
  process.stderr.write(` У токена ${scope ? 'этого' : 'нужного'} права нет.\n`);
258
- process.stderr.write(` Проверить права токена: pachca oauth token-info -o json\n`);
271
+ process.stderr.write(` Проверить токен и его права: pachca auth status${again}\n`);
259
272
  this.exit(3);
260
273
  }
261
274
  // The scope list in the profile is a copy taken at login. When it still names
@@ -265,7 +278,7 @@ export class BaseCommand extends Command {
265
278
  const listedLocally = !!scope && !!profile.scopes?.includes(scope);
266
279
  if (listedLocally) {
267
280
  process.stderr.write(` В сохранённом списке право есть, но у токена его уже нет: список устарел.\n`);
268
- process.stderr.write(` Обновить список: pachca auth status --remote${flag}\n`);
281
+ process.stderr.write(` Обновить список: pachca auth status --remote${again}\n`);
269
282
  }
270
283
  else {
271
284
  process.stderr.write(` У токена ${scope ? 'этого' : 'нужного'} права нет.\n`);
@@ -1,9 +1,10 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { BaseCommand } from '../../base-command.js';
3
- import { setProfile, setActiveProfile, getActiveProfile, getProfile, getSecretStorage, expiresAtFromNow, } from '../../profiles.js';
3
+ import { setProfile, setActiveProfile, getActiveProfile, getProfile, getSecretStorage, expiresAtFromNow, tokenSource, } from '../../profiles.js';
4
4
  import { isSecretStoreAvailable } from '../../secret-store.js';
5
5
  import { ApiError, request } from '../../client.js';
6
6
  import { outputError } from '../../output.js';
7
+ import { fetchIdentity } from '../../identity.js';
7
8
  import { OAuthError, copyToClipboard, deviceName, openBrowser, pollForToken, requestDeviceCode, } from '../../oauth.js';
8
9
  export default class AuthLogin extends BaseCommand {
9
10
  static description = 'Вход в аккаунт и сохранение профиля';
@@ -68,7 +69,7 @@ export default class AuthLogin extends BaseCommand {
68
69
  ? { access_token: flags.token }
69
70
  : await this.deviceLogin(profileName, !flags['no-browser'], flags.quiet);
70
71
  const tokenInfo = await this.verifyToken(grant.access_token);
71
- const identity = await this.fetchIdentity(grant.access_token, tokenInfo.user_id);
72
+ const identity = await fetchIdentity(grant.access_token, tokenInfo.user_id);
72
73
  const scopes = grant.scope ? grant.scope.split(' ').filter(Boolean) : tokenInfo.scopes;
73
74
  const profile = {
74
75
  type: identity.isBot ? 'bot' : 'user',
@@ -197,22 +198,6 @@ export default class AuthLogin extends BaseCommand {
197
198
  throw error;
198
199
  }
199
200
  }
200
- async fetchIdentity(token, userId) {
201
- try {
202
- const response = await request({ method: 'GET', path: '/profile', token }, { quiet: true });
203
- const data = response.data.data;
204
- const name = `${data.first_name || ''} ${data.last_name || ''}`.trim();
205
- return {
206
- name: name || `User #${userId}`,
207
- email: data.email || null,
208
- isBot: data.bot || false,
209
- };
210
- }
211
- catch {
212
- // profile:read scope not available — user_id from token info is enough
213
- return { name: `User #${userId}`, email: null, isBot: false };
214
- }
215
- }
216
201
  report(profileName, profile, scopes) {
217
202
  const format = this.getOutputFormat();
218
203
  if (format === 'json') {
@@ -243,7 +228,14 @@ export default class AuthLogin extends BaseCommand {
243
228
  else {
244
229
  process.stderr.write(` Токен в файле конфигурации (хранилище ключей недоступно)\n`);
245
230
  }
246
- process.stderr.write(` Состояние входа: pachca auth status\n`);
231
+ // A bare `auth status` shows the token other commands would use: PACHCA_TOKEN
232
+ // when it is set, or the active profile, which need not be the one just saved.
233
+ const src = tokenSource({});
234
+ const statusFlag = src.kind === 'profile' && src.profileName === profileName ? '' : ` --profile ${profileName}`;
235
+ process.stderr.write(` Состояние входа: pachca auth status${statusFlag}\n`);
236
+ if (src.kind === 'env') {
237
+ process.stderr.write(` Пока задан PACHCA_TOKEN, команды используют его, а не профиль ${profileName}\n`);
238
+ }
247
239
  }
248
240
  }
249
241
  /** Read a token piped into the command, so it never appears in the shell history. */
@@ -1,8 +1,9 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { BaseCommand } from '../../base-command.js';
3
- import { getActiveProfile, getAuthMethod, getProfile, getSecretStorage, updateProfileScopes, } from '../../profiles.js';
4
- import { request } from '../../client.js';
3
+ import { getActiveProfile, getAuthMethod, getProfile, getSecretStorage, listProfiles, tokenSource, updateProfileScopes, } from '../../profiles.js';
4
+ import { ApiError, request } from '../../client.js';
5
5
  import { outputError } from '../../output.js';
6
+ import { fetchIdentity } from '../../identity.js';
6
7
  /** Human-readable time left, or null when the token has no expiry at all. */
7
8
  function describeExpiry(profile) {
8
9
  if (getAuthMethod(profile) === 'token')
@@ -18,10 +19,11 @@ function describeExpiry(profile) {
18
19
  return minutes < 60 ? `осталось ${minutes} мин` : `осталось ${Math.round(minutes / 60)} ч`;
19
20
  }
20
21
  export default class AuthStatus extends BaseCommand {
21
- static description = 'Статус текущего профиля';
22
+ static description = 'Чей токен используют команды и какие у него права';
22
23
  static examples = [
23
24
  '<%= config.bin %> auth status',
24
25
  '<%= config.bin %> auth status -o json',
26
+ 'PACHCA_TOKEN=$TOKEN <%= config.bin %> auth status -o json',
25
27
  ];
26
28
  static flags = {
27
29
  ...BaseCommand.baseFlags,
@@ -33,12 +35,30 @@ export default class AuthStatus extends BaseCommand {
33
35
  async run() {
34
36
  const { flags } = await this.parse(AuthStatus);
35
37
  this.parsedFlags = flags;
36
- const profileName = flags.profile || process.env.PACHCA_PROFILE || getActiveProfile();
37
38
  const format = this.getOutputFormat();
38
- if (!profileName) {
39
- outputError({ error: 'No active profile. Run: pachca auth login', type: 'PACHCA_USAGE_ERROR', code: null }, format);
39
+ // The token every other command would use, picked in the same order:
40
+ // --token, --profile, PACHCA_TOKEN, PACHCA_PROFILE, the active profile.
41
+ // Agents and CI usually pass PACHCA_TOKEN with no profile at all, and this
42
+ // is the command they are sent to check the connection.
43
+ const source = tokenSource({ token: flags.token, profile: flags.profile });
44
+ if (source.kind === 'flag') {
45
+ await this.showPassedToken(flags.token, '--token');
46
+ return;
47
+ }
48
+ if (source.kind === 'env') {
49
+ await this.showPassedToken(process.env.PACHCA_TOKEN, 'PACHCA_TOKEN');
50
+ return;
51
+ }
52
+ if (source.kind === 'none') {
53
+ outputError({
54
+ error: 'No active profile. Run: pachca auth login',
55
+ type: 'PACHCA_USAGE_ERROR',
56
+ code: null,
57
+ hint: 'pachca auth login, or set PACHCA_TOKEN',
58
+ }, format);
40
59
  this.exit(2);
41
60
  }
61
+ const profileName = source.kind === 'profile' ? source.profileName : undefined;
42
62
  const profile = getProfile(profileName);
43
63
  if (!profile) {
44
64
  outputError({ error: `Profile "${profileName}" not found`, type: 'PACHCA_USAGE_ERROR', code: null }, format);
@@ -67,6 +87,7 @@ export default class AuthStatus extends BaseCommand {
67
87
  if (format === 'json') {
68
88
  this.output({
69
89
  profile: profileName,
90
+ source: source.kind === 'profile' ? source.via : null,
70
91
  type: profile.type,
71
92
  user: profile.user,
72
93
  email: profile.email || null,
@@ -111,10 +132,88 @@ export default class AuthStatus extends BaseCommand {
111
132
  process.stderr.write(` Список прав в профиле расходился с сервером и был обновлён\n`);
112
133
  }
113
134
  }
135
+ /**
136
+ * A token passed to the command rather than saved in a profile. Nothing about it
137
+ * is stored, so everything comes from the server: whose it is and what it carries.
138
+ * A revoked token or no network ends the command with the usual error and hint.
139
+ */
140
+ async showPassedToken(token, label) {
141
+ // A PACHCA_TOKEN left in the environment silently wins over the profile the
142
+ // person thinks they are using. Name that profile, so the mix-up is visible,
143
+ // also when the forgotten token no longer works.
144
+ const profileName = label === 'PACHCA_TOKEN' ? process.env.PACHCA_PROFILE || getActiveProfile() : undefined;
145
+ const unusedProfile = profileName && listProfiles()[profileName] ? profileName : null;
146
+ const clientFlags = { ...this.getClientFlags(), quiet: true };
147
+ let info;
148
+ try {
149
+ const response = await request({ method: 'GET', path: '/oauth/token/info', token }, clientFlags);
150
+ info = response.data.data;
151
+ }
152
+ catch (error) {
153
+ // A dead token (401) or one whose owner lost access (403 access_denied):
154
+ // the profile it hides may still work, so say so.
155
+ const dead = error instanceof ApiError &&
156
+ (error.details.code === 401 || (error.details.code === 403 && error.details.validation_code === 'access_denied'));
157
+ if (dead && unusedProfile) {
158
+ const human = this.getOutputFormat() !== 'json' && process.stderr.isTTY;
159
+ error.details.unused_profile = unusedProfile;
160
+ error.details.hint = human
161
+ ? `Профиль ${unusedProfile} не используется, пока задан PACHCA_TOKEN: уберите переменную или добавьте --profile ${unusedProfile}`
162
+ : `unset PACHCA_TOKEN, or add --profile ${unusedProfile}`;
163
+ }
164
+ throw error;
165
+ }
166
+ const identity = await fetchIdentity(token, info.user_id, clientFlags);
167
+ const type = identity.isBot === null ? null : identity.isBot ? 'bot' : 'user';
168
+ const user = identity.isBot ? `${identity.name} (бот)` : identity.name;
169
+ const scopes = info.scopes ?? [];
170
+ // A CLI login or an app's token lives an hour from issue, and nothing renews a
171
+ // token passed to the command. Other tokens have no lifetime at all.
172
+ const expiresIn = info.expires_in ?? null;
173
+ const issuedAt = info.created_at ? Date.parse(info.created_at) : Number.NaN;
174
+ const expiresAt = expiresIn && !Number.isNaN(issuedAt) ? new Date(issuedAt + expiresIn * 1000) : null;
175
+ if (this.getOutputFormat() === 'json') {
176
+ this.output({
177
+ profile: null,
178
+ source: label,
179
+ type,
180
+ user,
181
+ user_id: info.user_id,
182
+ email: identity.email,
183
+ auth: 'token',
184
+ expires_at: expiresAt ? expiresAt.toISOString() : null,
185
+ expires_in: expiresIn,
186
+ scopes,
187
+ scopes_source: 'server',
188
+ storage: null,
189
+ secret_readable: true,
190
+ unused_profile: unusedProfile,
191
+ });
192
+ return;
193
+ }
194
+ const emailStr = identity.email ? ` (${identity.email})` : '';
195
+ const from = label === '--token' ? 'флага --token' : 'PACHCA_TOKEN';
196
+ const typeStr = type ?? (identity.noScope ? 'бот или сотрудник: без права profile:read не узнать' : 'бот или сотрудник: профиль не прочитан');
197
+ process.stderr.write(` Подключён как: ${user}${emailStr} [${typeStr}]\n`);
198
+ if (expiresIn) {
199
+ const minutesLeft = expiresAt ? Math.round((expiresAt.getTime() - Date.now()) / 60_000) : null;
200
+ const left = minutesLeft === null ? `живёт ${Math.round(expiresIn / 60)} мин от выдачи` : minutesLeft > 0 ? `осталось ${minutesLeft} мин` : 'истёк';
201
+ process.stderr.write(` Вход: токеном из ${from}, ${left}, CLI его не продлевает\n`);
202
+ }
203
+ else {
204
+ process.stderr.write(` Вход: готовым токеном из ${from}\n`);
205
+ }
206
+ process.stderr.write(scopes.length
207
+ ? ` Права по данным сервера (${scopes.length}): ${scopes.join(', ')}\n`
208
+ : ` Права по данным сервера: ни одного\n`);
209
+ if (unusedProfile) {
210
+ process.stderr.write(` Профиль ${unusedProfile} не используется, пока задан PACHCA_TOKEN. Чтобы работать с ним, добавьте --profile ${unusedProfile} или уберите переменную.\n`);
211
+ }
212
+ }
114
213
  /** Ask the server what the token really carries. Null when it cannot be asked. */
115
214
  async fetchRemoteScopes(token) {
116
215
  try {
117
- const response = await request({ method: 'GET', path: '/oauth/token/info', token }, { quiet: true });
216
+ const response = await request({ method: 'GET', path: '/oauth/token/info', token }, { ...this.getClientFlags(), quiet: true });
118
217
  const info = response.data.data;
119
218
  return info?.scopes ?? null;
120
219
  }
@@ -16,38 +16,38 @@ export default class BotsCreate extends BaseCommand {
16
16
  description: "Имя бота (макс. 255 символов)",
17
17
  }),
18
18
  'nickname': Flags.string({
19
- description: "Никнейм бота. Должен заканчиваться на `_bot`. (макс. 255 символов)",
19
+ description: "Никнейм бота: латинские буквы, цифры и `_`, в конце `_bot`, свободный в пространстве. Ник не по формату отвечает `400` на поле `bot.nickname` с кодом `invalid`, а занятый ник — с кодом `taken`. Без поля ник собирается из имени в латинской транслитерации с `_bot` в конце, а если такой ник занят, с `_2_bot`, `_3_bot` и далее. (макс. 255 символов)",
20
20
  }),
21
21
  'outgoing-url': Flags.string({
22
- description: "URL исходящего вебхука",
22
+ description: "URL исходящего вебхука: публичный адрес с протоколом `http` или `https`. Иначе ответ `400` на поле `webhook.outgoing_url` с кодом `webhook_url_invalid`, `webhook_url_unresolved` или `webhook_url_forbidden`. (макс. 255 символов)",
23
23
  }),
24
24
  'events': Flags.string({
25
- description: "События, на которые подписан бот",
25
+ description: "События, на которые подписан бот. Без поля бот подписан на `message_new`.",
26
26
  }),
27
27
  'trigger-on': Flags.string({
28
28
  description: "Условие срабатывания исходящего вебхука",
29
29
  }),
30
30
  'commands': Flags.string({
31
- description: "Команды бота (триггер-слова), на которые он реагирует при trigger_on = commands. Событие о сообщении приходит боту, когда первое слово текста совпадает с одной из команд целиком, с учётом регистра. Команду можно задать и без слэша в начале. Суммарная длина команд, объединённых через запятую, не должна превышать 255 символов.",
31
+ description: "Команды бота (триггер-слова), на которые он реагирует при trigger_on = commands. Событие о сообщении приходит боту, когда первое слово текста совпадает с одной из команд целиком, с учётом регистра. Команду можно задать и без слэша в начале. Суммарная длина команд вместе с разделителями `, ` (запятая и пробел) не должна превышать 255 символов.",
32
32
  }),
33
33
  'scopes': Flags.string({
34
34
  description: "Скоупы (права доступа) токена бота. Если не указано, бот получает набор по умолчанию. Боту доступны не все скоупы: часть из них разрешена только пользовательским ролям, и попытка назначить такой скоуп возвращает `400`. Служебные значения `bot` и `all` назначать нельзя.",
35
35
  }),
36
36
  'template': Flags.string({
37
- description: "Шаблон форматирования входящего вебхука",
37
+ description: "Шаблон форматирования входящего вебхука. Шаблон с синтаксической ошибкой у источника `simple`, в том числе у пустого бота с `kind: simple`, даёт ответ `400` на этом поле с кодом `invalid`, и бот не создаётся. Причина приходит в `message`.",
38
38
  }),
39
39
  'template-engine': Flags.string({
40
40
  description: "Шаблонизатор для обработки шаблона входящего вебхука. Без этого поля бот получает `liquid`, а пустой бот (`empty: true`) — `mustache`.",
41
41
  }),
42
42
  'challenge-key': Flags.string({
43
- description: "Название поля проверки для верификации входящего вебхука",
43
+ description: "Название поля проверки для верификации входящего вебхука (макс. 255 символов)",
44
44
  }),
45
45
  'link-preview-enabled': Flags.boolean({
46
46
  description: "Показывать превью ссылок в сообщениях входящего вебхука",
47
47
  allowNo: true,
48
48
  }),
49
49
  'ignore-self-messages': Flags.boolean({
50
- description: "Не присылать боту события о его собственных сообщениях и реакциях",
50
+ description: "Не присылать боту события о сообщениях, которые он отправил сам, и о реакциях на эти сообщения, кто бы их ни поставил или снял. Нажатия кнопок приходят всегда.",
51
51
  allowNo: true,
52
52
  }),
53
53
  'events-history-enabled': Flags.boolean({
@@ -71,7 +71,7 @@ export default class BotsCreate extends BaseCommand {
71
71
  description: "Домены, ссылки на которые бот разворачивает, не больше 5. Работают вместе с событием `message_link_shared`.",
72
72
  }),
73
73
  'empty': Flags.boolean({
74
- description: "Создать пустого бота: без токена и без входящего вебхука. Без этого поля бот создаётся с входящим вебхуком в своём формате, а его токен приходит в `access_token`.",
74
+ description: "Создать пустого бота: без входящего вебхука и, если не переданы скоупы, без токена. Без этого поля бот создаётся с входящим вебхуком в своём формате, а его токен приходит в `access_token`.",
75
75
  allowNo: true,
76
76
  }),
77
77
  'oauth-client': Flags.string({
@@ -117,6 +117,12 @@ export default class BotsCreate extends BaseCommand {
117
117
  if (flags['nickname'] && String(flags['nickname']).length > 255) {
118
118
  validationErrors.push({ message: `--nickname: максимум 255 символов (передано: ${String(flags['nickname']).length})`, flag: 'nickname' });
119
119
  }
120
+ if (flags['outgoing-url'] && String(flags['outgoing-url']).length > 255) {
121
+ validationErrors.push({ message: `--outgoing-url: максимум 255 символов (передано: ${String(flags['outgoing-url']).length})`, flag: 'outgoing-url' });
122
+ }
123
+ if (flags['challenge-key'] && String(flags['challenge-key']).length > 255) {
124
+ validationErrors.push({ message: `--challenge-key: максимум 255 символов (передано: ${String(flags['challenge-key']).length})`, flag: 'challenge-key' });
125
+ }
120
126
  if (validationErrors.length > 0) {
121
127
  this.validationError(validationErrors);
122
128
  }
@@ -11,7 +11,7 @@ export default class BotsList extends BaseCommand {
11
11
  static flags = {
12
12
  ...BaseCommand.baseFlags,
13
13
  'query': Flags.string({
14
- description: "Поисковая фраза для фильтрации ботов по имени",
14
+ description: "Поисковая фраза для фильтрации ботов по имени или нику",
15
15
  }),
16
16
  limit: Flags.integer({
17
17
  description: 'Количество результатов на страницу',
@@ -13,14 +13,14 @@ export default class BotsUpdateWebhook extends BaseCommand {
13
13
  static flags = {
14
14
  ...BaseCommand.baseFlags,
15
15
  'outgoing-url': Flags.string({
16
- description: "URL исходящего вебхука. Пустая строка отключает вебхук.",
16
+ description: "URL исходящего вебхука: публичный адрес с протоколом `http` или `https`. Иначе ответ `400` на поле `webhook.outgoing_url` с кодом `webhook_url_invalid`, `webhook_url_unresolved` или `webhook_url_forbidden`. Пустая строка отключает вебхук. (макс. 255 символов)",
17
17
  }),
18
18
  };
19
19
  async run() {
20
20
  const { args, flags } = await this.parse(BotsUpdateWebhook);
21
21
  this.parsedFlags = flags;
22
22
  const missingRequired = [
23
- { flag: 'outgoing-url', label: "URL исходящего вебхука. Пустая строка отключает вебхук.", type: 'string' },
23
+ { flag: 'outgoing-url', label: "URL исходящего вебхука: публичный адрес с протоколом `http` или `https`. Иначе ответ `400` на поле `webhook.outgoing_url` с кодом `webhook_url_invalid`, `webhook_url_unresolved` или `webhook_url_forbidden`. Пустая строка отключает вебхук.", type: 'string' },
24
24
  ].filter((f) => flags[f.flag] === undefined || flags[f.flag] === null);
25
25
  if (missingRequired.length > 0) {
26
26
  if (this.isInteractive()) {
@@ -45,6 +45,13 @@ export default class BotsUpdateWebhook extends BaseCommand {
45
45
  this.validationError(missingRequired.map((f) => ({ message: `Обязательный флаг --${f.flag} не передан`, flag: f.flag })), { hint: "Обязательные: --outgoing-url <string>. pachca introspect bots update-webhook" });
46
46
  }
47
47
  }
48
+ const validationErrors = [];
49
+ if (flags['outgoing-url'] && String(flags['outgoing-url']).length > 255) {
50
+ validationErrors.push({ message: `--outgoing-url: максимум 255 символов (передано: ${String(flags['outgoing-url']).length})`, flag: 'outgoing-url' });
51
+ }
52
+ if (validationErrors.length > 0) {
53
+ this.validationError(validationErrors);
54
+ }
48
55
  const body = { webhook: {
49
56
  outgoing_url: flags['outgoing-url'],
50
57
  } };
@@ -20,10 +20,10 @@ export default class BotsUpdate extends BaseCommand {
20
20
  description: "Имя бота (макс. 255 символов)",
21
21
  }),
22
22
  'nickname': Flags.string({
23
- description: "Никнейм бота. Должен заканчиваться на `_bot`. (макс. 255 символов)",
23
+ description: "Никнейм бота: латинские буквы, цифры и `_`, в конце `_bot`, свободный в пространстве. Ник не по формату отвечает `400` на поле `bot.nickname` с кодом `invalid`, а занятый ник — с кодом `taken`. (макс. 255 символов)",
24
24
  }),
25
25
  'outgoing-url': Flags.string({
26
- description: "URL исходящего вебхука",
26
+ description: "URL исходящего вебхука: публичный адрес с протоколом `http` или `https`. Иначе ответ `400` на поле `webhook.outgoing_url` с кодом `webhook_url_invalid`, `webhook_url_unresolved` или `webhook_url_forbidden`. (макс. 255 символов)",
27
27
  }),
28
28
  'events': Flags.string({
29
29
  description: "События, на которые подписан бот. Список заменяется целиком.",
@@ -32,26 +32,26 @@ export default class BotsUpdate extends BaseCommand {
32
32
  description: "Условие срабатывания исходящего вебхука",
33
33
  }),
34
34
  'commands': Flags.string({
35
- description: "Команды бота (триггер-слова), на которые он реагирует при trigger_on = commands. Событие о сообщении приходит боту, когда первое слово текста совпадает с одной из команд целиком, с учётом регистра. Команду можно задать и без слэша в начале. Суммарная длина команд, объединённых через запятую, не должна превышать 255 символов.",
35
+ description: "Команды бота (триггер-слова), на которые он реагирует при trigger_on = commands. Событие о сообщении приходит боту, когда первое слово текста совпадает с одной из команд целиком, с учётом регистра. Команду можно задать и без слэша в начале. Суммарная длина команд вместе с разделителями `, ` (запятая и пробел) не должна превышать 255 символов.",
36
36
  }),
37
37
  'scopes': Flags.string({
38
38
  description: "Скоупы (права доступа) токена бота. Если не указано, бот получает набор по умолчанию. Боту доступны не все скоупы: часть из них разрешена только пользовательским ролям, и попытка назначить такой скоуп возвращает `400`. Служебные значения `bot` и `all` назначать нельзя.",
39
39
  }),
40
40
  'template': Flags.string({
41
- description: "Шаблон форматирования входящего вебхука",
41
+ description: "Шаблон форматирования входящего вебхука. Шаблон проверяется, когда в запросе меняются `template`, `template_engine` или `kind`, поэтому смена одного шаблонизатора или включение `kind: simple` проверяет и уже сохранённый шаблон. Шаблон с синтаксической ошибкой у источника `simple` даёт ответ `400` на этом поле с кодом `invalid`, и запрос не применяется. Причина приходит в `message`.",
42
42
  }),
43
43
  'template-engine': Flags.string({
44
44
  description: "Шаблонизатор для обработки шаблона входящего вебхука",
45
45
  }),
46
46
  'challenge-key': Flags.string({
47
- description: "Название поля проверки для верификации входящего вебхука",
47
+ description: "Название поля проверки для верификации входящего вебхука (макс. 255 символов)",
48
48
  }),
49
49
  'link-preview-enabled': Flags.boolean({
50
50
  description: "Показывать превью ссылок в сообщениях входящего вебхука",
51
51
  allowNo: true,
52
52
  }),
53
53
  'ignore-self-messages': Flags.boolean({
54
- description: "Не присылать боту события о его собственных сообщениях и реакциях",
54
+ description: "Не присылать боту события о сообщениях, которые он отправил сам, и о реакциях на эти сообщения, кто бы их ни поставил или снял. Нажатия кнопок приходят всегда.",
55
55
  allowNo: true,
56
56
  }),
57
57
  'events-history-enabled': Flags.boolean({
@@ -91,6 +91,12 @@ export default class BotsUpdate extends BaseCommand {
91
91
  if (flags['nickname'] && String(flags['nickname']).length > 255) {
92
92
  validationErrors.push({ message: `--nickname: максимум 255 символов (передано: ${String(flags['nickname']).length})`, flag: 'nickname' });
93
93
  }
94
+ if (flags['outgoing-url'] && String(flags['outgoing-url']).length > 255) {
95
+ validationErrors.push({ message: `--outgoing-url: максимум 255 символов (передано: ${String(flags['outgoing-url']).length})`, flag: 'outgoing-url' });
96
+ }
97
+ if (flags['challenge-key'] && String(flags['challenge-key']).length > 255) {
98
+ validationErrors.push({ message: `--challenge-key: максимум 255 символов (передано: ${String(flags['challenge-key']).length})`, flag: 'challenge-key' });
99
+ }
94
100
  if (validationErrors.length > 0) {
95
101
  this.validationError(validationErrors);
96
102
  }
@@ -16,7 +16,7 @@ export default class ChatsList extends BaseCommand {
16
16
  default: "id",
17
17
  }),
18
18
  'order': Flags.string({
19
- description: "Направление сортировки. Применяется вместе с `sort`, например `sort=last_message_at&order=desc`. Без `sort` чаты идут по убыванию `id` при любом `order`.",
19
+ description: "Направление сортировки по полю `sort`, например `sort=last_message_at&order=desc`. Без `sort` направление применяется к `id`.",
20
20
  options: ["asc", "desc"],
21
21
  default: "desc",
22
22
  }),
@@ -16,7 +16,7 @@ export default class ChatsUpdate extends BaseCommand {
16
16
  static flags = {
17
17
  ...BaseCommand.baseFlags,
18
18
  'name': Flags.string({
19
- description: "Название (макс. 255 символов)",
19
+ description: "Название беседы или канала. Пустое название или название из одних пробелов отвечает `400` с кодом `blank`. (макс. 255 символов)",
20
20
  }),
21
21
  'public': Flags.boolean({
22
22
  description: "Открытый доступ",
@@ -20,7 +20,7 @@ export default class DraftsCreate extends BaseCommand {
20
20
  description: "Идентификатор того, что названо в `entity_type`: беседы или канала, треда либо сотрудника (pachca chats list | pachca users list)",
21
21
  }),
22
22
  'content': Flags.string({
23
- description: "Текст черновика. Можно не передавать, если есть вложения, но пустым черновик быть не может. До 40 000 байт в UTF-8.",
23
+ description: "Текст черновика. Можно не передавать, если есть вложения, но пустым черновик быть не может. Поддерживает упоминания, как текст в методе [Новое сообщение](POST /messages). `<@user_id>` остаётся в тексте черновика как передан, а при отправке становится `@nickname`. До 40 000 байт в UTF-8.",
24
24
  }),
25
25
  'parent-message-id': nullableInteger({
26
26
  description: "Идентификатор сообщения, ответом на которое уйдёт черновик. Сообщение должно быть в том же чате.",
@@ -17,7 +17,7 @@ export default class DraftsUpdate extends BaseCommand {
17
17
  static flags = {
18
18
  ...BaseCommand.baseFlags,
19
19
  'content': Flags.string({
20
- description: "Текст черновика. До 40 000 байт в UTF-8.",
20
+ description: "Текст черновика. Поддерживает упоминания, как текст в методе [Новое сообщение](POST /messages). `<@user_id>` остаётся в тексте черновика как передан, а при отправке становится `@nickname`. До 40 000 байт в UTF-8.",
21
21
  }),
22
22
  'parent-message-id': nullableInteger({
23
23
  description: "Идентификатор сообщения, ответом на которое уйдёт черновик. `null` снимает ответ. Чтобы передать null, укажите `--parent-message-id null`.",
@@ -31,10 +31,10 @@ export default class MessagesCreate extends BaseCommand {
31
31
  description: "Идентификатор сообщения, на которое вы отвечаете с цитатой. Ответить можно на сообщение в любом чате, в том числе в треде: ответ отправляют в тот же чат или тред, где лежит это сообщение, иначе запрос вернёт ошибку `422`. Чтобы написать в тред под сообщением, укажите `entity_type: \"thread\"` и `id` треда в `entity_id`.",
32
32
  }),
33
33
  'display-avatar-url': Flags.string({
34
- description: "Ссылка на специальную аватарку отправителя для этого сообщения. Использование этого поля возможно только с access_token бота. (макс. 255 символов)",
34
+ description: "Ссылка на специальную аватарку отправителя для этого сообщения. Использование этого поля возможно только с access_token бота. До 255 байт в UTF-8. (макс. 255 символов)",
35
35
  }),
36
36
  'display-name': Flags.string({
37
- description: "Полное специальное имя отправителя для этого сообщения. Вместе с `display_avatar_url` бот подписывает сообщение чужим именем и аватаром, и только это сообщение. Бейдж «Бот» остаётся, а в профиле отправителя видно настоящего бота. Так показывают автора из другой системы, когда переносят в Пачку чужую переписку. С личным токеном поле недоступно, ответ `403`. Подробности — в разделе [Боты](https://dev.pachca.com/guides/bots/overview). (макс. 255 символов)",
37
+ description: "Полное специальное имя отправителя для этого сообщения. Вместе с `display_avatar_url` бот подписывает сообщение чужим именем и аватаром, и только это сообщение. Бейдж «Бот» остаётся, а в профиле отправителя видно настоящего бота. Так показывают автора из другой системы, когда переносят в Пачку чужую переписку. С личным токеном поле недоступно, ответ `403`. До 255 байт в UTF-8, для кириллицы это около 127 символов. Подробности — в разделе [Боты](https://dev.pachca.com/guides/bots/overview). (макс. 255 символов)",
38
38
  }),
39
39
  'skip-invite-mentions': Flags.boolean({
40
40
  description: "Пропуск добавления упомянутых сотрудников в тред. В треде упоминание работает ещё и как приглашение: упомянутого сотрудника, которого в треде нет, Пачка туда добавляет, и он получает доступ к треду и родительскому сообщению. Если в пространстве включена настройка «Использовать сквозные треды» (так по умолчанию), упоминание добавляет и сотрудника не из родительского чата, а с `true` в этом поле упоминание никого не добавляет, и тот, кого в треде нет, не получает и уведомления. Упоминание от гостя или мульти-гостя при этом добавляет только тех, с кем у автора есть общий чат. Если настройка выключена, упоминание добавляет только участников родительского чата, и это поле ничего не меняет. В беседе и канале упоминание никого не добавляет, и этот параметр там ничего не меняет.",
@@ -26,7 +26,7 @@ export default class MessagesList extends BaseCommand {
26
26
  default: "id",
27
27
  }),
28
28
  'order': Flags.string({
29
- description: "Направление сортировки. Применяется вместе с `sort`: сообщения от старых к новым дают `sort=id` и `order=asc`. Без `sort` сообщения идут от новых к старым при любом `order`.",
29
+ description: "Направление сортировки по `id`: `asc` отдаёт сообщения от старых к новым, `desc` от новых к старым.",
30
30
  options: ["asc", "desc"],
31
31
  default: "desc",
32
32
  }),
@@ -25,10 +25,10 @@ export default class MessagesUpdate extends BaseCommand {
25
25
  description: "Массив строк, каждая из которых представлена массивом кнопок. Максимум 100 кнопок у сообщения, до 8 кнопок в строке. Для удаления кнопок пришлите пустой массив.",
26
26
  }),
27
27
  'display-avatar-url': Flags.string({
28
- description: "Ссылка на специальную аватарку отправителя для этого сообщения. Использование этого поля возможно только с access_token бота.",
28
+ description: "Ссылка на специальную аватарку отправителя для этого сообщения. Использование этого поля возможно только с access_token бота. До 255 байт в UTF-8. (макс. 255 символов)",
29
29
  }),
30
30
  'display-name': Flags.string({
31
- description: "Полное специальное имя отправителя для этого сообщения. Использование этого поля возможно только с access_token бота.",
31
+ description: "Полное специальное имя отправителя для этого сообщения. Использование этого поля возможно только с access_token бота. До 255 байт в UTF-8, для кириллицы это около 127 символов. (макс. 255 символов)",
32
32
  }),
33
33
  };
34
34
  async run() {
@@ -44,6 +44,16 @@ export default class MessagesUpdate extends BaseCommand {
44
44
  if (piped)
45
45
  flags['content'] = piped;
46
46
  }
47
+ const validationErrors = [];
48
+ if (flags['display-avatar-url'] && String(flags['display-avatar-url']).length > 255) {
49
+ validationErrors.push({ message: `--display-avatar-url: максимум 255 символов (передано: ${String(flags['display-avatar-url']).length})`, flag: 'display-avatar-url' });
50
+ }
51
+ if (flags['display-name'] && String(flags['display-name']).length > 255) {
52
+ validationErrors.push({ message: `--display-name: максимум 255 символов (передано: ${String(flags['display-name']).length})`, flag: 'display-name' });
53
+ }
54
+ if (validationErrors.length > 0) {
55
+ this.validationError(validationErrors);
56
+ }
47
57
  const body = { message: {
48
58
  content: flags['content'],
49
59
  files: flags['files'] ? this.parseJSON(flags['files'], 'files') : undefined,
@@ -1,6 +1,7 @@
1
1
  // Auto-generated from openapi.yaml — DO NOT EDIT
2
2
  import { Flags } from '@oclif/core';
3
3
  import { BaseCommand } from '../../base-command.js';
4
+ import { nullableString } from '../../lib/flags.js';
4
5
  export default class ProfileUpdateStatus extends BaseCommand {
5
6
  static description = "Новый свой статус";
6
7
  static scope = "profile_status:write";
@@ -16,15 +17,15 @@ export default class ProfileUpdateStatus extends BaseCommand {
16
17
  'title': Flags.string({
17
18
  description: "Текст статуса. Обязателен, если статуса ещё нет. (макс. 50 символов)",
18
19
  }),
19
- 'expires-at': Flags.string({
20
- description: "Срок жизни статуса (ISO-8601). Смещение вида `+03:00` учитывается, время без смещения считается UTC.",
20
+ 'expires-at': nullableString({
21
+ description: "Срок жизни статуса (ISO-8601). Смещение вида `+03:00` учитывается, время без смещения считается UTC. `null` убирает срок: статус не снимется сам. Чтобы передать null, укажите `--expires-at null`.",
21
22
  }),
22
23
  'is-away': Flags.boolean({
23
24
  description: "Режим «Нет на месте»",
24
25
  allowNo: true,
25
26
  }),
26
- 'away-message': Flags.string({
27
- description: "Текст сообщения при режиме «Нет на месте». Отображается в профиле и при личных сообщениях/упоминаниях. (макс. 1024 символов)",
27
+ 'away-message': nullableString({
28
+ description: "Текст сообщения при режиме «Нет на месте». Отображается в профиле и при личных сообщениях/упоминаниях. Поддерживает Markdown и упоминания `@nickname` и `<@user_id>`, но сохраняется и возвращается обычным текстом: разметка убирается, а упоминание сотрудника заменяется на `@` с его именем и фамилией. Предел 1024 символа действует и на присланный текст, и на получившийся, сверх него ответ `400` с кодом `too_long`. `null` или пустая строка убирает текст. Чтобы передать null, укажите `--away-message null`. (макс. 1024 символов)",
28
29
  }),
29
30
  };
30
31
  async run() {