Файлы
Файловое API позволяет плагинам принимать файлы от пользователей (фото, документы, аудио, видео), сохранять новые файлы и отправлять их обратно в чат.
Плагины работают только со ссылками (FileRef) — содержимое файлов не передаётся в event data. Для чтения и записи содержимого используются host-функции.
Direct upload для HTTP-trigger'ов
Для frontend/API-сценариев не отправляйте файлы в HTTP-trigger через multipart/form-data. Рекомендуемый поток такой:
- Инициализировать upload через host API
- Загрузить байты напрямую в object storage по
upload_url - Подтвердить upload и получить
file_id - Передать
file_idв JSON-теле HTTP-trigger'а
Инициализация upload
POST /api/files/init
Content-Type: application/json
Cookie: user_session=...
{
"plugin_id": "my-plugin",
"name": "report.pdf",
"mime_type": "application/pdf",
"size": 123456,
"file_type": "document"
}Ответ:
{
"file_id": "4e7d...",
"upload_url": "https://storage.example.com/...",
"upload_method": "PUT",
"upload_headers": {
"Content-Type": "application/pdf"
},
"expires_at": "2026-04-24T12:34:56Z"
}Загрузка байтов
Клиент загружает файл напрямую в upload_url:
await fetch(init.upload_url, {
method: init.upload_method,
headers: init.upload_headers,
body: file,
})Подтверждение upload
POST /api/files/4e7d.../complete
Cookie: user_session=...Ответ — обычный FileRef:
{
"id": "4e7d...",
"name": "report.pdf",
"mime_type": "application/pdf",
"size": 123456,
"file_type": "document"
}После этого file_id можно передавать в HTTP-trigger:
await fetch('/api/triggers/http/my-plugin/import', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file_ids: ['4e7d...'] }),
})Удаление незавершённого или ненужного файла
DELETE /api/files/4e7d...
Cookie: user_session=...ACL
Файлы, загруженные через /api/files/*, привязываются к plugin_id и текущему пользователю. Плагин может читать их через ctx.FileMeta, ctx.FileRead, ctx.FileURL только в HTTP-trigger'ах того же плагина и от имени того же пользователя.
Приём файлов
Когда пользователь отправляет файл с командой, плагин получает массив FileRef в событии:
Handler: func(ctx *wasmplugin.EventContext) error {
if !ctx.HasFiles() {
ctx.Reply(wasmplugin.NewMessage("Отправьте файл вместе с командой"))
return nil
}
for _, f := range ctx.Files() {
ctx.Log(fmt.Sprintf("Файл: %s, тип: %s, размер: %d",
f.Name, f.FileType, f.Size))
}
return nil
}FileRef
| Поле | Тип | Описание |
|---|---|---|
ID | string | Уникальный идентификатор в FileStore |
Name | string | Имя файла (photo.jpg, report.pdf) |
MIMEType | string | MIME-тип (image/jpeg, application/pdf) |
Size | int64 | Размер в байтах |
FileType | string | Категория: photo, document, audio, video, voice, sticker |
Методы проверки
ctx.HasFiles() bool // есть ли прикреплённые файлы
ctx.Files() []FileRef // список файловЧтение содержимого
ctx.FileReadAll(fileID string) ([]byte, error)
Читает весь файл в память. Подходит для файлов до нескольких МБ.
data, err := ctx.FileReadAll(file.ID)
if err != nil {
ctx.LogError("не удалось прочитать файл: " + err.Error())
return nil
}
// data — содержимое файла ([]byte)ctx.FileRead(fileID string, offset, maxBytes int64) ([]byte, bool, error)
Чтение чанками. Полезно для больших файлов. Возвращает данные, флаг EOF и ошибку.
var buf bytes.Buffer
var offset int64
for {
chunk, eof, err := ctx.FileRead(file.ID, offset, 256*1024) // 256 KB
if err != nil {
return err
}
buf.Write(chunk)
if eof {
break
}
offset += int64(len(chunk))
}Лимит чанка
Максимальный размер одного чанка — 1 МБ. При maxBytes == 0 читается до 1 МБ.
Внутренний механизм чтения
Публичный API для плагинов не меняется: для чтения файлов используйте ctx.FileRead(...) и ctx.FileReadAll(...).
Начиная с protocol v4 метод ctx.FileRead(...) внутри использует host ABI file_read_into:
- Плагин сам выделяет буфер в памяти WASM-модуля.
- Передаёт в host
fileID,offset, указатель на буфер и его длину. - Host читает чанк из FileStore и пишет данные прямо в память плагина.
- В ответ возвращаются только
bytes_readиeof.
Это уменьшает число копирований, снимает зависимость от фиксированного response-буфера и лучше подходит для больших чанков.
Deprecated host ABI
Низкоуровневая host-функция file_read помечена как deprecated и сохранена только для обратной совместимости со старыми WASM-плагинами. Публичные методы SDK ctx.FileRead(...) и ctx.FileReadAll(...) не устарели.
Метаданные
ctx.FileMeta(fileID string) (*FileRef, error)
Получает метаданные файла без скачивания содержимого:
meta, err := ctx.FileMeta(fileID)
if err != nil {
return err
}
fmt.Printf("%s (%d байт, %s)\n", meta.Name, meta.Size, meta.MIMEType)Сохранение файлов
ctx.FileStore(name, mimeType, fileType string, data []byte) (*FileRef, error)
Сохраняет новый файл в FileStore:
pdfData := generateReport()
ref, err := ctx.FileStore("report.pdf", "application/pdf", "document", pdfData)
if err != nil {
return err
}
// ref.ID — можно использовать для отправки или храненияctx.FileStoreWithTTL(..., ttl time.Duration) (*FileRef, error)
Сохраняет файл с ограниченным временем жизни:
ref, err := ctx.FileStoreWithTTL("temp.png", "image/png", "photo", imgData, 1*time.Hour)Отправка файлов
Файлы отправляются через тип Message с методом .File(ref, caption):
ref, _ := ctx.FileStore("export.csv", "text/csv", "document", csvData)
ctx.Reply(wasmplugin.NewMessage("").File(*ref, ""))Можно отправлять несколько файлов и комбинировать с текстом в одном вызове:
ctx.Reply(wasmplugin.NewMessage("Вот ваш отчёт:").File(*pdfRef, "").File(*csvRef, ""))Метод .File(ref, caption) принимает FileRef и необязательный caption (подпись к файлу). Можно вызывать цепочкой для отправки нескольких файлов.
Отправка входящего файла обратно
for _, f := range ctx.Files() {
ctx.Reply(wasmplugin.NewMessage("").File(f, "")) // эхо — отправить обратно
}Получение URL
ctx.FileURL(fileID string) (string, error)
Возвращает временный URL для скачивания файла. Полезно при S3-бэкенде для передачи ссылки внешним системам.
url, err := ctx.FileURL(file.ID)
if url != "" {
ctx.Reply(wasmplugin.NewMessage("Скачать: " + url))
}Ограничение backend-а
Если файловый backend не поддерживает выдачу прямых временных ссылок, метод возвращает пустую строку.
Справочник API
| Метод | Сигнатура | Описание |
|---|---|---|
HasFiles | () bool | Есть ли файлы во входящем сообщении |
Files | () []FileRef | Список прикреплённых файлов |
FileMeta | (fileID string) (*FileRef, error) | Метаданные файла |
FileRead | (fileID string, offset, maxBytes int64) ([]byte, bool, error) | Чтение чанками |
FileReadAll | (fileID string) ([]byte, error) | Чтение целиком |
FileURL | (fileID string) (string, error) | Временный URL |
FileStore | (name, mimeType, fileType string, data []byte) (*FileRef, error) | Сохранение |
FileStoreWithTTL | (name, mimeType, fileType string, data []byte, ttl time.Duration) (*FileRef, error) | Сохранение с TTL |
Reply (с файлом) | (msg Message) | Отправка файла через wasmplugin.NewMessage("").File(ref, caption) |
Необходимое требование:
wasmplugin.File("Описание для чего нужны файлы")Полный пример: галерея фото
wasmplugin.Plugin{
ID: "gallery",
Name: "Photo Gallery",
Version: "1.0.0",
Requirements: []wasmplugin.Requirement{
wasmplugin.File("Приём и хранение фото").Build(),
wasmplugin.KV("Хранение списков фото").Build(),
},
Triggers: []wasmplugin.Trigger{
{
Name: "save",
Type: wasmplugin.TriggerMessenger,
Handler: func(ctx *wasmplugin.EventContext) error {
if !ctx.HasFiles() {
ctx.Reply(wasmplugin.NewMessage("Отправьте фото вместе с командой /save"))
return nil
}
for _, f := range ctx.Files() {
// Прочитать содержимое
data, err := ctx.FileReadAll(f.ID)
if err != nil {
ctx.LogError("чтение: " + err.Error())
continue
}
// Сохранить в FileStore
stored, err := ctx.FileStore(f.Name, f.MIMEType, f.FileType, data)
if err != nil {
ctx.LogError("сохранение: " + err.Error())
continue
}
// Запомнить ID в KV
key := fmt.Sprintf("user:%d:photos", ctx.Messenger.UserID)
existing, _, _ := ctx.KVGet(key)
if existing != "" {
existing += ","
}
ctx.KVSet(key, existing+stored.ID)
}
ctx.Reply(wasmplugin.NewMessage("Фото сохранены!"))
return nil
},
},
{
Name: "show",
Type: wasmplugin.TriggerMessenger,
Handler: func(ctx *wasmplugin.EventContext) error {
key := fmt.Sprintf("user:%d:photos", ctx.Messenger.UserID)
val, found, _ := ctx.KVGet(key)
if !found || val == "" {
ctx.Reply(wasmplugin.NewMessage("Нет сохранённых фото"))
return nil
}
ids := strings.Split(val, ",")
msg := wasmplugin.NewMessage(fmt.Sprintf("Ваши фото (%d):", len(ids)))
for _, id := range ids {
meta, err := ctx.FileMeta(id)
if err != nil {
continue
}
msg = msg.File(*meta, "")
}
ctx.Reply(msg)
return nil
},
},
},
}