diff --git a/apps/docs/app/api/(guides)/[slug]/page.tsx b/apps/docs/app/api/(guides)/[slug]/page.tsx index f4b7931b..31116d84 100644 --- a/apps/docs/app/api/(guides)/[slug]/page.tsx +++ b/apps/docs/app/api/(guides)/[slug]/page.tsx @@ -28,13 +28,11 @@ export async function generateMetadata({ const firstParagraph = extractFirstParagraph(data.content); const pageTitle = data.frontmatter.title; - const section = getSectionTitle(`/api/${slug}`); - const title = section ? `${section}: ${pageTitle}` : pageTitle; const description: string | undefined = data.frontmatter.description || firstParagraph; const ogImage = `/api/og?type=guide&slug=api/${slug}`; return { - title, + title: { absolute: `${pageTitle} | Документация API` }, description, alternates: { canonical: `/api/${slug}`, diff --git a/apps/docs/app/api/[...slug]/page.tsx b/apps/docs/app/api/[...slug]/page.tsx index 0f10851e..4224ef4f 100644 --- a/apps/docs/app/api/[...slug]/page.tsx +++ b/apps/docs/app/api/[...slug]/page.tsx @@ -44,7 +44,7 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str `${endpoint.method} ${endpoint.path}` + (descriptionBody ? `\n${descriptionBody}` : ''); return { - title: endpoint.title, + title: { absolute: `${endpoint.title} | Документация API` }, description, alternates: { canonical: path, diff --git a/apps/docs/app/guides/[...slug]/page.tsx b/apps/docs/app/guides/[...slug]/page.tsx index 28079a86..ef36b790 100644 --- a/apps/docs/app/guides/[...slug]/page.tsx +++ b/apps/docs/app/guides/[...slug]/page.tsx @@ -3,7 +3,7 @@ import { getAdjacentItems } from '@/lib/navigation'; import { StaticPageHeader } from '@/components/api/static-page-header'; import { MarkdownContent } from '@/components/api/markdown-content'; import { getGuideData, getAllGuideSlugs, extractFirstParagraph } from '@/lib/content-loader'; -import { getSectionTitle } from '@/lib/tabs-config'; +import { getSectionTitle, getNestedParentTitle } from '@/lib/tabs-config'; import { notFound } from 'next/navigation'; import type { Metadata } from 'next'; @@ -30,13 +30,13 @@ export async function generateMetadata({ const firstParagraph = extractFirstParagraph(data.content); const pageTitle = data.frontmatter.title; const pageUrl = `/guides/${slugPath}`; - const section = getSectionTitle(pageUrl); - const title = section ? `${section}: ${pageTitle}` : pageTitle; + const parent = getNestedParentTitle(pageUrl); + const baseTitle = parent ? `${parent}: ${pageTitle}` : pageTitle; const description: string | undefined = data.frontmatter.description || firstParagraph; const ogImage = `/api/og?type=guide&slug=${slugPath}`; return { - title, + title: { absolute: `${baseTitle} | Руководство разработчика` }, description, alternates: { canonical: pageUrl, diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index 2229a610..130a4d29 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -23,8 +23,8 @@ export const viewport: Viewport = { export const metadata: Metadata = { metadataBase: new URL('https://dev.pachca.com'), title: { - default: 'Обзор - Пачка для разработчиков', - template: '%s - Пачка для разработчиков', + default: 'Обзор | Пачка для разработчиков', + template: '%s | Пачка для разработчиков', }, description: 'Создавайте уникальные решения на одной платформе', openGraph: { diff --git a/apps/docs/app/updates/[date]/page.tsx b/apps/docs/app/updates/[date]/page.tsx index 65b609a8..627641f8 100644 --- a/apps/docs/app/updates/[date]/page.tsx +++ b/apps/docs/app/updates/[date]/page.tsx @@ -37,7 +37,7 @@ export async function generateMetadata({ .trim(); return { - title, + title: { absolute: `${title} | Руководство разработчика` }, description, alternates: { canonical: `/updates/${date}`, diff --git a/apps/docs/app/updates/page.tsx b/apps/docs/app/updates/page.tsx index 6f44fa6c..ba509b2a 100644 --- a/apps/docs/app/updates/page.tsx +++ b/apps/docs/app/updates/page.tsx @@ -13,7 +13,7 @@ export async function generateMetadata(): Promise { const description = data.frontmatter.description || extractFirstParagraph(data.content); return { - title: data.frontmatter.title, + title: { absolute: `${data.frontmatter.title} | Руководство разработчика` }, description, alternates: { canonical: '/updates', diff --git a/apps/docs/content/api/errors.mdx b/apps/docs/content/api/errors.mdx index f4e35e0f..75eb0d32 100644 --- a/apps/docs/content/api/errors.mdx +++ b/apps/docs/content/api/errors.mdx @@ -69,3 +69,83 @@ description: Коды ошибок HTTP и rate limits - **Лимиты гибкие:** они ориентировочные и могут меняться, чтобы всё работало гладко; - **Должно хватать на всё:** мы настроили их так, чтобы вам было комфортно в любых сценариях; - **Если упёрлись в лимит:** при ошибке `429` смотрите заголовок `Retry-After` — он подскажет, через сколько секунд повторить запрос (или используйте экспоненциальный `backoff`, если хотите перестраховаться). + +## Повторные запросы (Retry) + +[SDK](/guides/sdk/overview) ([TypeScript](/guides/sdk/typescript), [Python](/guides/sdk/python)) уже включают автоматический retry с экспоненциальным backoff. Ниже — реализация для кастомных HTTP-клиентов. + +### TypeScript + +```typescript +async function withRetry( + fn: () => Promise, + maxRetries = 3 +): Promise { + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await fn() + } catch (error: any) { + // 429 Too Many Requests — ждём Retry-After или backoff + if (error.status === 429) { + const retryAfter = error.headers?.["retry-after"] + const delay = retryAfter + ? parseInt(retryAfter) * 1000 + : Math.pow(2, attempt) * 1000 * (0.5 + Math.random()) + await new Promise(r => setTimeout(r, delay)) + continue + } + // 5xx — серверная ошибка, backoff с jitter + if (error.status >= 500 && attempt < maxRetries) { + const delay = Math.pow(2, attempt) * 1000 * (0.5 + Math.random()) + await new Promise(r => setTimeout(r, delay)) + continue + } + // 4xx (кроме 429) — не повторяем + throw error + } + } + throw new Error("Max retries exceeded") +} + +// Использование +const users = await withRetry(() => client.users.listUsers()) +``` + +### Python + +```python +import time, random + +async def with_retry(fn, max_retries=3): + for attempt in range(max_retries + 1): + try: + return await fn() + except Exception as e: + status = getattr(e, "status_code", getattr(e, "status", 0)) + headers = getattr(e, "headers", {}) + # 429 Too Many Requests + if status == 429: + retry_after = headers.get("Retry-After") + delay = int(retry_after) if retry_after else (2 ** attempt) * (0.5 + random.random()) + time.sleep(delay) + continue + # 5xx — серверная ошибка + if status >= 500 and attempt < max_retries: + time.sleep((2 ** attempt) * (0.5 + random.random())) + continue + raise + raise Exception("Max retries exceeded") + +# Использование +users = await with_retry(lambda: client.users.list_users()) +``` + +### Стратегия повторов + +| Код | Действие | Задержка | +|-----|----------|----------| +| `429` | Повторить | `Retry-After` header или exponential backoff: 1с, 2с, 4с × jitter | +| `500`, `502`, `503`, `504` | Повторить | Exponential backoff с jitter: ~1с, ~2с, ~4с | +| `400`, `401`, `403`, `404`, `422` | **Не повторять** | Ошибка клиента — нужно исправить запрос | + +Максимум **3 повтора** на каждый запрос. Jitter (случайный множитель 0.5–1.5) предотвращает «thundering herd» при массовых 429. diff --git a/apps/docs/content/api/file-uploads.mdx b/apps/docs/content/api/file-uploads.mdx index 8880cee0..c19f0577 100644 --- a/apps/docs/content/api/file-uploads.mdx +++ b/apps/docs/content/api/file-uploads.mdx @@ -71,59 +71,115 @@ pachca messages create --entity-id 12345 \ | Файл | `file` | — | | Изображение | `image` | `width`, `height` — размеры в пикселях | -## Полный пример +## Примеры полного цикла -```javascript title="Node.js: загрузка и отправка файла" -const fs = require('node:fs'); -const path = require('node:path'); +### TypeScript SDK -const TOKEN = 'ваш_токен'; -const BASE = 'https://api.pachca.com/api/shared/v1'; -const headers = { Authorization: `Bearer ${TOKEN}` }; +```typescript +import { PachcaClient, FileUploadRequest, FileType } from "@pachca/sdk" +import fs from "fs" +import path from "path" + +const client = new PachcaClient("YOUR_TOKEN") + +const filePath = "./report.pdf" +const fileName = path.basename(filePath) +const fileBuffer = fs.readFileSync(filePath) // Шаг 1: Получить параметры загрузки -const { data: params } = await fetch(`${BASE}/uploads`, { - method: 'POST', headers -}).then(r => r.json()); - -// Шаг 2: Загрузить файл -const filePath = './report.pdf'; -const fileName = path.basename(filePath); -const fileBuffer = fs.readFileSync(filePath); - -const form = new FormData(); -form.append('Content-Disposition', params['Content-Disposition']); -form.append('acl', params.acl); -form.append('policy', params.policy); -form.append('x-amz-credential', params['x-amz-credential']); -form.append('x-amz-algorithm', params['x-amz-algorithm']); -form.append('x-amz-date', params['x-amz-date']); -form.append('x-amz-signature', params['x-amz-signature']); -form.append('key', params.key); -form.append('file', new File([fileBuffer], fileName)); - -await fetch(params.direct_url, { method: 'POST', body: form }); +const params = await client.common.getUploadParams() + +// Шаг 2: Загрузить файл на S3 (direct_url — внешний presigned URL) +await client.common.uploadFile(params.directUrl, { + contentDisposition: params.contentDisposition, + acl: params.acl, + policy: params.policy, + xAmzCredential: params.xAmzCredential, + xAmzAlgorithm: params.xAmzAlgorithm, + xAmzDate: params.xAmzDate, + xAmzSignature: params.xAmzSignature, + key: params.key, + file: new File([fileBuffer], fileName) +}) // Шаг 3: Отправить сообщение с файлом -const fileKey = params.key.replace('${filename}', fileName); -await fetch(`${BASE}/messages`, { - method: 'POST', - headers: { ...headers, 'Content-Type': 'application/json' }, - body: JSON.stringify({ - message: { - entity_type: 'discussion', - entity_id: 12345, - content: 'Отчёт прикреплён', - files: [{ key: fileKey, name: fileName, file_type: 'file', size: fileBuffer.length }] - } - }) -}); +const fileKey = params.key.replace("${filename}", fileName) +await client.messages.createMessage({ + message: { + entityType: "discussion", + entityId: 12345, + content: "Отчёт прикреплён", + files: [{ key: fileKey, name: fileName, fileType: FileType.File, size: fileBuffer.length }] + } +}) +``` + +### Python SDK + +```python +from pachca.client import PachcaClient +from pachca.models import FileUploadRequest, MessageCreateRequest, MessageCreateRequestMessage, MessageCreateRequestFile, FileType +import os + +client = PachcaClient("YOUR_TOKEN") + +file_path = "report.pdf" +file_name = os.path.basename(file_path) + +# Шаг 1: Получить параметры загрузки +params = await client.common.get_upload_params() + +# Шаг 2: Загрузить файл на S3 (direct_url — внешний presigned URL) +with open(file_path, "rb") as f: + await client.common.upload_file( + direct_url=params.direct_url, + request=FileUploadRequest( + content_disposition=params.content_disposition, + acl=params.acl, + policy=params.policy, + x_amz_credential=params.x_amz_credential, + x_amz_algorithm=params.x_amz_algorithm, + x_amz_date=params.x_amz_date, + x_amz_signature=params.x_amz_signature, + key=params.key, + file=f.read() + ) + ) + +# Шаг 3: Отправить сообщение с файлом +file_key = params.key.replace("${filename}", file_name) +await client.messages.create_message(MessageCreateRequest( + message=MessageCreateRequestMessage( + entity_type="discussion", + entity_id=12345, + content="Отчёт прикреплён", + files=[MessageCreateRequestFile(key=file_key, name=file_name, file_type=FileType.FILE, size=os.path.getsize(file_path))] + ) +)) ``` +## Поля multipart-формы для S3 + +Все поля из ответа [Получение подписи](POST /uploads) передаются на S3 **как есть** в multipart-форме: + +| Поле | Описание | +|------|----------| +| `Content-Disposition` | Заголовок для скачивания | +| `acl` | Права доступа S3 | +| `policy` | Base64-кодированная политика загрузки | +| `x-amz-credential` | AWS credential | +| `x-amz-algorithm` | Всегда `AWS4-HMAC-SHA256` | +| `x-amz-date` | Дата подписи | +| `x-amz-signature` | Подпись запроса | +| `key` | Путь файла на S3 (содержит `${filename}` — заменить на реальное имя) | +| `file` | Сам файл — **обязательно последнее поле** | + +`direct_url` — это внешний presigned URL от S3. Он **не** является эндпоинтом Пачки, не требует заголовка `Authorization` и имеет ограниченное время действия. + ## Частые ошибки | Ошибка | Причина | Решение | |--------|---------|---------| -| `403 Forbidden` при загрузке | Истекла подпись | Параметры загрузки действительны ограниченное время. Запросите новые через `POST /uploads` | +| `403 Forbidden` при загрузке | Истекла подпись | Параметры загрузки действительны ограниченное время. Запросите новые через [Получение подписи](POST /uploads) | | `400 Bad Request` | Неправильный Content-Type | Убедитесь, что запрос отправляется как `multipart/form-data`, а не `application/json` | | Файл не отображается | Неверный `key` | Проверьте, что `${filename}` в ключе заменён на реальное имя файла | diff --git a/apps/docs/content/guides/webhook.mdx b/apps/docs/content/guides/webhook.mdx index d856624a..9070892d 100644 --- a/apps/docs/content/guides/webhook.mdx +++ b/apps/docs/content/guides/webhook.mdx @@ -146,6 +146,130 @@ content-length: 358 +## Реализация webhook handler + +Полный пример обработки вебхуков на TypeScript (Express.js) и Python (Flask) с проверкой подписи, защитой от replay-атак и обработкой всех типов событий. + +### TypeScript (Express.js) + +```typescript +import express from "express" +import crypto from "crypto" + +const SIGNING_SECRET = "your_signing_secret" // Из настроек бота → Исходящий Webhook → Signing Secret +const app = express() + +// Важно: используем express.raw для получения сырого тела запроса (для корректной проверки HMAC) +app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => { + // 1. Проверка подписи HMAC-SHA256 + const signature = crypto.createHmac("sha256", SIGNING_SECRET) + .update(req.body).digest("hex") + if (signature !== req.headers["pachca-signature"]) { + return res.status(401).send("Invalid signature") + } + + // 2. Защита от replay-атак (±60 секунд) + const event = JSON.parse(req.body.toString()) + if (Math.abs(Date.now() / 1000 - event.webhook_timestamp) > 60) { + return res.status(401).send("Expired event") + } + + // 3. Обработка события по типу + switch (event.type) { + case "message": + if (event.event === "new") { + console.log(`Новое сообщение от ${event.user_id}: ${event.content}`) + } else if (event.event === "update") { + console.log(`Сообщение ${event.id} отредактировано`) + } else if (event.event === "delete") { + console.log(`Сообщение ${event.id} удалено`) + } + break + case "reaction": + console.log(`${event.event === "new" ? "Добавлена" : "Удалена"} реакция ${event.emoji}`) + break + case "button": + console.log(`Нажата кнопка: ${event.data}`) + // trigger_id доступен 3 секунды — используйте его для открытия формы + break + case "view_submit": + console.log(`Форма заполнена:`, event.payload) + break + } + + res.status(200).send("OK") +}) +app.listen(3000) +``` + +### Python (Flask) + +```python +import hmac, hashlib, json, time +from flask import Flask, request, abort + +SIGNING_SECRET = "your_signing_secret" # Из настроек бота → Исходящий Webhook → Signing Secret +app = Flask(__name__) + +@app.route("/webhook", methods=["POST"]) +def webhook(): + raw_body = request.get_data() + + # 1. Проверка подписи HMAC-SHA256 + expected = hmac.new(SIGNING_SECRET.encode(), raw_body, hashlib.sha256).hexdigest() + if expected != request.headers.get("Pachca-Signature"): + abort(401) + + # 2. Защита от replay-атак (±60 секунд) + event = json.loads(raw_body) + if abs(time.time() - event["webhook_timestamp"]) > 60: + abort(401) + + # 3. Обработка события + if event["type"] == "message" and event["event"] == "new": + print(f"Новое сообщение от {event['user_id']}: {event['content']}") + elif event["type"] == "button": + print(f"Нажата кнопка: {event['data']}") + elif event["type"] == "view_submit": + print(f"Форма заполнена: {event['payload']}") + + return "OK", 200 +``` + +### Идемпотентная обработка + +Пачка использует **at-least-once delivery** — один и тот же вебхук может прийти повторно. Обработчик должен быть идемпотентным: + +```typescript +// Дедупликация по уникальным полям события +const processed = new Set() + +function getEventKey(event: any): string { + // Уникальный ключ: тип + событие + id объекта + return `${event.type}:${event.event}:${event.id || event.message_id || ""}` +} + +app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => { + // ... проверка подписи ... + const event = JSON.parse(req.body.toString()) + const key = getEventKey(event) + if (processed.has(key)) { + return res.status(200).send("Already processed") + } + processed.add(key) + processEvent(event) // Ваша логика обработки + res.status(200).send("OK") +}) +``` + +### Обработка ошибок доставки + +Если ваш сервер не ответил `2xx` в течение таймаута, Пачка повторит попытку доставки. Рекомендации: + +- Отвечайте `200 OK` как можно быстрее — выносите тяжёлую обработку в фоновую очередь +- При временных ошибках отвечайте `503` — Пачка повторит позже +- При постоянных ошибках (невалидные данные) — `200 OK` чтобы избежать бесконечных повторов + ## Поллинг Если у вас нет возможности принимать входящие HTTP-запросы (локальная разработка, жёсткие firewall-правила), используйте **поллинг** — получение событий через API. @@ -156,3 +280,23 @@ content-length: 358 - [Удалить событие](DELETE /webhooks/events/{id}) — удалить обработанное событие из очереди Периодически запрашивайте список событий, обрабатывайте каждое и удаляйте обработанные. + +### Пример поллинга (TypeScript) + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_BOT_TOKEN") + +async function pollEvents() { + const events = await client.bots.getWebhookEvents() + for (const event of events.data) { + console.log("Событие:", event.type, event.event) + // Обработать событие... + await client.bots.deleteWebhookEvent(event.id) // Удалить из очереди + } +} + +// Запускать каждые 5 секунд +setInterval(pollEvents, 5000) +``` diff --git a/apps/docs/lib/sdk-examples.ts b/apps/docs/lib/sdk-examples.ts index 6c803501..325ef51e 100644 --- a/apps/docs/lib/sdk-examples.ts +++ b/apps/docs/lib/sdk-examples.ts @@ -132,6 +132,64 @@ export function getSdkExampleForLang( return parts.join('\n\n'); } +/** + * Build-time validation: returns all valid SDK symbols (service.method pairs and import names) + * extracted from examples.json for a given language. + */ +export function getValidSdkSymbols(lang: string): { + methods: Map>; + imports: Set; +} { + const all = loadAll(); + const sdkLang = lang as SdkLanguage; + const langData = all[sdkLang]; + if (!langData) return { methods: new Map(), imports: new Set() }; + + const methods = new Map>(); + const imports = new Set(); + const methodRegex = /client\.(\w+)\.(\w+)\s*\(/g; + + for (const [opId, entry] of Object.entries(langData)) { + if (opId === 'Client_Init') continue; + + // Extract service.method pairs from usage code + let match; + while ((match = methodRegex.exec(entry.usage)) !== null) { + const [, service, method] = match; + if (!methods.has(service)) methods.set(service, new Set()); + methods.get(service)!.add(method); + } + methodRegex.lastIndex = 0; + + if (entry.imports) { + for (const imp of entry.imports) { + imports.add(imp); + } + } + } + + // Client_Init imports (PachcaClient etc.) + const initEntry = langData['Client_Init']; + if (initEntry?.imports) { + for (const imp of initEntry.imports) { + imports.add(imp); + } + } + + // Add *All / *_all pagination variants (auto-generated wrappers not in examples.json) + for (const methodSet of methods.values()) { + const variants: string[] = []; + for (const method of methodSet) { + variants.push(lang === 'python' ? method + '_all' : method + 'All'); + } + for (const v of variants) { + methodSet.add(v); + } + } + + return { methods, imports }; +} + export function getSdkExamples(operationId: string): Record { const all = loadAll(); const result: Record = {}; diff --git a/apps/docs/lib/tabs-config.ts b/apps/docs/lib/tabs-config.ts index bfebaf65..f564af70 100644 --- a/apps/docs/lib/tabs-config.ts +++ b/apps/docs/lib/tabs-config.ts @@ -150,6 +150,23 @@ export function getActiveTab(pathname: string): TabId | null { return null; } +/** + * Get the parent item title for a nested page (e.g. "n8n" for /guides/n8n/advanced). + * Returns null for top-level pages. Used to disambiguate shared child titles like "Обзор". + */ +export function getNestedParentTitle(pathname: string): string | null { + for (const section of GUIDE_SECTIONS) { + for (const item of section.items) { + if (!item.children) continue; + if (item.path === pathname) return item.title; + for (const child of item.children) { + if (child.path === pathname) return item.title; + } + } + } + return null; +} + /** * Get the section title for a given pathname (for breadcrumb labels). */ diff --git a/apps/docs/public/api/errors.md b/apps/docs/public/api/errors.md index 6f4269f2..d0f9fe62 100644 --- a/apps/docs/public/api/errors.md +++ b/apps/docs/public/api/errors.md @@ -100,3 +100,84 @@ - **Лимиты гибкие:** они ориентировочные и могут меняться, чтобы всё работало гладко; - **Должно хватать на всё:** мы настроили их так, чтобы вам было комфортно в любых сценариях; - **Если упёрлись в лимит:** при ошибке `429` смотрите заголовок `Retry-After` — он подскажет, через сколько секунд повторить запрос (или используйте экспоненциальный `backoff`, если хотите перестраховаться). + +## Повторные запросы (Retry) + +[SDK](/guides/sdk/overview) ([TypeScript](/guides/sdk/typescript), [Python](/guides/sdk/python)) уже включают автоматический retry с экспоненциальным backoff. Ниже — реализация для кастомных HTTP-клиентов. + +### TypeScript + +```typescript +async function withRetry( + fn: () => Promise, + maxRetries = 3 +): Promise { + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await fn() + } catch (error: any) { + // 429 Too Many Requests — ждём Retry-After или backoff + if (error.status === 429) { + const retryAfter = error.headers?.["retry-after"] + const delay = retryAfter + ? parseInt(retryAfter) * 1000 + : Math.pow(2, attempt) * 1000 * (0.5 + Math.random()) + await new Promise(r => setTimeout(r, delay)) + continue + } + // 5xx — серверная ошибка, backoff с jitter + if (error.status >= 500 && attempt < maxRetries) { + const delay = Math.pow(2, attempt) * 1000 * (0.5 + Math.random()) + await new Promise(r => setTimeout(r, delay)) + continue + } + // 4xx (кроме 429) — не повторяем + throw error + } + } + throw new Error("Max retries exceeded") +} + +// Использование +const users = await withRetry(() => client.users.listUsers()) +``` + +### Python + +```python +import time, random + +async def with_retry(fn, max_retries=3): + for attempt in range(max_retries + 1): + try: + return await fn() + except Exception as e: + status = getattr(e, "status_code", getattr(e, "status", 0)) + headers = getattr(e, "headers", {}) + # 429 Too Many Requests + if status == 429: + retry_after = headers.get("Retry-After") + delay = int(retry_after) if retry_after else (2 ** attempt) * (0.5 + random.random()) + time.sleep(delay) + continue + # 5xx — серверная ошибка + if status >= 500 and attempt < max_retries: + time.sleep((2 ** attempt) * (0.5 + random.random())) + continue + raise + raise Exception("Max retries exceeded") + +# Использование +users = await with_retry(lambda: client.users.list_users()) +``` + +### Стратегия повторов + +| Код | Действие | Задержка | +|-----|----------|----------| +| `429` | Повторить | `Retry-After` header или exponential backoff: 1с, 2с, 4с × jitter | +| `500`, `502`, `503`, `504` | Повторить | Exponential backoff с jitter: ~1с, ~2с, ~4с | +| `400`, `401`, `403`, `404`, `422` | **Не повторять** | Ошибка клиента — нужно исправить запрос | + +> Максимум **3 повтора** на каждый запрос. Jitter (случайный множитель 0.5–1.5) предотвращает «thundering herd» при массовых 429. + diff --git a/apps/docs/public/api/file-uploads.md b/apps/docs/public/api/file-uploads.md index c9ff0c3e..8d16b373 100644 --- a/apps/docs/public/api/file-uploads.md +++ b/apps/docs/public/api/file-uploads.md @@ -92,59 +92,116 @@ pachca messages create --entity-id 12345 \ | Файл | `file` | — | | Изображение | `image` | `width`, `height` — размеры в пикселях | -## Полный пример +## Примеры полного цикла -```javascript title="Node.js: загрузка и отправка файла" -const fs = require('node:fs'); -const path = require('node:path'); +### TypeScript SDK -const TOKEN = 'ваш_токен'; -const BASE = 'https://api.pachca.com/api/shared/v1'; -const headers = { Authorization: `Bearer ${TOKEN}` }; +```typescript +import { PachcaClient, FileUploadRequest, FileType } from "@pachca/sdk" +import fs from "fs" +import path from "path" + +const client = new PachcaClient("YOUR_TOKEN") + +const filePath = "./report.pdf" +const fileName = path.basename(filePath) +const fileBuffer = fs.readFileSync(filePath) // Шаг 1: Получить параметры загрузки -const { data: params } = await fetch(`${BASE}/uploads`, { - method: 'POST', headers -}).then(r => r.json()); - -// Шаг 2: Загрузить файл -const filePath = './report.pdf'; -const fileName = path.basename(filePath); -const fileBuffer = fs.readFileSync(filePath); - -const form = new FormData(); -form.append('Content-Disposition', params['Content-Disposition']); -form.append('acl', params.acl); -form.append('policy', params.policy); -form.append('x-amz-credential', params['x-amz-credential']); -form.append('x-amz-algorithm', params['x-amz-algorithm']); -form.append('x-amz-date', params['x-amz-date']); -form.append('x-amz-signature', params['x-amz-signature']); -form.append('key', params.key); -form.append('file', new File([fileBuffer], fileName)); - -await fetch(params.direct_url, { method: 'POST', body: form }); +const params = await client.common.getUploadParams() + +// Шаг 2: Загрузить файл на S3 (direct_url — внешний presigned URL) +await client.common.uploadFile(params.directUrl, { + contentDisposition: params.contentDisposition, + acl: params.acl, + policy: params.policy, + xAmzCredential: params.xAmzCredential, + xAmzAlgorithm: params.xAmzAlgorithm, + xAmzDate: params.xAmzDate, + xAmzSignature: params.xAmzSignature, + key: params.key, + file: new File([fileBuffer], fileName) +}) // Шаг 3: Отправить сообщение с файлом -const fileKey = params.key.replace('${filename}', fileName); -await fetch(`${BASE}/messages`, { - method: 'POST', - headers: { ...headers, 'Content-Type': 'application/json' }, - body: JSON.stringify({ - message: { - entity_type: 'discussion', - entity_id: 12345, - content: 'Отчёт прикреплён', - files: [{ key: fileKey, name: fileName, file_type: 'file', size: fileBuffer.length }] - } - }) -}); +const fileKey = params.key.replace("${filename}", fileName) +await client.messages.createMessage({ + message: { + entityType: "discussion", + entityId: 12345, + content: "Отчёт прикреплён", + files: [{ key: fileKey, name: fileName, fileType: FileType.File, size: fileBuffer.length }] + } +}) +``` + +### Python SDK + +```python +from pachca.client import PachcaClient +from pachca.models import FileUploadRequest, MessageCreateRequest, MessageCreateRequestMessage, MessageCreateRequestFile, FileType +import os + +client = PachcaClient("YOUR_TOKEN") + +file_path = "report.pdf" +file_name = os.path.basename(file_path) + +# Шаг 1: Получить параметры загрузки +params = await client.common.get_upload_params() + +# Шаг 2: Загрузить файл на S3 (direct_url — внешний presigned URL) +with open(file_path, "rb") as f: + await client.common.upload_file( + direct_url=params.direct_url, + request=FileUploadRequest( + content_disposition=params.content_disposition, + acl=params.acl, + policy=params.policy, + x_amz_credential=params.x_amz_credential, + x_amz_algorithm=params.x_amz_algorithm, + x_amz_date=params.x_amz_date, + x_amz_signature=params.x_amz_signature, + key=params.key, + file=f.read() + ) + ) + +# Шаг 3: Отправить сообщение с файлом +file_key = params.key.replace("${filename}", file_name) +await client.messages.create_message(MessageCreateRequest( + message=MessageCreateRequestMessage( + entity_type="discussion", + entity_id=12345, + content="Отчёт прикреплён", + files=[MessageCreateRequestFile(key=file_key, name=file_name, file_type=FileType.FILE, size=os.path.getsize(file_path))] + ) +)) ``` +## Поля multipart-формы для S3 + +Все поля из ответа [Получение подписи](POST /uploads) передаются на S3 **как есть** в multipart-форме: + +| Поле | Описание | +|------|----------| +| `Content-Disposition` | Заголовок для скачивания | +| `acl` | Права доступа S3 | +| `policy` | Base64-кодированная политика загрузки | +| `x-amz-credential` | AWS credential | +| `x-amz-algorithm` | Всегда `AWS4-HMAC-SHA256` | +| `x-amz-date` | Дата подписи | +| `x-amz-signature` | Подпись запроса | +| `key` | Путь файла на S3 (содержит `${filename}` — заменить на реальное имя) | +| `file` | Сам файл — **обязательно последнее поле** | + +> **Внимание:** `direct_url` — это внешний presigned URL от S3. Он **не** является эндпоинтом Пачки, не требует заголовка `Authorization` и имеет ограниченное время действия. + + ## Частые ошибки | Ошибка | Причина | Решение | |--------|---------|---------| -| `403 Forbidden` при загрузке | Истекла подпись | Параметры загрузки действительны ограниченное время. Запросите новые через `POST /uploads` | +| `403 Forbidden` при загрузке | Истекла подпись | Параметры загрузки действительны ограниченное время. Запросите новые через [Получение подписи](POST /uploads) | | `400 Bad Request` | Неправильный Content-Type | Убедитесь, что запрос отправляется как `multipart/form-data`, а не `application/json` | | Файл не отображается | Неверный `key` | Проверьте, что `${filename}` в ключе заменён на реальное имя файла | diff --git a/apps/docs/public/guides/webhook.md b/apps/docs/public/guides/webhook.md index 172f3588..add022e2 100644 --- a/apps/docs/public/guides/webhook.md +++ b/apps/docs/public/guides/webhook.md @@ -243,6 +243,130 @@ content-length: 358 IP-адрес Пачки: `37.200.70.177` +## Реализация webhook handler + +Полный пример обработки вебхуков на TypeScript (Express.js) и Python (Flask) с проверкой подписи, защитой от replay-атак и обработкой всех типов событий. + +### TypeScript (Express.js) + +```typescript +import express from "express" +import crypto from "crypto" + +const SIGNING_SECRET = "your_signing_secret" // Из настроек бота → Исходящий Webhook → Signing Secret +const app = express() + +// Важно: используем express.raw для получения сырого тела запроса (для корректной проверки HMAC) +app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => { + // 1. Проверка подписи HMAC-SHA256 + const signature = crypto.createHmac("sha256", SIGNING_SECRET) + .update(req.body).digest("hex") + if (signature !== req.headers["pachca-signature"]) { + return res.status(401).send("Invalid signature") + } + + // 2. Защита от replay-атак (±60 секунд) + const event = JSON.parse(req.body.toString()) + if (Math.abs(Date.now() / 1000 - event.webhook_timestamp) > 60) { + return res.status(401).send("Expired event") + } + + // 3. Обработка события по типу + switch (event.type) { + case "message": + if (event.event === "new") { + console.log(`Новое сообщение от ${event.user_id}: ${event.content}`) + } else if (event.event === "update") { + console.log(`Сообщение ${event.id} отредактировано`) + } else if (event.event === "delete") { + console.log(`Сообщение ${event.id} удалено`) + } + break + case "reaction": + console.log(`${event.event === "new" ? "Добавлена" : "Удалена"} реакция ${event.emoji}`) + break + case "button": + console.log(`Нажата кнопка: ${event.data}`) + // trigger_id доступен 3 секунды — используйте его для открытия формы + break + case "view_submit": + console.log(`Форма заполнена:`, event.payload) + break + } + + res.status(200).send("OK") +}) +app.listen(3000) +``` + +### Python (Flask) + +```python +import hmac, hashlib, json, time +from flask import Flask, request, abort + +SIGNING_SECRET = "your_signing_secret" # Из настроек бота → Исходящий Webhook → Signing Secret +app = Flask(__name__) + +@app.route("/webhook", methods=["POST"]) +def webhook(): + raw_body = request.get_data() + + # 1. Проверка подписи HMAC-SHA256 + expected = hmac.new(SIGNING_SECRET.encode(), raw_body, hashlib.sha256).hexdigest() + if expected != request.headers.get("Pachca-Signature"): + abort(401) + + # 2. Защита от replay-атак (±60 секунд) + event = json.loads(raw_body) + if abs(time.time() - event["webhook_timestamp"]) > 60: + abort(401) + + # 3. Обработка события + if event["type"] == "message" and event["event"] == "new": + print(f"Новое сообщение от {event['user_id']}: {event['content']}") + elif event["type"] == "button": + print(f"Нажата кнопка: {event['data']}") + elif event["type"] == "view_submit": + print(f"Форма заполнена: {event['payload']}") + + return "OK", 200 +``` + +### Идемпотентная обработка + +Пачка использует **at-least-once delivery** — один и тот же вебхук может прийти повторно. Обработчик должен быть идемпотентным: + +```typescript +// Дедупликация по уникальным полям события +const processed = new Set() + +function getEventKey(event: any): string { + // Уникальный ключ: тип + событие + id объекта + return `${event.type}:${event.event}:${event.id || event.message_id || ""}` +} + +app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => { + // ... проверка подписи ... + const event = JSON.parse(req.body.toString()) + const key = getEventKey(event) + if (processed.has(key)) { + return res.status(200).send("Already processed") + } + processed.add(key) + processEvent(event) // Ваша логика обработки + res.status(200).send("OK") +}) +``` + +### Обработка ошибок доставки + +Если ваш сервер не ответил `2xx` в течение таймаута, Пачка повторит попытку доставки. Рекомендации: + +- Отвечайте `200 OK` как можно быстрее — выносите тяжёлую обработку в фоновую очередь +- При временных ошибках отвечайте `503` — Пачка повторит позже +- При постоянных ошибках (невалидные данные) — `200 OK` чтобы избежать бесконечных повторов + ## Поллинг Если у вас нет возможности принимать входящие HTTP-запросы (локальная разработка, жёсткие firewall-правила), используйте **поллинг** — получение событий через API. @@ -254,3 +378,23 @@ content-length: 358 > Периодически запрашивайте список событий, обрабатывайте каждое и удаляйте обработанные. + +### Пример поллинга (TypeScript) + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_BOT_TOKEN") + +async function pollEvents() { + const events = await client.bots.getWebhookEvents() + for (const event of events.data) { + console.log("Событие:", event.type, event.event) + // Обработать событие... + await client.bots.deleteWebhookEvent(event.id) // Удалить из очереди + } +} + +// Запускать каждые 5 секунд +setInterval(pollEvents, 5000) +``` diff --git a/apps/docs/public/llms-full.txt b/apps/docs/public/llms-full.txt index 5a09627b..40acd96c 100644 --- a/apps/docs/public/llms-full.txt +++ b/apps/docs/public/llms-full.txt @@ -65,17 +65,92 @@ - [Bots](#api-bots) - [Security](#api-security) -### SDK -- [SDK](#sdk) ---- +## Document Map -# SDK +| Section | Description | Lines | +|---------|-------------|-------| +| LIBRARY RULES | Core rules, auth, pagination, rate limits, SDK overview | 80–162 | +| How-to Guides | Step-by-step solutions with TypeScript + Python code | 163–622 | +| Guides | Full SDK docs (6 languages), webhooks, bots, forms, n8n | 623–9745 | +| API Reference | Complete REST API — every endpoint with schemas and examples | 9746–24664 | -Типизированные клиенты для 6 языков. Единый паттерн: `PachcaClient(token)` → `client.service.method(request)`. +--- -| Язык | Пакет | Установка | -|------|-------|----------| +# LIBRARY RULES + +## Authentication +- All requests require Bearer token in Authorization header: `Authorization: Bearer ` +- Token types: **admin** (full access — manage users, tags, delete messages), **bot** (send messages with custom name/avatar, receive webhooks), **user** (limited access) +- Get admin token: Settings → Automations → API. Get bot token: per-bot in Settings → Automations → Integrations +- Tokens are long-lived and do not expire. Can be reset by admin in Settings +- TypeScript SDK: `const client = new PachcaClient("YOUR_TOKEN")` +- Python SDK: `client = PachcaClient("YOUR_TOKEN")` + +## Pagination +- Cursor-based: use `limit` (1–50, default 50) and `cursor` query parameters +- Response includes `meta.paginate.next_page` — cursor for next page +- End of data: when `data` array is empty (cursor is never null) +- TypeScript auto-pagination: `client.users.listUsersAll()` returns flat array of all results +- Python auto-pagination: `await client.users.list_users_all()` returns list of all results +- Available for: users, chats, messages, members, tags, reactions, tasks, search results, audit events, webhook events + +## Rate Limiting +- Messages (POST/PUT/DELETE /messages): ~4 req/sec per chat (burst: 30/sec for 5s) +- Message read (GET /messages): ~10 req/sec +- Other endpoints: ~50 req/sec +- On `429 Too Many Requests`: respect `Retry-After` header value (seconds) +- Recommended retry strategy: exponential backoff with jitter — base delay × 2^attempt × random(0.5–1.5) +- SDK (@pachca/sdk, pachca-sdk) handles retry automatically: 3 retries, respects Retry-After, exponential backoff for 5xx + +## Webhooks (Real-time Events) +- Create a bot in Pachca: Automations → Integrations → Bots +- Set webhook URL in bot settings → Outgoing Webhook tab +- Events: `new_message`, `edit_message`, `delete_message`, `new_reaction`, `delete_reaction`, `button_pressed`, `view_submit`, `chat_member_changed`, `company_member_changed`, `link_shared` +- Verify: HMAC-SHA256 of raw body with bot's Signing Secret +- Header: `Pachca-Signature` contains hex digest +- Replay protection: check `webhook_timestamp` within ±60 seconds of current time +- Alternative to webhooks: polling via GET /webhooks/events (enable "Save event history" in bot settings) +- IP whitelist: Pachca webhook IP is `37.200.70.177` + +## File Uploads (3-step process) +- Step 1: POST /uploads → get S3 presigned params (`direct_url`, `key`, `policy`, `x-amz-signature`, etc.) +- Step 2: POST to `direct_url` (external S3 URL, NOT a Pachca API endpoint) with multipart/form-data — all params + `file` as LAST field +- Step 3: Replace `${filename}` in `key` with actual filename, include in message `files` array +- Note: `direct_url` is an external S3 presigned URL and does not require Authorization header + +## User Status +- Get any user's status: GET /users/{id}/status → `{ emoji, title, expires_at, is_away, away_message }` +- Set own status: PUT /profile/status `{ status: { emoji, title, expires_at, is_away, away_message } }` +- Clear own status: DELETE /profile/status +- Admin can manage any user: PUT /users/{id}/status, DELETE /users/{id}/status +- No real-time presence webhooks — use polling with ≥60s interval for monitoring status changes + +## Error Handling +- `400`: validation errors — `{ errors: [{ key, value, message, code }] }` with codes: `blank`, `invalid`, `taken`, `too_short`, `too_long`, `not_a_number` +- `401`: unauthorized — `{ error, error_description }` (OAuthError) +- `403`: forbidden — insufficient permissions. May return ApiError (business logic) or OAuthError (`insufficient_scope`) +- `404`: not found +- `409`: conflict (duplicate) +- `422`: unprocessable — `{ errors: [{ key, value, message, code }] }` +- `429`: rate limited — respect `Retry-After` header, use exponential backoff with jitter +- SDK auto-retries `429` and `5xx` errors (3 attempts with exponential backoff) + +## Idempotency and Reliability +- Pachca API operations are NOT idempotent by default — duplicate POST requests create duplicate resources +- Client-side deduplication: track request IDs, check before sending, store results with TTL +- Webhooks use at-least-once delivery — handlers MUST be idempotent (dedup by event fields: id + type + event) +- For multi-step operations: implement compensating actions (saga pattern) for failure recovery +- Separate critical operations (create chat) from non-critical (send welcome message) with independent error handling + +## SDK (Typed Clients for 6 Languages) +- Unified pattern: `PachcaClient(token)` → `client.service.method(request)` +- **Input**: path params and body fields (≤2) expand to method arguments; otherwise a single request object +- **Output**: if API response has a single `data` field, SDK returns its contents directly +- Service, method, and field names match operationId and parameters from OpenAPI + +| Language | Package | Install | +|----------|---------|---------| | TypeScript | `@pachca/sdk` | `npm install @pachca/sdk` | | Python | `pachca-sdk` | `pip install pachca-sdk` | | Go | `github.com/pachca/go-sdk` | `go get github.com/pachca/openapi/sdk/go/generated` | @@ -83,58 +158,466 @@ | Swift | `PachcaSDK` | SPM: `https://github.com/pachca/openapi` | | C# | `Pachca.Sdk` | `dotnet add package Pachca.Sdk` | -## Конвенции SDK +--- + +# How-to Guides -- **Вход**: path-параметры и body-поля (если ≤2) разворачиваются в аргументы метода. Иначе — один объект-запрос. -- **Выход**: если ответ API содержит единственное поле `data`, SDK возвращает его содержимое напрямую. -- Имена сервисов, методов и полей соответствуют operationId и параметрам из OpenAPI. +## How to authenticate with the API -### Примеры вызова по языкам +All API requests require a Bearer token in the Authorization header. -**TypeScript:** +### curl +```bash +curl -H "Authorization: Bearer YOUR_TOKEN" https://api.pachca.com/api/shared/v1/users +``` + +### TypeScript SDK ```typescript -import { PachcaClient } from "@pachca/sdk"; -const pachca = new PachcaClient("YOUR_TOKEN"); -const users = await pachca.users.listUsers(); -await pachca.reactions.addReaction(messageId, { code: "👍" }); +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") +const profile = await client.profile.getProfile() +console.log(profile.id, profile.firstName) ``` -**Python:** +### Python SDK ```python -from pachca import PachcaClient +from pachca.client import PachcaClient + client = PachcaClient("YOUR_TOKEN") -users = await client.users.list_users() -await client.reactions.add_reaction(message_id, ReactionRequest(code="👍")) +profile = await client.profile.get_profile() +print(profile.id, profile.first_name) ``` -**Go:** -```go -client := pachca.NewPachcaClient("YOUR_TOKEN") -users, err := client.Users.ListUsers(ctx, nil) -reaction, err := client.Reactions.AddReaction(ctx, messageId, pachca.ReactionRequest{Code: "👍"}) +Token types: **admin** (full access, get in Settings → Automations → API), **bot** (messaging + webhooks, per-bot in Integrations), **user** (limited). + +## How to paginate through results + +### TypeScript SDK — auto-pagination +```typescript +// Returns all results as a flat array (handles cursors automatically) +const allUsers = await client.users.listUsersAll() +console.log(`Total: ${allUsers.length}`) + +// Manual pagination with cursor control +let cursor: string | undefined +for (;;) { + const response = await client.users.listUsers({ limit: 50, cursor }) + if (response.data.length === 0) break + for (const user of response.data) { + console.log(user.firstName, user.lastName) + } + cursor = response.meta.paginate.nextPage +} ``` -**Kotlin:** -```kotlin -val pachca = PachcaClient("YOUR_TOKEN") -val users = pachca.users.listUsers() -pachca.reactions.addReaction(messageId, ReactionRequest(code = "👍")) +### Python SDK — auto-pagination +```python +# Returns all results as a list (handles cursors automatically) +all_users = await client.users.list_users_all() +print(f"Total: {len(all_users)}") + +# Manual pagination with cursor control +from pachca.models import ListUsersParams + +cursor = None +while True: + response = await client.users.list_users(ListUsersParams(limit=50, cursor=cursor)) + if not response.data: + break + for user in response.data: + print(user.first_name, user.last_name) + cursor = response.meta.paginate.next_page ``` -**Swift:** -```swift -let pachca = PachcaClient(token: "YOUR_TOKEN") -let users = try await pachca.users.listUsers() -try await pachca.reactions.addReaction(messageId, ReactionRequest(code: "👍")) +Auto-pagination methods: `listUsersAll()`, `listChatsAll()`, `listChatMessagesAll()`, `listMembersAll()`, `listTagsAll()`, `listTasksAll()`, `searchMessagesAll()`, `searchChatsAll()`, `searchUsersAll()`, `listReactionsAll()`, `getAuditEventsAll()`, `getWebhookEventsAll()`. + +## How to create chats and manage members + +### TypeScript SDK +```typescript +import { PachcaClient, MessageEntityType } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +// Create a group chat with initial members +const chat = await client.chats.createChat({ + chat: { name: "Project Discussion", memberIds: [1, 2, 3], channel: false, public: false } +}) +console.log("Created chat:", chat.id, chat.name) + +// Add more members later +await client.members.addMembers(chat.id, { memberIds: [4, 5] }) + +// Remove a member +await client.members.removeMember(chat.id, 5) + +// List current members +const members = await client.members.listMembersAll(chat.id) +console.log("Members:", members.map(m => m.firstName)) + +// Send a message to the chat +await client.messages.createMessage({ + message: { entityType: MessageEntityType.Discussion, entityId: chat.id, content: "Welcome everyone!" } +}) ``` -**C#:** -```csharp -using var client = new PachcaClient("YOUR_TOKEN"); -var users = await client.Users.ListUsersAsync(); -await client.Reactions.AddReactionAsync(messageId, new ReactionRequest { Code = "👍" }); +### Python SDK +```python +from pachca.client import PachcaClient +from pachca.models import ChatCreateRequest, ChatCreateRequestChat, MessageCreateRequest, MessageCreateRequestMessage, AddMembersRequest + +client = PachcaClient("YOUR_TOKEN") + +# Create a group chat with initial members +chat = await client.chats.create_chat(ChatCreateRequest( + chat=ChatCreateRequestChat(name="Project Discussion", member_ids=[1, 2, 3], channel=False, public=False) +)) +print("Created chat:", chat.id, chat.name) + +# Add more members later +await client.members.add_members(chat.id, AddMembersRequest(member_ids=[4, 5])) + +# List current members +members = await client.members.list_members_all(chat.id) + +# Send a message to the chat +await client.messages.create_message(MessageCreateRequest( + message=MessageCreateRequestMessage(entity_type="discussion", entity_id=chat.id, content="Welcome everyone!") +)) +``` + +Chat types: `channel: true` creates a channel (one-way announcements), `channel: false` creates a group chat (everyone can write). `public: true` makes it visible to all workspace members. + +## How to set up webhooks for real-time updates + +### Step-by-step setup +1. Create a bot in Pachca: **Automations** → **Integrations** → **Bots** +2. In bot settings, go to **Outgoing Webhook** tab and set your HTTPS URL +3. Copy the **Signing Secret** for signature verification +4. Select event types: new messages, reactions, button presses, form submissions, etc. +5. Add the bot to chats where you want to receive events (global events like company member changes work without adding to chat) + +### TypeScript webhook handler (Express.js) +```typescript +import express from "express" +import crypto from "crypto" + +const SIGNING_SECRET = "your_signing_secret" // From bot settings → Outgoing Webhook +const app = express() + +app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => { + // Step 1: Verify HMAC-SHA256 signature + const signature = crypto.createHmac("sha256", SIGNING_SECRET) + .update(req.body).digest("hex") + if (signature !== req.headers["pachca-signature"]) { + return res.status(401).send("Invalid signature") + } + + // Step 2: Check timestamp for replay protection (±60 seconds) + const event = JSON.parse(req.body.toString()) + if (Math.abs(Date.now() / 1000 - event.webhook_timestamp) > 60) { + return res.status(401).send("Expired event") + } + + // Step 3: Process event by type + switch (event.type) { + case "message": + if (event.event === "new") console.log("New message:", event.content, "from user:", event.user_id) + if (event.event === "update") console.log("Message edited:", event.id) + if (event.event === "delete") console.log("Message deleted:", event.id) + break + case "reaction": + console.log(event.event === "new" ? "Reaction added:" : "Reaction removed:", event.emoji) + break + case "button": + console.log("Button pressed:", event.data, "by user:", event.user_id) + // Use event.trigger_id within 3 seconds to open a form + break + case "view_submit": + console.log("Form submitted:", event.payload) + break + } + + res.status(200).send("OK") +}) +app.listen(3000) +``` + +### Python webhook handler (Flask) +```python +import hmac, hashlib, json, time +from flask import Flask, request, abort + +SIGNING_SECRET = "your_signing_secret" # From bot settings → Outgoing Webhook +app = Flask(__name__) + +@app.route("/webhook", methods=["POST"]) +def webhook(): + raw_body = request.get_data() + + # Step 1: Verify HMAC-SHA256 signature + expected = hmac.new(SIGNING_SECRET.encode(), raw_body, hashlib.sha256).hexdigest() + if expected != request.headers.get("Pachca-Signature"): + abort(401) + + # Step 2: Check timestamp for replay protection + event = json.loads(raw_body) + if abs(time.time() - event["webhook_timestamp"]) > 60: + abort(401) + + # Step 3: Process event by type + if event["type"] == "message" and event["event"] == "new": + print("New message:", event["content"], "from user:", event["user_id"]) + elif event["type"] == "button": + print("Button pressed:", event["data"]) + + return "OK", 200 +``` + +### Webhook event types +| Event type | Description | Fields | +|-----------|-------------|--------| +| message (new) | New message in chat | id, content, user_id, chat_id, entity_type, entity_id, created_at, url | +| message (update) | Message edited | id, content, user_id, chat_id | +| message (delete) | Message deleted | id, user_id, chat_id | +| reaction (new/delete) | Reaction added/removed | message_id, user_id, emoji, chat_id | +| button | Button pressed | message_id, user_id, data, trigger_id, chat_id | +| view_submit | Form submitted | payload (form field values), user_id, trigger_id | +| chat_member (new/delete) | Member added/removed from chat | chat_id, user_id, event | +| company_member (new/update/delete) | Workspace member changes | user_id, event (no chat needed) | +| link_shared | URL shared (unfurl bots) | url, message_id, chat_id | + +### Alternative: Polling (when webhook URL is not available) +Enable "Save event history" in bot settings, then poll: +```typescript +// Poll for events periodically +const events = await client.bots.getWebhookEvents() +for (const event of events.data) { + processEvent(event) + await client.bots.deleteWebhookEvent(event.id) // Remove processed event +} +``` + +## How to handle rate limits and implement retry logic + +Both SDKs handle retry automatically — 3 retries with exponential backoff for `429` and `5xx` errors. No extra code needed: + +### TypeScript SDK +```typescript +import { PachcaClient, ApiError } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +// SDK retries 429 and 5xx automatically (3 attempts, exponential backoff) +// Just call methods normally — retries are transparent +const users = await client.users.listUsersAll() + +// If all retries are exhausted, ApiError is thrown +try { + await client.messages.createMessage({ + message: { entityId: 12345, content: "Hello" } + }) +} catch (error) { + if (error instanceof ApiError) { + // After 3 retries, the error is surfaced — check error.errors for details + for (const e of error.errors ?? []) { + console.error(e.key, e.message) + } + } +} +``` + +### Python SDK +```python +from pachca.client import PachcaClient +from pachca.models import ApiError + +client = PachcaClient("YOUR_TOKEN") + +# SDK retries 429 and 5xx automatically (3 attempts, exponential backoff) +# Just call methods normally — retries are transparent +users = await client.users.list_users_all() + +# If all retries are exhausted, ApiError is raised +try: + await client.messages.create_message(request) +except ApiError as e: + # After 3 retries, the error is surfaced — check e.errors for details + for err in e.errors: + print(err.key, err.message) +``` + +Rate limits by endpoint category: +- Messages send/edit/delete: ~4 req/sec per chat (burst: 30/sec for 5s) +- Messages read: ~10 req/sec per token +- All other endpoints: ~50 req/sec per token +- On 429: SDK respects `Retry-After` header automatically + +## How to upload files and attach to messages + +File upload is a 3-step process: get presigned params → upload to S3 → attach to message. + +### TypeScript SDK +```typescript +import { PachcaClient, FileType } from "@pachca/sdk" +import fs from "fs" +import path from "path" + +const client = new PachcaClient("YOUR_TOKEN") + +const filePath = "./report.pdf" +const fileName = path.basename(filePath) +const fileBuffer = fs.readFileSync(filePath) + +// Step 1: Get S3 presigned upload parameters +const params = await client.common.getUploadParams() + +// Step 2: Upload file to S3 (direct_url is an external presigned URL, not Pachca API) +await client.common.uploadFile(params.directUrl, { + contentDisposition: params.contentDisposition, + acl: params.acl, + policy: params.policy, + xAmzCredential: params.xAmzCredential, + xAmzAlgorithm: params.xAmzAlgorithm, + xAmzDate: params.xAmzDate, + xAmzSignature: params.xAmzSignature, + key: params.key, + file: new File([fileBuffer], fileName) +}) + +// Step 3: Attach file to message (replace ${filename} in key with actual name) +const fileKey = params.key.replace("${filename}", fileName) +await client.messages.createMessage({ + message: { + entityId: 12345, + content: "Report attached", + files: [{ key: fileKey, name: fileName, fileType: FileType.File, size: fileBuffer.length }] + } +}) +``` + +### Python SDK +```python +from pachca.client import PachcaClient +from pachca.models import ( + FileUploadRequest, MessageCreateRequest, MessageCreateRequestMessage, + MessageCreateRequestFile, FileType +) +import os + +client = PachcaClient("YOUR_TOKEN") + +file_path = "report.pdf" +file_name = os.path.basename(file_path) + +# Step 1: Get S3 presigned upload parameters +params = await client.common.get_upload_params() + +# Step 2: Upload file to S3 (direct_url is an external presigned URL, not Pachca API) +with open(file_path, "rb") as f: + await client.common.upload_file( + direct_url=params.direct_url, + request=FileUploadRequest( + content_disposition=params.content_disposition, + acl=params.acl, + policy=params.policy, + x_amz_credential=params.x_amz_credential, + x_amz_algorithm=params.x_amz_algorithm, + x_amz_date=params.x_amz_date, + x_amz_signature=params.x_amz_signature, + key=params.key, + file=f.read() + ) + ) + +# Step 3: Attach file to message (replace ${filename} in key with actual name) +file_key = params.key.replace("${filename}", file_name) +await client.messages.create_message(MessageCreateRequest( + message=MessageCreateRequestMessage( + entity_id=12345, + content="Report attached", + files=[MessageCreateRequestFile(key=file_key, name=file_name, file_type=FileType.FILE, size=os.path.getsize(file_path))] + ) +)) +``` + +File types: `FileType.File` / `FileType.FILE` (any file), `FileType.Image` / `FileType.IMAGE` (add `width` and `height`). + +## How to monitor user status and presence + +### TypeScript SDK +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +// Get any user's status +const status = await client.users.getUserStatus(userId) +console.log(status.emoji, status.title, status.isAway, status.awayMessage) + +// Set your own status +await client.profile.updateStatus({ + status: { emoji: "🏖️", title: "On vacation", isAway: true, awayMessage: "Back on Monday" } +}) + +// Set status with expiration +await client.profile.updateStatus({ + status: { emoji: "🍽️", title: "Lunch break", expiresAt: new Date(Date.now() + 3600000).toISOString() } +}) + +// Clear your status +await client.profile.deleteStatus() +``` + +### Python SDK +```python +from pachca.client import PachcaClient +from pachca.models import StatusUpdateRequest, StatusUpdateRequestStatus + +client = PachcaClient("YOUR_TOKEN") + +# Get any user's status +status = await client.users.get_user_status(user_id) +print(status.emoji, status.title, status.is_away, status.away_message) + +# Set your own status +await client.profile.update_status(StatusUpdateRequest( + status=StatusUpdateRequestStatus(emoji="🏖️", title="On vacation", is_away=True, away_message="Back on Monday") +)) + +# Clear your status +await client.profile.delete_status() ``` +### Polling pattern for monitoring status changes +Pachca does not provide real-time webhooks for status/presence changes. Use polling with caching: + +```typescript +const cache = new Map() +const CACHE_TTL = 60_000 // Minimum 60 seconds between polls per user + +async function getUserPresence(userId: number) { + const cached = cache.get(userId) + if (cached && Date.now() - cached.fetchedAt < CACHE_TTL) return cached.status + const status = await client.users.getUserStatus(userId) + cache.set(userId, { status, fetchedAt: Date.now() }) + return status +} + +// Batch refresh for a team +async function refreshTeamPresences(userIds: number[]) { + const results = [] + for (const id of userIds) { + cache.delete(id) + results.push(await getUserPresence(id)) + } + return results +} +``` + +Status fields: `emoji` (string), `title` (string), `expires_at` (ISO datetime or null), `is_away` (boolean), `away_message` (string). Admin can manage any user's status via PUT/DELETE /users/{id}/status. + --- # Руководства @@ -4646,6 +5129,130 @@ content-length: 358 IP-адрес Пачки: `37.200.70.177` +## Реализация webhook handler + +Полный пример обработки вебхуков на TypeScript (Express.js) и Python (Flask) с проверкой подписи, защитой от replay-атак и обработкой всех типов событий. + +### TypeScript (Express.js) + +```typescript +import express from "express" +import crypto from "crypto" + +const SIGNING_SECRET = "your_signing_secret" // Из настроек бота → Исходящий Webhook → Signing Secret +const app = express() + +// Важно: используем express.raw для получения сырого тела запроса (для корректной проверки HMAC) +app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => { + // 1. Проверка подписи HMAC-SHA256 + const signature = crypto.createHmac("sha256", SIGNING_SECRET) + .update(req.body).digest("hex") + if (signature !== req.headers["pachca-signature"]) { + return res.status(401).send("Invalid signature") + } + + // 2. Защита от replay-атак (±60 секунд) + const event = JSON.parse(req.body.toString()) + if (Math.abs(Date.now() / 1000 - event.webhook_timestamp) > 60) { + return res.status(401).send("Expired event") + } + + // 3. Обработка события по типу + switch (event.type) { + case "message": + if (event.event === "new") { + console.log(`Новое сообщение от ${event.user_id}: ${event.content}`) + } else if (event.event === "update") { + console.log(`Сообщение ${event.id} отредактировано`) + } else if (event.event === "delete") { + console.log(`Сообщение ${event.id} удалено`) + } + break + case "reaction": + console.log(`${event.event === "new" ? "Добавлена" : "Удалена"} реакция ${event.emoji}`) + break + case "button": + console.log(`Нажата кнопка: ${event.data}`) + // trigger_id доступен 3 секунды — используйте его для открытия формы + break + case "view_submit": + console.log(`Форма заполнена:`, event.payload) + break + } + + res.status(200).send("OK") +}) +app.listen(3000) +``` + +### Python (Flask) + +```python +import hmac, hashlib, json, time +from flask import Flask, request, abort + +SIGNING_SECRET = "your_signing_secret" # Из настроек бота → Исходящий Webhook → Signing Secret +app = Flask(__name__) + +@app.route("/webhook", methods=["POST"]) +def webhook(): + raw_body = request.get_data() + + # 1. Проверка подписи HMAC-SHA256 + expected = hmac.new(SIGNING_SECRET.encode(), raw_body, hashlib.sha256).hexdigest() + if expected != request.headers.get("Pachca-Signature"): + abort(401) + + # 2. Защита от replay-атак (±60 секунд) + event = json.loads(raw_body) + if abs(time.time() - event["webhook_timestamp"]) > 60: + abort(401) + + # 3. Обработка события + if event["type"] == "message" and event["event"] == "new": + print(f"Новое сообщение от {event['user_id']}: {event['content']}") + elif event["type"] == "button": + print(f"Нажата кнопка: {event['data']}") + elif event["type"] == "view_submit": + print(f"Форма заполнена: {event['payload']}") + + return "OK", 200 +``` + +### Идемпотентная обработка + +Пачка использует **at-least-once delivery** — один и тот же вебхук может прийти повторно. Обработчик должен быть идемпотентным: + +```typescript +// Дедупликация по уникальным полям события +const processed = new Set() + +function getEventKey(event: any): string { + // Уникальный ключ: тип + событие + id объекта + return `${event.type}:${event.event}:${event.id || event.message_id || ""}` +} + +app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => { + // ... проверка подписи ... + const event = JSON.parse(req.body.toString()) + const key = getEventKey(event) + if (processed.has(key)) { + return res.status(200).send("Already processed") + } + processed.add(key) + processEvent(event) // Ваша логика обработки + res.status(200).send("OK") +}) +``` + +### Обработка ошибок доставки + +Если ваш сервер не ответил `2xx` в течение таймаута, Пачка повторит попытку доставки. Рекомендации: + +- Отвечайте `200 OK` как можно быстрее — выносите тяжёлую обработку в фоновую очередь +- При временных ошибках отвечайте `503` — Пачка повторит позже +- При постоянных ошибках (невалидные данные) — `200 OK` чтобы избежать бесконечных повторов + ## Поллинг Если у вас нет возможности принимать входящие HTTP-запросы (локальная разработка, жёсткие firewall-правила), используйте **поллинг** — получение событий через API. @@ -4658,6 +5265,26 @@ content-length: 358 > Периодически запрашивайте список событий, обрабатывайте каждое и удаляйте обработанные. +### Пример поллинга (TypeScript) + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_BOT_TOKEN") + +async function pollEvents() { + const events = await client.bots.getWebhookEvents() + for (const event of events.data) { + console.log("Событие:", event.type, event.event) + // Обработать событие... + await client.bots.deleteWebhookEvent(event.id) // Удалить из очереди + } +} + +// Запускать каждые 5 секунд +setInterval(pollEvents, 5000) +``` + --- @@ -8263,60 +8890,117 @@ pachca messages create --entity-id 12345 \ | Файл | `file` | — | | Изображение | `image` | `width`, `height` — размеры в пикселях | -## Полный пример +## Примеры полного цикла -```javascript title="Node.js: загрузка и отправка файла" -const fs = require('node:fs'); -const path = require('node:path'); +### TypeScript SDK -const TOKEN = 'ваш_токен'; -const BASE = 'https://api.pachca.com/api/shared/v1'; -const headers = { Authorization: `Bearer ${TOKEN}` }; +```typescript +import { PachcaClient, FileUploadRequest, FileType } from "@pachca/sdk" +import fs from "fs" +import path from "path" + +const client = new PachcaClient("YOUR_TOKEN") + +const filePath = "./report.pdf" +const fileName = path.basename(filePath) +const fileBuffer = fs.readFileSync(filePath) // Шаг 1: Получить параметры загрузки -const { data: params } = await fetch(`${BASE}/uploads`, { - method: 'POST', headers -}).then(r => r.json()); - -// Шаг 2: Загрузить файл -const filePath = './report.pdf'; -const fileName = path.basename(filePath); -const fileBuffer = fs.readFileSync(filePath); - -const form = new FormData(); -form.append('Content-Disposition', params['Content-Disposition']); -form.append('acl', params.acl); -form.append('policy', params.policy); -form.append('x-amz-credential', params['x-amz-credential']); -form.append('x-amz-algorithm', params['x-amz-algorithm']); -form.append('x-amz-date', params['x-amz-date']); -form.append('x-amz-signature', params['x-amz-signature']); -form.append('key', params.key); -form.append('file', new File([fileBuffer], fileName)); - -await fetch(params.direct_url, { method: 'POST', body: form }); +const params = await client.common.getUploadParams() + +// Шаг 2: Загрузить файл на S3 (direct_url — внешний presigned URL) +await client.common.uploadFile(params.directUrl, { + contentDisposition: params.contentDisposition, + acl: params.acl, + policy: params.policy, + xAmzCredential: params.xAmzCredential, + xAmzAlgorithm: params.xAmzAlgorithm, + xAmzDate: params.xAmzDate, + xAmzSignature: params.xAmzSignature, + key: params.key, + file: new File([fileBuffer], fileName) +}) // Шаг 3: Отправить сообщение с файлом -const fileKey = params.key.replace('${filename}', fileName); -await fetch(`${BASE}/messages`, { - method: 'POST', - headers: { ...headers, 'Content-Type': 'application/json' }, - body: JSON.stringify({ - message: { - entity_type: 'discussion', - entity_id: 12345, - content: 'Отчёт прикреплён', - files: [{ key: fileKey, name: fileName, file_type: 'file', size: fileBuffer.length }] - } - }) -}); +const fileKey = params.key.replace("${filename}", fileName) +await client.messages.createMessage({ + message: { + entityType: "discussion", + entityId: 12345, + content: "Отчёт прикреплён", + files: [{ key: fileKey, name: fileName, fileType: FileType.File, size: fileBuffer.length }] + } +}) +``` + +### Python SDK + +```python +from pachca.client import PachcaClient +from pachca.models import FileUploadRequest, MessageCreateRequest, MessageCreateRequestMessage, MessageCreateRequestFile, FileType +import os + +client = PachcaClient("YOUR_TOKEN") + +file_path = "report.pdf" +file_name = os.path.basename(file_path) + +# Шаг 1: Получить параметры загрузки +params = await client.common.get_upload_params() + +# Шаг 2: Загрузить файл на S3 (direct_url — внешний presigned URL) +with open(file_path, "rb") as f: + await client.common.upload_file( + direct_url=params.direct_url, + request=FileUploadRequest( + content_disposition=params.content_disposition, + acl=params.acl, + policy=params.policy, + x_amz_credential=params.x_amz_credential, + x_amz_algorithm=params.x_amz_algorithm, + x_amz_date=params.x_amz_date, + x_amz_signature=params.x_amz_signature, + key=params.key, + file=f.read() + ) + ) + +# Шаг 3: Отправить сообщение с файлом +file_key = params.key.replace("${filename}", file_name) +await client.messages.create_message(MessageCreateRequest( + message=MessageCreateRequestMessage( + entity_type="discussion", + entity_id=12345, + content="Отчёт прикреплён", + files=[MessageCreateRequestFile(key=file_key, name=file_name, file_type=FileType.FILE, size=os.path.getsize(file_path))] + ) +)) ``` +## Поля multipart-формы для S3 + +Все поля из ответа [Получение подписи](POST /uploads) передаются на S3 **как есть** в multipart-форме: + +| Поле | Описание | +|------|----------| +| `Content-Disposition` | Заголовок для скачивания | +| `acl` | Права доступа S3 | +| `policy` | Base64-кодированная политика загрузки | +| `x-amz-credential` | AWS credential | +| `x-amz-algorithm` | Всегда `AWS4-HMAC-SHA256` | +| `x-amz-date` | Дата подписи | +| `x-amz-signature` | Подпись запроса | +| `key` | Путь файла на S3 (содержит `${filename}` — заменить на реальное имя) | +| `file` | Сам файл — **обязательно последнее поле** | + +> **Внимание:** `direct_url` — это внешний presigned URL от S3. Он **не** является эндпоинтом Пачки, не требует заголовка `Authorization` и имеет ограниченное время действия. + + ## Частые ошибки | Ошибка | Причина | Решение | |--------|---------|---------| -| `403 Forbidden` при загрузке | Истекла подпись | Параметры загрузки действительны ограниченное время. Запросите новые через `POST /uploads` | +| `403 Forbidden` при загрузке | Истекла подпись | Параметры загрузки действительны ограниченное время. Запросите новые через [Получение подписи](POST /uploads) | | `400 Bad Request` | Неправильный Content-Type | Убедитесь, что запрос отправляется как `multipart/form-data`, а не `application/json` | | Файл не отображается | Неверный `key` | Проверьте, что `${filename}` в ключе заменён на реальное имя файла | @@ -8425,14 +9109,95 @@ await fetch(`${BASE}/messages`, { - **Должно хватать на всё:** мы настроили их так, чтобы вам было комфортно в любых сценариях; - **Если упёрлись в лимит:** при ошибке `429` смотрите заголовок `Retry-After` — он подскажет, через сколько секунд повторить запрос (или используйте экспоненциальный `backoff`, если хотите перестраховаться). ---- +## Повторные запросы (Retry) +[SDK](/guides/sdk/overview) ([TypeScript](/guides/sdk/typescript), [Python](/guides/sdk/python)) уже включают автоматический retry с экспоненциальным backoff. Ниже — реализация для кастомных HTTP-клиентов. -# Модели +### TypeScript -Все модели данных, возвращаемые в ответах API. Каждая модель содержит связанные методы и таблицу свойств. +```typescript +async function withRetry( + fn: () => Promise, + maxRetries = 3 +): Promise { + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await fn() + } catch (error: any) { + // 429 Too Many Requests — ждём Retry-After или backoff + if (error.status === 429) { + const retryAfter = error.headers?.["retry-after"] + const delay = retryAfter + ? parseInt(retryAfter) * 1000 + : Math.pow(2, attempt) * 1000 * (0.5 + Math.random()) + await new Promise(r => setTimeout(r, delay)) + continue + } + // 5xx — серверная ошибка, backoff с jitter + if (error.status >= 500 && attempt < maxRetries) { + const delay = Math.pow(2, attempt) * 1000 * (0.5 + Math.random()) + await new Promise(r => setTimeout(r, delay)) + continue + } + // 4xx (кроме 429) — не повторяем + throw error + } + } + throw new Error("Max retries exceeded") +} -> Методы [Получение подписи](POST /uploads), [Загрузка файла](POST /direct_url), [Загрузка аватара](PUT /profile/avatar), [Удаление аватара](DELETE /profile/avatar), [Загрузка аватара сотрудника](PUT /users/{user_id}/avatar) и [Удаление аватара сотрудника](DELETE /users/{user_id}/avatar) не возвращают модели данных. +// Использование +const users = await withRetry(() => client.users.listUsers()) +``` + +### Python + +```python +import time, random + +async def with_retry(fn, max_retries=3): + for attempt in range(max_retries + 1): + try: + return await fn() + except Exception as e: + status = getattr(e, "status_code", getattr(e, "status", 0)) + headers = getattr(e, "headers", {}) + # 429 Too Many Requests + if status == 429: + retry_after = headers.get("Retry-After") + delay = int(retry_after) if retry_after else (2 ** attempt) * (0.5 + random.random()) + time.sleep(delay) + continue + # 5xx — серверная ошибка + if status >= 500 and attempt < max_retries: + time.sleep((2 ** attempt) * (0.5 + random.random())) + continue + raise + raise Exception("Max retries exceeded") + +# Использование +users = await with_retry(lambda: client.users.list_users()) +``` + +### Стратегия повторов + +| Код | Действие | Задержка | +|-----|----------|----------| +| `429` | Повторить | `Retry-After` header или exponential backoff: 1с, 2с, 4с × jitter | +| `500`, `502`, `503`, `504` | Повторить | Exponential backoff с jitter: ~1с, ~2с, ~4с | +| `400`, `401`, `403`, `404`, `422` | **Не повторять** | Ошибка клиента — нужно исправить запрос | + +> Максимум **3 повтора** на каждый запрос. Jitter (случайный множитель 0.5–1.5) предотвращает «thundering herd» при массовых 429. + + +--- + + +# Модели + +Все модели данных, возвращаемые в ответах API. Каждая модель содержит связанные методы и таблицу свойств. + +> Методы [Получение подписи](POST /uploads), [Загрузка файла](POST /direct_url), [Загрузка аватара](PUT /profile/avatar), [Удаление аватара](DELETE /profile/avatar), [Загрузка аватара сотрудника](PUT /users/{user_id}/avatar) и [Удаление аватара сотрудника](DELETE /users/{user_id}/avatar) не возвращают модели данных. ## Дополнительное поле @@ -9156,6 +9921,43 @@ curl "https://api.pachca.com/api/shared/v1/chats/exports" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { ExportRequest, PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const request: ExportRequest = { + startAt: "2025-03-20", + endAt: "2025-03-20", + webhookUrl: "https://webhook.site/9227d3b8-6e82-4e64-bf5d-ad972ad270f2", + chatIds: [123], + skipChatsFile: false +} +client.common.requestExport(request) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import ExportRequest + +client = PachcaClient("YOUR_TOKEN") + +request = ExportRequest( + start_at="2025-03-20", + end_at="2025-03-20", + webhook_url="https://webhook.site/9227d3b8-6e82-4e64-bf5d-ad972ad270f2", + chat_ids=[123], + skip_chats_file=False +) +await client.common.request_export(request=request) +``` + --- # Скачать архив экспорта @@ -9259,6 +10061,30 @@ curl "https://api.pachca.com/api/shared/v1/chats/exports/22322" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.common.downloadExport(22322) +// → string +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +response = await client.common.download_export(id=22322) +# → str +``` + --- # Список дополнительных полей @@ -9439,6 +10265,32 @@ curl "https://api.pachca.com/api/shared/v1/custom_properties?entity_type=User" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient, SearchEntityType } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.common.listProperties({ entityType: SearchEntityType.User }) +// → ListPropertiesResponse({ data: CustomPropertyDefinition[] }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import ListPropertiesParams, SearchEntityType + +client = PachcaClient("YOUR_TOKEN") + +params = ListPropertiesParams(entity_type=SearchEntityType.USER) +response = await client.common.list_properties(params=params) +# → ListPropertiesResponse(data: list[CustomPropertyDefinition]) +``` + --- # Загрузка файла @@ -9475,6 +10327,51 @@ curl "$DIRECT_URL" \ ### 204: There is no content to send for this request, but the headers may be useful. +## SDK примеры + +### TypeScript + +```typescript +import { FileUploadRequest, PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const request: FileUploadRequest = { + contentDisposition: "example", + acl: "example", + policy: "example", + xAmzCredential: "example", + xAmzAlgorithm: "example", + xAmzDate: "example", + xAmzSignature: "example", + key: "example", + file: new Blob([]) +} +client.common.uploadFile("https://example.com", request) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import FileUploadRequest + +client = PachcaClient("YOUR_TOKEN") + +request = FileUploadRequest( + content_disposition="example", + acl="example", + policy="example", + x_amz_credential="example", + x_amz_algorithm="example", + x_amz_date="example", + x_amz_signature="example", + key="example", + file=b"" +) +await client.common.upload_file(direct_url="https://example.com", request=request) +``` + --- # Получение подписи, ключа и других параметров @@ -9591,6 +10488,30 @@ curl -X POST "https://api.pachca.com/api/shared/v1/uploads" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.common.getUploadParams() +// → UploadParams({ contentDisposition: string, acl: string, policy: string, xAmzCredential: string, xAmzAlgorithm: string, xAmzDate: string, xAmzSignature: string, key: string, directUrl: string }) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +response = await client.common.get_upload_params() +# → UploadParams(content_disposition: str, acl: str, policy: str, x_amz_credential: str, x_amz_algorithm: str, x_amz_date: str, x_amz_signature: str, key: str, direct_url: str) +``` + --- ## API: Profile @@ -9665,6 +10586,30 @@ curl "https://api.pachca.com/api/shared/v1/oauth/token/info" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.profile.getTokenInfo() +// → AccessTokenInfo({ id: number, token: string, name: string | null, userId: number, scopes: OAuthScope[], createdAt: string, revokedAt: string | null, expiresIn: number | null, lastUsedAt: string | null }) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +response = await client.profile.get_token_info() +# → AccessTokenInfo(id: int, token: str, name: str | None, user_id: int, scopes: list[OAuthScope], created_at: datetime, revoked_at: datetime | None, expires_in: int | None, last_used_at: datetime | None) +``` + --- # Информация о профиле @@ -9805,6 +10750,30 @@ curl "https://api.pachca.com/api/shared/v1/profile" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.profile.getProfile() +// → User({ id: number, firstName: string, lastName: string, nickname: string, email: string, phoneNumber: string, department: string, title: string, role: UserRole, suspended: boolean, inviteStatus: InviteStatus, listTags: string[], customProperties: CustomProperty({ id: number, name: string, dataType: CustomPropertyDataType, value: string })[], userStatus: UserStatus({ emoji: string, title: string, expiresAt: string | null, isAway: boolean, awayMessage: UserStatusAwayMessage({ text: string }) | null }) | null, bot: boolean, sso: boolean, createdAt: string, lastActivityAt: string, timeZone: string, imageUrl: string | null }) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +response = await client.profile.get_profile() +# → User(id: int, first_name: str, last_name: str, nickname: str, email: str, phone_number: str, department: str, title: str, role: UserRole, suspended: bool, invite_status: InviteStatus, list_tags: list[str], custom_properties: list[CustomProperty(id: int, name: str, data_type: CustomPropertyDataType, value: str)], user_status: UserStatus(emoji: str, title: str, expires_at: datetime | None, is_away: bool, away_message: UserStatusAwayMessage(text: str) | None) | None, bot: bool, sso: bool, created_at: datetime, last_activity_at: datetime, time_zone: str, image_url: str | None) +``` + --- # Загрузка аватара @@ -9960,6 +10929,31 @@ curl -X PUT "https://api.pachca.com/api/shared/v1/profile/avatar" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const image = new Blob([]) +const response = client.profile.updateProfileAvatar(image) +// → AvatarData({ imageUrl: string }) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +response = await client.profile.update_profile_avatar(image=b"") +# → AvatarData(image_url: str) +``` + --- # Удаление аватара @@ -10066,6 +11060,28 @@ curl -X DELETE "https://api.pachca.com/api/shared/v1/profile/avatar" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +client.profile.deleteProfileAvatar() +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +await client.profile.delete_profile_avatar() +``` + --- # Текущий статус @@ -10148,6 +11164,30 @@ curl "https://api.pachca.com/api/shared/v1/profile/status" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.profile.getStatus() +// → unknown +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +response = await client.profile.get_status() +# → object +``` + --- # Новый статус @@ -10329,6 +11369,49 @@ curl -X PUT "https://api.pachca.com/api/shared/v1/profile/status" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient, StatusUpdateRequest, StatusUpdateRequestStatus } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const request: StatusUpdateRequest = { + status: { + emoji: "🎮", + title: "Очень занят", + expiresAt: "2024-04-08T10:00:00.000Z", + isAway: true, + awayMessage: "Вернусь после 15:00" + } +} +const response = client.profile.updateStatus(request) +// → UserStatus({ emoji: string, title: string, expiresAt: string | null, isAway: boolean, awayMessage: UserStatusAwayMessage({ text: string }) | null }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import StatusUpdateRequest, StatusUpdateRequestStatus + +client = PachcaClient("YOUR_TOKEN") + +request = StatusUpdateRequest( + status=StatusUpdateRequestStatus( + emoji="🎮", + title="Очень занят", + expires_at=datetime.fromisoformat("2024-04-08T10:00:00.000Z"), + is_away=True, + away_message="Вернусь после 15:00" + ) +) +response = await client.profile.update_status(request=request) +# → UserStatus(emoji: str, title: str, expires_at: datetime | None, is_away: bool, away_message: UserStatusAwayMessage(text: str) | None) +``` + --- # Удаление статуса @@ -10385,6 +11468,28 @@ curl -X DELETE "https://api.pachca.com/api/shared/v1/profile/status" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +client.profile.deleteStatus() +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +await client.profile.delete_status() +``` + --- ## API: Users @@ -10698,6 +11803,63 @@ curl "https://api.pachca.com/api/shared/v1/users" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient, UserCreateRequest, UserCreateRequestCustomProperty, UserCreateRequestUser, UserRoleInput } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const request: UserCreateRequest = { + user: { + firstName: "Олег", + lastName: "Петров", + email: "olegp@example.com", + phoneNumber: "+79001234567", + nickname: "olegpetrov", + department: "Продукт", + title: "CIO", + role: UserRoleInput.User, + suspended: false, + listTags: ["example"], + customProperties: [{ id: 1678, value: "Санкт-Петербург" }] + }, + skipEmailNotify: true +} +const response = client.users.createUser(request) +// → User({ id: number, firstName: string, lastName: string, nickname: string, email: string, phoneNumber: string, department: string, title: string, role: UserRole, suspended: boolean, inviteStatus: InviteStatus, listTags: string[], customProperties: CustomProperty({ id: number, name: string, dataType: CustomPropertyDataType, value: string })[], userStatus: UserStatus({ emoji: string, title: string, expiresAt: string | null, isAway: boolean, awayMessage: UserStatusAwayMessage({ text: string }) | null }) | null, bot: boolean, sso: boolean, createdAt: string, lastActivityAt: string, timeZone: string, imageUrl: string | null }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import UserCreateRequest, UserCreateRequestCustomProperty, UserCreateRequestUser, UserRoleInput + +client = PachcaClient("YOUR_TOKEN") + +request = UserCreateRequest( + user=UserCreateRequestUser( + first_name="Олег", + last_name="Петров", + email="olegp@example.com", + phone_number="+79001234567", + nickname="olegpetrov", + department="Продукт", + title="CIO", + role=UserRoleInput.USER, + suspended=False, + list_tags=["example"], + custom_properties=[UserCreateRequestCustomProperty(id=1678, value="Санкт-Петербург")] + ), + skip_email_notify=True +) +response = await client.users.create_user(request=request) +# → User(id: int, first_name: str, last_name: str, nickname: str, email: str, phone_number: str, department: str, title: str, role: UserRole, suspended: bool, invite_status: InviteStatus, list_tags: list[str], custom_properties: list[CustomProperty(id: int, name: str, data_type: CustomPropertyDataType, value: str)], user_status: UserStatus(emoji: str, title: str, expires_at: datetime | None, is_away: bool, away_message: UserStatusAwayMessage(text: str) | None) | None, bot: bool, sso: bool, created_at: datetime, last_activity_at: datetime, time_zone: str, image_url: str | None) +``` + --- # Список сотрудников @@ -10948,15 +12110,49 @@ curl "https://api.pachca.com/api/shared/v1/users?query=Олег&limit=1" \ ``` ---- - -# Информация о сотруднике +## SDK примеры -**Метод**: `GET` +### TypeScript -**Путь**: `/users/{id}` +```typescript +import { PachcaClient } from "@pachca/sdk" -> **Скоуп:** `users:read` +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.users.listUsers({ + query: "Олег", + limit: 1, + cursor: "eyJpZCI6MTAsImRpciI6ImFzYyJ9" +}) +// → ListUsersResponse({ data: User[], meta: PaginationMeta }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import ListUsersParams + +client = PachcaClient("YOUR_TOKEN") + +params = ListUsersParams( + query="Олег", + limit=1, + cursor="eyJpZCI6MTAsImRpciI6ImFzYyJ9" +) +response = await client.users.list_users(params=params) +# → ListUsersResponse(data: list[User], meta: PaginationMeta) +``` + +--- + +# Информация о сотруднике + +**Метод**: `GET` + +**Путь**: `/users/{id}` + +> **Скоуп:** `users:read` Метод для получения информации о сотруднике. @@ -11157,6 +12353,30 @@ curl "https://api.pachca.com/api/shared/v1/users/12" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.users.getUser(12) +// → User({ id: number, firstName: string, lastName: string, nickname: string, email: string, phoneNumber: string, department: string, title: string, role: UserRole, suspended: boolean, inviteStatus: InviteStatus, listTags: string[], customProperties: CustomProperty({ id: number, name: string, dataType: CustomPropertyDataType, value: string })[], userStatus: UserStatus({ emoji: string, title: string, expiresAt: string | null, isAway: boolean, awayMessage: UserStatusAwayMessage({ text: string }) | null }) | null, bot: boolean, sso: boolean, createdAt: string, lastActivityAt: string, timeZone: string, imageUrl: string | null }) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +response = await client.users.get_user(id=12) +# → User(id: int, first_name: str, last_name: str, nickname: str, email: str, phone_number: str, department: str, title: str, role: UserRole, suspended: bool, invite_status: InviteStatus, list_tags: list[str], custom_properties: list[CustomProperty(id: int, name: str, data_type: CustomPropertyDataType, value: str)], user_status: UserStatus(emoji: str, title: str, expires_at: datetime | None, is_away: bool, away_message: UserStatusAwayMessage(text: str) | None) | None, bot: bool, sso: bool, created_at: datetime, last_activity_at: datetime, time_zone: str, image_url: str | None) +``` + --- # Редактирование сотрудника @@ -11500,6 +12720,61 @@ curl -X PUT "https://api.pachca.com/api/shared/v1/users/12" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient, UserRoleInput, UserUpdateRequest, UserUpdateRequestCustomProperty, UserUpdateRequestUser } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const request: UserUpdateRequest = { + user: { + firstName: "Олег", + lastName: "Петров", + email: "olegpetrov@example.com", + phoneNumber: "+79001234567", + nickname: "olegpetrov", + department: "Отдел разработки", + title: "Старший разработчик", + role: UserRoleInput.User, + suspended: false, + listTags: ["example"], + customProperties: [{ id: 1678, value: "Санкт-Петербург" }] + } +} +const response = client.users.updateUser(12, request) +// → User({ id: number, firstName: string, lastName: string, nickname: string, email: string, phoneNumber: string, department: string, title: string, role: UserRole, suspended: boolean, inviteStatus: InviteStatus, listTags: string[], customProperties: CustomProperty({ id: number, name: string, dataType: CustomPropertyDataType, value: string })[], userStatus: UserStatus({ emoji: string, title: string, expiresAt: string | null, isAway: boolean, awayMessage: UserStatusAwayMessage({ text: string }) | null }) | null, bot: boolean, sso: boolean, createdAt: string, lastActivityAt: string, timeZone: string, imageUrl: string | null }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import UserRoleInput, UserUpdateRequest, UserUpdateRequestCustomProperty, UserUpdateRequestUser + +client = PachcaClient("YOUR_TOKEN") + +request = UserUpdateRequest( + user=UserUpdateRequestUser( + first_name="Олег", + last_name="Петров", + email="olegpetrov@example.com", + phone_number="+79001234567", + nickname="olegpetrov", + department="Отдел разработки", + title="Старший разработчик", + role=UserRoleInput.USER, + suspended=False, + list_tags=["example"], + custom_properties=[UserUpdateRequestCustomProperty(id=1678, value="Санкт-Петербург")] + ) +) +response = await client.users.update_user(id=12, request=request) +# → User(id: int, first_name: str, last_name: str, nickname: str, email: str, phone_number: str, department: str, title: str, role: UserRole, suspended: bool, invite_status: InviteStatus, list_tags: list[str], custom_properties: list[CustomProperty(id: int, name: str, data_type: CustomPropertyDataType, value: str)], user_status: UserStatus(emoji: str, title: str, expires_at: datetime | None, is_away: bool, away_message: UserStatusAwayMessage(text: str) | None) | None, bot: bool, sso: bool, created_at: datetime, last_activity_at: datetime, time_zone: str, image_url: str | None) +``` + --- # Удаление сотрудника @@ -11625,6 +12900,28 @@ curl -X DELETE "https://api.pachca.com/api/shared/v1/users/12" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +client.users.deleteUser(12) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +await client.users.delete_user(id=12) +``` + --- # Загрузка аватара сотрудника @@ -11817,6 +13114,31 @@ curl -X PUT "https://api.pachca.com/api/shared/v1/users/12/avatar" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const image = new Blob([]) +const response = client.users.updateUserAvatar(12, image) +// → AvatarData({ imageUrl: string }) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +response = await client.users.update_user_avatar(user_id=12, image=b"") +# → AvatarData(image_url: str) +``` + --- # Удаление аватара сотрудника @@ -11960,6 +13282,28 @@ curl -X DELETE "https://api.pachca.com/api/shared/v1/users/12/avatar" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +client.users.deleteUserAvatar(12) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +await client.users.delete_user_avatar(user_id=12) +``` + --- # Статус сотрудника @@ -12079,6 +13423,30 @@ curl "https://api.pachca.com/api/shared/v1/users/12/status" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.users.getUserStatus(12) +// → unknown +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +response = await client.users.get_user_status(user_id=12) +# → object +``` + --- # Новый статус сотрудника @@ -12297,6 +13665,49 @@ curl -X PUT "https://api.pachca.com/api/shared/v1/users/12/status" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient, StatusUpdateRequest, StatusUpdateRequestStatus } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const request: StatusUpdateRequest = { + status: { + emoji: "🎮", + title: "Очень занят", + expiresAt: "2024-04-08T10:00:00.000Z", + isAway: true, + awayMessage: "Вернусь после 15:00" + } +} +const response = client.users.updateUserStatus(12, request) +// → UserStatus({ emoji: string, title: string, expiresAt: string | null, isAway: boolean, awayMessage: UserStatusAwayMessage({ text: string }) | null }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import StatusUpdateRequest, StatusUpdateRequestStatus + +client = PachcaClient("YOUR_TOKEN") + +request = StatusUpdateRequest( + status=StatusUpdateRequestStatus( + emoji="🎮", + title="Очень занят", + expires_at=datetime.fromisoformat("2024-04-08T10:00:00.000Z"), + is_away=True, + away_message="Вернусь после 15:00" + ) +) +response = await client.users.update_user_status(user_id=12, request=request) +# → UserStatus(emoji: str, title: str, expires_at: datetime | None, is_away: bool, away_message: UserStatusAwayMessage(text: str) | None) +``` + --- # Удаление статуса сотрудника @@ -12390,6 +13801,28 @@ curl -X DELETE "https://api.pachca.com/api/shared/v1/users/12/status" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +client.users.deleteUserStatus(12) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +await client.users.delete_user_status(user_id=12) +``` + --- ## API: Group tags @@ -12584,6 +14017,33 @@ curl "https://api.pachca.com/api/shared/v1/group_tags" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { GroupTagRequest, GroupTagRequestGroupTag, PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const request: GroupTagRequest = { groupTag: { name: "Новое название тега" } } +const response = client.groupTags.createTag(request) +// → GroupTag({ id: number, name: string, usersCount: number }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import GroupTagRequest, GroupTagRequestGroupTag + +client = PachcaClient("YOUR_TOKEN") + +request = GroupTagRequest(group_tag=GroupTagRequestGroupTag(name="Новое название тега")) +response = await client.group_tags.create_tag(request=request) +# → GroupTag(id: int, name: str, users_count: int) +``` + --- # Список тегов сотрудников @@ -12769,6 +14229,40 @@ curl "https://api.pachca.com/api/shared/v1/group_tags?names[]=Design&names[]=Pro ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.groupTags.listTags({ + names: ["example"], + limit: 1, + cursor: "eyJpZCI6MTAsImRpciI6ImFzYyJ9" +}) +// → ListTagsResponse({ data: GroupTag[], meta: PaginationMeta }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import ListTagsParams + +client = PachcaClient("YOUR_TOKEN") + +params = ListTagsParams( + names=["example"], + limit=1, + cursor="eyJpZCI6MTAsImRpciI6ImFzYyJ9" +) +response = await client.group_tags.list_tags(params=params) +# → ListTagsResponse(data: list[GroupTag], meta: PaginationMeta) +``` + --- # Информация о теге @@ -12913,6 +14407,30 @@ curl "https://api.pachca.com/api/shared/v1/group_tags/9111" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.groupTags.getTag(9111) +// → GroupTag({ id: number, name: string, usersCount: number }) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +response = await client.group_tags.get_tag(id=9111) +# → GroupTag(id: int, name: str, users_count: int) +``` + --- # Редактирование тега @@ -13144,6 +14662,33 @@ curl -X PUT "https://api.pachca.com/api/shared/v1/group_tags/9111" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { GroupTagRequest, GroupTagRequestGroupTag, PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const request: GroupTagRequest = { groupTag: { name: "Новое название тега" } } +const response = client.groupTags.updateTag(9111, request) +// → GroupTag({ id: number, name: string, usersCount: number }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import GroupTagRequest, GroupTagRequestGroupTag + +client = PachcaClient("YOUR_TOKEN") + +request = GroupTagRequest(group_tag=GroupTagRequestGroupTag(name="Новое название тега")) +response = await client.group_tags.update_tag(id=9111, request=request) +# → GroupTag(id: int, name: str, users_count: int) +``` + --- # Удаление тега @@ -13269,11 +14814,33 @@ curl -X DELETE "https://api.pachca.com/api/shared/v1/group_tags/9111" \ ``` ---- - -# Список сотрудников тега +## SDK примеры -**Метод**: `GET` +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +client.groupTags.deleteTag(9111) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +await client.group_tags.delete_tag(id=9111) +``` + +--- + +# Список сотрудников тега + +**Метод**: `GET` **Путь**: `/group_tags/{id}/users` @@ -13552,6 +15119,32 @@ curl "https://api.pachca.com/api/shared/v1/group_tags/9111/users?limit=1" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.groupTags.getTagUsers(9111, { limit: 1, cursor: "eyJpZCI6MTAsImRpciI6ImFzYyJ9" }) +// → GetTagUsersResponse({ data: User[], meta: PaginationMeta }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import GetTagUsersParams + +client = PachcaClient("YOUR_TOKEN") + +params = GetTagUsersParams(limit=1, cursor="eyJpZCI6MTAsImRpciI6ImFzYyJ9") +response = await client.group_tags.get_tag_users(id=9111, params=params) +# → GetTagUsersResponse(data: list[User], meta: PaginationMeta) +``` + --- ## API: Chats @@ -13796,6 +15389,49 @@ curl "https://api.pachca.com/api/shared/v1/chats" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { ChatCreateRequest, ChatCreateRequestChat, PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const request: ChatCreateRequest = { + chat: { + name: "🤿 aqua", + memberIds: [123], + groupTagIds: [123], + channel: true, + public: false + } +} +const response = client.chats.createChat(request) +// → Chat({ id: number, name: string, createdAt: string, ownerId: number, memberIds: number[], groupTagIds: number[], channel: boolean, personal: boolean, public: boolean, lastMessageAt: string, meetRoomUrl: string }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import ChatCreateRequest, ChatCreateRequestChat + +client = PachcaClient("YOUR_TOKEN") + +request = ChatCreateRequest( + chat=ChatCreateRequestChat( + name="🤿 aqua", + member_ids=[123], + group_tag_ids=[123], + channel=True, + public=False + ) +) +response = await client.chats.create_chat(request=request) +# → Chat(id: int, name: str, created_at: datetime, owner_id: int, member_ids: list[int], group_tag_ids: list[int], channel: bool, personal: bool, public: bool, last_message_at: datetime, meet_room_url: str) +``` + --- # Список чатов @@ -14008,6 +15644,50 @@ curl "https://api.pachca.com/api/shared/v1/chats?sort=id&order=desc&availability ``` +## SDK примеры + +### TypeScript + +```typescript +import { ChatAvailability, ChatSortField, PachcaClient, SortOrder } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.chats.listChats({ + sort: ChatSortField.Id, + order: SortOrder.Desc, + availability: ChatAvailability.IsMember, + lastMessageAtAfter: "2025-01-01T00:00:00.000Z", + lastMessageAtBefore: "2025-02-01T00:00:00.000Z", + personal: false, + limit: 1, + cursor: "eyJpZCI6MTAsImRpciI6ImFzYyJ9" +}) +// → ListChatsResponse({ data: Chat[], meta: PaginationMeta }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import ChatAvailability, ChatSortField, ListChatsParams, SortOrder + +client = PachcaClient("YOUR_TOKEN") + +params = ListChatsParams( + sort=ChatSortField.ID, + order=SortOrder.DESC, + availability=ChatAvailability.IS_MEMBER, + last_message_at_after=datetime.fromisoformat("2025-01-01T00:00:00.000Z"), + last_message_at_before=datetime.fromisoformat("2025-02-01T00:00:00.000Z"), + personal=False, + limit=1, + cursor="eyJpZCI6MTAsImRpciI6ImFzYyJ9" +) +response = await client.chats.list_chats(params=params) +# → ListChatsResponse(data: list[Chat], meta: PaginationMeta) +``` + --- # Информация о чате @@ -14174,6 +15854,30 @@ curl "https://api.pachca.com/api/shared/v1/chats/334" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.chats.getChat(334) +// → Chat({ id: number, name: string, createdAt: string, ownerId: number, memberIds: number[], groupTagIds: number[], channel: boolean, personal: boolean, public: boolean, lastMessageAt: string, meetRoomUrl: string }) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +response = await client.chats.get_chat(id=334) +# → Chat(id: int, name: str, created_at: datetime, owner_id: int, member_ids: list[int], group_tag_ids: list[int], channel: bool, personal: bool, public: bool, last_message_at: datetime, meet_room_url: str) +``` + --- # Обновление чата @@ -14430,6 +16134,33 @@ curl -X PUT "https://api.pachca.com/api/shared/v1/chats/334" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { ChatUpdateRequest, ChatUpdateRequestChat, PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const request: ChatUpdateRequest = { chat: { name: "Бассейн", public: true } } +const response = client.chats.updateChat(334, request) +// → Chat({ id: number, name: string, createdAt: string, ownerId: number, memberIds: number[], groupTagIds: number[], channel: boolean, personal: boolean, public: boolean, lastMessageAt: string, meetRoomUrl: string }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import ChatUpdateRequest, ChatUpdateRequestChat + +client = PachcaClient("YOUR_TOKEN") + +request = ChatUpdateRequest(chat=ChatUpdateRequestChat(name="Бассейн", public=True)) +response = await client.chats.update_chat(id=334, request=request) +# → Chat(id: int, name: str, created_at: datetime, owner_id: int, member_ids: list[int], group_tag_ids: list[int], channel: bool, personal: bool, public: bool, last_message_at: datetime, meet_room_url: str) +``` + --- # Архивация чата @@ -14525,6 +16256,28 @@ curl -X PUT "https://api.pachca.com/api/shared/v1/chats/334/archive" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +client.chats.archiveChat(334) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +await client.chats.archive_chat(id=334) +``` + --- # Разархивация чата @@ -14620,6 +16373,28 @@ curl -X PUT "https://api.pachca.com/api/shared/v1/chats/334/unarchive" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +client.chats.unarchiveChat(334) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +await client.chats.unarchive_chat(id=334) +``` + --- ## API: Members @@ -14805,6 +16580,30 @@ curl "https://api.pachca.com/api/shared/v1/chats/334/group_tags" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const groupTagIds = [123] +client.members.addTags(334, groupTagIds) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +group_tag_ids = [123] +await client.members.add_tags(id=334, group_tag_ids=group_tag_ids) +``` + --- # Исключение тега @@ -14901,6 +16700,28 @@ curl -X DELETE "https://api.pachca.com/api/shared/v1/chats/334/group_tags/86" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +client.members.removeTag(334, 86) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +await client.members.remove_tag(id=334, tag_id=86) +``` + --- # Выход из беседы или канала @@ -15054,6 +16875,28 @@ curl -X DELETE "https://api.pachca.com/api/shared/v1/chats/334/leave" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +client.members.leaveChat(334) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +await client.members.leave_chat(id=334) +``` + --- # Список участников чата @@ -15310,6 +17153,40 @@ curl "https://api.pachca.com/api/shared/v1/chats/334/members?role=all&limit=1" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { ChatMemberRoleFilter, PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.members.listMembers(334, { + role: ChatMemberRoleFilter.All, + limit: 1, + cursor: "eyJpZCI6MTAsImRpciI6ImFzYyJ9" +}) +// → ListMembersResponse({ data: User[], meta: PaginationMeta }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import ChatMemberRoleFilter, ListMembersParams + +client = PachcaClient("YOUR_TOKEN") + +params = ListMembersParams( + role=ChatMemberRoleFilter.ALL, + limit=1, + cursor="eyJpZCI6MTAsImRpciI6ImFzYyJ9" +) +response = await client.members.list_members(id=334, params=params) +# → ListMembersResponse(data: list[User], meta: PaginationMeta) +``` + --- # Добавление пользователей @@ -15494,6 +17371,31 @@ curl "https://api.pachca.com/api/shared/v1/chats/334/members" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { AddMembersRequest, PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const request: AddMembersRequest = { memberIds: [123], silent: true } +client.members.addMembers(334, request) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import AddMembersRequest + +client = PachcaClient("YOUR_TOKEN") + +request = AddMembersRequest(member_ids=[123], silent=True) +await client.members.add_members(id=334, request=request) +``` + --- # Исключение пользователя @@ -15590,6 +17492,28 @@ curl -X DELETE "https://api.pachca.com/api/shared/v1/chats/334/members/186" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +client.members.removeMember(334, 186) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +await client.members.remove_member(id=334, user_id=186) +``` + --- # Редактирование роли @@ -15771,6 +17695,29 @@ curl -X PUT "https://api.pachca.com/api/shared/v1/chats/334/members/186" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { ChatMemberRole, PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +client.members.updateMemberRole(334, 186, ChatMemberRole.Admin) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import ChatMemberRole + +client = PachcaClient("YOUR_TOKEN") + +await client.members.update_member_role(id=334, user_id=186, role=ChatMemberRole.ADMIN) +``` + --- ## API: Threads @@ -15891,6 +17838,30 @@ curl -X POST "https://api.pachca.com/api/shared/v1/messages/154332686/thread" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.threads.createThread(154332686) +// → Thread({ id: number, chatId: number, messageId: number, messageChatId: number, updatedAt: string }) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +response = await client.threads.create_thread(id=154332686) +# → Thread(id: int, chat_id: int, message_id: int, message_chat_id: int, updated_at: datetime) +``` + --- # Информация о треде @@ -16039,6 +18010,30 @@ curl "https://api.pachca.com/api/shared/v1/threads/265142" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.threads.getThread(265142) +// → Thread({ id: number, chatId: number, messageId: number, messageChatId: number, updatedAt: string }) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +response = await client.threads.get_thread(id=265142) +# → Thread(id: int, chat_id: int, message_id: int, message_chat_id: int, updated_at: datetime) +``` + --- ## API: Messages @@ -16389,6 +18384,81 @@ curl "https://api.pachca.com/api/shared/v1/messages" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { Button, FileType, MessageCreateRequest, MessageCreateRequestFile, MessageCreateRequestMessage, MessageEntityType, PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const request: MessageCreateRequest = { + message: { + entityType: MessageEntityType.Discussion, + entityId: 334, + content: "Вчера мы продали 756 футболок (что на 10% больше, чем в прошлое воскресенье)", + files: [{ + key: "attaches/files/93746/e354fd79-4f3e-4b5a-9c8d-1a2b3c4d5e6f/logo.png", + name: "logo.png", + fileType: FileType.Image, + size: 12345, + width: 800, + height: 600 + }], + buttons: [[{ + text: "Подробнее", + url: "https://example.com/details", + data: "awesome" + }]], + parentMessageId: 194270, + displayAvatarUrl: "https://example.com/avatar.png", + displayName: "Бот Поддержки", + skipInviteMentions: false + }, + linkPreview: false +} +const response = client.messages.createMessage(request) +// → Message({ id: number, entityType: MessageEntityType, entityId: number, chatId: number, rootChatId: number, content: string, userId: number, createdAt: string, url: string, files: File({ id: number, key: string, name: string, fileType: FileType, url: string, width?: number | null, height?: number | null })[], buttons: Button({ text: string, url?: string, data?: string })[][] | null, thread: MessageThread({ id: number, chatId: number }) | null, forwarding: Forwarding({ originalMessageId: number, originalChatId: number, authorId: number, originalCreatedAt: string, originalThreadId: number | null, originalThreadMessageId: number | null, originalThreadParentChatId: number | null }) | null, parentMessageId: number | null, displayAvatarUrl: string | null, displayName: string | null, changedAt: string | null, deletedAt: string | null }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import Button, FileType, MessageCreateRequest, MessageCreateRequestFile, MessageCreateRequestMessage, MessageEntityType + +client = PachcaClient("YOUR_TOKEN") + +request = MessageCreateRequest( + message=MessageCreateRequestMessage( + entity_type=MessageEntityType.DISCUSSION, + entity_id=334, + content="Вчера мы продали 756 футболок (что на 10% больше, чем в прошлое воскресенье)", + files=[MessageCreateRequestFile( + key="attaches/files/93746/e354fd79-4f3e-4b5a-9c8d-1a2b3c4d5e6f/logo.png", + name="logo.png", + file_type=FileType.IMAGE, + size=12345, + width=800, + height=600 + )], + buttons=[[Button( + text="Подробнее", + url="https://example.com/details", + data="awesome" + )]], + parent_message_id=194270, + display_avatar_url="https://example.com/avatar.png", + display_name="Бот Поддержки", + skip_invite_mentions=False + ), + link_preview=False +) +response = await client.messages.create_message(request=request) +# → Message(id: int, entity_type: MessageEntityType, entity_id: int, chat_id: int, root_chat_id: int, content: str, user_id: int, created_at: datetime, url: str, files: list[File(id: int, key: str, name: str, file_type: FileType, url: str, width: int | None, height: int | None)], buttons: list[list[Button(text: str, url: str | None, data: str | None)]] | None, thread: MessageThread(id: int, chat_id: int) | None, forwarding: Forwarding(original_message_id: int, original_chat_id: int, author_id: int, original_created_at: datetime, original_thread_id: int | None, original_thread_message_id: int | None, original_thread_parent_chat_id: int | None) | None, parent_message_id: int | None, display_avatar_url: str | None, display_name: str | None, changed_at: datetime | None, deleted_at: datetime | None) +``` + --- # Список сообщений чата @@ -16666,23 +18736,61 @@ curl "https://api.pachca.com/api/shared/v1/messages?chat_id=198&sort=id&order=de **Структура значений Record:** - Тип значения: `any` -**Пример ответа:** +**Пример ответа:** + +```json +{ + "errors": [ + { + "key": "field.name", + "value": "invalid_value", + "message": "Поле не может быть пустым", + "code": "blank", + "payload": null + } + ] +} +``` + + +## SDK примеры + +### TypeScript + +```typescript +import { MessageSortField, PachcaClient, SortOrder } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.messages.listChatMessages({ + chatId: 198, + sort: MessageSortField.Id, + order: SortOrder.Desc, + limit: 1, + cursor: "eyJpZCI6MTAsImRpciI6ImFzYyJ9" +}) +// → ListChatMessagesResponse({ data: Message[], meta: PaginationMeta }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import ListChatMessagesParams, MessageSortField, SortOrder + +client = PachcaClient("YOUR_TOKEN") -```json -{ - "errors": [ - { - "key": "field.name", - "value": "invalid_value", - "message": "Поле не может быть пустым", - "code": "blank", - "payload": null - } - ] -} +params = ListChatMessagesParams( + chat_id=198, + sort=MessageSortField.ID, + order=SortOrder.DESC, + limit=1, + cursor="eyJpZCI6MTAsImRpciI6ImFzYyJ9" +) +response = await client.messages.list_chat_messages(params=params) +# → ListChatMessagesResponse(data: list[Message], meta: PaginationMeta) ``` - --- # Информация о сообщении @@ -16904,6 +19012,30 @@ curl "https://api.pachca.com/api/shared/v1/messages/194275" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.messages.getMessage(194275) +// → Message({ id: number, entityType: MessageEntityType, entityId: number, chatId: number, rootChatId: number, content: string, userId: number, createdAt: string, url: string, files: File({ id: number, key: string, name: string, fileType: FileType, url: string, width?: number | null, height?: number | null })[], buttons: Button({ text: string, url?: string, data?: string })[][] | null, thread: MessageThread({ id: number, chatId: number }) | null, forwarding: Forwarding({ originalMessageId: number, originalChatId: number, authorId: number, originalCreatedAt: string, originalThreadId: number | null, originalThreadMessageId: number | null, originalThreadParentChatId: number | null }) | null, parentMessageId: number | null, displayAvatarUrl: string | null, displayName: string | null, changedAt: string | null, deletedAt: string | null }) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +response = await client.messages.get_message(id=194275) +# → Message(id: int, entity_type: MessageEntityType, entity_id: int, chat_id: int, root_chat_id: int, content: str, user_id: int, created_at: datetime, url: str, files: list[File(id: int, key: str, name: str, file_type: FileType, url: str, width: int | None, height: int | None)], buttons: list[list[Button(text: str, url: str | None, data: str | None)]] | None, thread: MessageThread(id: int, chat_id: int) | None, forwarding: Forwarding(original_message_id: int, original_chat_id: int, author_id: int, original_created_at: datetime, original_thread_id: int | None, original_thread_message_id: int | None, original_thread_parent_chat_id: int | None) | None, parent_message_id: int | None, display_avatar_url: str | None, display_name: str | None, changed_at: datetime | None, deleted_at: datetime | None) +``` + --- # Редактирование сообщения @@ -17262,6 +19394,71 @@ curl -X PUT "https://api.pachca.com/api/shared/v1/messages/194275" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { Button, MessageUpdateRequest, MessageUpdateRequestFile, MessageUpdateRequestMessage, PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const request: MessageUpdateRequest = { + message: { + content: "Вот попробуйте написать правильно это с первого раза: Будущий, Полощи, Прийти, Грейпфрут, Мозаика, Бюллетень, Дуршлаг, Винегрет.", + files: [{ + key: "attaches/files/93746/e354fd79-4f3e-4b5a-9c8d-1a2b3c4d5e6f/logo.png", + name: "logo.png", + fileType: "image", + size: 12345, + width: 800, + height: 600 + }], + buttons: [[{ + text: "Подробнее", + url: "https://example.com/details", + data: "awesome" + }]], + displayAvatarUrl: "https://example.com/avatar.png", + displayName: "Бот Поддержки" + } +} +const response = client.messages.updateMessage(194275, request) +// → Message({ id: number, entityType: MessageEntityType, entityId: number, chatId: number, rootChatId: number, content: string, userId: number, createdAt: string, url: string, files: File({ id: number, key: string, name: string, fileType: FileType, url: string, width?: number | null, height?: number | null })[], buttons: Button({ text: string, url?: string, data?: string })[][] | null, thread: MessageThread({ id: number, chatId: number }) | null, forwarding: Forwarding({ originalMessageId: number, originalChatId: number, authorId: number, originalCreatedAt: string, originalThreadId: number | null, originalThreadMessageId: number | null, originalThreadParentChatId: number | null }) | null, parentMessageId: number | null, displayAvatarUrl: string | null, displayName: string | null, changedAt: string | null, deletedAt: string | null }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import Button, MessageUpdateRequest, MessageUpdateRequestFile, MessageUpdateRequestMessage + +client = PachcaClient("YOUR_TOKEN") + +request = MessageUpdateRequest( + message=MessageUpdateRequestMessage( + content="Вот попробуйте написать правильно это с первого раза: Будущий, Полощи, Прийти, Грейпфрут, Мозаика, Бюллетень, Дуршлаг, Винегрет.", + files=[MessageUpdateRequestFile( + key="attaches/files/93746/e354fd79-4f3e-4b5a-9c8d-1a2b3c4d5e6f/logo.png", + name="logo.png", + file_type="image", + size=12345, + width=800, + height=600 + )], + buttons=[[Button( + text="Подробнее", + url="https://example.com/details", + data="awesome" + )]], + display_avatar_url="https://example.com/avatar.png", + display_name="Бот Поддержки" + ) +) +response = await client.messages.update_message(id=194275, request=request) +# → Message(id: int, entity_type: MessageEntityType, entity_id: int, chat_id: int, root_chat_id: int, content: str, user_id: int, created_at: datetime, url: str, files: list[File(id: int, key: str, name: str, file_type: FileType, url: str, width: int | None, height: int | None)], buttons: list[list[Button(text: str, url: str | None, data: str | None)]] | None, thread: MessageThread(id: int, chat_id: int) | None, forwarding: Forwarding(original_message_id: int, original_chat_id: int, author_id: int, original_created_at: datetime, original_thread_id: int | None, original_thread_message_id: int | None, original_thread_parent_chat_id: int | None) | None, parent_message_id: int | None, display_avatar_url: str | None, display_name: str | None, changed_at: datetime | None, deleted_at: datetime | None) +``` + --- # Удаление сообщения @@ -17389,6 +19586,28 @@ curl -X DELETE "https://api.pachca.com/api/shared/v1/messages/194275" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +client.messages.deleteMessage(194275) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +await client.messages.delete_message(id=194275) +``` + --- # Закрепление сообщения @@ -17486,6 +19705,28 @@ curl -X POST "https://api.pachca.com/api/shared/v1/messages/194275/pin" \ ### 422: Client error +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +client.messages.pinMessage(194275) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +await client.messages.pin_message(id=194275) +``` + --- # Открепление сообщения @@ -17581,6 +19822,28 @@ curl -X DELETE "https://api.pachca.com/api/shared/v1/messages/194275/pin" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +client.messages.unpinMessage(194275) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +await client.messages.unpin_message(id=194275) +``` + --- ## API: Read members @@ -17794,6 +20057,32 @@ curl "https://api.pachca.com/api/shared/v1/messages/194275/read_member_ids?limit ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.readMembers.listReadMembers(194275, { limit: 300, cursor: "eyJpZCI6MTAsImRpciI6ImFzYyJ9" }) +// → unknown +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import ListReadMembersParams + +client = PachcaClient("YOUR_TOKEN") + +params = ListReadMembersParams(limit=300, cursor="eyJpZCI6MTAsImRpciI6ImFzYyJ9") +response = await client.read_members.list_read_members(id=194275, params=params) +# → object +``` + --- ## API: Reactions @@ -18000,6 +20289,33 @@ curl "https://api.pachca.com/api/shared/v1/messages/7231942/reactions" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient, ReactionRequest } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const request: ReactionRequest = { code: "👍", name: ":+1:" } +const response = client.reactions.addReaction(7231942, request) +// → Reaction({ userId: number, createdAt: string, code: string, name: string | null }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import ReactionRequest + +client = PachcaClient("YOUR_TOKEN") + +request = ReactionRequest(code="👍", name=":+1:") +response = await client.reactions.add_reaction(id=7231942, request=request) +# → Reaction(user_id: int, created_at: datetime, code: str, name: str | None) +``` + --- # Удаление реакции @@ -18162,6 +20478,30 @@ curl -X DELETE "https://api.pachca.com/api/shared/v1/messages/7231942/reactions? ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +client.reactions.removeReaction(7231942, { code: "👍", name: ":+1:" }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import RemoveReactionParams + +client = PachcaClient("YOUR_TOKEN") + +params = RemoveReactionParams(code="👍", name=":+1:") +await client.reactions.remove_reaction(id=7231942, params=params) +``` + --- # Список реакций @@ -18352,6 +20692,32 @@ curl "https://api.pachca.com/api/shared/v1/messages/194275/reactions?limit=1" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.reactions.listReactions(194275, { limit: 1, cursor: "eyJpZCI6MTAsImRpciI6ImFzYyJ9" }) +// → ListReactionsResponse({ data: Reaction[], meta: PaginationMeta }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import ListReactionsParams + +client = PachcaClient("YOUR_TOKEN") + +params = ListReactionsParams(limit=1, cursor="eyJpZCI6MTAsImRpciI6ImFzYyJ9") +response = await client.reactions.list_reactions(id=194275, params=params) +# → ListReactionsResponse(data: list[Reaction], meta: PaginationMeta) +``` + --- ## API: Link Previews @@ -18559,6 +20925,53 @@ curl "https://api.pachca.com/api/shared/v1/messages/194275/link_previews" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { LinkPreview, LinkPreviewImage, LinkPreviewsRequest, PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const request: LinkPreviewsRequest = { + linkPreviews: { key: { + title: "Статья: Отправка файлов", + description: "Пример отправки файлов на удаленный сервер", + imageUrl: "https://website.com/img/landing.png", + image: { + key: "attaches/files/93746/e354fd79-9jh6-f2hd-fj83-709dae24c763/${filename}", + name: "files-to-server.jpg", + size: 695604 + } + } } +} +client.linkPreviews.createLinkPreviews(194275, request) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import LinkPreview, LinkPreviewImage, LinkPreviewsRequest + +client = PachcaClient("YOUR_TOKEN") + +request = LinkPreviewsRequest( + link_previews={"key": LinkPreview( + title="Статья: Отправка файлов", + description="Пример отправки файлов на удаленный сервер", + image_url="https://website.com/img/landing.png", + image=LinkPreviewImage( + key="attaches/files/93746/e354fd79-9jh6-f2hd-fj83-709dae24c763/${filename}", + name="files-to-server.jpg", + size=695604 + ) + )} +) +await client.link_previews.create_link_previews(id=194275, request=request) +``` + --- ## API: Search @@ -18748,6 +21161,52 @@ curl "https://api.pachca.com/api/shared/v1/search/chats?query=Разработк ``` +## SDK примеры + +### TypeScript + +```typescript +import { ChatSubtype, PachcaClient, SortOrder } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.search.searchChats({ + query: "Разработка", + limit: 10, + cursor: "eyJpZCI6MTAsImRpciI6ImFzYyJ9", + order: SortOrder.Desc, + createdFrom: "2025-01-01T00:00:00.000Z", + createdTo: "2025-02-01T00:00:00.000Z", + active: true, + chatSubtype: ChatSubtype.Discussion, + personal: false +}) +// → SearchChatsResponse({ data: Chat[], meta: SearchPaginationMeta }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import ChatSubtype, SearchChatsParams, SortOrder + +client = PachcaClient("YOUR_TOKEN") + +params = SearchChatsParams( + query="Разработка", + limit=10, + cursor="eyJpZCI6MTAsImRpciI6ImFzYyJ9", + order=SortOrder.DESC, + created_from=datetime.fromisoformat("2025-01-01T00:00:00.000Z"), + created_to=datetime.fromisoformat("2025-02-01T00:00:00.000Z"), + active=True, + chat_subtype=ChatSubtype.DISCUSSION, + personal=False +) +response = await client.search.search_chats(params=params) +# → SearchChatsResponse(data: list[Chat], meta: SearchPaginationMeta) +``` + --- # Поиск сообщений @@ -18987,6 +21446,52 @@ curl "https://api.pachca.com/api/shared/v1/search/messages?query=футболк ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient, SortOrder } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.search.searchMessages({ + query: "футболки", + limit: 10, + cursor: "eyJpZCI6MTAsImRpciI6ImFzYyJ9", + order: SortOrder.Desc, + createdFrom: "2025-01-01T00:00:00.000Z", + createdTo: "2025-02-01T00:00:00.000Z", + chatIds: [123], + userIds: [123], + active: true +}) +// → SearchMessagesResponse({ data: Message[], meta: SearchPaginationMeta }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import SearchMessagesParams, SortOrder + +client = PachcaClient("YOUR_TOKEN") + +params = SearchMessagesParams( + query="футболки", + limit=10, + cursor="eyJpZCI6MTAsImRpciI6ImFzYyJ9", + order=SortOrder.DESC, + created_from=datetime.fromisoformat("2025-01-01T00:00:00.000Z"), + created_to=datetime.fromisoformat("2025-02-01T00:00:00.000Z"), + chat_ids=[123], + user_ids=[123], + active=True +) +response = await client.search.search_messages(params=params) +# → SearchMessagesResponse(data: list[Message], meta: SearchPaginationMeta) +``` + --- # Поиск сотрудников @@ -19216,6 +21721,50 @@ curl "https://api.pachca.com/api/shared/v1/search/users?query=Олег&limit=10& ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient, SearchSortOrder, SortOrder, UserRole } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.search.searchUsers({ + query: "Олег", + limit: 10, + cursor: "eyJpZCI6MTAsImRpciI6ImFzYyJ9", + sort: SearchSortOrder.ByScore, + order: SortOrder.Desc, + createdFrom: "2025-01-01T00:00:00.000Z", + createdTo: "2025-02-01T00:00:00.000Z", + companyRoles: [UserRole.Admin] +}) +// → SearchUsersResponse({ data: User[], meta: SearchPaginationMeta }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import SearchSortOrder, SearchUsersParams, SortOrder, UserRole + +client = PachcaClient("YOUR_TOKEN") + +params = SearchUsersParams( + query="Олег", + limit=10, + cursor="eyJpZCI6MTAsImRpciI6ImFzYyJ9", + sort=SearchSortOrder.BY_SCORE, + order=SortOrder.DESC, + created_from=datetime.fromisoformat("2025-01-01T00:00:00.000Z"), + created_to=datetime.fromisoformat("2025-02-01T00:00:00.000Z"), + company_roles=[UserRole.ADMIN] +) +response = await client.search.search_users(params=params) +# → SearchUsersResponse(data: list[User], meta: SearchPaginationMeta) +``` + --- ## API: Tasks @@ -19522,6 +22071,55 @@ curl "https://api.pachca.com/api/shared/v1/tasks" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient, TaskCreateRequest, TaskCreateRequestCustomProperty, TaskCreateRequestTask, TaskKind } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const request: TaskCreateRequest = { + task: { + kind: TaskKind.Reminder, + content: "Забрать со склада 21 заказ", + dueAt: "2020-06-05T12:00:00.000+03:00", + priority: 2, + performerIds: [123], + chatId: 456, + allDay: false, + customProperties: [{ id: 78, value: "Синий склад" }] + } +} +const response = client.tasks.createTask(request) +// → Task({ id: number, kind: TaskKind, content: string, dueAt: string | null, priority: number, userId: number, chatId: number | null, status: TaskStatus, createdAt: string, performerIds: number[], allDay: boolean, customProperties: CustomProperty({ id: number, name: string, dataType: CustomPropertyDataType, value: string })[] }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import TaskCreateRequest, TaskCreateRequestCustomProperty, TaskCreateRequestTask, TaskKind + +client = PachcaClient("YOUR_TOKEN") + +request = TaskCreateRequest( + task=TaskCreateRequestTask( + kind=TaskKind.REMINDER, + content="Забрать со склада 21 заказ", + due_at=datetime.fromisoformat("2020-06-05T12:00:00.000+03:00"), + priority=2, + performer_ids=[123], + chat_id=456, + all_day=False, + custom_properties=[TaskCreateRequestCustomProperty(id=78, value="Синий склад")] + ) +) +response = await client.tasks.create_task(request=request) +# → Task(id: int, kind: TaskKind, content: str, due_at: datetime | None, priority: int, user_id: int, chat_id: int | None, status: TaskStatus, created_at: datetime, performer_ids: list[int], all_day: bool, custom_properties: list[CustomProperty(id: int, name: str, data_type: CustomPropertyDataType, value: str)]) +``` + --- # Список напоминаний @@ -19710,6 +22308,32 @@ curl "https://api.pachca.com/api/shared/v1/tasks?limit=1" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.tasks.listTasks({ limit: 1, cursor: "eyJpZCI6MTAsImRpciI6ImFzYyJ9" }) +// → ListTasksResponse({ data: Task[], meta: PaginationMeta }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import ListTasksParams + +client = PachcaClient("YOUR_TOKEN") + +params = ListTasksParams(limit=1, cursor="eyJpZCI6MTAsImRpciI6ImFzYyJ9") +response = await client.tasks.list_tasks(params=params) +# → ListTasksResponse(data: list[Task], meta: PaginationMeta) +``` + --- # Информация о напоминании @@ -19888,6 +22512,30 @@ curl "https://api.pachca.com/api/shared/v1/tasks/22283" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.tasks.getTask(22283) +// → Task({ id: number, kind: TaskKind, content: string, dueAt: string | null, priority: number, userId: number, chatId: number | null, status: TaskStatus, createdAt: string, performerIds: number[], allDay: boolean, customProperties: CustomProperty({ id: number, name: string, dataType: CustomPropertyDataType, value: string })[] }) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +response = await client.tasks.get_task(id=22283) +# → Task(id: int, kind: TaskKind, content: str, due_at: datetime | None, priority: int, user_id: int, chat_id: int | None, status: TaskStatus, created_at: datetime, performer_ids: list[int], all_day: bool, custom_properties: list[CustomProperty(id: int, name: str, data_type: CustomPropertyDataType, value: str)]) +``` + --- # Редактирование напоминания @@ -20195,6 +22843,57 @@ curl -X PUT "https://api.pachca.com/api/shared/v1/tasks/22283" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient, TaskKind, TaskStatus, TaskUpdateRequest, TaskUpdateRequestCustomProperty, TaskUpdateRequestTask } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const request: TaskUpdateRequest = { + task: { + kind: TaskKind.Reminder, + content: "Забрать со склада 21 заказ", + dueAt: "2020-06-05T12:00:00.000+03:00", + priority: 2, + performerIds: [123], + status: TaskStatus.Done, + allDay: false, + doneAt: "2020-06-05T12:00:00.000Z", + customProperties: [{ id: 78, value: "Синий склад" }] + } +} +const response = client.tasks.updateTask(22283, request) +// → Task({ id: number, kind: TaskKind, content: string, dueAt: string | null, priority: number, userId: number, chatId: number | null, status: TaskStatus, createdAt: string, performerIds: number[], allDay: boolean, customProperties: CustomProperty({ id: number, name: string, dataType: CustomPropertyDataType, value: string })[] }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import TaskKind, TaskStatus, TaskUpdateRequest, TaskUpdateRequestCustomProperty, TaskUpdateRequestTask + +client = PachcaClient("YOUR_TOKEN") + +request = TaskUpdateRequest( + task=TaskUpdateRequestTask( + kind=TaskKind.REMINDER, + content="Забрать со склада 21 заказ", + due_at=datetime.fromisoformat("2020-06-05T12:00:00.000+03:00"), + priority=2, + performer_ids=[123], + status=TaskStatus.DONE, + all_day=False, + done_at=datetime.fromisoformat("2020-06-05T12:00:00.000Z"), + custom_properties=[TaskUpdateRequestCustomProperty(id=78, value="Синий склад")] + ) +) +response = await client.tasks.update_task(id=22283, request=request) +# → Task(id: int, kind: TaskKind, content: str, due_at: datetime | None, priority: int, user_id: int, chat_id: int | None, status: TaskStatus, created_at: datetime, performer_ids: list[int], all_day: bool, custom_properties: list[CustomProperty(id: int, name: str, data_type: CustomPropertyDataType, value: str)]) +``` + --- # Удаление напоминания @@ -20320,6 +23019,28 @@ curl -X DELETE "https://api.pachca.com/api/shared/v1/tasks/22283" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +client.tasks.deleteTask(22283) +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +await client.tasks.delete_task(id=22283) +``` + --- ## API: Views @@ -20843,6 +23564,53 @@ curl "https://api.pachca.com/api/shared/v1/views/open" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { OpenViewRequest, OpenViewRequestView, PachcaClient, ViewBlockUnion } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const request: OpenViewRequest = { + type: "modal", + triggerId: "791a056b-006c-49dd-834b-c633fde52fe8", + privateMetadata: "{\"timeoff_id\":4378}", + callbackId: "timeoff_reguest_form", + view: { + title: "Уведомление об отпуске", + closeText: "Закрыть", + submitText: "Отправить заявку", + blocks: [{ type: "header", text: "Основная информация" }] + } +} +client.views.openView(request) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import OpenViewRequest, OpenViewRequestView, ViewBlockUnion + +client = PachcaClient("YOUR_TOKEN") + +request = OpenViewRequest( + type="modal", + trigger_id="791a056b-006c-49dd-834b-c633fde52fe8", + private_metadata="{\"timeoff_id\":4378}", + callback_id="timeoff_reguest_form", + view=OpenViewRequestView( + title="Уведомление об отпуске", + close_text="Закрыть", + submit_text="Отправить заявку", + blocks=[ViewBlockHeader(type="header", text="Основная информация")] + ) +) +await client.views.open_view(request=request) +``` + --- ## API: Bots @@ -21084,6 +23852,33 @@ curl -X PUT "https://api.pachca.com/api/shared/v1/bots/1738816" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { BotUpdateRequest, BotUpdateRequestBot, BotUpdateRequestBotWebhook, PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const request: BotUpdateRequest = { bot: { webhook: { outgoingUrl: "https://www.website.com/tasks/new" } } } +const response = client.bots.updateBot(1738816, request) +// → BotResponse({ id: number, webhook: BotResponseWebhook({ outgoingUrl: string }) }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import BotUpdateRequest, BotUpdateRequestBot, BotUpdateRequestBotWebhook + +client = PachcaClient("YOUR_TOKEN") + +request = BotUpdateRequest(bot=BotUpdateRequestBot(webhook=BotUpdateRequestBotWebhook(outgoing_url="https://www.website.com/tasks/new"))) +response = await client.bots.update_bot(id=1738816, request=request) +# → BotResponse(id: int, webhook: BotResponseWebhook(outgoing_url: str)) +``` + --- # История событий @@ -21377,6 +24172,32 @@ curl "https://api.pachca.com/api/shared/v1/webhooks/events?limit=1" \ ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.bots.getWebhookEvents({ limit: 1, cursor: "eyJpZCI6MTAsImRpciI6ImFzYyJ9" }) +// → GetWebhookEventsResponse({ data: WebhookEvent[], meta: PaginationMeta }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import GetWebhookEventsParams + +client = PachcaClient("YOUR_TOKEN") + +params = GetWebhookEventsParams(limit=1, cursor="eyJpZCI6MTAsImRpciI6ImFzYyJ9") +response = await client.bots.get_webhook_events(params=params) +# → GetWebhookEventsResponse(data: list[WebhookEvent], meta: PaginationMeta) +``` + --- # Удаление события @@ -21504,6 +24325,28 @@ curl -X DELETE "https://api.pachca.com/api/shared/v1/webhooks/events/01KAJZ2XDSS ``` +## SDK примеры + +### TypeScript + +```typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +client.bots.deleteWebhookEvent("01KAJZ2XDSS2S3DSW9EXJZ0TBV") +``` + +### Python + +```python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") + +await client.bots.delete_webhook_event(id="01KAJZ2XDSS2S3DSW9EXJZ0TBV") +``` + --- ## API: Security @@ -21762,6 +24605,52 @@ curl "https://api.pachca.com/api/shared/v1/audit_events?start_time=2025-05-01T09 ``` +## SDK примеры + +### TypeScript + +```typescript +import { AuditEventKey, PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +const response = client.security.getAuditEvents({ + startTime: "2025-05-01T09:11:00Z", + endTime: "2025-05-02T09:11:00Z", + eventKey: AuditEventKey.UserLogin, + actorId: "98765", + actorType: "User", + entityId: "98765", + entityType: "User", + limit: 1, + cursor: "eyJpZCI6MTAsImRpciI6ImFzYyJ9" +}) +// → GetAuditEventsResponse({ data: AuditEvent[], meta: PaginationMeta }) +``` + +### Python + +```python +from pachca.client import PachcaClient +from pachca.models import AuditEventKey, GetAuditEventsParams + +client = PachcaClient("YOUR_TOKEN") + +params = GetAuditEventsParams( + start_time=datetime.fromisoformat("2025-05-01T09:11:00Z"), + end_time=datetime.fromisoformat("2025-05-02T09:11:00Z"), + event_key=AuditEventKey.USER_LOGIN, + actor_id="98765", + actor_type="User", + entity_id="98765", + entity_type="User", + limit=1, + cursor="eyJpZCI6MTAsImRpciI6ImFzYyJ9" +) +response = await client.security.get_audit_events(params=params) +# → GetAuditEventsResponse(data: list[AuditEvent], meta: PaginationMeta) +``` + --- ## Дополнительная информация diff --git a/apps/docs/scripts/generate-llms.ts b/apps/docs/scripts/generate-llms.ts index 4ab9d115..a81a7d15 100644 --- a/apps/docs/scripts/generate-llms.ts +++ b/apps/docs/scripts/generate-llms.ts @@ -12,6 +12,7 @@ import type { Endpoint } from '../lib/openapi/types'; import { generateRequestExample, generateExample } from '../lib/openapi/example-generator'; import { generateAllSkills } from './skills/generate'; import { SKILL_TAG_MAP, ROUTER_SKILL_CONFIG } from './skills/config'; +import { getSdkExamples, getValidSdkSymbols } from '../lib/sdk-examples'; const SITE_URL = 'https://dev.pachca.com'; @@ -118,6 +119,669 @@ function generateLlmsTxt(api: Awaited>) { return content; } +function generateLibraryRules(): string { + return `# LIBRARY RULES + +## Authentication +- All requests require Bearer token in Authorization header: \`Authorization: Bearer \` +- Token types: **admin** (full access — manage users, tags, delete messages), **bot** (send messages with custom name/avatar, receive webhooks), **user** (limited access) +- Get admin token: Settings → Automations → API. Get bot token: per-bot in Settings → Automations → Integrations +- Tokens are long-lived and do not expire. Can be reset by admin in Settings +- TypeScript SDK: \`const client = new PachcaClient("YOUR_TOKEN")\` +- Python SDK: \`client = PachcaClient("YOUR_TOKEN")\` + +## Pagination +- Cursor-based: use \`limit\` (1–50, default 50) and \`cursor\` query parameters +- Response includes \`meta.paginate.next_page\` — cursor for next page +- End of data: when \`data\` array is empty (cursor is never null) +- TypeScript auto-pagination: \`client.users.listUsersAll()\` returns flat array of all results +- Python auto-pagination: \`await client.users.list_users_all()\` returns list of all results +- Available for: users, chats, messages, members, tags, reactions, tasks, search results, audit events, webhook events + +## Rate Limiting +- Messages (POST/PUT/DELETE /messages): ~4 req/sec per chat (burst: 30/sec for 5s) +- Message read (GET /messages): ~10 req/sec +- Other endpoints: ~50 req/sec +- On \`429 Too Many Requests\`: respect \`Retry-After\` header value (seconds) +- Recommended retry strategy: exponential backoff with jitter — base delay × 2^attempt × random(0.5–1.5) +- SDK (@pachca/sdk, pachca-sdk) handles retry automatically: 3 retries, respects Retry-After, exponential backoff for 5xx + +## Webhooks (Real-time Events) +- Create a bot in Pachca: Automations → Integrations → Bots +- Set webhook URL in bot settings → Outgoing Webhook tab +- Events: \`new_message\`, \`edit_message\`, \`delete_message\`, \`new_reaction\`, \`delete_reaction\`, \`button_pressed\`, \`view_submit\`, \`chat_member_changed\`, \`company_member_changed\`, \`link_shared\` +- Verify: HMAC-SHA256 of raw body with bot's Signing Secret +- Header: \`Pachca-Signature\` contains hex digest +- Replay protection: check \`webhook_timestamp\` within ±60 seconds of current time +- Alternative to webhooks: polling via GET /webhooks/events (enable "Save event history" in bot settings) +- IP whitelist: Pachca webhook IP is \`37.200.70.177\` + +## File Uploads (3-step process) +- Step 1: POST /uploads → get S3 presigned params (\`direct_url\`, \`key\`, \`policy\`, \`x-amz-signature\`, etc.) +- Step 2: POST to \`direct_url\` (external S3 URL, NOT a Pachca API endpoint) with multipart/form-data — all params + \`file\` as LAST field +- Step 3: Replace \`\${filename}\` in \`key\` with actual filename, include in message \`files\` array +- Note: \`direct_url\` is an external S3 presigned URL and does not require Authorization header + +## User Status +- Get any user's status: GET /users/{id}/status → \`{ emoji, title, expires_at, is_away, away_message }\` +- Set own status: PUT /profile/status \`{ status: { emoji, title, expires_at, is_away, away_message } }\` +- Clear own status: DELETE /profile/status +- Admin can manage any user: PUT /users/{id}/status, DELETE /users/{id}/status +- No real-time presence webhooks — use polling with ≥60s interval for monitoring status changes + +## Error Handling +- \`400\`: validation errors — \`{ errors: [{ key, value, message, code }] }\` with codes: \`blank\`, \`invalid\`, \`taken\`, \`too_short\`, \`too_long\`, \`not_a_number\` +- \`401\`: unauthorized — \`{ error, error_description }\` (OAuthError) +- \`403\`: forbidden — insufficient permissions. May return ApiError (business logic) or OAuthError (\`insufficient_scope\`) +- \`404\`: not found +- \`409\`: conflict (duplicate) +- \`422\`: unprocessable — \`{ errors: [{ key, value, message, code }] }\` +- \`429\`: rate limited — respect \`Retry-After\` header, use exponential backoff with jitter +- SDK auto-retries \`429\` and \`5xx\` errors (3 attempts with exponential backoff) + +## Idempotency and Reliability +- Pachca API operations are NOT idempotent by default — duplicate POST requests create duplicate resources +- Client-side deduplication: track request IDs, check before sending, store results with TTL +- Webhooks use at-least-once delivery — handlers MUST be idempotent (dedup by event fields: id + type + event) +- For multi-step operations: implement compensating actions (saga pattern) for failure recovery +- Separate critical operations (create chat) from non-critical (send welcome message) with independent error handling + +## SDK (Typed Clients for 6 Languages) +- Unified pattern: \`PachcaClient(token)\` → \`client.service.method(request)\` +- **Input**: path params and body fields (≤2) expand to method arguments; otherwise a single request object +- **Output**: if API response has a single \`data\` field, SDK returns its contents directly +- Service, method, and field names match operationId and parameters from OpenAPI + +| Language | Package | Install | +|----------|---------|---------| +| TypeScript | \`@pachca/sdk\` | \`npm install @pachca/sdk\` | +| Python | \`pachca-sdk\` | \`pip install pachca-sdk\` | +| Go | \`github.com/pachca/go-sdk\` | \`go get github.com/pachca/openapi/sdk/go/generated\` | +| Kotlin | \`com.pachca:sdk\` | \`implementation("com.pachca:pachca-sdk:1.0.1")\` | +| Swift | \`PachcaSDK\` | SPM: \`https://github.com/pachca/openapi\` | +| C# | \`Pachca.Sdk\` | \`dotnet add package Pachca.Sdk\` | + +`; +} + +// SDK types that are valid exports but never appear in examples.json operation imports +const ALWAYS_VALID_SDK_IMPORTS = new Set(['PachcaClient', 'ApiError', 'OAuthError']); + +/** + * Build-time validation: extracts client.service.method() calls and SDK imports + * from How-to Guide code blocks, validates them against examples.json data. + * Throws on first batch of errors to fail the build with a clear message. + */ +function validateSdkCodeBlocks(sectionName: string, content: string): void { + const tsSymbols = getValidSdkSymbols('typescript'); + const pySymbols = getValidSdkSymbols('python'); + + const codeBlockRegex = /```(typescript|python)\n([\s\S]*?)```/g; + const errors: string[] = []; + + let match; + while ((match = codeBlockRegex.exec(content)) !== null) { + const lang = match[1] as 'typescript' | 'python'; + const code = match[2]; + const symbols = lang === 'typescript' ? tsSymbols : pySymbols; + + // Validate client.service.method() calls + const methodRegex = /client\.(\w+)\.(\w+)\s*\(/g; + const hasMethodCalls = methodRegex.test(code); + methodRegex.lastIndex = 0; + + let m; + while ((m = methodRegex.exec(code)) !== null) { + const [, service, method] = m; + if (!symbols.methods.has(service)) { + errors.push( + `[${lang}] Unknown service: client.${service}.${method}() — valid services: ${[...symbols.methods.keys()].join(', ')}` + ); + } else if (!symbols.methods.get(service)!.has(method)) { + errors.push( + `[${lang}] Unknown method: client.${service}.${method}() — valid for ${service}: ${[...symbols.methods.get(service)!].join(', ')}` + ); + } + } + + // Only validate imports in code blocks with actual SDK method calls + // (skip showcase/reference blocks that just list all available types) + if (!hasMethodCalls) continue; + + // Validate TypeScript SDK imports + if (lang === 'typescript') { + const importRegex = /import\s*\{([^}]+)\}\s*from\s*["']@pachca\/sdk["']/g; + let im; + while ((im = importRegex.exec(code)) !== null) { + const names = im[1] + .split(',') + .map((s) => s.trim()) + .filter(Boolean); + for (const name of names) { + if (!symbols.imports.has(name) && !ALWAYS_VALID_SDK_IMPORTS.has(name)) { + errors.push(`[typescript] Unknown SDK import: ${name}`); + } + } + } + } + + // Validate Python SDK imports (single-line and parenthesized multiline) + if (lang === 'python') { + const pyImportRegex = /from\s+pachca\.models\s+import\s+(?:\(([\s\S]*?)\)|([^\n]+))/g; + let im; + while ((im = pyImportRegex.exec(code)) !== null) { + const raw = im[1] || im[2]; + // Strip Python comments and extract only PascalCase type names + const names = raw + .split('\n') + .map((line) => line.replace(/#.*/, '')) + .join(',') + .split(',') + .map((s) => s.trim()) + .filter((s) => s.length > 0 && /^[A-Z]/.test(s)); + for (const name of names) { + if (!symbols.imports.has(name) && !ALWAYS_VALID_SDK_IMPORTS.has(name)) { + errors.push(`[python] Unknown SDK import: ${name} from pachca.models`); + } + } + } + } + } + + if (errors.length > 0) { + throw new Error( + `\n❌ SDK code validation failed in "${sectionName}"!\n` + + `The following SDK symbols don't match examples.json:\n\n` + + errors.map((e) => ` • ${e}`).join('\n') + + `\n\nFix the code or update the SDK.\n` + ); + } +} + +function generateHowToGuides(): string { + let content = '# How-to Guides\n\n'; + + // Q1: Auth + TypeScript SDK + content += `## How to authenticate with the API + +All API requests require a Bearer token in the Authorization header. + +### curl +\`\`\`bash +curl -H "Authorization: Bearer YOUR_TOKEN" https://api.pachca.com/api/shared/v1/users +\`\`\` + +### TypeScript SDK +\`\`\`typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") +const profile = await client.profile.getProfile() +console.log(profile.id, profile.firstName) +\`\`\` + +### Python SDK +\`\`\`python +from pachca.client import PachcaClient + +client = PachcaClient("YOUR_TOKEN") +profile = await client.profile.get_profile() +print(profile.id, profile.first_name) +\`\`\` + +Token types: **admin** (full access, get in Settings → Automations → API), **bot** (messaging + webhooks, per-bot in Integrations), **user** (limited). + +`; + + // Q2: Pagination SDK + content += `## How to paginate through results + +### TypeScript SDK — auto-pagination +\`\`\`typescript +// Returns all results as a flat array (handles cursors automatically) +const allUsers = await client.users.listUsersAll() +console.log(\`Total: \${allUsers.length}\`) + +// Manual pagination with cursor control +let cursor: string | undefined +for (;;) { + const response = await client.users.listUsers({ limit: 50, cursor }) + if (response.data.length === 0) break + for (const user of response.data) { + console.log(user.firstName, user.lastName) + } + cursor = response.meta.paginate.nextPage +} +\`\`\` + +### Python SDK — auto-pagination +\`\`\`python +# Returns all results as a list (handles cursors automatically) +all_users = await client.users.list_users_all() +print(f"Total: {len(all_users)}") + +# Manual pagination with cursor control +from pachca.models import ListUsersParams + +cursor = None +while True: + response = await client.users.list_users(ListUsersParams(limit=50, cursor=cursor)) + if not response.data: + break + for user in response.data: + print(user.first_name, user.last_name) + cursor = response.meta.paginate.next_page +\`\`\` + +Auto-pagination methods: \`listUsersAll()\`, \`listChatsAll()\`, \`listChatMessagesAll()\`, \`listMembersAll()\`, \`listTagsAll()\`, \`listTasksAll()\`, \`searchMessagesAll()\`, \`searchChatsAll()\`, \`searchUsersAll()\`, \`listReactionsAll()\`, \`getAuditEventsAll()\`, \`getWebhookEventsAll()\`. + +`; + + // Q4: Create chat + add members (SDK) + content += `## How to create chats and manage members + +### TypeScript SDK +\`\`\`typescript +import { PachcaClient, MessageEntityType } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +// Create a group chat with initial members +const chat = await client.chats.createChat({ + chat: { name: "Project Discussion", memberIds: [1, 2, 3], channel: false, public: false } +}) +console.log("Created chat:", chat.id, chat.name) + +// Add more members later +await client.members.addMembers(chat.id, { memberIds: [4, 5] }) + +// Remove a member +await client.members.removeMember(chat.id, 5) + +// List current members +const members = await client.members.listMembersAll(chat.id) +console.log("Members:", members.map(m => m.firstName)) + +// Send a message to the chat +await client.messages.createMessage({ + message: { entityType: MessageEntityType.Discussion, entityId: chat.id, content: "Welcome everyone!" } +}) +\`\`\` + +### Python SDK +\`\`\`python +from pachca.client import PachcaClient +from pachca.models import ChatCreateRequest, ChatCreateRequestChat, MessageCreateRequest, MessageCreateRequestMessage, AddMembersRequest + +client = PachcaClient("YOUR_TOKEN") + +# Create a group chat with initial members +chat = await client.chats.create_chat(ChatCreateRequest( + chat=ChatCreateRequestChat(name="Project Discussion", member_ids=[1, 2, 3], channel=False, public=False) +)) +print("Created chat:", chat.id, chat.name) + +# Add more members later +await client.members.add_members(chat.id, AddMembersRequest(member_ids=[4, 5])) + +# List current members +members = await client.members.list_members_all(chat.id) + +# Send a message to the chat +await client.messages.create_message(MessageCreateRequest( + message=MessageCreateRequestMessage(entity_type="discussion", entity_id=chat.id, content="Welcome everyone!") +)) +\`\`\` + +Chat types: \`channel: true\` creates a channel (one-way announcements), \`channel: false\` creates a group chat (everyone can write). \`public: true\` makes it visible to all workspace members. + +`; + + // Q3+Q7: Webhooks + content += `## How to set up webhooks for real-time updates + +### Step-by-step setup +1. Create a bot in Pachca: **Automations** → **Integrations** → **Bots** +2. In bot settings, go to **Outgoing Webhook** tab and set your HTTPS URL +3. Copy the **Signing Secret** for signature verification +4. Select event types: new messages, reactions, button presses, form submissions, etc. +5. Add the bot to chats where you want to receive events (global events like company member changes work without adding to chat) + +### TypeScript webhook handler (Express.js) +\`\`\`typescript +import express from "express" +import crypto from "crypto" + +const SIGNING_SECRET = "your_signing_secret" // From bot settings → Outgoing Webhook +const app = express() + +app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => { + // Step 1: Verify HMAC-SHA256 signature + const signature = crypto.createHmac("sha256", SIGNING_SECRET) + .update(req.body).digest("hex") + if (signature !== req.headers["pachca-signature"]) { + return res.status(401).send("Invalid signature") + } + + // Step 2: Check timestamp for replay protection (±60 seconds) + const event = JSON.parse(req.body.toString()) + if (Math.abs(Date.now() / 1000 - event.webhook_timestamp) > 60) { + return res.status(401).send("Expired event") + } + + // Step 3: Process event by type + switch (event.type) { + case "message": + if (event.event === "new") console.log("New message:", event.content, "from user:", event.user_id) + if (event.event === "update") console.log("Message edited:", event.id) + if (event.event === "delete") console.log("Message deleted:", event.id) + break + case "reaction": + console.log(event.event === "new" ? "Reaction added:" : "Reaction removed:", event.emoji) + break + case "button": + console.log("Button pressed:", event.data, "by user:", event.user_id) + // Use event.trigger_id within 3 seconds to open a form + break + case "view_submit": + console.log("Form submitted:", event.payload) + break + } + + res.status(200).send("OK") +}) +app.listen(3000) +\`\`\` + +### Python webhook handler (Flask) +\`\`\`python +import hmac, hashlib, json, time +from flask import Flask, request, abort + +SIGNING_SECRET = "your_signing_secret" # From bot settings → Outgoing Webhook +app = Flask(__name__) + +@app.route("/webhook", methods=["POST"]) +def webhook(): + raw_body = request.get_data() + + # Step 1: Verify HMAC-SHA256 signature + expected = hmac.new(SIGNING_SECRET.encode(), raw_body, hashlib.sha256).hexdigest() + if expected != request.headers.get("Pachca-Signature"): + abort(401) + + # Step 2: Check timestamp for replay protection + event = json.loads(raw_body) + if abs(time.time() - event["webhook_timestamp"]) > 60: + abort(401) + + # Step 3: Process event by type + if event["type"] == "message" and event["event"] == "new": + print("New message:", event["content"], "from user:", event["user_id"]) + elif event["type"] == "button": + print("Button pressed:", event["data"]) + + return "OK", 200 +\`\`\` + +### Webhook event types +| Event type | Description | Fields | +|-----------|-------------|--------| +| message (new) | New message in chat | id, content, user_id, chat_id, entity_type, entity_id, created_at, url | +| message (update) | Message edited | id, content, user_id, chat_id | +| message (delete) | Message deleted | id, user_id, chat_id | +| reaction (new/delete) | Reaction added/removed | message_id, user_id, emoji, chat_id | +| button | Button pressed | message_id, user_id, data, trigger_id, chat_id | +| view_submit | Form submitted | payload (form field values), user_id, trigger_id | +| chat_member (new/delete) | Member added/removed from chat | chat_id, user_id, event | +| company_member (new/update/delete) | Workspace member changes | user_id, event (no chat needed) | +| link_shared | URL shared (unfurl bots) | url, message_id, chat_id | + +### Alternative: Polling (when webhook URL is not available) +Enable "Save event history" in bot settings, then poll: +\`\`\`typescript +// Poll for events periodically +const events = await client.bots.getWebhookEvents() +for (const event of events.data) { + processEvent(event) + await client.bots.deleteWebhookEvent(event.id) // Remove processed event +} +\`\`\` + +`; + + // Q5: Rate limiting + retry + content += `## How to handle rate limits and implement retry logic + +Both SDKs handle retry automatically — 3 retries with exponential backoff for \`429\` and \`5xx\` errors. No extra code needed: + +### TypeScript SDK +\`\`\`typescript +import { PachcaClient, ApiError } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +// SDK retries 429 and 5xx automatically (3 attempts, exponential backoff) +// Just call methods normally — retries are transparent +const users = await client.users.listUsersAll() + +// If all retries are exhausted, ApiError is thrown +try { + await client.messages.createMessage({ + message: { entityId: 12345, content: "Hello" } + }) +} catch (error) { + if (error instanceof ApiError) { + // After 3 retries, the error is surfaced — check error.errors for details + for (const e of error.errors ?? []) { + console.error(e.key, e.message) + } + } +} +\`\`\` + +### Python SDK +\`\`\`python +from pachca.client import PachcaClient +from pachca.models import ApiError + +client = PachcaClient("YOUR_TOKEN") + +# SDK retries 429 and 5xx automatically (3 attempts, exponential backoff) +# Just call methods normally — retries are transparent +users = await client.users.list_users_all() + +# If all retries are exhausted, ApiError is raised +try: + await client.messages.create_message(request) +except ApiError as e: + # After 3 retries, the error is surfaced — check e.errors for details + for err in e.errors: + print(err.key, err.message) +\`\`\` + +Rate limits by endpoint category: +- Messages send/edit/delete: ~4 req/sec per chat (burst: 30/sec for 5s) +- Messages read: ~10 req/sec per token +- All other endpoints: ~50 req/sec per token +- On 429: SDK respects \`Retry-After\` header automatically + +`; + + // Q6: File upload + content += `## How to upload files and attach to messages + +File upload is a 3-step process: get presigned params → upload to S3 → attach to message. + +### TypeScript SDK +\`\`\`typescript +import { PachcaClient, FileType } from "@pachca/sdk" +import fs from "fs" +import path from "path" + +const client = new PachcaClient("YOUR_TOKEN") + +const filePath = "./report.pdf" +const fileName = path.basename(filePath) +const fileBuffer = fs.readFileSync(filePath) + +// Step 1: Get S3 presigned upload parameters +const params = await client.common.getUploadParams() + +// Step 2: Upload file to S3 (direct_url is an external presigned URL, not Pachca API) +await client.common.uploadFile(params.directUrl, { + contentDisposition: params.contentDisposition, + acl: params.acl, + policy: params.policy, + xAmzCredential: params.xAmzCredential, + xAmzAlgorithm: params.xAmzAlgorithm, + xAmzDate: params.xAmzDate, + xAmzSignature: params.xAmzSignature, + key: params.key, + file: new File([fileBuffer], fileName) +}) + +// Step 3: Attach file to message (replace ${'$'}{filename} in key with actual name) +const fileKey = params.key.replace("${'$'}{filename}", fileName) +await client.messages.createMessage({ + message: { + entityId: 12345, + content: "Report attached", + files: [{ key: fileKey, name: fileName, fileType: FileType.File, size: fileBuffer.length }] + } +}) +\`\`\` + +### Python SDK +\`\`\`python +from pachca.client import PachcaClient +from pachca.models import ( + FileUploadRequest, MessageCreateRequest, MessageCreateRequestMessage, + MessageCreateRequestFile, FileType +) +import os + +client = PachcaClient("YOUR_TOKEN") + +file_path = "report.pdf" +file_name = os.path.basename(file_path) + +# Step 1: Get S3 presigned upload parameters +params = await client.common.get_upload_params() + +# Step 2: Upload file to S3 (direct_url is an external presigned URL, not Pachca API) +with open(file_path, "rb") as f: + await client.common.upload_file( + direct_url=params.direct_url, + request=FileUploadRequest( + content_disposition=params.content_disposition, + acl=params.acl, + policy=params.policy, + x_amz_credential=params.x_amz_credential, + x_amz_algorithm=params.x_amz_algorithm, + x_amz_date=params.x_amz_date, + x_amz_signature=params.x_amz_signature, + key=params.key, + file=f.read() + ) + ) + +# Step 3: Attach file to message (replace ${'$'}{filename} in key with actual name) +file_key = params.key.replace("${'$'}{filename}", file_name) +await client.messages.create_message(MessageCreateRequest( + message=MessageCreateRequestMessage( + entity_id=12345, + content="Report attached", + files=[MessageCreateRequestFile(key=file_key, name=file_name, file_type=FileType.FILE, size=os.path.getsize(file_path))] + ) +)) +\`\`\` + +File types: \`FileType.File\` / \`FileType.FILE\` (any file), \`FileType.Image\` / \`FileType.IMAGE\` (add \`width\` and \`height\`). + +`; + + // Q8: User status + presence + content += `## How to monitor user status and presence + +### TypeScript SDK +\`\`\`typescript +import { PachcaClient } from "@pachca/sdk" + +const client = new PachcaClient("YOUR_TOKEN") + +// Get any user's status +const status = await client.users.getUserStatus(userId) +console.log(status.emoji, status.title, status.isAway, status.awayMessage) + +// Set your own status +await client.profile.updateStatus({ + status: { emoji: "🏖️", title: "On vacation", isAway: true, awayMessage: "Back on Monday" } +}) + +// Set status with expiration +await client.profile.updateStatus({ + status: { emoji: "🍽️", title: "Lunch break", expiresAt: new Date(Date.now() + 3600000).toISOString() } +}) + +// Clear your status +await client.profile.deleteStatus() +\`\`\` + +### Python SDK +\`\`\`python +from pachca.client import PachcaClient +from pachca.models import StatusUpdateRequest, StatusUpdateRequestStatus + +client = PachcaClient("YOUR_TOKEN") + +# Get any user's status +status = await client.users.get_user_status(user_id) +print(status.emoji, status.title, status.is_away, status.away_message) + +# Set your own status +await client.profile.update_status(StatusUpdateRequest( + status=StatusUpdateRequestStatus(emoji="🏖️", title="On vacation", is_away=True, away_message="Back on Monday") +)) + +# Clear your status +await client.profile.delete_status() +\`\`\` + +### Polling pattern for monitoring status changes +Pachca does not provide real-time webhooks for status/presence changes. Use polling with caching: + +\`\`\`typescript +const cache = new Map() +const CACHE_TTL = 60_000 // Minimum 60 seconds between polls per user + +async function getUserPresence(userId: number) { + const cached = cache.get(userId) + if (cached && Date.now() - cached.fetchedAt < CACHE_TTL) return cached.status + const status = await client.users.getUserStatus(userId) + cache.set(userId, { status, fetchedAt: Date.now() }) + return status +} + +// Batch refresh for a team +async function refreshTeamPresences(userIds: number[]) { + const results = [] + for (const id of userIds) { + cache.delete(id) + results.push(await getUserPresence(id)) + } + return results +} +\`\`\` + +Status fields: \`emoji\` (string), \`title\` (string), \`expires_at\` (ISO datetime or null), \`is_away\` (boolean), \`away_message\` (string). Admin can manage any user's status via PUT/DELETE /users/{id}/status. + +`; + + content += '---\n\n'; + return content; +} + async function generateLlmsFullTxt(api: Awaited>) { const baseUrl = api.servers[0]?.url; const grouped = groupByTag(api.endpoints); @@ -147,53 +811,25 @@ async function generateLlmsFullTxt(api: Awaited> } content += '\n'; - content += '### SDK\n'; - content += '- [SDK](#sdk)\n'; content += '\n---\n\n'; - content += '# SDK\n\n'; - content += - 'Типизированные клиенты для 6 языков. Единый паттерн: `PachcaClient(token)` → `client.service.method(request)`.\n\n'; - content += '| Язык | Пакет | Установка |\n'; - content += '|------|-------|----------|\n'; - content += '| TypeScript | `@pachca/sdk` | `npm install @pachca/sdk` |\n'; - content += '| Python | `pachca-sdk` | `pip install pachca-sdk` |\n'; - content += - '| Go | `github.com/pachca/go-sdk` | `go get github.com/pachca/openapi/sdk/go/generated` |\n'; - content += '| Kotlin | `com.pachca:sdk` | `implementation("com.pachca:pachca-sdk:1.0.1")` |\n'; - content += '| Swift | `PachcaSDK` | SPM: `https://github.com/pachca/openapi` |\n'; - content += '| C# | `Pachca.Sdk` | `dotnet add package Pachca.Sdk` |\n\n'; - content += '## Конвенции SDK\n\n'; - content += - '- **Вход**: path-параметры и body-поля (если ≤2) разворачиваются в аргументы метода. Иначе — один объект-запрос.\n'; - content += - '- **Выход**: если ответ API содержит единственное поле `data`, SDK возвращает его содержимое напрямую.\n'; - content += - '- Имена сервисов, методов и полей соответствуют operationId и параметрам из OpenAPI.\n\n'; - content += '### Примеры вызова по языкам\n\n'; - content += - '**TypeScript:**\n```typescript\nimport { PachcaClient } from "@pachca/sdk";\nconst pachca = new PachcaClient("YOUR_TOKEN");\nconst users = await pachca.users.listUsers();\nawait pachca.reactions.addReaction(messageId, { code: "👍" });\n```\n\n'; - content += - '**Python:**\n```python\nfrom pachca import PachcaClient\nclient = PachcaClient("YOUR_TOKEN")\nusers = await client.users.list_users()\nawait client.reactions.add_reaction(message_id, ReactionRequest(code="👍"))\n```\n\n'; - content += - '**Go:**\n```go\nclient := pachca.NewPachcaClient("YOUR_TOKEN")\nusers, err := client.Users.ListUsers(ctx, nil)\nreaction, err := client.Reactions.AddReaction(ctx, messageId, pachca.ReactionRequest{Code: "👍"})\n```\n\n'; - content += - '**Kotlin:**\n```kotlin\nval pachca = PachcaClient("YOUR_TOKEN")\nval users = pachca.users.listUsers()\npachca.reactions.addReaction(messageId, ReactionRequest(code = "👍"))\n```\n\n'; - content += - '**Swift:**\n```swift\nlet pachca = PachcaClient(token: "YOUR_TOKEN")\nlet users = try await pachca.users.listUsers()\ntry await pachca.reactions.addReaction(messageId, ReactionRequest(code: "👍"))\n```\n\n'; - content += - '**C#:**\n```csharp\nusing var client = new PachcaClient("YOUR_TOKEN");\nvar users = await client.Users.ListUsersAsync();\nawait client.Reactions.AddReactionAsync(messageId, new ReactionRequest { Code = "👍" });\n```\n\n'; - + content += generateLibraryRules(); content += '---\n\n'; + const howToContent = generateHowToGuides(); + validateSdkCodeBlocks('How-to Guides', howToContent); + content += howToContent; content += '# Руководства\n\n'; + let guidesContent = ''; for (const guide of guidePages) { const guideContent = await generateStaticPageMarkdownAsync(guide.path); if (guideContent) { - content += guideContent; - content += '\n---\n\n'; + guidesContent += guideContent; + guidesContent += '\n---\n\n'; } } + validateSdkCodeBlocks('Guides (MDX)', guidesContent); + content += guidesContent; content += '# API Методы\n\n'; for (const tag of sortedTags) { @@ -202,6 +838,19 @@ async function generateLlmsFullTxt(api: Awaited> for (const endpoint of endpoints) { const endpointMarkdown = generateEndpointMarkdown(endpoint, baseUrl); content += endpointMarkdown; + + // Add TypeScript and Python SDK examples for each endpoint + const sdkExamples = getSdkExamples(endpoint.id); + if (sdkExamples.typescript || sdkExamples.python) { + content += '\n## SDK примеры\n'; + if (sdkExamples.typescript) { + content += '\n### TypeScript\n\n```typescript\n' + sdkExamples.typescript + '\n```\n'; + } + if (sdkExamples.python) { + content += '\n### Python\n\n```python\n' + sdkExamples.python + '\n```\n'; + } + } + content += '\n---\n\n'; } } @@ -212,9 +861,81 @@ async function generateLlmsFullTxt(api: Awaited> content += '---\n\n'; content += '_Документация автоматически сгенерирована из OpenAPI спецификации_\n'; + // Post-process: insert Document Map with calculated line ranges + content = insertDocumentMap(content); + return content; } +/** + * Post-process: scan generated content for section headers, + * build a Document Map with real line ranges, and insert it after TOC. + */ +function insertDocumentMap(content: string): string { + const lines = content.split('\n'); + + // Find section start lines (1-based) + const markers: { name: string; desc: string; line: number }[] = []; + for (let i = 0; i < lines.length; i++) { + const l = lines[i]; + if (l === '# LIBRARY RULES') + markers.push({ + name: 'LIBRARY RULES', + desc: 'Core rules, auth, pagination, rate limits, SDK overview', + line: i + 1, + }); + else if (l === '# How-to Guides') + markers.push({ + name: 'How-to Guides', + desc: 'Step-by-step solutions with TypeScript + Python code', + line: i + 1, + }); + else if (l === '# Руководства') + markers.push({ + name: 'Guides', + desc: 'Full SDK docs (6 languages), webhooks, bots, forms, n8n', + line: i + 1, + }); + else if (l === '# API Методы') + markers.push({ + name: 'API Reference', + desc: 'Complete REST API — every endpoint with schemas and examples', + line: i + 1, + }); + } + + if (markers.length === 0) return content; + + // Build the map block + const mapLines = [ + '', + '## Document Map', + '', + '| Section | Description | Lines |', + '|---------|-------------|-------|', + ]; + const mapBlockSize = mapLines.length + markers.length; // join adds length-1 newlines + + // Adjust line numbers by the map block we're about to insert + for (const m of markers) { + m.line += mapBlockSize; + } + const totalLines = lines.length + mapBlockSize; + + for (let i = 0; i < markers.length; i++) { + const start = markers[i].line; + const end = i < markers.length - 1 ? markers[i + 1].line - 1 : totalLines; + mapLines.push(`| ${markers[i].name} | ${markers[i].desc} | ${start}–${end} |`); + } + mapLines.push(''); + + // Insert after TOC (before the first ---) + const insertIdx = content.indexOf('\n---\n\n# LIBRARY RULES'); + if (insertIdx === -1) return content; + + return content.slice(0, insertIdx) + mapLines.join('\n') + content.slice(insertIdx); +} + function generateWorkflowsSection(): string { let content = '## Common Workflows\n\n'; content += '### CLI Quick Start\n\n'; diff --git a/integrations/n8n/CHANGELOG.md b/integrations/n8n/CHANGELOG.md index b0faa228..6061f9ea 100644 --- a/integrations/n8n/CHANGELOG.md +++ b/integrations/n8n/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog +## 2.0.5 (2026-04-09) + +### Bug Fixes + +- Fix v1 file attachments — `fileType` → `file_type` mapping now applied to both v1 top-level `files` and v2 `additionalFields.files` (previously v1 blocks sent camelCase and got 422 `system: null`) +- Fix buttons clear in raw JSON mode — `[]` now clears buttons in PUT message (previously only `[[]]` worked; v1 behavior restored) +- Auto-retry on rate limit and server errors — 429 and 5xx responses are retried up to 3 times, honoring `Retry-After` header, with exponential backoff and jitter (aligns with Pachca's documented retry strategy); removes the need for manual Wait nodes + ## 2.0.4 (2026-04-08) ### Bug Fixes diff --git a/integrations/n8n/README.md b/integrations/n8n/README.md index 29c58083..3d4fac01 100644 --- a/integrations/n8n/README.md +++ b/integrations/n8n/README.md @@ -24,7 +24,7 @@ Or install from archive (Docker, custom n8n images): # Download from GitHub Releases # Find the latest n8n-nodes-pachca.tgz at: # https://github.com/pachca/openapi/releases?q=n8n -wget https://github.com/pachca/openapi/releases/download/n8n-v2.0.4/n8n-nodes-pachca.tgz +wget https://github.com/pachca/openapi/releases/download/n8n-v2.0.5/n8n-nodes-pachca.tgz # Via npm (recommended) cd ~/.n8n/nodes && npm install ./n8n-nodes-pachca.tgz diff --git a/integrations/n8n/nodes/Pachca/GenericFunctions.ts b/integrations/n8n/nodes/Pachca/GenericFunctions.ts index edcf104d..131aa987 100644 --- a/integrations/n8n/nodes/Pachca/GenericFunctions.ts +++ b/integrations/n8n/nodes/Pachca/GenericFunctions.ts @@ -293,49 +293,60 @@ export async function makeApiRequest( ignoreHttpStatusErrors: true, }; - const response = (await this.helpers.httpRequestWithAuthentication.call( - this, 'pachcaApi', options, - )) as IN8nHttpFullResponse; - - // Handle empty/null body (e.g. 204 No Content) - if (!response.body || typeof response.body !== 'object') { - if (response.statusCode >= 200 && response.statusCode < 300) { - return { success: true } as unknown as IDataObject; + const MAX_RETRIES = 3; + for (let attempt = 0; ; attempt++) { + const response = (await this.helpers.httpRequestWithAuthentication.call( + this, 'pachcaApi', options, + )) as IN8nHttpFullResponse; + + // Handle empty/null body (e.g. 204 No Content) + if (!response.body || typeof response.body !== 'object') { + if (response.statusCode >= 200 && response.statusCode < 300) { + return { success: true } as unknown as IDataObject; + } } - } - if (response.statusCode >= 400) { - const resBody = (response.body ?? {}) as IDataObject; - const errors = resBody.errors as Array<{ key: string; value: string }> | undefined; - - let message: string; - if (errors?.length) { - message = errors.map(e => { - const displayName = FIELD_DISPLAY_NAMES[e.key] || e.key; - return `${displayName}: ${e.value}`; - }).join('; '); - } else { - message = ((resBody.message || resBody.error || `Request failed with status ${response.statusCode}`) as string); + // Auto-retry on 429 (rate limit) and 5xx (server errors) + if ((response.statusCode === 429 || response.statusCode >= 500) && attempt < MAX_RETRIES) { + const respHeaders = response.headers as Record | undefined; + const retryAfter = parseInt(respHeaders?.['retry-after'] ?? '', 10); + const delaySec = retryAfter || Math.pow(2, attempt) * (0.5 + Math.random()); + await sleep(delaySec * 1000); + continue; } - const hint = STATUS_HINTS[response.statusCode]; - const description = hint ? `${hint}\n${message}` : message; + if (response.statusCode >= 400) { + const resBody = (response.body ?? {}) as IDataObject; + const errors = resBody.errors as Array<{ key: string; value: string }> | undefined; - const apiError = new NodeApiError(this.getNode(), resBody as JsonObject, { - message, - httpCode: String(response.statusCode), - description, - itemIndex, - }); - // Attach Retry-After for pagination retry logic - const headers = response.headers as Record | undefined; - if (headers?.['retry-after']) { - (apiError as NodeApiError & { retryAfter?: number }).retryAfter = parseInt(headers['retry-after'], 10) || 2; + let message: string; + if (errors?.length) { + message = errors.map(e => { + const displayName = FIELD_DISPLAY_NAMES[e.key] || e.key; + return `${displayName}: ${e.value}`; + }).join('; '); + } else { + message = ((resBody.message || resBody.error || `Request failed with status ${response.statusCode}`) as string); + } + + const hint = STATUS_HINTS[response.statusCode]; + const description = hint ? `${hint}\n${message}` : message; + + const apiError = new NodeApiError(this.getNode(), resBody as JsonObject, { + message, + httpCode: String(response.statusCode), + description, + itemIndex, + }); + const respHeaders = response.headers as Record | undefined; + if (respHeaders?.['retry-after']) { + (apiError as NodeApiError & { retryAfter?: number }).retryAfter = parseInt(respHeaders['retry-after'], 10) || 2; + } + throw apiError; } - throw apiError; - } - return response.body as IDataObject; + return response.body as IDataObject; + } } /** @@ -396,8 +407,6 @@ export async function makeApiRequestAllPages( const limit = returnAll ? 0 : ((this.getNodeParameter('limit', itemIndex, 50) as number) || 50); const results: IDataObject[] = []; let cursor: string | undefined; - let totalRetries = 0; - const MAX_RETRIES = 5; const MAX_PAGES = 1000; let pageCount = 0; @@ -406,27 +415,8 @@ export async function makeApiRequestAllPages( const pageQs: IDataObject = { ...qs, limit: perPage }; if (cursor) pageQs.cursor = cursor; - // Inner retry loop: avoids `continue` on the outer do-while, which - // would re-evaluate `while(cursor && ...)` and exit prematurely when - // cursor is undefined (e.g. 429 on the very first page). - let response: IDataObject; - for (;;) { - try { - response = await makeApiRequest.call(this, method, endpoint, undefined, pageQs, itemIndex); - break; - } catch (error: unknown) { - const err = error instanceof NodeApiError ? error : null; - const code = err?.httpCode; - if ((code === '429' || code === '502' || code === '503') && totalRetries < MAX_RETRIES) { - totalRetries++; - const retryAfter = (err as NodeApiError & { retryAfter?: number })?.retryAfter; - const waitSec = retryAfter ?? (code === '429' ? 2 : 1); - await sleep(waitSec * 1000); - continue; - } - throw error; - } - } + // Retry logic (429/5xx) is handled inside makeApiRequest. + const response = await makeApiRequest.call(this, method, endpoint, undefined, pageQs, itemIndex); const items = (response.data as IDataObject[]) ?? []; results.push(...items); @@ -510,15 +500,17 @@ export function resolveResourceLocator( export function buildButtonRows( ctx: IExecuteFunctions, itemIndex: number, -): IDataObject[][] { +): IDataObject[][] | null { let buttonLayout: string; - try { buttonLayout = ctx.getNodeParameter('buttonLayout', itemIndex, 'none') as string; } catch { return []; } - if (buttonLayout === 'none') return []; + try { buttonLayout = ctx.getNodeParameter('buttonLayout', itemIndex, 'none') as string; } catch { return null; } + if (buttonLayout === 'none') return null; if (buttonLayout === 'raw_json') { let rawJson: string; - try { rawJson = ctx.getNodeParameter('rawJsonButtons', itemIndex, '') as string; } catch { return []; } - if (!rawJson || rawJson.trim() === '' || rawJson.trim() === '[]') return []; + try { rawJson = ctx.getNodeParameter('rawJsonButtons', itemIndex, '') as string; } catch { return null; } + if (!rawJson || rawJson.trim() === '') return null; + // [] means "remove all buttons" — send empty array so the API clears them + if (rawJson.trim() === '[]') return []; let parsed: unknown; try { parsed = JSON.parse(rawJson); } catch { @@ -538,9 +530,9 @@ export function buildButtonRows( // Visual mode (single_row / multiple_rows) let buttonsParam: { button?: IDataObject[]; buttonRow?: IDataObject[] } | undefined; - try { buttonsParam = ctx.getNodeParameter('buttons', itemIndex, {}) as typeof buttonsParam; } catch { return []; } + try { buttonsParam = ctx.getNodeParameter('buttons', itemIndex, {}) as typeof buttonsParam; } catch { return null; } const items = buttonsParam?.button ?? buttonsParam?.buttonRow ?? []; - if (items.length === 0) return []; + if (items.length === 0) return null; if (buttonLayout === 'single_row') { return [items.map(btn => buildButton(btn))]; @@ -568,10 +560,15 @@ export function cleanFileAttachments( itemIndex: number, ): IDataObject[] { let filesRaw: unknown; + // v2: files inside additionalFields try { const additional = ctx.getNodeParameter('additionalFields', itemIndex, {}) as IDataObject; filesRaw = additional.files; - } catch { return []; } + } catch { /* no additionalFields */ } + // v1 compat: files as top-level parameter + if (!filesRaw) { + try { filesRaw = ctx.getNodeParameter('files', itemIndex, undefined); } catch { /* not present */ } + } if (!filesRaw) return []; let files: IDataObject[]; diff --git a/integrations/n8n/nodes/Pachca/SharedRouter.ts b/integrations/n8n/nodes/Pachca/SharedRouter.ts index 03be2e1a..c253aad1 100644 --- a/integrations/n8n/nodes/Pachca/SharedRouter.ts +++ b/integrations/n8n/nodes/Pachca/SharedRouter.ts @@ -910,7 +910,7 @@ async function executeRoute( // === Special handlers === if (route.special === 'messageButtons') { const buttons = buildButtonRows(this, i); - if (buttons.length) body.buttons = buttons; + if (buttons !== null) body.buttons = buttons; const files = cleanFileAttachments(this, i); if (files.length) body.files = files; } diff --git a/integrations/n8n/package.json b/integrations/n8n/package.json index edfceafa..78fafde9 100644 --- a/integrations/n8n/package.json +++ b/integrations/n8n/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-pachca", - "version": "2.0.4", + "version": "2.0.5", "description": "Pachca node for n8n workflow automation", "license": "MIT", "main": "index.js", diff --git a/integrations/n8n/scripts/generate-n8n.ts b/integrations/n8n/scripts/generate-n8n.ts index 868db502..8e77a38e 100644 --- a/integrations/n8n/scripts/generate-n8n.ts +++ b/integrations/n8n/scripts/generate-n8n.ts @@ -3030,7 +3030,7 @@ async function executeRoute( \t// === Special handlers === \tif (route.special === 'messageButtons') { \t\tconst buttons = buildButtonRows(this, i); -\t\tif (buttons.length) body.buttons = buttons; +\t\tif (buttons !== null) body.buttons = buttons; \t\tconst files = cleanFileAttachments(this, i); \t\tif (files.length) body.files = files; \t} diff --git a/integrations/n8n/tests/error-paths.test.ts b/integrations/n8n/tests/error-paths.test.ts index 395712d7..0b160a39 100644 --- a/integrations/n8n/tests/error-paths.test.ts +++ b/integrations/n8n/tests/error-paths.test.ts @@ -1,6 +1,13 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import type { IDataObject, IExecuteFunctions, IHookFunctions, INode } from 'n8n-workflow'; import { NodeApiError } from 'n8n-workflow'; + +// Mock sleep to avoid real delays in retry tests +vi.mock('n8n-workflow', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, sleep: vi.fn(async () => {}) }; +}); + import { makeApiRequest, makeApiRequestAllPages, @@ -137,7 +144,7 @@ describe('makeApiRequest error paths', () => { } }); - it('should attach Retry-After header to error', async () => { + it('should retry on 429 and attach Retry-After header to error after exhausting retries', async () => { const ctx = createExecCtx({ httpResponse: { statusCode: 429, @@ -152,6 +159,8 @@ describe('makeApiRequest error paths', () => { } catch (error) { expect(error).toBeInstanceOf(NodeApiError); expect((error as NodeApiError & { retryAfter?: number }).retryAfter).toBe(5); + // Should have retried 3 times + 1 final attempt = 4 total calls + expect(ctx.helpers.httpRequestWithAuthentication).toHaveBeenCalledTimes(4); } }); @@ -168,7 +177,7 @@ describe('makeApiRequest error paths', () => { expect(result).toEqual({ success: true }); }); - it('should handle 500 with generic error message', async () => { + it('should retry on 500 and throw after exhausting retries', async () => { const ctx = createExecCtx({ httpResponse: { statusCode: 500, @@ -183,6 +192,8 @@ describe('makeApiRequest error paths', () => { } catch (error) { expect(error).toBeInstanceOf(NodeApiError); expect((error as NodeApiError).message).toContain('500'); + // Should have retried 3 times + 1 final attempt = 4 total calls + expect(ctx.helpers.httpRequestWithAuthentication).toHaveBeenCalledTimes(4); } }); @@ -285,17 +296,17 @@ describe('makeApiRequestAllPages error paths', () => { headers: { 'retry-after': '1' }, }; - // initial + 5 retries = 6 calls, then 7th throws + // Retry is handled inside makeApiRequest: 1 initial + 3 retries = 4 calls const ctx = createExecCtx({ - httpResponses: Array(7).fill(rateLimitResponse), + httpResponses: Array(5).fill(rateLimitResponse), params: { returnAll: false, limit: 10 }, }); await expect( makeApiRequestAllPages.call(ctx, 'GET', '/users', {}, 0, 'user', 2), ).rejects.toThrow(NodeApiError); - // 6 calls total: 1 initial + 5 retries, 6th exceeds MAX_RETRIES - expect((ctx.helpers.httpRequestWithAuthentication as ReturnType).mock.calls).toHaveLength(6); + // 4 calls total: 1 initial + 3 retries inside makeApiRequest + expect((ctx.helpers.httpRequestWithAuthentication as ReturnType).mock.calls).toHaveLength(4); }); it('should throw non-retryable errors immediately (e.g. 403)', async () => { @@ -381,7 +392,10 @@ describe('makeApiRequestAllPages error paths', () => { expect(results).toEqual([]); }); - it('should pass Retry-After value to setTimeout', async () => { + it('should pass Retry-After value to sleep', async () => { + const { sleep: mockSleep } = await import('n8n-workflow'); + (mockSleep as ReturnType).mockClear(); + const rateLimitResponse = { statusCode: 429, body: { error: 'Rate limited' }, @@ -398,12 +412,11 @@ describe('makeApiRequestAllPages error paths', () => { params: { returnAll: false, limit: 10 }, }); - const setTimeoutSpy = globalThis.setTimeout as unknown as ReturnType; const results = await makeApiRequestAllPages.call(ctx, 'GET', '/users', {}, 0, 'user', 2); expect(results).toEqual([{ json: { id: 1 } }]); - // Retry-After: 3 → retryAfter = parseInt('3') || 2 = 3 → setTimeout(fn, 3000) - expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 3000); + // Retry-After: 3 → sleep(3000) + expect(mockSleep).toHaveBeenCalledWith(3000); }); }); diff --git a/integrations/n8n/tests/execute-helpers.test.ts b/integrations/n8n/tests/execute-helpers.test.ts index aefacc0c..e9fbd745 100644 --- a/integrations/n8n/tests/execute-helpers.test.ts +++ b/integrations/n8n/tests/execute-helpers.test.ts @@ -236,14 +236,14 @@ describe('resolveResourceLocator', () => { // ============================================================================ describe('buildButtonRows', () => { - it('should return empty array when buttonLayout is none', () => { + it('should return null when buttonLayout is none', () => { const ctx = createMockCtx({ buttonLayout: 'none' }); - expect(buildButtonRows(ctx, 0)).toEqual([]); + expect(buildButtonRows(ctx, 0)).toBeNull(); }); - it('should return empty array when buttonLayout param is missing', () => { + it('should return null when buttonLayout param is missing', () => { const ctx = createMockCtx({}); - expect(buildButtonRows(ctx, 0)).toEqual([]); + expect(buildButtonRows(ctx, 0)).toBeNull(); }); it('should build a single row from visual builder', () => { diff --git a/integrations/n8n/tests/router.test.ts b/integrations/n8n/tests/router.test.ts index bf7d616b..fa53cb10 100644 --- a/integrations/n8n/tests/router.test.ts +++ b/integrations/n8n/tests/router.test.ts @@ -1,4 +1,11 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// Mock sleep to avoid real delays in retry tests +vi.mock('n8n-workflow', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, sleep: vi.fn(async () => {}) }; +}); + import { router } from '../nodes/Pachca/SharedRouter'; import * as GenericFunctions from '../nodes/Pachca/GenericFunctions'; @@ -1040,14 +1047,15 @@ describe('Multi-item processing', () => { }); ctx.helpers.httpRequestWithAuthentication.mockImplementation(async () => { callCount++; - if (callCount === 1) { + // First 4 calls (1 initial + 3 retries) return 500 for item 1 + if (callCount <= 4) { return { statusCode: 500, body: { message: 'Internal error' } }; } return { statusCode: 200, body: { data: { id: 1 } } }; }); const result = await runRouter(ctx); expect(result[0]).toHaveLength(2); - expect(result[0][0].json).toHaveProperty('error'); // first failed + expect(result[0][0].json).toHaveProperty('error'); // first failed after retries expect(result[0][1].json).toHaveProperty('id', 1); // second succeeded }); });