CreateChannel: оставить единый актуальный формат, убрать legacy v2/v3

This commit is contained in:
AidarKC 2026-05-13 01:22:07 +03:00
parent e95f65ac78
commit ddeaf82bfd
7 changed files with 51 additions and 183 deletions

View File

@ -1,7 +1,7 @@
# Типы каналов и CreateChannel v3 # Типы каналов и CreateChannel
## 1. Формат `CreateChannelBody v3` ## 1. Формат `CreateChannelBody`
Формат `TECH_CREATE_CHANNEL` поддерживает `version=3` и включает: Формат `TECH_CREATE_CHANNEL` поддерживает единственный текущий `version=1` и включает:
1. line-поля канала (`lineCode`, `prevLineNumber`, `prevLineHash32`, `thisLineNumber`); 1. line-поля канала (`lineCode`, `prevLineNumber`, `prevLineHash32`, `thisLineNumber`);
2. `channelName`; 2. `channelName`;

View File

@ -2,7 +2,7 @@
## 2026-05-13 00:02:32 +0300 ## 2026-05-13 00:02:32 +0300
- Базовый коммит-ориентир: `f63f40f1eb2f`. - Базовый коммит-ориентир: `f63f40f1eb2f`.
- Добавлен `CreateChannelBody v3` с полями `channelType (2 байта)` и `channelTypeVersion (2 байта)`. - Добавлен текущий формат `CreateChannelBody` с полями `channelType (2 байта)` и `channelTypeVersion (2 байта)`.
- Зафиксированы типы каналов: `0=stories`, `1=public`, `100=personal`, `200=group`. - Зафиксированы типы каналов: `0=stories`, `1=public`, `100=personal`, `200=group`.
- Серверная уникальность имени канала изменена на `owner + type + name(slug)`. - Серверная уникальность имени канала изменена на `owner + type + name(slug)`.
- Root-канал `0` переименован в `stories` на уровне API-чтения. - Root-канал `0` переименован в `stories` на уровне API-чтения.

View File

@ -4,8 +4,8 @@
Этот набор файлов — актуальная документация по текущему формату блокчейна SHiNE для каналов, их типов и командных сообщений. Этот набор файлов — актуальная документация по текущему формату блокчейна SHiNE для каналов, их типов и командных сообщений.
## Оглавление ## Оглавление
1. [01_Channel_Types_and_CreateChannel_v3.md](./01_Channel_Types_and_CreateChannel_v3.md) 1. [01_Channel_Types_and_CreateChannel.md](./01_Channel_Types_and_CreateChannel.md)
Формат `CreateChannelBody v3`, типы каналов, уникальность имён и правила `stories`. Текущий формат `CreateChannelBody`, типы каналов, уникальность имён и правила `stories`.
2. [02_Channel_Commands.md](./02_Channel_Commands.md) 2. [02_Channel_Commands.md](./02_Channel_Commands.md)
Командные сообщения в каналах: `/.desc`, `/.add`, `/.remove`. Командные сообщения в каналах: `/.desc`, `/.add`, `/.remove`.
3. [CHANGELOG.md](./CHANGELOG.md) 3. [CHANGELOG.md](./CHANGELOG.md)

View File

@ -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;
@ -409,7 +409,7 @@ function normalizeChannelDescription(value) {
return text; return text;
} }
function makeCreateChannelBodyV3Bytes({ function makeCreateChannelBodyBytes({
lineCode, lineCode,
prevLineNumber, prevLineNumber,
prevLineHashHex, prevLineHashHex,
@ -1080,7 +1080,7 @@ export class AuthService {
msgType: MSG_TYPE_TECH, msgType: MSG_TYPE_TECH,
msgSubType: MSG_SUBTYPE_TECH_CREATE_CHANNEL, msgSubType: MSG_SUBTYPE_TECH_CREATE_CHANNEL,
msgVersion: CREATE_CHANNEL_BODY_VERSION, msgVersion: CREATE_CHANNEL_BODY_VERSION,
bodyBytes: makeCreateChannelBodyV3Bytes({ bodyBytes: makeCreateChannelBodyBytes({
lineCode: 0, lineCode: 0,
prevLineNumber, prevLineNumber,
prevLineHashHex, prevLineHashHex,

View File

@ -16,15 +16,13 @@ public final class BodyRecordParser {
int v = version & 0xFFFF; int v = version & 0xFFFF;
int st = subType & 0xFFFF; int st = subType & 0xFFFF;
// TECH supports Header v1 and CreateChannel v1/v2. // TECH supports Header v1 and CreateChannel current format (ver=1).
if (t == (CreateChannelBody.TYPE & 0xFFFF)) { if (t == (CreateChannelBody.TYPE & 0xFFFF)) {
if (st == (HeaderBody.SUBTYPE_COMPAT & 0xFFFF) && v == (HeaderBody.VER & 0xFFFF)) { if (st == (HeaderBody.SUBTYPE_COMPAT & 0xFFFF) && v == (HeaderBody.VER & 0xFFFF)) {
return new HeaderBody(subType, version, bodyBytes).check(); return new HeaderBody(subType, version, bodyBytes).check();
} }
if (st == (CreateChannelBody.SUBTYPE & 0xFFFF) if (st == (CreateChannelBody.SUBTYPE & 0xFFFF)
&& (v == (CreateChannelBody.VER & 0xFFFF) && (v == (CreateChannelBody.VER & 0xFFFF))) {
|| v == (CreateChannelBody.VER2 & 0xFFFF)
|| v == (CreateChannelBody.VER3 & 0xFFFF))) {
return new CreateChannelBody(subType, version, bodyBytes).check(); return new CreateChannelBody(subType, version, bodyBytes).check();
} }
throw new IllegalArgumentException( throw new IllegalArgumentException(

View File

@ -11,25 +11,7 @@ import java.util.Objects;
/** /**
* TECH body for create channel. * TECH body for create channel.
* *
* v1 body bytes: * body bytes:
* [4] lineCode
* [4] prevLineNumber
* [32] prevLineHash32
* [4] thisLineNumber
* [1] channelNameLen
* [N] channelName UTF-8
*
* v2 body bytes:
* [4] lineCode
* [4] prevLineNumber
* [32] prevLineHash32
* [4] thisLineNumber
* [1] channelNameLen
* [N] channelName UTF-8
* [2] channelDescriptionLen
* [M] channelDescription UTF-8 (0..200 bytes)
*
* v3 body bytes:
* [4] lineCode * [4] lineCode
* [4] prevLineNumber * [4] prevLineNumber
* [32] prevLineHash32 * [32] prevLineHash32
@ -45,13 +27,7 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
public static final short TYPE = 0; public static final short TYPE = 0;
public static final short VER = 1; public static final short VER = 1;
public static final short VER2 = 2;
public static final short VER3 = 3;
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF); public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
public static final int KEY_V2 = ((TYPE & 0xFFFF) << 16) | (VER2 & 0xFFFF);
public static final int KEY_V3 = ((TYPE & 0xFFFF) << 16) | (VER3 & 0xFFFF);
public static final short SUBTYPE = MsgSubType.TECH_CREATE_CHANNEL; public static final short SUBTYPE = MsgSubType.TECH_CREATE_CHANNEL;
public static final short CHANNEL_TYPE_STORIES = 0; public static final short CHANNEL_TYPE_STORIES = 0;
@ -79,35 +55,30 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
public CreateChannelBody(short subType, short version, byte[] bodyBytes) { public CreateChannelBody(short subType, short version, byte[] bodyBytes) {
Objects.requireNonNull(bodyBytes, "bodyBytes == null"); Objects.requireNonNull(bodyBytes, "bodyBytes == null");
this.subType = subType; this.subType = subType;
this.version = version; this.version = version;
int ver = this.version & 0xFFFF; int ver = this.version & 0xFFFF;
if (ver != (VER & 0xFFFF) && ver != (VER2 & 0xFFFF) && ver != (VER3 & 0xFFFF)) { if (ver != (VER & 0xFFFF)) {
throw new IllegalArgumentException("CreateChannelBody version must be 1, 2 or 3, got=" + ver); throw new IllegalArgumentException("CreateChannelBody version must be 1, got=" + ver);
} }
if ((this.subType & 0xFFFF) != (SUBTYPE & 0xFFFF)) { if ((this.subType & 0xFFFF) != (SUBTYPE & 0xFFFF)) {
throw new IllegalArgumentException("CreateChannelBody subType must be TECH_CREATE_CHANNEL(1), got=" + (this.subType & 0xFFFF)); throw new IllegalArgumentException("CreateChannelBody subType must be TECH_CREATE_CHANNEL(1), got=" + (this.subType & 0xFFFF));
} }
if (bodyBytes.length < 4 + (4 + 32 + 4) + 1 + 1 + 2 + 4) {
if (bodyBytes.length < 4 + (4 + 32 + 4) + 1 + 1) {
throw new IllegalArgumentException("CreateChannelBody too short"); throw new IllegalArgumentException("CreateChannelBody too short");
} }
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN); ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
this.lineCode = bb.getInt(); this.lineCode = bb.getInt();
this.prevLineNumber = bb.getInt(); this.prevLineNumber = bb.getInt();
this.prevLineHash32 = new byte[32]; this.prevLineHash32 = new byte[32];
bb.get(this.prevLineHash32); bb.get(this.prevLineHash32);
this.thisLineNumber = bb.getInt(); this.thisLineNumber = bb.getInt();
int nameLen = Byte.toUnsignedInt(bb.get()); int nameLen = Byte.toUnsignedInt(bb.get());
if (nameLen <= 0) throw new IllegalArgumentException("channelNameLen is 0"); if (nameLen <= 0) throw new IllegalArgumentException("channelNameLen is 0");
if (bb.remaining() < nameLen) { if (bb.remaining() < nameLen + 2 + 4) {
throw new IllegalArgumentException("CreateChannelBody tail mismatch: remaining=" + bb.remaining() + " nameLen=" + nameLen); throw new IllegalArgumentException("CreateChannelBody tail mismatch: remaining=" + bb.remaining() + " nameLen=" + nameLen);
} }
@ -115,19 +86,13 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
bb.get(nameBytes); bb.get(nameBytes);
this.channelName = new String(nameBytes, StandardCharsets.UTF_8); this.channelName = new String(nameBytes, StandardCharsets.UTF_8);
if (ver == (VER2 & 0xFFFF) || ver == (VER3 & 0xFFFF)) {
if (bb.remaining() < 2) {
throw new IllegalArgumentException("CreateChannelBody v2/v3 missing channelDescriptionLen");
}
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 <=200");
} }
if (bb.remaining() != descriptionLen) { if (bb.remaining() != descriptionLen + 4) {
throw new IllegalArgumentException("CreateChannelBody v2 tail mismatch: remaining=" + bb.remaining() + " descriptionLen=" + descriptionLen); throw new IllegalArgumentException("CreateChannelBody tail mismatch: remaining=" + bb.remaining() + " descriptionLen=" + descriptionLen);
} }
if (descriptionLen == 0) { if (descriptionLen == 0) {
this.channelDescription = ""; this.channelDescription = "";
} else { } else {
@ -135,46 +100,11 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
bb.get(descriptionBytes); bb.get(descriptionBytes);
this.channelDescription = normalizeDescription(new String(descriptionBytes, StandardCharsets.UTF_8)); this.channelDescription = normalizeDescription(new String(descriptionBytes, StandardCharsets.UTF_8));
} }
if (ver == (VER3 & 0xFFFF)) {
if (bb.remaining() < 4) {
throw new IllegalArgumentException("CreateChannelBody v3 missing channelTypeCode/channelTypeVersion");
}
this.channelTypeCode = bb.getShort(); this.channelTypeCode = bb.getShort();
this.channelTypeVersion = bb.getShort(); this.channelTypeVersion = bb.getShort();
} else {
this.channelTypeCode = CHANNEL_TYPE_PUBLIC;
this.channelTypeVersion = CHANNEL_TYPE_VERSION_DEFAULT;
}
if (bb.remaining() != 0) { if (bb.remaining() != 0) {
throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining()); throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
} }
return;
}
this.channelDescription = "";
this.channelTypeCode = CHANNEL_TYPE_PUBLIC;
this.channelTypeVersion = CHANNEL_TYPE_VERSION_DEFAULT;
if (bb.remaining() != 0) {
throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
}
}
public CreateChannelBody(int lineCode,
int prevLineNumber,
byte[] prevLineHash32,
int thisLineNumber,
String channelName) {
this(lineCode, prevLineNumber, prevLineHash32, thisLineNumber, channelName, "", VER);
}
public CreateChannelBody(int lineCode,
int prevLineNumber,
byte[] prevLineHash32,
int thisLineNumber,
String channelName,
String channelDescription) {
this(lineCode, prevLineNumber, prevLineHash32, thisLineNumber, channelName, channelDescription, VER2);
} }
public CreateChannelBody(int lineCode, public CreateChannelBody(int lineCode,
@ -185,41 +115,15 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
String channelDescription, String channelDescription,
short channelTypeCode, short channelTypeCode,
short channelTypeVersion) { short channelTypeVersion) {
this(lineCode, prevLineNumber, prevLineHash32, thisLineNumber, channelName, channelDescription,
channelTypeCode, channelTypeVersion, VER3);
}
private CreateChannelBody(int lineCode,
int prevLineNumber,
byte[] prevLineHash32,
int thisLineNumber,
String channelName,
String channelDescription,
short version) {
this(lineCode, prevLineNumber, prevLineHash32, thisLineNumber, channelName, channelDescription,
CHANNEL_TYPE_PUBLIC, CHANNEL_TYPE_VERSION_DEFAULT, version);
}
private CreateChannelBody(int lineCode,
int prevLineNumber,
byte[] prevLineHash32,
int thisLineNumber,
String channelName,
String channelDescription,
short channelTypeCode,
short channelTypeVersion,
short version) {
Objects.requireNonNull(channelName, "channelName == null"); Objects.requireNonNull(channelName, "channelName == null");
if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0"); if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
this.subType = SUBTYPE; this.subType = SUBTYPE;
this.version = version; this.version = VER;
this.lineCode = lineCode; this.lineCode = lineCode;
this.prevLineNumber = prevLineNumber; this.prevLineNumber = prevLineNumber;
this.prevLineHash32 = (prevLineHash32 == null ? ZERO32 : Arrays.copyOf(prevLineHash32, 32)); this.prevLineHash32 = (prevLineHash32 == null ? ZERO32 : Arrays.copyOf(prevLineHash32, 32));
this.thisLineNumber = thisLineNumber; this.thisLineNumber = thisLineNumber;
this.channelName = channelName; this.channelName = channelName;
this.channelDescription = channelDescription == null ? "" : channelDescription; this.channelDescription = channelDescription == null ? "" : channelDescription;
this.channelTypeCode = channelTypeCode; this.channelTypeCode = channelTypeCode;
@ -229,20 +133,14 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
@Override @Override
public CreateChannelBody check() { public CreateChannelBody check() {
if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0"); if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
if ((subType & 0xFFFF) != (SUBTYPE & 0xFFFF)) { if ((subType & 0xFFFF) != (SUBTYPE & 0xFFFF)) {
throw new IllegalArgumentException("CreateChannelBody subType must be TECH_CREATE_CHANNEL(1)"); throw new IllegalArgumentException("CreateChannelBody subType must be TECH_CREATE_CHANNEL(1)");
} }
String normalizedName = normalizeDisplayName(channelName); String normalizedName = normalizeDisplayName(channelName);
if (normalizedName.isEmpty()) { if (normalizedName.isEmpty()) throw new IllegalArgumentException("channelName is blank");
throw new IllegalArgumentException("channelName is blank");
}
int cpLen = normalizedName.codePointCount(0, normalizedName.length()); int cpLen = normalizedName.codePointCount(0, normalizedName.length());
if (cpLen > MAX_NAME_LENGTH) { if (cpLen > MAX_NAME_LENGTH) throw new IllegalArgumentException("channelName length must be <=32");
throw new IllegalArgumentException("channelName length must be <=32");
}
String normalizedDescription = normalizeDescription(channelDescription); String normalizedDescription = normalizeDescription(channelDescription);
byte[] descUtf8 = normalizedDescription.getBytes(StandardCharsets.UTF_8); byte[] descUtf8 = normalizedDescription.getBytes(StandardCharsets.UTF_8);
@ -252,23 +150,12 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
int typeCode = Short.toUnsignedInt(channelTypeCode); int typeCode = Short.toUnsignedInt(channelTypeCode);
int typeVer = Short.toUnsignedInt(channelTypeVersion); int typeVer = Short.toUnsignedInt(channelTypeVersion);
if (typeCode < 0 || typeCode > 0xFFFF) { if (typeCode < 0 || typeCode > 0xFFFF) throw new IllegalArgumentException("channelTypeCode invalid");
throw new IllegalArgumentException("channelTypeCode invalid"); if (typeVer < 0 || typeVer > 0xFFFF) throw new IllegalArgumentException("channelTypeVersion invalid");
}
if (typeVer < 0 || typeVer > 0xFFFF) {
throw new IllegalArgumentException("channelTypeVersion invalid");
}
if (prevLineNumber < 0) {
throw new IllegalArgumentException("prevLineNumber must be >=0 for CreateChannelBody");
}
if (prevLineHash32 == null || prevLineHash32.length != 32) {
throw new IllegalArgumentException("prevLineHash32 invalid");
}
if (thisLineNumber <= 0) {
throw new IllegalArgumentException("thisLineNumber must be >=1 for CreateChannelBody");
}
if (prevLineNumber < 0) throw new IllegalArgumentException("prevLineNumber must be >=0 for CreateChannelBody");
if (prevLineHash32 == null || prevLineHash32.length != 32) throw new IllegalArgumentException("prevLineHash32 invalid");
if (thisLineNumber <= 0) throw new IllegalArgumentException("thisLineNumber must be >=1 for CreateChannelBody");
return this; return this;
} }
@ -288,53 +175,33 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
if (nameUtf8.length == 0 || nameUtf8.length > 255) { if (nameUtf8.length == 0 || nameUtf8.length > 255) {
throw new IllegalArgumentException("channelName utf8 len must be 1..255"); throw new IllegalArgumentException("channelName utf8 len must be 1..255");
} }
boolean isV2 = (version & 0xFFFF) == (VER2 & 0xFFFF);
boolean isV3 = (version & 0xFFFF) == (VER3 & 0xFFFF);
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 <=200");
} }
int cap = 4 + (4 + 32 + 4) + 1 + nameUtf8.length int cap = 4 + (4 + 32 + 4) + 1 + nameUtf8.length + 2 + descriptionUtf8.length + 4;
+ ((isV2 || isV3) ? 2 + descriptionUtf8.length : 0)
+ (isV3 ? 4 : 0);
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN); ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
bb.putInt(lineCode); bb.putInt(lineCode);
bb.putInt(prevLineNumber); bb.putInt(prevLineNumber);
bb.put(prevLineHash32 == null ? ZERO32 : Arrays.copyOf(prevLineHash32, 32)); bb.put(prevLineHash32 == null ? ZERO32 : Arrays.copyOf(prevLineHash32, 32));
bb.putInt(thisLineNumber); bb.putInt(thisLineNumber);
bb.put((byte) nameUtf8.length); bb.put((byte) nameUtf8.length);
bb.put(nameUtf8); bb.put(nameUtf8);
if (isV2 || isV3) {
bb.putShort((short) (descriptionUtf8.length & 0xFFFF)); bb.putShort((short) (descriptionUtf8.length & 0xFFFF));
if (descriptionUtf8.length > 0) { if (descriptionUtf8.length > 0) bb.put(descriptionUtf8);
bb.put(descriptionUtf8);
}
}
if (isV3) {
bb.putShort(channelTypeCode); bb.putShort(channelTypeCode);
bb.putShort(channelTypeVersion); bb.putShort(channelTypeVersion);
}
return bb.array(); return bb.array();
} }
@Override @Override
public int lineCode() { return lineCode; } public int lineCode() { return lineCode; }
@Override @Override
public int prevLineBlockGlobalNumber() { return prevLineNumber; } public int prevLineBlockGlobalNumber() { return prevLineNumber; }
@Override @Override
public byte[] prevLineBlockHash32() { public byte[] prevLineBlockHash32() { return prevLineHash32 == null ? null : Arrays.copyOf(prevLineHash32, 32); }
return prevLineHash32 == null ? null : Arrays.copyOf(prevLineHash32, 32);
}
@Override @Override
public int lineSeq() { return thisLineNumber; } public int lineSeq() { return thisLineNumber; }
} }

View File

@ -105,7 +105,10 @@ public class IT_03_AddBlock_NoAuth {
sender1.send(new CreateChannelBody( sender1.send(new CreateChannelBody(
0, // lineCode TECH 0, // lineCode TECH
ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber, ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
"News" "News",
"",
CreateChannelBody.CHANNEL_TYPE_PUBLIC,
CreateChannelBody.CHANNEL_TYPE_VERSION_DEFAULT
), t); ), t);
newsRootBlock = st1.lastBlockNumber(); // root канала = blockNumber этого CREATE_CHANNEL newsRootBlock = st1.lastBlockNumber(); // root канала = blockNumber этого CREATE_CHANNEL