Каналы: создавать профиль одним блоком

This commit is contained in:
AidarKC
2026-08-01 02:19:29 +04:00
parent 8125d1a124
commit 8303682bd7
11 changed files with 201 additions and 118 deletions
@@ -19,7 +19,7 @@ import java.util.Objects;
* [1] channelNameLen * [1] channelNameLen
* [N] channelName UTF-8 * [N] channelName UTF-8
* [2] channelDescriptionLen * [2] channelDescriptionLen
* [M] channelDescription UTF-8 (0..200 bytes) * [M] channelDescription UTF-8 (0..2048 bytes)
* [2] channelTypeCode (uint16) * [2] channelTypeCode (uint16)
* [2] channelTypeVersion (uint16) * [2] channelTypeVersion (uint16)
*/ */
@@ -38,7 +38,7 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
private static final byte[] ZERO32 = new byte[32]; private static final byte[] ZERO32 = new byte[32];
private static final int MAX_NAME_LENGTH = 32; private static final int MAX_NAME_LENGTH = 32;
private static final int MAX_DESCRIPTION_UTF8_LEN = 200; public static final int MAX_DESCRIPTION_UTF8_LEN = 2048;
public final short subType; public final short subType;
public final short version; public final short version;
@@ -88,7 +88,7 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
int descriptionLen = Short.toUnsignedInt(bb.getShort()); int descriptionLen = Short.toUnsignedInt(bb.getShort());
if (descriptionLen > MAX_DESCRIPTION_UTF8_LEN) { if (descriptionLen > MAX_DESCRIPTION_UTF8_LEN) {
throw new IllegalArgumentException("channelDescription utf8 len must be <=200"); throw new IllegalArgumentException("channelDescription utf8 len must be <=2048");
} }
if (bb.remaining() != descriptionLen + 4) { if (bb.remaining() != descriptionLen + 4) {
throw new IllegalArgumentException("CreateChannelBody tail mismatch: remaining=" + bb.remaining() + " descriptionLen=" + descriptionLen); throw new IllegalArgumentException("CreateChannelBody tail mismatch: remaining=" + bb.remaining() + " descriptionLen=" + descriptionLen);
@@ -145,7 +145,7 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
String normalizedDescription = normalizeDescription(channelDescription); String normalizedDescription = normalizeDescription(channelDescription);
byte[] descUtf8 = normalizedDescription.getBytes(StandardCharsets.UTF_8); byte[] descUtf8 = normalizedDescription.getBytes(StandardCharsets.UTF_8);
if (descUtf8.length > MAX_DESCRIPTION_UTF8_LEN) { if (descUtf8.length > MAX_DESCRIPTION_UTF8_LEN) {
throw new IllegalArgumentException("channelDescription utf8 len must be <=200"); throw new IllegalArgumentException("channelDescription utf8 len must be <=2048");
} }
int typeCode = Short.toUnsignedInt(channelTypeCode); int typeCode = Short.toUnsignedInt(channelTypeCode);
@@ -166,7 +166,7 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
private static String normalizeDescription(String value) { private static String normalizeDescription(String value) {
if (value == null) return ""; if (value == null) return "";
return value.trim().replaceAll("\\s+", " "); return value.trim().replace("\r\n", "\n").replace('\r', '\n');
} }
@Override @Override
@@ -177,7 +177,7 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
} }
byte[] descriptionUtf8 = normalizeDescription(channelDescription).getBytes(StandardCharsets.UTF_8); byte[] descriptionUtf8 = normalizeDescription(channelDescription).getBytes(StandardCharsets.UTF_8);
if (descriptionUtf8.length > MAX_DESCRIPTION_UTF8_LEN) { if (descriptionUtf8.length > MAX_DESCRIPTION_UTF8_LEN) {
throw new IllegalArgumentException("channelDescription utf8 len must be <=200"); throw new IllegalArgumentException("channelDescription utf8 len must be <=2048");
} }
int cap = 4 + (4 + 32 + 4) + 1 + nameUtf8.length + 2 + descriptionUtf8.length + 4; int cap = 4 + (4 + 32 + 4) + 1 + nameUtf8.length + 2 + descriptionUtf8.length + 4;
@@ -204,4 +204,3 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
@Override @Override
public int lineSeq() { return thisLineNumber; } public int lineSeq() { return thisLineNumber; }
} }
@@ -304,14 +304,36 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex); return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex);
} }
String displayName = normalizedName;
String channelDescription = createChannelBody.channelDescription == null
? ""
: ChannelNameRules.normalizeDisplayName(createChannelBody.channelDescription);
String avaAr = "";
String avaSha256 = "";
long avaSize = 0L;
long metaUpdatedAtMs = 0L;
if (channelTypeCode == (CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF)) {
try {
ChannelMetaTextParser.Parsed createMeta = ChannelMetaTextParser.parseStrict(channelDescription, normalizedName);
displayName = createMeta.title();
channelDescription = createMeta.description();
avaAr = createMeta.avaAr();
avaSha256 = createMeta.avaSha256();
avaSize = createMeta.avaSize();
metaUpdatedAtMs = block.timestamp * 1000L;
} catch (IllegalArgumentException e) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, e.getMessage(), serverLastNum, serverLastHashHex);
}
}
channelNameStateEntry = new ChannelNameStateEntry(); channelNameStateEntry = new ChannelNameStateEntry();
channelNameStateEntry.setSlug(slug); channelNameStateEntry.setSlug(slug);
channelNameStateEntry.setDisplayName(normalizedName); channelNameStateEntry.setDisplayName(displayName);
channelNameStateEntry.setChannelDescription( channelNameStateEntry.setChannelDescription(channelDescription);
createChannelBody.channelDescription == null channelNameStateEntry.setAvaAr(avaAr);
? "" channelNameStateEntry.setAvaSha256(avaSha256);
: ChannelNameRules.normalizeDisplayName(createChannelBody.channelDescription) channelNameStateEntry.setAvaSize(avaSize);
); channelNameStateEntry.setMetaUpdatedAtMs(metaUpdatedAtMs);
channelNameStateEntry.setOwnerLogin(login); channelNameStateEntry.setOwnerLogin(login);
channelNameStateEntry.setOwnerBlockchainName(blockchainName); channelNameStateEntry.setOwnerBlockchainName(blockchainName);
channelNameStateEntry.setChannelTypeCode(channelTypeCode); channelNameStateEntry.setChannelTypeCode(channelTypeCode);
@@ -1,7 +1,9 @@
package server.logic.ws_protocol.JSON.handlers.channels; package server.logic.ws_protocol.JSON.handlers.channels;
import blockchain.BchBlockEntry; import blockchain.BchBlockEntry;
import blockchain.MsgSubType;
import blockchain.body.CreateChannelBody; import blockchain.body.CreateChannelBody;
import blockchain.body.TextLineBody;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import shine.db.DbController; import shine.db.DbController;
@@ -20,6 +22,7 @@ import java.util.Map;
public final class ChannelNamesStateBootstrapper { public final class ChannelNamesStateBootstrapper {
private static final Logger log = LoggerFactory.getLogger(ChannelNamesStateBootstrapper.class); private static final Logger log = LoggerFactory.getLogger(ChannelNamesStateBootstrapper.class);
private static final int MSG_TYPE_TECH = 0; private static final int MSG_TYPE_TECH = 0;
private static final int MSG_TYPE_TEXT = 1;
private static final int MSG_SUB_TYPE_CREATE_CHANNEL = 1; private static final int MSG_SUB_TYPE_CREATE_CHANNEL = 1;
private static volatile boolean bootstrapped; private static volatile boolean bootstrapped;
@@ -74,17 +77,30 @@ public final class ChannelNamesStateBootstrapper {
continue; continue;
} }
final String displayName; String displayName;
final String slug; final String slug;
final String channelDescription; String channelDescription;
final int channelTypeCode; final int channelTypeCode;
final int channelTypeVersion; final int channelTypeVersion;
String avaAr = "";
String avaSha256 = "";
long avaSize = 0L;
long metaUpdatedAtMs = 0L;
try { try {
displayName = ChannelNameRules.normalizeDisplayName(createChannelBody.channelName); displayName = ChannelNameRules.normalizeDisplayName(createChannelBody.channelName);
slug = ChannelNameRules.toCanonicalSlug(displayName); slug = ChannelNameRules.toCanonicalSlug(displayName);
channelDescription = ChannelNameRules.normalizeDisplayName(createChannelBody.channelDescription); channelDescription = ChannelNameRules.normalizeDisplayName(createChannelBody.channelDescription);
channelTypeCode = Short.toUnsignedInt(createChannelBody.channelTypeCode); channelTypeCode = Short.toUnsignedInt(createChannelBody.channelTypeCode);
channelTypeVersion = Short.toUnsignedInt(createChannelBody.channelTypeVersion); channelTypeVersion = Short.toUnsignedInt(createChannelBody.channelTypeVersion);
if (channelTypeCode == (CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF)) {
ChannelMetaTextParser.Parsed createMeta = ChannelMetaTextParser.parseStrict(channelDescription, displayName);
displayName = createMeta.title();
channelDescription = createMeta.description();
avaAr = createMeta.avaAr();
avaSha256 = createMeta.avaSha256();
avaSize = createMeta.avaSize();
metaUpdatedAtMs = parsed.timestamp * 1000L;
}
} catch (Exception badName) { } catch (Exception badName) {
skipped.add(ownerBch + "#" + blockNumber + " (invalid_name)"); skipped.add(ownerBch + "#" + blockNumber + " (invalid_name)");
continue; continue;
@@ -102,6 +118,10 @@ public final class ChannelNamesStateBootstrapper {
entry.setSlug(slug); entry.setSlug(slug);
entry.setDisplayName(displayName); entry.setDisplayName(displayName);
entry.setChannelDescription(channelDescription == null ? "" : channelDescription); entry.setChannelDescription(channelDescription == null ? "" : channelDescription);
entry.setAvaAr(avaAr);
entry.setAvaSha256(avaSha256);
entry.setAvaSize(avaSize);
entry.setMetaUpdatedAtMs(metaUpdatedAtMs);
entry.setOwnerLogin(ownerLogin); entry.setOwnerLogin(ownerLogin);
entry.setOwnerBlockchainName(ownerBch); entry.setOwnerBlockchainName(ownerBch);
entry.setChannelTypeCode(channelTypeCode); entry.setChannelTypeCode(channelTypeCode);
@@ -116,6 +136,8 @@ public final class ChannelNamesStateBootstrapper {
dao.clearAll(c); dao.clearAll(c);
dao.insertAll(c, entries); dao.insertAll(c, entries);
applyMetaUpdates(c, dao, entries, skipped);
c.commit(); c.commit();
log.info("channel_names_state bootstrapped: {}", entries.size()); log.info("channel_names_state bootstrapped: {}", entries.size());
if (!conflicts.isEmpty()) { if (!conflicts.isEmpty()) {
@@ -148,4 +170,53 @@ public final class ChannelNamesStateBootstrapper {
throw new RuntimeException("Failed to bootstrap channel_names_state", e); throw new RuntimeException("Failed to bootstrap channel_names_state", e);
} }
} }
private static void applyMetaUpdates(Connection c,
ChannelNameStateDAO dao,
List<ChannelNameStateEntry> entries,
List<String> skipped) throws Exception {
Map<String, String> fallbackTitles = new LinkedHashMap<>();
for (ChannelNameStateEntry entry : entries) {
fallbackTitles.put(entry.getOwnerBlockchainName() + "#" + entry.getChannelRootBlockNumber(), entry.getSlug());
}
String sql = """
SELECT bch_name, block_number, block_bytes
FROM blocks
WHERE msg_type = ? AND msg_sub_type = ?
ORDER BY bch_name, block_number
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setInt(1, MSG_TYPE_TEXT);
ps.setInt(2, MsgSubType.TEXT_CHANNEL_META);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
String ownerBch = rs.getString("bch_name");
int blockNumber = rs.getInt("block_number");
byte[] blockBytes = rs.getBytes("block_bytes");
try {
BchBlockEntry parsed = new BchBlockEntry(blockBytes);
if (!(parsed.body instanceof TextLineBody metaBody)) continue;
int lineCode = metaBody.lineCode();
String fallbackTitle = fallbackTitles.get(ownerBch + "#" + lineCode);
if (fallbackTitle == null) continue;
ChannelMetaTextParser.Parsed parsedMeta = ChannelMetaTextParser.parseStrict(metaBody.message, fallbackTitle);
ChannelNameStateEntry update = new ChannelNameStateEntry();
update.setOwnerBlockchainName(ownerBch);
update.setChannelRootBlockNumber(lineCode);
update.setDisplayName(parsedMeta.title());
update.setChannelDescription(parsedMeta.description());
update.setAvaAr(parsedMeta.avaAr());
update.setAvaSha256(parsedMeta.avaSha256());
update.setAvaSize(parsedMeta.avaSize());
update.setMetaUpdatedAtMs(parsed.timestamp * 1000L);
dao.updateMeta(c, update);
} catch (Exception e) {
skipped.add(ownerBch + "#" + blockNumber + " (invalid_meta)");
}
}
}
}
}
} }
@@ -99,6 +99,15 @@ final class ChannelsReadSupport {
meta.channelDescription = ccb.channelDescription == null ? "" : ccb.channelDescription; meta.channelDescription = ccb.channelDescription == null ? "" : ccb.channelDescription;
meta.channelTypeCode = Short.toUnsignedInt(ccb.channelTypeCode); meta.channelTypeCode = Short.toUnsignedInt(ccb.channelTypeCode);
meta.channelTypeVersion = Short.toUnsignedInt(ccb.channelTypeVersion); meta.channelTypeVersion = Short.toUnsignedInt(ccb.channelTypeVersion);
if (meta.channelTypeCode == (CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF)) {
ChannelMetaTextParser.Parsed parsedMeta = ChannelMetaTextParser.parseStrict(meta.channelDescription, meta.channelName);
meta.displayName = parsedMeta.title();
meta.channelDescription = parsedMeta.description();
meta.avaAr = parsedMeta.avaAr();
meta.avaSha256 = parsedMeta.avaSha256();
meta.avaSize = parsedMeta.avaSize();
meta.metaUpdatedAtMs = e.timestamp * 1000L;
}
} else { } else {
meta.channelName = null; meta.channelName = null;
meta.displayName = null; meta.displayName = null;
@@ -415,6 +424,14 @@ final class ChannelsReadSupport {
if (entry.body instanceof CreateChannelBody ccb) { if (entry.body instanceof CreateChannelBody ccb) {
event.title = ccb.channelName; event.title = ccb.channelName;
event.description = ccb.channelDescription == null ? "" : ccb.channelDescription; event.description = ccb.channelDescription == null ? "" : ccb.channelDescription;
if (Short.toUnsignedInt(ccb.channelTypeCode) == (CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF)) {
ChannelMetaTextParser.Parsed parsedMeta = ChannelMetaTextParser.parseStrict(event.description, ccb.channelName);
event.title = parsedMeta.title();
event.description = parsedMeta.description();
event.avaAr = parsedMeta.avaAr();
event.avaSha256 = parsedMeta.avaSha256();
event.avaSize = parsedMeta.avaSize();
}
} }
} catch (Exception ignored) { } catch (Exception ignored) {
event.createdAtMs = 0L; event.createdAtMs = 0L;
@@ -423,9 +440,8 @@ final class ChannelsReadSupport {
event.title = base.displayName == null || base.displayName.isBlank() ? base.channelName : base.displayName; event.title = base.displayName == null || base.displayName.isBlank() ? base.channelName : base.displayName;
} }
if (event.description == null) event.description = ""; if (event.description == null) event.description = "";
event.avaAr = ""; if (event.avaAr == null) event.avaAr = "";
event.avaSha256 = ""; if (event.avaSha256 == null) event.avaSha256 = "";
event.avaSize = 0L;
out.add(event); out.add(event);
} }
} }
+2 -2
View File
@@ -1,2 +1,2 @@
client.version=1.4.11 client.version=1.4.12
server.version=1.4.2 server.version=1.4.3
@@ -9,6 +9,23 @@ Payload включает:
4. `channelType` (`uint16`, 2 байта); 4. `channelType` (`uint16`, 2 байта);
5. `channelTypeVersion` (`uint16`, 2 байта). 5. `channelTypeVersion` (`uint16`, 2 байта).
### `channelDescription`
`channelDescription` хранится как `uint16 length + UTF-8`, максимум `2048` байт.
Для публичных каналов (`channelType=1`) поле является начальным снимком профиля канала и использует тот же текстовый meta-формат, что `TEXT_CHANNEL_META`:
```text
<SHiNE:title;v=1;Название канала>
<SHiNE:avatar;v=1;size=248193;sha256=3f2c8a...;ar=AbCdEf...>
Описание канала.
```
Если `title` отсутствует, человекочитаемым именем считается техническое `channelName`.
Если `avatar` отсутствует, канал создаётся без аватара.
Новые UI-клиенты создают публичный канал одним блоком `TECH_CREATE_CHANNEL`; отдельный `TEXT_CHANNEL_META` при создании не пишется. `TEXT_CHANNEL_META` используется для последующих изменений профиля канала.
## 2. Типы каналов ## 2. Типы каналов
- `0``stories` (root-канал пользователя). - `0``stories` (root-канал пользователя).
- `1` — публичный канал. - `1` — публичный канал.
+4 -2
View File
@@ -1,6 +1,6 @@
# TEXT_CHANNEL_META: профиль канала # TEXT_CHANNEL_META: профиль канала
`TEXT_CHANNEL_META` — скрытый технический `TEXT`-блок для полного снимка профиля канала. `TEXT_CHANNEL_META` — скрытый технический `TEXT`-блок для полного снимка профиля канала после создания.
## Блок ## Блок
@@ -11,6 +11,8 @@
Блок пишется только в линию канала владельца. В обычной ленте сообщений он не отображается как пользовательский пост. Блок пишется только в линию канала владельца. В обычной ленте сообщений он не отображается как пользовательский пост.
Первый снимок профиля публичного канала хранится прямо в `TECH_CREATE_CHANNEL.channelDescription` тем же текстовым форматом. Отдельный `TEXT_CHANNEL_META` при создании нового публичного канала UI не пишет.
## Текстовый формат ## Текстовый формат
В начале текста могут идти технические теги, после них обычный текст описания: В начале текста могут идти технические теги, после них обычный текст описания:
@@ -48,7 +50,7 @@
## UI ## UI
- При создании канала UI может сразу после `TECH_CREATE_CHANNEL` записать первый `TEXT_CHANNEL_META` с человекочитаемым именем, аватаром и описанием. - При создании публичного канала UI записывает человекочитаемое имя, аватар и описание в `TECH_CREATE_CHANNEL.channelDescription`.
- При создании канала UI показывает системную карточку `Создан канал ...`. - При создании канала UI показывает системную карточку `Создан канал ...`.
- При записи `TEXT_CHANNEL_META` UI показывает системную карточку `Изменён профиль канала`. - При записи `TEXT_CHANNEL_META` UI показывает системную карточку `Изменён профиль канала`.
- У системных карточек нет лайка, ответа, репоста, редактирования и других действий обычного сообщения. - У системных карточек нет лайка, ответа, репоста, редактирования и других действий обычного сообщения.
+7
View File
@@ -121,6 +121,13 @@
- Команда `/.desc` удалена из актуальной модели изменения описания канала; профиль канала меняется только через `TEXT_CHANNEL_META`. - Команда `/.desc` удалена из актуальной модели изменения описания канала; профиль канала меняется только через `TEXT_CHANNEL_META`.
- API чтения каналов расширен полями `displayName`, `avaAr`, `avaSha256`, `avaSize`, `metaUpdatedAtMs` и списком `metaEvents`. - API чтения каналов расширен полями `displayName`, `avaAr`, `avaSha256`, `avaSize`, `metaUpdatedAtMs` и списком `metaEvents`.
## 2026-08-01 02:18:00 +0400
- Базовый коммит-ориентир: `8125d1a`.
- Увеличен лимит `CreateChannelBody.channelDescription` до `2048` UTF-8 байт.
- Для публичных каналов начальный профиль канала теперь хранится прямо в `TECH_CREATE_CHANNEL.channelDescription` тем же meta-форматом, что `TEXT_CHANNEL_META`.
- UI создания публичного канала больше не пишет отдельный `TEXT_CHANNEL_META` сразу после создания; отдельный meta-блок используется только для последующих изменений профиля.
- Bootstrap `channel_names_state` при старте сервера теперь применяет последующие `TEXT_CHANNEL_META` поверх начального профиля из `TECH_CREATE_CHANNEL`.
## 2026-07-31 17:40:31 +0400 ## 2026-07-31 17:40:31 +0400
- Базовый коммит-ориентир: `d640dd6`. - Базовый коммит-ориентир: `d640dd6`.
- Формат блокчейна не менялся. - Формат блокчейна не менялся.
+6 -14
View File
@@ -36,7 +36,7 @@ function renderAvatarPreview(slot, avatar, title) {
slot.innerHTML = ''; slot.innerHTML = '';
const wrap = document.createElement('div'); const wrap = document.createElement('div');
wrap.className = 'channel-profile-avatar'; wrap.className = 'channel-profile-avatar';
wrap.style.setProperty('--channel-avatar-size', '82px'); wrap.style.setProperty('--channel-avatar-size', '104px');
if (avatar?.ar) { if (avatar?.ar) {
const img = document.createElement('img'); const img = document.createElement('img');
img.alt = 'Аватар канала'; img.alt = 'Аватар канала';
@@ -180,25 +180,17 @@ export function render({ navigate }) {
errorEl.textContent = ''; errorEl.textContent = '';
try { try {
const createResult = await authService.addBlockCreateChannel({ await authService.addBlockCreateChannel({
login, login,
storagePwd, storagePwd,
channelName: normalizeChannelDisplayName(check.name), channelName: normalizeChannelDisplayName(check.name),
channelDescription: '', channelDescription: check.description,
channelProfileTitle: check.title,
channelAvatar: selectedAvatar,
channelType: CHANNEL_TYPE_PUBLIC, channelType: CHANNEL_TYPE_PUBLIC,
channelTypeVersion: 1, channelTypeVersion: 1,
}); });
if (check.title || check.description || selectedAvatar?.ar) { if (selectedAvatar?.ar) markArweaveAttachmentPlaced(login, selectedAvatar);
await authService.addBlockChannelMeta({
login,
storagePwd,
channel: createResult.channel,
title: check.title,
description: check.description,
avatar: selectedAvatar,
});
if (selectedAvatar?.ar) markArweaveAttachmentPlaced(login, selectedAvatar);
}
persistCreateSuccessFlash(`Канал "${normalizeChannelDisplayName(check.name)}" создан.`); persistCreateSuccessFlash(`Канал "${normalizeChannelDisplayName(check.name)}" создан.`);
navigate('channels-list'); navigate('channels-list');
+32 -76
View File
@@ -65,6 +65,7 @@ const CHANNEL_TYPE_PUBLIC = 1;
const CHANNEL_TYPE_PERSONAL = 100; const CHANNEL_TYPE_PERSONAL = 100;
const CHANNEL_TYPE_GROUP = 200; const CHANNEL_TYPE_GROUP = 200;
const CHANNEL_TYPE_VERSION_DEFAULT = 1; const CHANNEL_TYPE_VERSION_DEFAULT = 1;
const CREATE_CHANNEL_DESCRIPTION_MAX_BYTES = 2048;
const SESSION_TYPE_CLIENT = 1; const SESSION_TYPE_CLIENT = 1;
const SESSION_TYPE_WALLET = 50; const SESSION_TYPE_WALLET = 50;
const SESSION_TYPE_HOMESERVER = 100; const SESSION_TYPE_HOMESERVER = 100;
@@ -691,10 +692,10 @@ function makeConnectionBodyBytes({
} }
function normalizeChannelDescription(value) { function normalizeChannelDescription(value) {
const text = String(value == null ? '' : value).trim().replace(/\s+/g, ' '); const text = String(value == null ? '' : value).trim().replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const bytes = utf8Bytes(text); const bytes = utf8Bytes(text);
if (bytes.length > 200) { if (bytes.length > CREATE_CHANNEL_DESCRIPTION_MAX_BYTES) {
throw new Error('Описание канала слишком длинное: максимум 200 символов.'); throw new Error('Описание канала слишком длинное: максимум 2048 байт.');
} }
return text; return text;
} }
@@ -771,8 +772,8 @@ function makeCreateChannelBodyBytes({
} }
const descriptionBytes = utf8Bytes(cleanDescription); const descriptionBytes = utf8Bytes(cleanDescription);
if (descriptionBytes.length > 200) { if (descriptionBytes.length > CREATE_CHANNEL_DESCRIPTION_MAX_BYTES) {
throw new Error('Описание канала слишком длинное: максимум 200 символов.'); throw new Error('Описание канала слишком длинное: максимум 2048 байт.');
} }
const typeVer = Number(channelTypeVersion); const typeVer = Number(channelTypeVersion);
if (!Number.isFinite(typeCode) || typeCode < 0 || typeCode > 65535) { if (!Number.isFinite(typeCode) || typeCode < 0 || typeCode > 65535) {
@@ -796,31 +797,6 @@ function makeCreateChannelBodyBytes({
); );
} }
function makeCreateChannelBodyBytesLegacy({
lineCode,
prevLineNumber,
prevLineHashHex,
thisLineNumber,
channelName,
}) {
const check = validatePersonalChannelName(channelName);
if (!check.ok) throw new Error(check.error);
const cleanName = check.normalized;
const nameBytes = utf8Bytes(cleanName);
if (nameBytes.length < 1 || nameBytes.length > 255) {
throw new Error('Channel name must be 1..255 bytes');
}
return concatBytes(
int32Bytes(lineCode),
int32Bytes(prevLineNumber),
hexToBytes(normalizeHex32(prevLineHashHex)),
int32Bytes(thisLineNumber),
int8Byte(nameBytes.length),
nameBytes,
);
}
function makeTextPostBodyBytes({ lineCode, prevLineNumber, prevLineHashHex, thisLineNumber, text }) { function makeTextPostBodyBytes({ lineCode, prevLineNumber, prevLineHashHex, thisLineNumber, text }) {
const message = String(text || '').trim(); const message = String(text || '').trim();
if (!message) throw new Error('Message text is required'); if (!message) throw new Error('Message text is required');
@@ -2084,6 +2060,8 @@ export class AuthService {
login, login,
channelName, channelName,
channelDescription = '', channelDescription = '',
channelProfileTitle = '',
channelAvatar = null,
channelType = CHANNEL_TYPE_PUBLIC, channelType = CHANNEL_TYPE_PUBLIC,
channelTypeVersion = CHANNEL_TYPE_VERSION_DEFAULT, channelTypeVersion = CHANNEL_TYPE_VERSION_DEFAULT,
storagePwd, storagePwd,
@@ -2099,7 +2077,13 @@ export class AuthService {
throw new Error(typeCode === CHANNEL_TYPE_PERSONAL ? nameCheck.error : channelNameErrorText(nameCheck.code)); throw new Error(typeCode === CHANNEL_TYPE_PERSONAL ? nameCheck.error : channelNameErrorText(nameCheck.code));
} }
const cleanChannelName = normalizeChannelDisplayName(nameCheck.normalized); const cleanChannelName = normalizeChannelDisplayName(nameCheck.normalized);
const cleanChannelDescription = normalizeChannelDescription(channelDescription); const cleanChannelDescription = typeCode === CHANNEL_TYPE_PUBLIC
? composeChannelMetaText({
title: channelProfileTitle,
description: channelDescription,
avatar: channelAvatar,
})
: normalizeChannelDescription(channelDescription);
const channelSlug = toCanonicalChannelSlug(cleanChannelName); const channelSlug = toCanonicalChannelSlug(cleanChannelName);
const typeVersion = Number(channelTypeVersion); const typeVersion = Number(channelTypeVersion);
const key = `create-channel:${cleanLogin}:${typeCode}:${channelSlug || cleanChannelName.toLowerCase()}`; const key = `create-channel:${cleanLogin}:${typeCode}:${channelSlug || cleanChannelName.toLowerCase()}`;
@@ -2131,51 +2115,23 @@ export class AuthService {
thisLineNumber = createdChannels.length + 1; thisLineNumber = createdChannels.length + 1;
} }
let payload; const payload = await this.addBlockSigned({
try { login: cleanLogin,
payload = await this.addBlockSigned({ storagePwd,
login: cleanLogin, msgType: MSG_TYPE_TECH,
storagePwd, msgSubType: MSG_SUBTYPE_TECH_CREATE_CHANNEL,
msgType: MSG_TYPE_TECH, msgVersion: CREATE_CHANNEL_BODY_VERSION,
msgSubType: MSG_SUBTYPE_TECH_CREATE_CHANNEL, bodyBytes: makeCreateChannelBodyBytes({
msgVersion: CREATE_CHANNEL_BODY_VERSION, lineCode: 0,
bodyBytes: makeCreateChannelBodyBytes({ prevLineNumber,
lineCode: 0, prevLineHashHex,
prevLineNumber, thisLineNumber,
prevLineHashHex, channelName: cleanChannelName,
thisLineNumber, channelDescription: cleanChannelDescription,
channelName: cleanChannelName, channelType: typeCode,
channelDescription: cleanChannelDescription, channelTypeVersion: typeVersion,
channelType: typeCode, }),
channelTypeVersion: typeVersion, });
}),
});
} catch (error) {
const rawCode = String(error?.code || '').toUpperCase();
const rawText = String(error?.message || '').toLowerCase();
const isLegacyFormatMismatch = (
rawCode === 'BAD_BLOCK_FORMAT' ||
rawText.includes('bad_block_format') ||
rawText.includes('некорректный формат блока')
);
if (!isLegacyFormatMismatch) throw error;
// Совместимость со старыми серверами, где CreateChannel body без description/type.
payload = await this.addBlockSigned({
login: cleanLogin,
storagePwd,
msgType: MSG_TYPE_TECH,
msgSubType: MSG_SUBTYPE_TECH_CREATE_CHANNEL,
msgVersion: 1,
bodyBytes: makeCreateChannelBodyBytesLegacy({
lineCode: 0,
prevLineNumber,
prevLineHashHex,
thisLineNumber,
channelName: cleanChannelName,
}),
});
}
const selector = { const selector = {
ownerBlockchainName: blockchainName, ownerBlockchainName: blockchainName,
+7 -6
View File
@@ -3649,10 +3649,11 @@ textarea.input {
} }
.channel-row .avatar { .channel-row .avatar {
width: 42px; width: 58px;
height: 42px; height: 58px;
min-width: 42px; min-width: 58px;
min-height: 42px; min-height: 58px;
border-radius: 50%;
background: linear-gradient(138deg, #efd193, #c18c42); background: linear-gradient(138deg, #efd193, #c18c42);
color: #17233f; color: #17233f;
box-shadow: inset 0 1px 0 rgba(255, 244, 208, 0.58); box-shadow: inset 0 1px 0 rgba(255, 244, 208, 0.58);
@@ -3920,14 +3921,14 @@ textarea.input {
} }
.channel-profile-avatar { .channel-profile-avatar {
--channel-avatar-size: 72px; --channel-avatar-size: 96px;
width: var(--channel-avatar-size); width: var(--channel-avatar-size);
height: var(--channel-avatar-size); height: var(--channel-avatar-size);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
align-self: center; align-self: center;
border-radius: 22px; border-radius: 50%;
background: linear-gradient(145deg, rgba(255, 214, 122, 0.22), rgba(56, 41, 22, 0.65)); background: linear-gradient(145deg, rgba(255, 214, 122, 0.22), rgba(56, 41, 22, 0.65));
border: 1px solid rgba(255, 226, 155, 0.24); border: 1px solid rgba(255, 226, 155, 0.24);
color: rgba(255, 236, 194, 0.95); color: rgba(255, 236, 194, 0.95);