Files

306 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Плагины Komet
Плагин добавляет в чат slash-команды. Пользователь пишет `/weather Москва`, Komet
запускает обработчик плагина в изолированном движке JavaScript, и тот работает с
чатом через модуль `komet:api`.
Плагин распространяется файлом `.kinet` — это zip-архив с `manifest.json` и
ES-модулями. Установка: настройки Komet → «Плагины» → «Установить .kinet» или
«Установить по URL».
В этом репозитории:
| Файл | Что это |
|------------------------------------------|-------------------------------------------|
| `plugin_sdk/komet-api.d.ts` | типы `komet:api` для подсказок в редакторе |
| `plugin_sdk/manifest.schema.json` | JSON Schema для `manifest.json` |
| `plugin_sdk/update-manifest.schema.json` | JSON Schema для манифеста обновлений |
| `tool/kinet_sign.dart` | ключи и подпись пакетов |
| `assets/plugins/` | встроенные плагины — готовые примеры |
## Быстрый старт
```
hello/
├── manifest.json
└── main.js
```
`manifest.json`:
```json
{
"$schema": "https://raw.githubusercontent.com/KometTeam/Komet/main/plugin_sdk/manifest.schema.json",
"schemaVersion": 1,
"id": "com.example.hello",
"name": "Hello",
"version": "1.0.0",
"apiVersion": 1,
"description": "Приветствие",
"author": "example",
"main": "main.js",
"permissions": ["chat.write"],
"commands": [
{
"name": "/hello",
"description": "поздороваться",
"handler": "hello",
"arguments": [
{"name": "name", "description": "кого приветствовать", "required": false, "rest": true}
]
}
]
}
```
`main.js`:
```js
import { chat } from 'komet:api';
/** @param {import('komet:api').CommandContext} context */
export async function hello(context) {
const name = context.arguments.name || 'мир';
await chat.sendText(`Привет, ${name}!`);
}
```
Сборка пакета — `manifest.json` должен лежать в корне архива:
```bash
cd hello && zip -r ../hello.kinet . -i 'manifest.json' '*.js'
```
Для подсказок в редакторе держите `komet-api.d.ts` рядом с проектом, но не кладите в
пакет: в `.kinet` допустимы только `manifest.json` и файлы `.js`.
## Манифест
| Поле | Обязательно | Описание |
|-----------------|-------------|----------|
| `schemaVersion` | да | всегда `1` |
| `id` | да | уникальный id в обратной доменной записи: `com.example.hello`. Префикс `pw.komet.` занят встроенными плагинами |
| `name` | да | название в списке плагинов |
| `version` | да | SemVer: `1.2.3`, `1.2.3-beta.1`, `1.2.3+5` |
| `apiVersion` | да | версия `komet:api`, которую ждёт плагин. Сейчас `1`; плагин с большей версией не установится |
| `description` | нет | описание в списке плагинов |
| `author` | нет | автор |
| `main` | да | путь к главному модулю внутри пакета, `.js` |
| `permissions` | да | разрешения, см. ниже. Может быть пустым массивом |
| `commands` | да | хотя бы одна команда |
| `updateUrl` | нет | HTTPS-адрес манифеста обновлений, см. «Обновления» |
| `signature` | нет | подпись; её добавляет `tool/kinet_sign.dart` |
### Команды
| Поле | Обязательно | Описание |
|---------------|-------------|----------|
| `name` | да | `/` и латиница, цифры, `_`, `-`, до 32 символов после `/`. Ведущий `/` можно не писать |
| `description` | да | текст в подсказке команд |
| `handler` | да | имя функции, экспортированной из `main` |
| `arguments` | нет | аргументы, см. ниже |
| `hidden` | нет | `true` — команда работает, но не показывается в подсказках |
Имена команд внутри плагина не должны повторяться без учёта регистра. Если имя уже
занято встроенной командой или другим плагином, команда не появится.
### Аргументы
| Поле | По умолчанию | Описание |
|---------------|--------------|----------|
| `name` | — | латиница, цифры, `_`, `-`, до 32 символов, начинается с буквы |
| `description` | `""` | подпись поля ввода |
| `required` | `true` | без значения команда не запустится, Komet покажет формат вызова |
| `rest` | `false` | забирает весь остаток строки; только у последнего аргумента |
Аргументы разделяются пробелами. Значение с пробелами берут в двойные или одинарные
кавычки, внутри кавычек `\` экранирует следующий символ. Если у команды есть
аргументы, при её выборе в подсказке Komet показывает поле для каждого из них.
## Контекст команды
Обработчик получает один объект:
| Поле | Тип | Описание |
|--------------|--------------------------|----------|
| `args` | `string` | всё, что написано после имени команды |
| `arguments` | `Record<string, string>` | значения аргументов по имени; не заданные — пустая строка |
| `reply` | `object \| null` | сообщение, на которое отвечает пользователь. Только с разрешением `message.readReply`, иначе `null` |
| `apiVersion` | `number` | версия `komet:api` в приложении |
`reply`: `{ id, senderId, text, time, attachments: [{ type }] }`. `time` — миллисекунды
Unix, `text` может быть `null`, `type` — `photo`, `video`, `audio`, `file`, `sticker` и
другие значения из `komet-api.d.ts`.
## Разрешения
Пользователь видит список разрешений при установке и выдаёт их все сразу. Вызов без
нужного разрешения завершается ошибкой `Нет разрешения <id>`.
| Разрешение | Что даёт |
|---------------------|----------|
| `chat.write` | `chat.sendText` |
| `chat.edit` | `chat.editText` |
| `chat.photo` | `chat.sendPhoto` |
| `chat.file` | `chat.sendFile` |
| `ui.notify` | `ui.notify` |
| `contact.read` | `contact.getPeer` |
| `message.readReply` | `context.reply` |
| `network` | `network.fetch` и загрузка по `url` в `chat.sendPhoto` / `chat.sendFile` |
| `storage` | `storage.get`, `storage.set`, `storage.remove` |
Методы `runtime.*` разрешений не требуют.
## API `komet:api`
```js
import { chat, network, ui, contact, runtime, storage } from 'komet:api';
```
Все методы возвращают `Promise`. При ошибке `Promise` отклоняется с `Error`, в
`message` — причина.
### chat
`chat.sendText(text)` → `Promise<string>` — отправляет сообщение в текущий чат и
возвращает его id. Текст — до 65 536 символов. В зашифрованном чате текст шифруется
так же, как обычное сообщение.
`chat.editText(messageId, text)` → `Promise<void>` — меняет текст сообщения. Можно
редактировать только сообщения, отправленные этим же запуском команды.
`chat.sendPhoto({ base64, url, filename, caption })` → `Promise<void>` — отправляет
фото до 15 МБ. Нужен `base64` или `url`; если заданы оба, берётся `base64`. Загрузка
по `url` требует разрешения `network`.
`chat.sendFile({ base64, url, filename })` → `Promise<void>` — отправляет файл до
25 МБ, источник задаётся так же. В зашифрованных чатах пока недоступен.
В `filename` символы кроме латиницы, кириллицы, цифр, `.`, `_`, `-` и пробела
заменяются на `_`, длина обрезается до 120 символов. Загрузка по `url` принимает
только HTTPS, следует не более чем 5 редиректам и делает до 3 попыток.
### network
`network.fetch(url, { method, headers, body })` →
`Promise<{ status, headers, body, base64 }>`.
- Только `https://`. `localhost` и частные IP-адреса запрещены.
- Методы: `GET` (по умолчанию), `POST`, `PUT`, `PATCH`, `DELETE`.
- Редиректы не выполняются: ответ 301, 302, 303, 307 или 308 завершается ошибкой.
- Заголовки `Host`, `Content-Length`, `Connection`, `Proxy-Authorization` игнорируются.
- `body` — строка. Объект сначала превратите в JSON: `JSON.stringify(data)`.
- Таймауты: соединение 10 с, заголовки ответа 15 с, тело 20 с. Ответ — до 1 МБ.
- `body` ответа — текст в UTF-8 (некорректные байты заменяются), `base64` — те же
байты без перекодирования, например для `chat.sendPhoto`.
- Коды 4xx и 5xx не считаются ошибкой — проверяйте `status`.
### ui
`ui.notify(message)` → `Promise<void>` — показывает уведомление Komet внутри
приложения, пока открыт чат.
### contact
`contact.getPeer()` → `Promise<Peer | null>` — данные собеседника в личном диалоге,
всегда свежие с сервера. Вне диалога возвращает `null`.
`Peer`: `{ id, displayName, country, registrationTime, updateTime, options }`.
`registrationTime` и `updateTime` — миллисекунды Unix или `null`, `options` — массив
строк-флагов профиля.
### runtime
`runtime.sleep(milliseconds)` → `Promise<void>` — пауза от 0 до 10 000 мс.
`runtime.isOnline()` → `Promise<boolean>` — есть ли соединение с сервером.
`runtime.isActive()` → `Promise<boolean>` — открыт ли ещё чат, из которого вызвана
команда. Проверяйте в длинных сценариях, чтобы не работать впустую.
### storage
Постоянное хранилище плагина. Оно переживает обновления и удаляется вместе с плагином.
`storage.get(key)` → `Promise<value | null>` — значение или `null`, если ключа нет.
`storage.set(key, value)` → `Promise<void>` — сохраняет любое JSON-значение.
`storage.remove(key)` → `Promise<void>` — удаляет ключ.
Ключ — от 1 до 64 символов из `A-Z a-z 0-9 . _ -`. Всё хранилище плагина в JSON — до
64 КБ.
## Среда выполнения
- Движок — QuickJS. Доступны стандартные объекты языка: `Promise`, `async`/`await`,
`JSON`, `Date`, `Math`, `Map`, `RegExp` и т. д.
- Нет `console`, `setTimeout`, глобального `fetch`, `require` и модулей Node. Вместо
них — `runtime.sleep` и `network.fetch`.
- Импортировать можно только `komet:api` и модули своего пакета по относительному
пути (`./utils.js`). Выход за пределы пакета (`../`) запрещён.
- Каждый запуск команды — новый движок: переменные модуля между запусками не
сохраняются, для этого есть `storage`.
- Лимиты: 16 МБ памяти, 256 КБ стека, 30 секунд на запуск. По истечении времени
движок останавливается.
- Необработанное исключение показывается пользователю как «Ошибка плагина: …».
## Пакет `.kinet`
- zip-архив до 5 МБ, не больше 128 файлов;
- каждый файл до 2 МБ, всё вместе в распакованном виде до 10 МБ;
- только `manifest.json` в корне и файлы `.js`;
- без символических ссылок, абсолютных путей и `..`;
- файл `main` должен быть в архиве.
## Подпись
Подписанный плагин показывает при установке отпечаток ключа автора. Обновления
подписанного плагина принимаются, только если они подписаны тем же ключом; снять
подпись в обновлении тоже нельзя.
```bash
dart run tool/kinet_sign.dart generate-key ~/.komet/hello-key.json
dart run tool/kinet_sign.dart sign hello.kinet ~/.komet/hello-key.json hello-signed.kinet
dart run tool/kinet_sign.dart verify hello-signed.kinet
```
`generate-key` создаёт ключ Ed25519 с правами `0600` и печатает отпечаток. `sign`
добавляет в манифест поле `signature`; без третьего аргумента пакет перезаписывается
на месте. Подпись покрывает манифест без поля `signature` и SHA-256 каждого `.js`
модуля, поэтому после подписи пакет менять нельзя.
Храните приватный ключ в секрете. Если он потерян, обновить плагин у пользователей не
получится — им придётся удалить его и установить заново.
## Обновления
Укажите в манифесте `updateUrl` — HTTPS-адрес JSON-файла:
```json
{
"$schema": "https://raw.githubusercontent.com/KometTeam/Komet/main/plugin_sdk/update-manifest.schema.json",
"version": "1.1.0",
"packageUrl": "hello-1.1.0.kinet",
"size": 2048,
"sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
```
| Поле | Описание |
|--------------|----------|
| `version` | версия нового пакета, совпадает с `version` в его манифесте |
| `packageUrl` | адрес `.kinet`, абсолютный или относительный к `updateUrl`; итоговый адрес — только HTTPS |
| `size` | размер пакета в байтах; `0` отключает проверку |
| `sha256` | SHA-256 пакета в hex (`shasum -a 256 hello.kinet`); пустая строка отключает проверку |
Пользователь проверяет обновления из меню плагина. Komet предлагает обновление, только
если `version` больше установленной по правилам SemVer, и отказывается его ставить,
если:
- у пакета другой `id` или `version`;
- не сошлись `size` или `sha256`;
- обновление просит разрешения, которых нет у установленной версии, — такой плагин
нужно переустановить;
- пакет подписан другим ключом или подпись снята.