UI: исправить каналы и добавить MCP-док по чтению/дозаписи
This commit is contained in:
parent
0fdb5b245c
commit
01b38952e5
@ -0,0 +1,162 @@
|
|||||||
|
# MCP: чтение и дозапись персонального публичного чата (type=100)
|
||||||
|
|
||||||
|
Документ для реализации MCP-инструмента, который:
|
||||||
|
- читает переписку между двумя логинами (`from`, `to`);
|
||||||
|
- добавляет новое сообщение от отправителя через серверный `AddBlock`.
|
||||||
|
|
||||||
|
Важно: речь про **персональные публичные** каналы (`channelTypeCode=100`), а не приватные DM.
|
||||||
|
|
||||||
|
## 1. Базовые предпосылки
|
||||||
|
|
||||||
|
1. У каждого пользователя свой блокчейн (`<login>-001`).
|
||||||
|
2. Персональный публичный чат хранится как канал типа `100`:
|
||||||
|
- у `A` канал с `channelName = B`;
|
||||||
|
- у `B` зеркальный канал с `channelName = A`.
|
||||||
|
3. Сообщения канала — `TEXT_POST` в линии `line_code = rootBlockNumber` канала.
|
||||||
|
4. Запись блока возможна только при валидной подписи blockchain-ключом владельца цепочки.
|
||||||
|
|
||||||
|
## 2. Что должен уметь MCP-инструмент
|
||||||
|
|
||||||
|
Минимальный набор операций:
|
||||||
|
|
||||||
|
1. `read_personal_public_dialog(fromLogin, toLogin, limitPerSide=200)`
|
||||||
|
2. `append_personal_public_message(fromLogin, toLogin, text)`
|
||||||
|
|
||||||
|
## 3. Алгоритм чтения переписки
|
||||||
|
|
||||||
|
### 3.1 Найти оба канала (прямой и зеркальный)
|
||||||
|
|
||||||
|
Для `fromLogin = A`, `toLogin = B`:
|
||||||
|
|
||||||
|
1. Запросить `ListSubscriptionsFeed` для `A` и найти owned-канал:
|
||||||
|
- `ownerLogin == A`
|
||||||
|
- `channelName == B`
|
||||||
|
- `channelTypeCode == 100`
|
||||||
|
2. Запросить `ListSubscriptionsFeed` для `B` и найти owned-канал:
|
||||||
|
- `ownerLogin == B`
|
||||||
|
- `channelName == A`
|
||||||
|
- `channelTypeCode == 100`
|
||||||
|
|
||||||
|
Если какой-то из каналов не найден — вернуть частичный результат + флаг отсутствия зеркала.
|
||||||
|
|
||||||
|
### 3.2 Вычитать сообщения из каналов
|
||||||
|
|
||||||
|
Для каждого найденного канала вызвать `GetChannelMessages`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "GetChannelMessages",
|
||||||
|
"payload": {
|
||||||
|
"login": "<текущий-login-сессии>",
|
||||||
|
"channel": {
|
||||||
|
"ownerBlockchainName": "...",
|
||||||
|
"channelRootBlockNumber": 123,
|
||||||
|
"channelRootBlockHash": "..."
|
||||||
|
},
|
||||||
|
"limit": 200,
|
||||||
|
"sort": "asc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Склеить в единый диалог
|
||||||
|
|
||||||
|
1. Объединить массивы сообщений из `A->B` и `B->A`.
|
||||||
|
2. Отсортировать по `createdAtMs`, при равенстве — по `messageRef.blockNumber`.
|
||||||
|
3. Вернуть структуру:
|
||||||
|
- `messages[]`
|
||||||
|
- `directChannelFound` / `reverseChannelFound`
|
||||||
|
- метаданные обоих каналов.
|
||||||
|
|
||||||
|
## 4. Алгоритм дозаписи сообщения
|
||||||
|
|
||||||
|
Цель: добавить сообщение **от имени `fromLogin`** в его канал `fromLogin -> toLogin`.
|
||||||
|
|
||||||
|
### 4.1 Найти канал отправителя
|
||||||
|
|
||||||
|
Через `ListSubscriptionsFeed(fromLogin)` найти owned-канал:
|
||||||
|
- `channelName == toLogin`
|
||||||
|
- `channelTypeCode == 100`
|
||||||
|
|
||||||
|
Если канал не найден — вернуть ошибку `channel_not_found`.
|
||||||
|
|
||||||
|
### 4.2 Отправить `AddBlock` с `TEXT_POST`
|
||||||
|
|
||||||
|
Использовать клиентский/серверный helper формирования `TEXT_POST` body:
|
||||||
|
- `lineCode = channelRootBlockNumber`;
|
||||||
|
- `prevLineNumber/prevLineHash` берутся из последнего сообщения линии;
|
||||||
|
- подпись — blockchain private key пользователя `fromLogin`.
|
||||||
|
|
||||||
|
Если у вас в MCP нет приватного ключа пользователя, дозапись невозможна.
|
||||||
|
|
||||||
|
## 5. Контракт MCP (рекомендуемый)
|
||||||
|
|
||||||
|
## `read_personal_public_dialog`
|
||||||
|
|
||||||
|
Вход:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fromLogin": "alice",
|
||||||
|
"toLogin": "bob",
|
||||||
|
"limitPerSide": 200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Выход:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"directChannelFound": true,
|
||||||
|
"reverseChannelFound": true,
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"authorLogin": "alice",
|
||||||
|
"text": "Привет",
|
||||||
|
"createdAtMs": 1760000000000,
|
||||||
|
"channelSide": "alice->bob",
|
||||||
|
"messageRef": { "blockNumber": 11, "blockHash": "..." }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `append_personal_public_message`
|
||||||
|
|
||||||
|
Вход:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fromLogin": "alice",
|
||||||
|
"toLogin": "bob",
|
||||||
|
"text": "Тест"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Выход:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"serverLastGlobalNumber": 321,
|
||||||
|
"serverLastGlobalHash": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Ограничения и безопасность
|
||||||
|
|
||||||
|
1. Персональный канал типа `100` сейчас публичный по модели чтения (не E2E DM).
|
||||||
|
2. Нельзя дозаписать блок в чужой блокчейн без приватного ключа владельца (проверка подписи сервером).
|
||||||
|
3. Для прод-инструмента нужно:
|
||||||
|
- строгая авторизация MCP-вызовов;
|
||||||
|
- аудит, кто и от чьего имени запрашивал чтение/запись;
|
||||||
|
- лимиты/квоты на запись.
|
||||||
|
|
||||||
|
## 7. Мини-чеклист для реализации MCP
|
||||||
|
|
||||||
|
1. Реализовать helper поиска канала `findOwnedPersonalChannel(ownerLogin, peerLogin)`.
|
||||||
|
2. Реализовать чтение двух сторон и merge/sort.
|
||||||
|
3. Реализовать отправку `TEXT_POST` в найденный канал отправителя.
|
||||||
|
4. Добавить понятные ошибки:
|
||||||
|
- `user_not_found`
|
||||||
|
- `channel_not_found`
|
||||||
|
- `reverse_channel_not_found`
|
||||||
|
- `signature_required`
|
||||||
|
- `add_block_failed`
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
# Персональный публичный чат: исправление формата блока и обратный канал
|
||||||
|
|
||||||
|
- краткое описание фичи:
|
||||||
|
- Исправлен формат отправки `CreateChannel` из UI: для создания канала теперь используется версия body `1`, совместимая с серверным парсером.
|
||||||
|
- Убрана ошибка `AddBlock: Некорректный формат блока (BAD_BLOCK_FORMAT)` при создании персонального публичного чата (тип `100`) на актуальном сервере.
|
||||||
|
- В `channel-view` для персонального чата добавлена клиентская склейка диалога:
|
||||||
|
- основной канал `A -> B` (владелец `A`, имя канала `B`, тип `100`);
|
||||||
|
- зеркальный канал `B -> A` (владелец `B`, имя канала `A`, тип `100`);
|
||||||
|
- сообщения обоих каналов показываются в одном диалоге, отсортированном по времени.
|
||||||
|
- Если зеркальный канал не найден, показывается уведомление в шапке канала о том, что у собеседника пока не создан ответный чат.
|
||||||
|
- Исправлена ошибка `Идентификатор канала не готов` при добавлении сообщения в ряде сценариев (например, «мои сторис»): отправка теперь использует фактически загруженный селектор канала, а не только параметры маршрута.
|
||||||
|
- Улучшен резолв канала при открытии из поиска/прямой ссылки:
|
||||||
|
- сначала попытка по `ownerBlockchainName + channelName`;
|
||||||
|
- fallback по `ownerLogin + channelName`;
|
||||||
|
- дополнительный fallback через `GetUser(owner)` с сопоставлением `blockchainName`.
|
||||||
|
Это снижает число ложных `Канал не найден` при открытии сторис/каналов других пользователей.
|
||||||
|
|
||||||
|
- что именно проверять:
|
||||||
|
- Создать персональный публичный чат через UI (`Каналы -> Чаты -> Новый персональный публичный чат`) и убедиться, что ошибка `BAD_BLOCK_FORMAT` больше не появляется.
|
||||||
|
- Открыть созданный персональный чат `A -> B`, написать сообщение.
|
||||||
|
- С аккаунта `B` создать зеркальный чат `B -> A`, отправить ответ.
|
||||||
|
- Снова открыть чат у `A` и проверить, что в одном экране видны и исходящие, и входящие сообщения из зеркального канала.
|
||||||
|
- Проверить, что при отсутствии зеркального канала в шапке отображается предупреждение.
|
||||||
|
- Вкладка «Мои сторис»: открыть канал и отправить сообщение кнопкой «Добавить сообщение» — ошибка про неготовый идентификатор не должна появляться.
|
||||||
|
- Вкладка «Найти канал»: открыть чужой сторис/канал по формату `user/channel` и убедиться, что канал открывается (если реально существует и доступен в выдаче).
|
||||||
|
|
||||||
|
- ожидаемый результат:
|
||||||
|
- Персональный публичный чат создаётся без ошибки формата блока.
|
||||||
|
- При наличии зеркального канала переписка отображается единым диалогом.
|
||||||
|
- При отсутствии зеркального канала пользователь видит явное уведомление.
|
||||||
|
- В «мои сторис» сообщение добавляется без ошибки `Идентификатор канала не готов`.
|
||||||
|
- Открытие чужих каналов из поиска/ссылки работает стабильнее без ложного `Канал не найден`.
|
||||||
|
|
||||||
|
- статус:
|
||||||
|
- pending
|
||||||
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.55
|
client.version=1.2.56
|
||||||
server.version=1.2.49
|
server.version=1.2.50
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import {
|
|||||||
import { openSpeechInputModal } from '../components/speech-input-modal.js';
|
import { openSpeechInputModal } from '../components/speech-input-modal.js';
|
||||||
|
|
||||||
export const pageMeta = { id: 'channel-view', title: 'Канал' };
|
export const pageMeta = { id: 'channel-view', title: 'Канал' };
|
||||||
|
const CHANNEL_TYPE_PERSONAL = 100;
|
||||||
|
|
||||||
const pendingReactionActions = new Set();
|
const pendingReactionActions = new Set();
|
||||||
const pendingScrollByRoute = new Map();
|
const pendingScrollByRoute = new Map();
|
||||||
@ -408,18 +409,50 @@ function mapApiMessageToPost(message, selector, localNumber) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadFromApi(route, channelId) {
|
async function loadFromApi(route, channelId) {
|
||||||
|
let cachedFeed = null;
|
||||||
|
const ensureFeed = async () => {
|
||||||
|
if (cachedFeed) return cachedFeed;
|
||||||
|
cachedFeed = await authService.listSubscriptionsFeed(state.session.login, 1000);
|
||||||
|
return cachedFeed;
|
||||||
|
};
|
||||||
|
const getAllRows = async () => {
|
||||||
|
const feed = await ensureFeed();
|
||||||
|
return [
|
||||||
|
...(Array.isArray(feed?.ownedChannels) ? feed.ownedChannels : []),
|
||||||
|
...(Array.isArray(feed?.followedUsersChannels) ? feed.followedUsersChannels : []),
|
||||||
|
...(Array.isArray(feed?.followedChannels) ? feed.followedChannels : []),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
let selector = buildSelectorFromRoute(route, channelId);
|
let selector = buildSelectorFromRoute(route, channelId);
|
||||||
if (selector?.ownerBlockchainName && selector?.channelName) {
|
if (selector?.ownerBlockchainName && selector?.channelName) {
|
||||||
const ownFeed = await authService.listSubscriptionsFeed(state.session.login, 1000);
|
const routeOwnerRaw = String(selector.ownerBlockchainName || '').trim();
|
||||||
const allRows = [
|
const routeOwnerNormalized = routeOwnerRaw.toLowerCase();
|
||||||
...(Array.isArray(ownFeed?.ownedChannels) ? ownFeed.ownedChannels : []),
|
const allRows = await getAllRows();
|
||||||
...(Array.isArray(ownFeed?.followedUsersChannels) ? ownFeed.followedUsersChannels : []),
|
let channel = allRows.find((item) => (
|
||||||
...(Array.isArray(ownFeed?.followedChannels) ? ownFeed.followedChannels : []),
|
String(item?.channel?.ownerBlockchainName || '').trim().toLowerCase() === routeOwnerNormalized
|
||||||
];
|
|
||||||
const channel = allRows.find((item) => (
|
|
||||||
String(item?.channel?.ownerBlockchainName || '').trim() === selector.ownerBlockchainName
|
|
||||||
&& String(item?.channel?.channelName || '').trim().toLowerCase() === selector.channelName.toLowerCase()
|
&& String(item?.channel?.channelName || '').trim().toLowerCase() === selector.channelName.toLowerCase()
|
||||||
));
|
));
|
||||||
|
if (!channel) {
|
||||||
|
channel = allRows.find((item) => (
|
||||||
|
String(item?.channel?.ownerLogin || '').trim().toLowerCase() === routeOwnerNormalized
|
||||||
|
&& String(item?.channel?.channelName || '').trim().toLowerCase() === selector.channelName.toLowerCase()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (!channel) {
|
||||||
|
try {
|
||||||
|
const ownerUser = await authService.getUser(routeOwnerRaw);
|
||||||
|
const ownerBch = String(ownerUser?.blockchainName || '').trim().toLowerCase();
|
||||||
|
if (ownerBch) {
|
||||||
|
channel = allRows.find((item) => (
|
||||||
|
String(item?.channel?.ownerBlockchainName || '').trim().toLowerCase() === ownerBch
|
||||||
|
&& String(item?.channel?.channelName || '').trim().toLowerCase() === selector.channelName.toLowerCase()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore fallback lookup failures
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!channel?.channel?.ownerBlockchainName || channel?.channel?.channelRoot?.blockNumber == null) {
|
if (!channel?.channel?.ownerBlockchainName || channel?.channel?.channelRoot?.blockNumber == null) {
|
||||||
throw new Error('Канал не найден.');
|
throw new Error('Канал не найден.');
|
||||||
}
|
}
|
||||||
@ -437,8 +470,53 @@ async function loadFromApi(route, channelId) {
|
|||||||
|
|
||||||
const payload = await authService.getChannelMessages(selector, 200, 'asc', state.session.login);
|
const payload = await authService.getChannelMessages(selector, 200, 'asc', state.session.login);
|
||||||
const messages = Array.isArray(payload.messages) ? payload.messages : [];
|
const messages = Array.isArray(payload.messages) ? payload.messages : [];
|
||||||
const posts = messages.map((message, index) => mapApiMessageToPost(message, selector, index + 1));
|
let reverseChannelMissingWarning = '';
|
||||||
|
let mergedMessages = [...messages];
|
||||||
|
|
||||||
|
const currentLogin = String(state.session.login || '').trim();
|
||||||
const ownerLogin = String(payload.channel?.ownerLogin || '').trim();
|
const ownerLogin = String(payload.channel?.ownerLogin || '').trim();
|
||||||
|
const channelName = String(payload.channel?.channelName || '').trim();
|
||||||
|
const channelTypeCode = Number(payload.channel?.channelTypeCode ?? 1);
|
||||||
|
const canResolveReverse = (
|
||||||
|
channelTypeCode === CHANNEL_TYPE_PERSONAL
|
||||||
|
&& !!currentLogin
|
||||||
|
&& !!ownerLogin
|
||||||
|
&& !!channelName
|
||||||
|
&& ownerLogin.toLowerCase() === currentLogin.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (canResolveReverse) {
|
||||||
|
const allRows = await getAllRows();
|
||||||
|
const reverseSummary = allRows.find((item) => (
|
||||||
|
Number(item?.channel?.channelTypeCode ?? 1) === CHANNEL_TYPE_PERSONAL
|
||||||
|
&& String(item?.channel?.ownerLogin || '').trim().toLowerCase() === channelName.toLowerCase()
|
||||||
|
&& String(item?.channel?.channelName || '').trim().toLowerCase() === currentLogin.toLowerCase()
|
||||||
|
));
|
||||||
|
|
||||||
|
if (reverseSummary?.channel?.ownerBlockchainName && reverseSummary?.channel?.channelRoot?.blockNumber != null) {
|
||||||
|
const reverseSelector = {
|
||||||
|
ownerBlockchainName: String(reverseSummary.channel.ownerBlockchainName),
|
||||||
|
channelRootBlockNumber: Number(reverseSummary.channel.channelRoot.blockNumber),
|
||||||
|
channelRootBlockHash: normalizeRouteHash(reverseSummary.channel.channelRoot.blockHash),
|
||||||
|
};
|
||||||
|
const reversePayload = await authService.getChannelMessages(reverseSelector, 200, 'asc', state.session.login);
|
||||||
|
const reverseMessages = Array.isArray(reversePayload?.messages) ? reversePayload.messages : [];
|
||||||
|
mergedMessages = mergedMessages.concat(reverseMessages);
|
||||||
|
} else {
|
||||||
|
reverseChannelMissingWarning = `У собеседника ${channelName} пока не создан ответный персональный чат.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const posts = mergedMessages
|
||||||
|
.map((message, index) => mapApiMessageToPost(message, selector, index + 1))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const byTime = Number(a?.timestampMs || 0) - Number(b?.timestampMs || 0);
|
||||||
|
if (byTime !== 0) return byTime;
|
||||||
|
const aNum = Number(a?.messageRef?.blockNumber || 0);
|
||||||
|
const bNum = Number(b?.messageRef?.blockNumber || 0);
|
||||||
|
return aNum - bNum;
|
||||||
|
})
|
||||||
|
.map((post, index) => ({ ...post, localNumber: index + 1 }));
|
||||||
const isOwnChannel = ownerLogin.toLowerCase() === (state.session.login || '').toLowerCase();
|
const isOwnChannel = ownerLogin.toLowerCase() === (state.session.login || '').toLowerCase();
|
||||||
const followedRows = Array.isArray(state.channelsFeed?.followedChannels) ? state.channelsFeed.followedChannels : [];
|
const followedRows = Array.isArray(state.channelsFeed?.followedChannels) ? state.channelsFeed.followedChannels : [];
|
||||||
const isSubscribed = followedRows.some((row) => (
|
const isSubscribed = followedRows.some((row) => (
|
||||||
@ -455,6 +533,7 @@ async function loadFromApi(route, channelId) {
|
|||||||
ownerName: ownerLogin || 'неизвестно',
|
ownerName: ownerLogin || 'неизвестно',
|
||||||
},
|
},
|
||||||
posts,
|
posts,
|
||||||
|
reverseChannelMissingWarning,
|
||||||
isOwnChannel,
|
isOwnChannel,
|
||||||
isSubscribed,
|
isSubscribed,
|
||||||
selector,
|
selector,
|
||||||
@ -685,6 +764,12 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
|||||||
|
|
||||||
head.append(title);
|
head.append(title);
|
||||||
head.append(owner, headActions);
|
head.append(owner, headActions);
|
||||||
|
if (channelData.reverseChannelMissingWarning) {
|
||||||
|
const reverseWarning = document.createElement('p');
|
||||||
|
reverseWarning.className = 'channel-head-meta';
|
||||||
|
reverseWarning.textContent = channelData.reverseChannelMissingWarning;
|
||||||
|
head.append(reverseWarning);
|
||||||
|
}
|
||||||
|
|
||||||
const actionButton = document.createElement('button');
|
const actionButton = document.createElement('button');
|
||||||
actionButton.className = channelData.isOwnChannel
|
actionButton.className = channelData.isOwnChannel
|
||||||
@ -787,6 +872,7 @@ export function render({ navigate, route }) {
|
|||||||
const next = render({ navigate, route });
|
const next = render({ navigate, route });
|
||||||
current.replaceWith(next);
|
current.replaceWith(next);
|
||||||
};
|
};
|
||||||
|
let activeSelector = null;
|
||||||
|
|
||||||
const requireSigningSession = () => {
|
const requireSigningSession = () => {
|
||||||
const login = state.session.login;
|
const login = state.session.login;
|
||||||
@ -860,14 +946,14 @@ export function render({ navigate, route }) {
|
|||||||
|
|
||||||
const onAddPost = async (bodyText) => {
|
const onAddPost = async (bodyText) => {
|
||||||
const { login, storagePwd } = requireSigningSession();
|
const { login, storagePwd } = requireSigningSession();
|
||||||
if (!routeSelector?.ownerBlockchainName || routeSelector.channelRootBlockNumber == null) {
|
if (!activeSelector?.ownerBlockchainName || activeSelector.channelRootBlockNumber == null) {
|
||||||
throw new Error('Идентификатор канала не готов.');
|
throw new Error('Идентификатор канала не готов.');
|
||||||
}
|
}
|
||||||
|
|
||||||
await authService.addBlockTextPost({
|
await authService.addBlockTextPost({
|
||||||
login,
|
login,
|
||||||
storagePwd,
|
storagePwd,
|
||||||
channel: routeSelector,
|
channel: activeSelector,
|
||||||
text: bodyText,
|
text: bodyText,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -892,6 +978,7 @@ export function render({ navigate, route }) {
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const apiData = await loadFromApi(route, channelId);
|
const apiData = await loadFromApi(route, channelId);
|
||||||
|
activeSelector = apiData?.selector || null;
|
||||||
skeleton.remove();
|
skeleton.remove();
|
||||||
cleanupSeenTracking = renderBody(screen, navigate, routeKey, apiData, {
|
cleanupSeenTracking = renderBody(screen, navigate, routeKey, apiData, {
|
||||||
onToggleLike: async (messageRef, action) => {
|
onToggleLike: async (messageRef, action) => {
|
||||||
|
|||||||
@ -42,7 +42,7 @@ const MSG_SUBTYPE_REACTION_LIKE = 1;
|
|||||||
const MSG_SUBTYPE_REACTION_UNLIKE = 2;
|
const MSG_SUBTYPE_REACTION_UNLIKE = 2;
|
||||||
const MSG_SUBTYPE_CONNECTION_FOLLOW = 30;
|
const MSG_SUBTYPE_CONNECTION_FOLLOW = 30;
|
||||||
const MSG_SUBTYPE_CONNECTION_UNFOLLOW = 31;
|
const MSG_SUBTYPE_CONNECTION_UNFOLLOW = 31;
|
||||||
const CREATE_CHANNEL_BODY_VERSION = 3;
|
const CREATE_CHANNEL_BODY_VERSION = 1;
|
||||||
const CHANNEL_TYPE_STORIES = 0;
|
const CHANNEL_TYPE_STORIES = 0;
|
||||||
const CHANNEL_TYPE_PUBLIC = 1;
|
const CHANNEL_TYPE_PUBLIC = 1;
|
||||||
const CHANNEL_TYPE_PERSONAL = 100;
|
const CHANNEL_TYPE_PERSONAL = 100;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user