22 01 25
Да вроде всё работает и тесты проходят. И блоки добавляются все что надо для MVP
This commit is contained in:
parent
97840a45d6
commit
3f5f94a53f
@ -1,5 +1,7 @@
|
|||||||
package blockchain.body;
|
package blockchain.body;
|
||||||
|
|
||||||
|
import blockchain.MsgSubType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Парсер body выбирает класс по header: type/subType/version,
|
* Парсер body выбирает класс по header: type/subType/version,
|
||||||
* потому что bodyBytes больше НЕ содержат type/subType/version.
|
* потому что bodyBytes больше НЕ содержат type/subType/version.
|
||||||
@ -28,7 +30,23 @@ public final class BodyRecordParser {
|
|||||||
throw new IllegalArgumentException("Unknown TECH subType for type=0 ver=1: subType=" + st);
|
throw new IllegalArgumentException("Unknown TECH subType for type=0 ver=1: subType=" + st);
|
||||||
}
|
}
|
||||||
|
|
||||||
case TextBody.KEY -> new TextBody(subType, version, bodyBytes);
|
// TEXT type=1 ver=1: выбираем класс по subType
|
||||||
|
case TextBody.KEY -> {
|
||||||
|
int st = subType & 0xFFFF;
|
||||||
|
|
||||||
|
if (st == (MsgSubType.TEXT_POST & 0xFFFF)
|
||||||
|
|| st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
|
||||||
|
yield new TextLineBody(subType, version, bodyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)
|
||||||
|
|| st == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) {
|
||||||
|
yield new TextReplyBody(subType, version, bodyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("Unknown TEXT subType for type=1 ver=1: subType=" + st);
|
||||||
|
}
|
||||||
|
|
||||||
case ReactionBody.KEY -> new ReactionBody(subType, version, bodyBytes);
|
case ReactionBody.KEY -> new ReactionBody(subType, version, bodyBytes);
|
||||||
case ConnectionBody.KEY -> new ConnectionBody(subType, version, bodyBytes);
|
case ConnectionBody.KEY -> new ConnectionBody(subType, version, bodyBytes);
|
||||||
case UserParamBody.KEY -> new UserParamBody(subType, version, bodyBytes);
|
case UserParamBody.KEY -> new UserParamBody(subType, version, bodyBytes);
|
||||||
|
|||||||
@ -0,0 +1,265 @@
|
|||||||
|
package blockchain.body;
|
||||||
|
|
||||||
|
import blockchain.MsgSubType;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.charset.CharacterCodingException;
|
||||||
|
import java.nio.charset.CodingErrorAction;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TextLineBody — type=1, ver=1.
|
||||||
|
*
|
||||||
|
* subType:
|
||||||
|
* - POST (10)
|
||||||
|
* - EDIT_POST (11)
|
||||||
|
*
|
||||||
|
* Формат bodyBytes (BigEndian):
|
||||||
|
*
|
||||||
|
* POST:
|
||||||
|
* [4] lineCode
|
||||||
|
* [4] prevLineNumber
|
||||||
|
* [32] prevLineHash32
|
||||||
|
* [4] thisLineNumber
|
||||||
|
* [2] textLenBytes (uint16)
|
||||||
|
* [N] text UTF-8
|
||||||
|
*
|
||||||
|
* EDIT_POST:
|
||||||
|
* [4] lineCode
|
||||||
|
* [4] prevLineNumber
|
||||||
|
* [32] prevLineHash32
|
||||||
|
* [4] thisLineNumber
|
||||||
|
* [4] toBlockGlobalNumber (int32)
|
||||||
|
* [32] toBlockHash32
|
||||||
|
* [2] textLenBytes (uint16)
|
||||||
|
* [N] text UTF-8
|
||||||
|
*/
|
||||||
|
public final class TextLineBody implements BodyRecord, BodyHasLine, BodyHasTarget {
|
||||||
|
|
||||||
|
public static final short TYPE = 1;
|
||||||
|
public static final short VER = 1;
|
||||||
|
|
||||||
|
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
||||||
|
|
||||||
|
public final short subType; // из header
|
||||||
|
public final short version; // из header (=1)
|
||||||
|
|
||||||
|
// line
|
||||||
|
public final int lineCode;
|
||||||
|
public final int prevLineNumber;
|
||||||
|
public final byte[] prevLineHash32; // 32 (может быть нули)
|
||||||
|
public final int thisLineNumber;
|
||||||
|
|
||||||
|
// target (только для EDIT_POST)
|
||||||
|
public final Integer toBlockGlobalNumber; // nullable для POST
|
||||||
|
public final byte[] toBlockHash32; // nullable для POST
|
||||||
|
|
||||||
|
// text
|
||||||
|
public final String message;
|
||||||
|
|
||||||
|
/* ====================== parse from bytes ====================== */
|
||||||
|
|
||||||
|
public TextLineBody(short subType, short version, byte[] bodyBytes) {
|
||||||
|
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||||||
|
|
||||||
|
this.subType = subType;
|
||||||
|
this.version = version;
|
||||||
|
|
||||||
|
if ((this.version & 0xFFFF) != (VER & 0xFFFF)) {
|
||||||
|
throw new IllegalArgumentException("TextLineBody version must be 1, got=" + (this.version & 0xFFFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
int st = this.subType & 0xFFFF;
|
||||||
|
if (st != (MsgSubType.TEXT_POST & 0xFFFF) && st != (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
|
||||||
|
throw new IllegalArgumentException("TextLineBody supports only POST/EDIT_POST, got subType=" + st);
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
|
// минимум line + textLen(2)
|
||||||
|
ensureMin(bb, (4 + 4 + 32 + 4) + 2, "TextLineBody too short");
|
||||||
|
|
||||||
|
this.lineCode = bb.getInt();
|
||||||
|
this.prevLineNumber = bb.getInt();
|
||||||
|
|
||||||
|
this.prevLineHash32 = new byte[32];
|
||||||
|
bb.get(this.prevLineHash32);
|
||||||
|
|
||||||
|
this.thisLineNumber = bb.getInt();
|
||||||
|
|
||||||
|
if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
|
||||||
|
// нужен target
|
||||||
|
ensureMin(bb, (4 + 32) + 2, "EDIT_POST missing target");
|
||||||
|
int tgtNum = bb.getInt();
|
||||||
|
byte[] tgtHash = new byte[32];
|
||||||
|
bb.get(tgtHash);
|
||||||
|
|
||||||
|
this.toBlockGlobalNumber = tgtNum;
|
||||||
|
this.toBlockHash32 = tgtHash;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.toBlockGlobalNumber = null;
|
||||||
|
this.toBlockHash32 = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.message = readStrictUtf8Len16(bb, "TextLineBody text");
|
||||||
|
|
||||||
|
ensureNoTail(bb, "TextLineBody");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ====================== manual ctor ====================== */
|
||||||
|
|
||||||
|
public TextLineBody(int lineCode,
|
||||||
|
int prevLineNumber,
|
||||||
|
byte[] prevLineHash32,
|
||||||
|
int thisLineNumber,
|
||||||
|
short subType,
|
||||||
|
Integer toBlockGlobalNumber,
|
||||||
|
byte[] toBlockHash32,
|
||||||
|
String message) {
|
||||||
|
|
||||||
|
Objects.requireNonNull(message, "message == null");
|
||||||
|
|
||||||
|
int st = subType & 0xFFFF;
|
||||||
|
if (st != (MsgSubType.TEXT_POST & 0xFFFF) && st != (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
|
||||||
|
throw new IllegalArgumentException("TextLineBody supports only POST/EDIT_POST");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
|
||||||
|
if (message.isBlank()) throw new IllegalArgumentException("message is blank");
|
||||||
|
|
||||||
|
this.subType = subType;
|
||||||
|
this.version = VER;
|
||||||
|
|
||||||
|
this.lineCode = lineCode;
|
||||||
|
this.prevLineNumber = prevLineNumber;
|
||||||
|
this.prevLineHash32 = (prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
||||||
|
this.thisLineNumber = thisLineNumber;
|
||||||
|
|
||||||
|
if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
|
||||||
|
Objects.requireNonNull(toBlockGlobalNumber, "toBlockGlobalNumber == null");
|
||||||
|
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
||||||
|
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||||
|
if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||||
|
|
||||||
|
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
||||||
|
this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
|
||||||
|
} else {
|
||||||
|
this.toBlockGlobalNumber = null;
|
||||||
|
this.toBlockHash32 = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TextLineBody check() {
|
||||||
|
int st = subType & 0xFFFF;
|
||||||
|
if (st != (MsgSubType.TEXT_POST & 0xFFFF) && st != (MsgSubType.TEXT_EDIT_POST & 0xFFFF))
|
||||||
|
throw new IllegalArgumentException("Bad TextLineBody subType: " + st);
|
||||||
|
|
||||||
|
if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
|
||||||
|
if (prevLineHash32 == null || prevLineHash32.length != 32)
|
||||||
|
throw new IllegalArgumentException("prevLineHash32 invalid");
|
||||||
|
|
||||||
|
if (message == null || message.isBlank())
|
||||||
|
throw new IllegalArgumentException("Text message is blank");
|
||||||
|
|
||||||
|
if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
|
||||||
|
if (toBlockGlobalNumber == null || toBlockGlobalNumber < 0)
|
||||||
|
throw new IllegalArgumentException("EDIT_POST toBlockGlobalNumber invalid");
|
||||||
|
if (toBlockHash32 == null || toBlockHash32.length != 32)
|
||||||
|
throw new IllegalArgumentException("EDIT_POST toBlockHash32 invalid");
|
||||||
|
} else {
|
||||||
|
if (toBlockGlobalNumber != null || toBlockHash32 != null)
|
||||||
|
throw new IllegalArgumentException("POST must not contain target fields");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] toBytes() {
|
||||||
|
byte[] msgUtf8 = message.getBytes(StandardCharsets.UTF_8);
|
||||||
|
if (msgUtf8.length == 0) throw new IllegalArgumentException("Text payload is empty");
|
||||||
|
if (msgUtf8.length > 65535) throw new IllegalArgumentException("Text too long (>65535 bytes)");
|
||||||
|
|
||||||
|
int st = subType & 0xFFFF;
|
||||||
|
|
||||||
|
int cap;
|
||||||
|
if (st == (MsgSubType.TEXT_POST & 0xFFFF)) {
|
||||||
|
cap = (4 + 4 + 32 + 4) + 2 + msgUtf8.length;
|
||||||
|
} else {
|
||||||
|
// EDIT_POST
|
||||||
|
if (toBlockGlobalNumber == null) throw new IllegalArgumentException("EDIT_POST missing toBlockGlobalNumber");
|
||||||
|
if (toBlockHash32 == null || toBlockHash32.length != 32) throw new IllegalArgumentException("EDIT_POST toBlockHash32 != 32");
|
||||||
|
cap = (4 + 4 + 32 + 4) + (4 + 32) + 2 + msgUtf8.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
|
bb.putInt(lineCode);
|
||||||
|
bb.putInt(prevLineNumber);
|
||||||
|
bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
||||||
|
bb.putInt(thisLineNumber);
|
||||||
|
|
||||||
|
if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
|
||||||
|
bb.putInt(toBlockGlobalNumber);
|
||||||
|
bb.put(toBlockHash32);
|
||||||
|
}
|
||||||
|
|
||||||
|
bb.putShort((short) msgUtf8.length);
|
||||||
|
bb.put(msgUtf8);
|
||||||
|
|
||||||
|
return bb.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ====================== BodyHasLine ====================== */
|
||||||
|
@Override public int lineCode() { return lineCode; }
|
||||||
|
@Override public int prevLineNumber() { return prevLineNumber; }
|
||||||
|
@Override public byte[] prevLineHash32() { return Arrays.copyOf(prevLineHash32, 32); }
|
||||||
|
@Override public int thisLineNumber() { return thisLineNumber; }
|
||||||
|
|
||||||
|
/* ====================== BodyHasTarget ===================== */
|
||||||
|
@Override public String toBchName() { return null; } // по ТЗ: не хранить
|
||||||
|
@Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
|
||||||
|
@Override public byte[] toBlockHashBytes() { return toBlockHash32; }
|
||||||
|
|
||||||
|
/* ====================== helpers ====================== */
|
||||||
|
|
||||||
|
public boolean isEditPost() {
|
||||||
|
return (subType & 0xFFFF) == (MsgSubType.TEXT_EDIT_POST & 0xFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readStrictUtf8Len16(ByteBuffer bb, String fieldName) {
|
||||||
|
int len = Short.toUnsignedInt(bb.getShort());
|
||||||
|
if (len <= 0) throw new IllegalArgumentException(fieldName + " is empty");
|
||||||
|
if (bb.remaining() < len) throw new IllegalArgumentException(fieldName + " payload too short (len=" + len + ")");
|
||||||
|
|
||||||
|
byte[] bytes = new byte[len];
|
||||||
|
bb.get(bytes);
|
||||||
|
|
||||||
|
var decoder = StandardCharsets.UTF_8.newDecoder()
|
||||||
|
.onMalformedInput(CodingErrorAction.REPORT)
|
||||||
|
.onUnmappableCharacter(CodingErrorAction.REPORT);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String s = decoder.decode(ByteBuffer.wrap(bytes)).toString();
|
||||||
|
if (s.isBlank()) throw new IllegalArgumentException(fieldName + " is blank");
|
||||||
|
return s;
|
||||||
|
} catch (CharacterCodingException e) {
|
||||||
|
throw new IllegalArgumentException(fieldName + " is not valid UTF-8", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ensureMin(ByteBuffer bb, int need, String msg) {
|
||||||
|
if (bb.remaining() < need) throw new IllegalArgumentException(msg + " (need=" + need + ", remaining=" + bb.remaining() + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ensureNoTail(ByteBuffer bb, String ctx) {
|
||||||
|
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes for " + ctx + ", remaining=" + bb.remaining());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,244 @@
|
|||||||
|
package blockchain.body;
|
||||||
|
|
||||||
|
import blockchain.MsgSubType;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.charset.CharacterCodingException;
|
||||||
|
import java.nio.charset.CodingErrorAction;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TextReplyBody — type=1, ver=1.
|
||||||
|
*
|
||||||
|
* subType:
|
||||||
|
* - REPLY (20)
|
||||||
|
* - EDIT_REPLY (21)
|
||||||
|
*
|
||||||
|
* Форматы bodyBytes (BigEndian):
|
||||||
|
*
|
||||||
|
* REPLY:
|
||||||
|
* [1] toBlockchainNameLen (uint8)
|
||||||
|
* [N] toBlockchainName UTF-8
|
||||||
|
* [4] toBlockGlobalNumber
|
||||||
|
* [32] toBlockHash32
|
||||||
|
* [2] textLenBytes (uint16)
|
||||||
|
* [M] text UTF-8
|
||||||
|
*
|
||||||
|
* EDIT_REPLY:
|
||||||
|
* [4] toBlockGlobalNumber
|
||||||
|
* [32] toBlockHash32
|
||||||
|
* [2] textLenBytes (uint16)
|
||||||
|
* [N] text UTF-8
|
||||||
|
*/
|
||||||
|
public final class TextReplyBody implements BodyRecord, BodyHasTarget {
|
||||||
|
|
||||||
|
public static final short TYPE = 1;
|
||||||
|
public static final short VER = 1;
|
||||||
|
|
||||||
|
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
||||||
|
|
||||||
|
public final short subType; // из header
|
||||||
|
public final short version; // (=1)
|
||||||
|
|
||||||
|
// target
|
||||||
|
public final String toBlockchainName; // nullable для EDIT_REPLY
|
||||||
|
public final int toBlockGlobalNumber;
|
||||||
|
public final byte[] toBlockHash32; // 32
|
||||||
|
|
||||||
|
// text
|
||||||
|
public final String message;
|
||||||
|
|
||||||
|
public TextReplyBody(short subType, short version, byte[] bodyBytes) {
|
||||||
|
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||||||
|
|
||||||
|
this.subType = subType;
|
||||||
|
this.version = version;
|
||||||
|
|
||||||
|
if ((this.version & 0xFFFF) != (VER & 0xFFFF)) {
|
||||||
|
throw new IllegalArgumentException("TextReplyBody version must be 1, got=" + (this.version & 0xFFFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
int st = this.subType & 0xFFFF;
|
||||||
|
if (st != (MsgSubType.TEXT_REPLY & 0xFFFF) && st != (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) {
|
||||||
|
throw new IllegalArgumentException("TextReplyBody supports only REPLY/EDIT_REPLY, got subType=" + st);
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
|
if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) {
|
||||||
|
// минимум: nameLen[1]+name[1]+global[4]+hash[32]+textLen[2]
|
||||||
|
ensureMin(bb, 1 + 1 + 4 + 32 + 2, "REPLY too short");
|
||||||
|
|
||||||
|
int nameLen = Byte.toUnsignedInt(bb.get());
|
||||||
|
if (nameLen <= 0) throw new IllegalArgumentException("REPLY toBlockchainNameLen is 0");
|
||||||
|
ensureMin(bb, nameLen + 4 + 32 + 2, "REPLY payload too short");
|
||||||
|
|
||||||
|
byte[] nameBytes = new byte[nameLen];
|
||||||
|
bb.get(nameBytes);
|
||||||
|
this.toBlockchainName = new String(nameBytes, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
this.toBlockGlobalNumber = bb.getInt();
|
||||||
|
|
||||||
|
this.toBlockHash32 = new byte[32];
|
||||||
|
bb.get(this.toBlockHash32);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// EDIT_REPLY: target без имени
|
||||||
|
ensureMin(bb, (4 + 32) + 2, "EDIT_REPLY too short");
|
||||||
|
|
||||||
|
this.toBlockchainName = null;
|
||||||
|
this.toBlockGlobalNumber = bb.getInt();
|
||||||
|
|
||||||
|
this.toBlockHash32 = new byte[32];
|
||||||
|
bb.get(this.toBlockHash32);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.message = readStrictUtf8Len16(bb, "TextReplyBody text");
|
||||||
|
ensureNoTail(bb, "TextReplyBody");
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextReplyBody(short subType,
|
||||||
|
int toBlockGlobalNumber,
|
||||||
|
byte[] toBlockHash32,
|
||||||
|
String toBlockchainName,
|
||||||
|
String message) {
|
||||||
|
|
||||||
|
Objects.requireNonNull(message, "message == null");
|
||||||
|
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
||||||
|
|
||||||
|
int st = subType & 0xFFFF;
|
||||||
|
if (st != (MsgSubType.TEXT_REPLY & 0xFFFF) && st != (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) {
|
||||||
|
throw new IllegalArgumentException("TextReplyBody supports only REPLY/EDIT_REPLY");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.isBlank()) throw new IllegalArgumentException("message is blank");
|
||||||
|
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||||
|
if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||||
|
|
||||||
|
if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) {
|
||||||
|
Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
|
||||||
|
if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
|
||||||
|
this.toBlockchainName = toBlockchainName;
|
||||||
|
} else {
|
||||||
|
// EDIT_REPLY: имя не хранить
|
||||||
|
this.toBlockchainName = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subType = subType;
|
||||||
|
this.version = VER;
|
||||||
|
|
||||||
|
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
||||||
|
this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
|
||||||
|
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TextReplyBody check() {
|
||||||
|
int st = subType & 0xFFFF;
|
||||||
|
if (st != (MsgSubType.TEXT_REPLY & 0xFFFF) && st != (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF))
|
||||||
|
throw new IllegalArgumentException("Bad TextReplyBody subType: " + st);
|
||||||
|
|
||||||
|
if (message == null || message.isBlank())
|
||||||
|
throw new IllegalArgumentException("Text message is blank");
|
||||||
|
|
||||||
|
if (toBlockGlobalNumber < 0)
|
||||||
|
throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||||
|
if (toBlockHash32 == null || toBlockHash32.length != 32)
|
||||||
|
throw new IllegalArgumentException("toBlockHash32 invalid");
|
||||||
|
|
||||||
|
if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) {
|
||||||
|
if (toBlockchainName == null || toBlockchainName.isBlank())
|
||||||
|
throw new IllegalArgumentException("REPLY toBlockchainName is blank");
|
||||||
|
} else {
|
||||||
|
if (toBlockchainName != null)
|
||||||
|
throw new IllegalArgumentException("EDIT_REPLY must not contain toBlockchainName");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] toBytes() {
|
||||||
|
byte[] msgUtf8 = message.getBytes(StandardCharsets.UTF_8);
|
||||||
|
if (msgUtf8.length == 0) throw new IllegalArgumentException("Text payload is empty");
|
||||||
|
if (msgUtf8.length > 65535) throw new IllegalArgumentException("Text too long (>65535 bytes)");
|
||||||
|
|
||||||
|
int st = subType & 0xFFFF;
|
||||||
|
|
||||||
|
if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) {
|
||||||
|
if (toBlockchainName == null) throw new IllegalArgumentException("REPLY missing toBlockchainName");
|
||||||
|
|
||||||
|
byte[] nameUtf8 = toBlockchainName.getBytes(StandardCharsets.UTF_8);
|
||||||
|
if (nameUtf8.length == 0 || nameUtf8.length > 255)
|
||||||
|
throw new IllegalArgumentException("REPLY toBlockchainName utf8 len must be 1..255");
|
||||||
|
|
||||||
|
int cap = 1 + nameUtf8.length + 4 + 32 + 2 + msgUtf8.length;
|
||||||
|
|
||||||
|
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
bb.put((byte) nameUtf8.length);
|
||||||
|
bb.put(nameUtf8);
|
||||||
|
bb.putInt(toBlockGlobalNumber);
|
||||||
|
bb.put(toBlockHash32);
|
||||||
|
bb.putShort((short) msgUtf8.length);
|
||||||
|
bb.put(msgUtf8);
|
||||||
|
|
||||||
|
return bb.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
// EDIT_REPLY
|
||||||
|
int cap = (4 + 32) + 2 + msgUtf8.length;
|
||||||
|
|
||||||
|
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
bb.putInt(toBlockGlobalNumber);
|
||||||
|
bb.put(toBlockHash32);
|
||||||
|
bb.putShort((short) msgUtf8.length);
|
||||||
|
bb.put(msgUtf8);
|
||||||
|
|
||||||
|
return bb.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ====================== BodyHasTarget ====================== */
|
||||||
|
|
||||||
|
@Override public String toBchName() { return toBlockchainName; }
|
||||||
|
@Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
|
||||||
|
@Override public byte[] toBlockHashBytes() { return toBlockHash32; }
|
||||||
|
|
||||||
|
public boolean isEditReply() {
|
||||||
|
return (subType & 0xFFFF) == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ====================== helpers ====================== */
|
||||||
|
|
||||||
|
private static String readStrictUtf8Len16(ByteBuffer bb, String fieldName) {
|
||||||
|
int len = Short.toUnsignedInt(bb.getShort());
|
||||||
|
if (len <= 0) throw new IllegalArgumentException(fieldName + " is empty");
|
||||||
|
if (bb.remaining() < len) throw new IllegalArgumentException(fieldName + " payload too short (len=" + len + ")");
|
||||||
|
|
||||||
|
byte[] bytes = new byte[len];
|
||||||
|
bb.get(bytes);
|
||||||
|
|
||||||
|
var decoder = StandardCharsets.UTF_8.newDecoder()
|
||||||
|
.onMalformedInput(CodingErrorAction.REPORT)
|
||||||
|
.onUnmappableCharacter(CodingErrorAction.REPORT);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String s = decoder.decode(ByteBuffer.wrap(bytes)).toString();
|
||||||
|
if (s.isBlank()) throw new IllegalArgumentException(fieldName + " is blank");
|
||||||
|
return s;
|
||||||
|
} catch (CharacterCodingException e) {
|
||||||
|
throw new IllegalArgumentException(fieldName + " is not valid UTF-8", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ensureMin(ByteBuffer bb, int need, String msg) {
|
||||||
|
if (bb.remaining() < need) throw new IllegalArgumentException(msg + " (need=" + need + ", remaining=" + bb.remaining() + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ensureNoTail(ByteBuffer bb, String ctx) {
|
||||||
|
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes for " + ctx + ", remaining=" + bb.remaining());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,17 +14,17 @@ import java.sql.Statement;
|
|||||||
/**
|
/**
|
||||||
* DatabaseInitializer — создание новой SQLite-БД по схеме SHiNE.
|
* DatabaseInitializer — создание новой SQLite-БД по схеме SHiNE.
|
||||||
*
|
*
|
||||||
* Таблицы:
|
* В этой версии:
|
||||||
* - solana_users
|
* - создаём ТОЛЬКО таблицы/индексы
|
||||||
* - active_sessions
|
* - в конце вызываем DatabaseTriggersInstaller.createAllTriggers(st)
|
||||||
* - users_params
|
*
|
||||||
* - ip_geo_cache
|
* Зачем так:
|
||||||
* - blockchain_state
|
* - триггеры часто ломают совместимость с внешними SQLite-просмотрщиками/сборками
|
||||||
* - blocks
|
* - проще поддерживать/мигрировать
|
||||||
* - connections_state
|
|
||||||
* - message_stats
|
|
||||||
*/
|
*/
|
||||||
public class DatabaseInitializer {
|
public final class DatabaseInitializer {
|
||||||
|
|
||||||
|
private DatabaseInitializer() {}
|
||||||
|
|
||||||
/* ===================== TEXT (msg_type=1) ===================== */
|
/* ===================== TEXT (msg_type=1) ===================== */
|
||||||
|
|
||||||
@ -46,7 +46,6 @@ public class DatabaseInitializer {
|
|||||||
public static final short REACTION_LIKE = 1;
|
public static final short REACTION_LIKE = 1;
|
||||||
|
|
||||||
/* ===================== CONNECTION (msg_type=3) ===================== */
|
/* ===================== CONNECTION (msg_type=3) ===================== */
|
||||||
// Приведено к твоему shine.db.MsgSubType:
|
|
||||||
// FRIEND=10/11, CONTACT=20/21, FOLLOW=30/31
|
// FRIEND=10/11, CONTACT=20/21, FOLLOW=30/31
|
||||||
public static final short CONNECTION_FRIEND = 10;
|
public static final short CONNECTION_FRIEND = 10;
|
||||||
public static final short CONNECTION_UNFRIEND = 11;
|
public static final short CONNECTION_UNFRIEND = 11;
|
||||||
@ -264,138 +263,6 @@ public class DatabaseInitializer {
|
|||||||
ON blocks (bch_name, line_code, this_line_number);
|
ON blocks (bch_name, line_code, this_line_number);
|
||||||
""");
|
""");
|
||||||
|
|
||||||
// 6.1) TRIGGER: проверка целостности линии (только если line-поля реально переданы)
|
|
||||||
/* пока просто отключил этот тригер
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TRIGGER IF NOT EXISTS trg_blocks_line_integrity_bi
|
|
||||||
BEFORE INSERT ON blocks
|
|
||||||
WHEN
|
|
||||||
NEW.line_code IS NOT NULL
|
|
||||||
OR NEW.prev_line_number IS NOT NULL
|
|
||||||
OR NEW.prev_line_hash IS NOT NULL
|
|
||||||
OR NEW.this_line_number IS NOT NULL
|
|
||||||
BEGIN
|
|
||||||
-- ============================================================
|
|
||||||
-- LINE-INTEGRITY (BodyHasLine)
|
|
||||||
--
|
|
||||||
-- Этот триггер срабатывает ТОЛЬКО если при вставке передали хотя бы одно line-поле.
|
|
||||||
--
|
|
||||||
-- Типы, которые МОГУТ быть линейными (BodyHasLine в коде проекта):
|
|
||||||
-- - TECH (msg_type=0): CreateChannelBody (и т.п. тех-блоки с линией)
|
|
||||||
-- - TEXT (msg_type=1): TextBody в режиме линии (пост/редактирование поста в канале)
|
|
||||||
-- - CONNECTION (msg_type=3): ConnectionBody
|
|
||||||
-- - USER_PARAM (msg_type=4): UserParamBody
|
|
||||||
--
|
|
||||||
-- Проверки:
|
|
||||||
-- 1) Если передали line-поля -> обязаны передать ВСЕ 4:
|
|
||||||
-- line_code, prev_line_number, prev_line_hash, this_line_number.
|
|
||||||
-- 2) prev блок линии существует и p.block_hash == NEW.prev_line_hash
|
|
||||||
-- 3) line_code корректный:
|
|
||||||
-- - либо NEW.prev_line_number == NEW.line_code (первый шаг после root),
|
|
||||||
-- - либо у prev блока p.line_code == NEW.line_code
|
|
||||||
-- 4) this_line_number корректный:
|
|
||||||
-- - первый шаг после root:
|
|
||||||
-- TEXT: this=0
|
|
||||||
-- TECH/CONNECTION/USER_PARAM: this=1
|
|
||||||
-- - дальше:
|
|
||||||
-- TEXT: допускаем this = prev.this или prev.this + 1
|
|
||||||
-- TECH/CONNECTION/USER_PARAM: строго this = prev.this + 1
|
|
||||||
--
|
|
||||||
-- Ошибки: RAISE(ABORT, 'LINE_ERR_...') — чтобы Java могла понять причину.
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
-- 0) line-поля нельзя у неожиданных типов
|
|
||||||
SELECT RAISE(ABORT,
|
|
||||||
'LINE_ERR_UNSUPPORTED_TYPE_WITH_LINE: msg_type=' || NEW.msg_type || ' msg_sub_type=' || NEW.msg_sub_type
|
|
||||||
)
|
|
||||||
WHERE NOT (NEW.msg_type IN (0, 1, 3, 4));
|
|
||||||
|
|
||||||
-- 1) line-поля должны быть заполнены полностью (без “частично”)
|
|
||||||
SELECT RAISE(ABORT,
|
|
||||||
'LINE_ERR_PARTIAL_FIELDS: all of (line_code, prev_line_number, prev_line_hash, this_line_number) must be NOT NULL'
|
|
||||||
)
|
|
||||||
WHERE NEW.line_code IS NULL
|
|
||||||
OR NEW.prev_line_number IS NULL
|
|
||||||
OR NEW.prev_line_hash IS NULL
|
|
||||||
OR NEW.this_line_number IS NULL;
|
|
||||||
|
|
||||||
-- 2) prev существует?
|
|
||||||
SELECT RAISE(ABORT,
|
|
||||||
'LINE_ERR_NO_PREV: bch=' || NEW.bch_name || ' block=' || NEW.block_number || ' prev=' || NEW.prev_line_number
|
|
||||||
)
|
|
||||||
WHERE NOT EXISTS(
|
|
||||||
SELECT 1
|
|
||||||
FROM blocks p
|
|
||||||
WHERE p.bch_name = NEW.bch_name
|
|
||||||
AND p.block_number = NEW.prev_line_number
|
|
||||||
LIMIT 1
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 3) prev hash совпадает?
|
|
||||||
SELECT RAISE(ABORT,
|
|
||||||
'LINE_ERR_PREV_HASH_MISMATCH: bch=' || NEW.bch_name || ' block=' || NEW.block_number || ' prev=' || NEW.prev_line_number
|
|
||||||
)
|
|
||||||
WHERE NOT EXISTS(
|
|
||||||
SELECT 1
|
|
||||||
FROM blocks p
|
|
||||||
WHERE p.bch_name = NEW.bch_name
|
|
||||||
AND p.block_number = NEW.prev_line_number
|
|
||||||
AND p.block_hash = NEW.prev_line_hash
|
|
||||||
LIMIT 1
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 4) line_code корректный:
|
|
||||||
-- либо это первый шаг после root (prev_line_number == line_code),
|
|
||||||
-- либо prev уже в этой линии (p.line_code == NEW.line_code).
|
|
||||||
SELECT RAISE(ABORT,
|
|
||||||
'LINE_ERR_LINE_CODE_MISMATCH: bch=' || NEW.bch_name || ' block=' || NEW.block_number ||
|
|
||||||
' line_code=' || NEW.line_code || ' prev=' || NEW.prev_line_number
|
|
||||||
)
|
|
||||||
WHERE NEW.prev_line_number <> NEW.line_code
|
|
||||||
AND NOT EXISTS(
|
|
||||||
SELECT 1
|
|
||||||
FROM blocks p
|
|
||||||
WHERE p.bch_name = NEW.bch_name
|
|
||||||
AND p.block_number = NEW.prev_line_number
|
|
||||||
AND p.line_code = NEW.line_code
|
|
||||||
LIMIT 1
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 5) первый шаг после root: this_line_number
|
|
||||||
SELECT RAISE(ABORT,
|
|
||||||
'LINE_ERR_FIRST_STEP_BAD_THIS: expected this_line_number=0 for TEXT or =1 for other types'
|
|
||||||
)
|
|
||||||
WHERE NEW.prev_line_number = NEW.line_code
|
|
||||||
AND NEW.this_line_number <> (CASE WHEN NEW.msg_type = 1 THEN 0 ELSE 1 END);
|
|
||||||
|
|
||||||
-- 6) обычный шаг: this_line_number относительно prev
|
|
||||||
SELECT RAISE(ABORT,
|
|
||||||
'LINE_ERR_THIS_LINE_BAD_STEP: bch=' || NEW.bch_name || ' block=' || NEW.block_number ||
|
|
||||||
' this=' || NEW.this_line_number || ' prev=' || NEW.prev_line_number
|
|
||||||
)
|
|
||||||
WHERE NEW.prev_line_number <> NEW.line_code
|
|
||||||
AND NOT EXISTS(
|
|
||||||
SELECT 1
|
|
||||||
FROM blocks p
|
|
||||||
WHERE p.bch_name = NEW.bch_name
|
|
||||||
AND p.block_number = NEW.prev_line_number
|
|
||||||
AND p.this_line_number IS NOT NULL
|
|
||||||
AND (
|
|
||||||
-- TEXT: допускаем same или +1 (поддерживает “edit не увеличивает thisLineNumber”)
|
|
||||||
(NEW.msg_type = 1 AND
|
|
||||||
(NEW.this_line_number = p.this_line_number OR NEW.this_line_number = p.this_line_number + 1)
|
|
||||||
)
|
|
||||||
OR
|
|
||||||
-- TECH/CONNECTION/USER_PARAM: строго +1
|
|
||||||
(NEW.msg_type IN (0,3,4) AND
|
|
||||||
NEW.this_line_number = p.this_line_number + 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
LIMIT 1
|
|
||||||
);
|
|
||||||
END;
|
|
||||||
""");
|
|
||||||
*/
|
|
||||||
// 7) connections_state
|
// 7) connections_state
|
||||||
st.executeUpdate("""
|
st.executeUpdate("""
|
||||||
CREATE TABLE IF NOT EXISTS connections_state (
|
CREATE TABLE IF NOT EXISTS connections_state (
|
||||||
@ -427,59 +294,7 @@ public class DatabaseInitializer {
|
|||||||
ON connections_state (login, to_login);
|
ON connections_state (login, to_login);
|
||||||
""");
|
""");
|
||||||
|
|
||||||
// 8) Trigger: connection state
|
// 8) message_stats
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TRIGGER IF NOT EXISTS trg_blocks_connection_state_ai
|
|
||||||
AFTER INSERT ON blocks
|
|
||||||
WHEN NEW.msg_type = 3
|
|
||||||
BEGIN
|
|
||||||
|
|
||||||
INSERT INTO connections_state (
|
|
||||||
login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
NEW.login,
|
|
||||||
NEW.msg_sub_type,
|
|
||||||
NEW.to_login,
|
|
||||||
NEW.to_bch_name,
|
|
||||||
NEW.to_block_number,
|
|
||||||
NEW.to_block_hash
|
|
||||||
WHERE NEW.msg_sub_type IN (%d, %d, %d)
|
|
||||||
AND NEW.to_login IS NOT NULL
|
|
||||||
AND NEW.to_bch_name IS NOT NULL
|
|
||||||
ON CONFLICT(login, rel_type, to_login)
|
|
||||||
DO UPDATE SET
|
|
||||||
to_bch_name = excluded.to_bch_name,
|
|
||||||
to_block_number = excluded.to_block_number,
|
|
||||||
to_block_hash = excluded.to_block_hash;
|
|
||||||
|
|
||||||
DELETE FROM connections_state
|
|
||||||
WHERE login = NEW.login
|
|
||||||
AND to_login = NEW.to_login
|
|
||||||
AND rel_type = CASE NEW.msg_sub_type
|
|
||||||
WHEN %d THEN %d
|
|
||||||
WHEN %d THEN %d
|
|
||||||
WHEN %d THEN %d
|
|
||||||
ELSE rel_type
|
|
||||||
END
|
|
||||||
AND NEW.msg_sub_type IN (%d, %d, %d);
|
|
||||||
|
|
||||||
END;
|
|
||||||
""".formatted(
|
|
||||||
(int) CONNECTION_FRIEND,
|
|
||||||
(int) CONNECTION_CONTACT,
|
|
||||||
(int) CONNECTION_FOLLOW,
|
|
||||||
|
|
||||||
(int) CONNECTION_UNFRIEND, (int) CONNECTION_FRIEND,
|
|
||||||
(int) CONNECTION_UNCONTACT, (int) CONNECTION_CONTACT,
|
|
||||||
(int) CONNECTION_UNFOLLOW, (int) CONNECTION_FOLLOW,
|
|
||||||
|
|
||||||
(int) CONNECTION_UNFRIEND,
|
|
||||||
(int) CONNECTION_UNCONTACT,
|
|
||||||
(int) CONNECTION_UNFOLLOW
|
|
||||||
));
|
|
||||||
|
|
||||||
// 9) message_stats
|
|
||||||
st.executeUpdate("""
|
st.executeUpdate("""
|
||||||
CREATE TABLE IF NOT EXISTS message_stats (
|
CREATE TABLE IF NOT EXISTS message_stats (
|
||||||
to_login TEXT NOT NULL,
|
to_login TEXT NOT NULL,
|
||||||
@ -510,110 +325,8 @@ public class DatabaseInitializer {
|
|||||||
ON message_stats (to_login);
|
ON message_stats (to_login);
|
||||||
""");
|
""");
|
||||||
|
|
||||||
// 10) Trigger: LIKE
|
// ВАЖНО: триггеры ставим отдельно
|
||||||
st.executeUpdate("""
|
DatabaseTriggersInstaller.createAllTriggers(st);
|
||||||
CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_like_ai
|
|
||||||
AFTER INSERT ON blocks
|
|
||||||
WHEN NEW.msg_type = 2 AND NEW.msg_sub_type = %d
|
|
||||||
BEGIN
|
|
||||||
INSERT INTO message_stats (
|
|
||||||
to_login,
|
|
||||||
to_bch_name,
|
|
||||||
to_block_number,
|
|
||||||
to_block_hash,
|
|
||||||
likes_count,
|
|
||||||
replies_count,
|
|
||||||
edits_count
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
NEW.to_login,
|
|
||||||
NEW.to_bch_name,
|
|
||||||
NEW.to_block_number,
|
|
||||||
NEW.to_block_hash,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
WHERE NEW.to_login IS NOT NULL
|
|
||||||
AND NEW.to_bch_name IS NOT NULL
|
|
||||||
AND NEW.to_block_number IS NOT NULL
|
|
||||||
AND NEW.to_block_hash IS NOT NULL
|
|
||||||
ON CONFLICT(to_login, to_bch_name, to_block_number, to_block_hash)
|
|
||||||
DO UPDATE SET
|
|
||||||
likes_count = message_stats.likes_count + 1;
|
|
||||||
END;
|
|
||||||
""".formatted((int) REACTION_LIKE));
|
|
||||||
|
|
||||||
// 11) Trigger: REPLY
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_reply_ai
|
|
||||||
AFTER INSERT ON blocks
|
|
||||||
WHEN NEW.msg_type = 1 AND NEW.msg_sub_type = %d
|
|
||||||
BEGIN
|
|
||||||
INSERT INTO message_stats (
|
|
||||||
to_login,
|
|
||||||
to_bch_name,
|
|
||||||
to_block_number,
|
|
||||||
to_block_hash,
|
|
||||||
likes_count,
|
|
||||||
replies_count,
|
|
||||||
edits_count
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
NEW.to_login,
|
|
||||||
NEW.to_bch_name,
|
|
||||||
NEW.to_block_number,
|
|
||||||
NEW.to_block_hash,
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
0
|
|
||||||
WHERE NEW.to_login IS NOT NULL
|
|
||||||
AND NEW.to_bch_name IS NOT NULL
|
|
||||||
AND NEW.to_block_number IS NOT NULL
|
|
||||||
AND NEW.to_block_hash IS NOT NULL
|
|
||||||
ON CONFLICT(to_login, to_bch_name, to_block_number, to_block_hash)
|
|
||||||
DO UPDATE SET
|
|
||||||
replies_count = message_stats.replies_count + 1;
|
|
||||||
END;
|
|
||||||
""".formatted((int) TEXT_REPLY));
|
|
||||||
|
|
||||||
// 12) Trigger: EDIT
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TRIGGER IF NOT EXISTS trg_blocks_edit_apply_ai
|
|
||||||
AFTER INSERT ON blocks
|
|
||||||
WHEN NEW.msg_type = 1 AND NEW.msg_sub_type = %d
|
|
||||||
BEGIN
|
|
||||||
UPDATE blocks
|
|
||||||
SET edited_by_block_number = NEW.block_number
|
|
||||||
WHERE login = NEW.login
|
|
||||||
AND bch_name = NEW.bch_name
|
|
||||||
AND block_number = NEW.to_block_number;
|
|
||||||
|
|
||||||
INSERT INTO message_stats (
|
|
||||||
to_login,
|
|
||||||
to_bch_name,
|
|
||||||
to_block_number,
|
|
||||||
to_block_hash,
|
|
||||||
likes_count,
|
|
||||||
replies_count,
|
|
||||||
edits_count
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
NEW.to_login,
|
|
||||||
NEW.to_bch_name,
|
|
||||||
NEW.to_block_number,
|
|
||||||
NEW.to_block_hash,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
1
|
|
||||||
WHERE NEW.to_login IS NOT NULL
|
|
||||||
AND NEW.to_bch_name IS NOT NULL
|
|
||||||
AND NEW.to_block_number IS NOT NULL
|
|
||||||
AND NEW.to_block_hash IS NOT NULL
|
|
||||||
ON CONFLICT(to_login, to_bch_name, to_block_number, to_block_hash)
|
|
||||||
DO UPDATE SET
|
|
||||||
edits_count = message_stats.edits_count + 1;
|
|
||||||
END;
|
|
||||||
""".formatted((int) TEXT_EDIT));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,6 +2,7 @@ package server.logic.ws_protocol.JSON.handlers.blockchain;
|
|||||||
|
|
||||||
import blockchain.BchBlockEntry;
|
import blockchain.BchBlockEntry;
|
||||||
import blockchain.BchCryptoVerifier;
|
import blockchain.BchCryptoVerifier;
|
||||||
|
import blockchain.MsgSubType;
|
||||||
import blockchain.body.BodyHasLine;
|
import blockchain.body.BodyHasLine;
|
||||||
import blockchain.body.BodyHasTarget;
|
import blockchain.body.BodyHasTarget;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@ -33,13 +34,12 @@ import java.util.concurrent.locks.ReentrantLock;
|
|||||||
* 2) Проверяем:
|
* 2) Проверяем:
|
||||||
* - incoming.blockNumber == last+1
|
* - incoming.blockNumber == last+1
|
||||||
* - incoming.prevHash32 == last_hash (для genesis last_hash = 32 нулей)
|
* - incoming.prevHash32 == last_hash (для genesis last_hash = 32 нулей)
|
||||||
* 3) Считаем hash32 = SHA-256(preimage) (preimage = block_bytes без signature64)
|
* 3) Проверяем подпись Ed25519.verify(hash32(preimage), signature64, pubKey)
|
||||||
* 4) Проверяем подпись Ed25519.verify(hash32, signature64, pubKey)
|
* 4) Если тип имеет линию:
|
||||||
* 5) Если тип имеет линию:
|
* - если prevLineNumber != null:
|
||||||
* - если prevLineNumber != -1:
|
|
||||||
* достаём hash блока prevLineNumber из blocks
|
* достаём hash блока prevLineNumber из blocks
|
||||||
* сравниваем с prevLineHash32 из body
|
* сравниваем с prevLineHash32 из body
|
||||||
* 6) Сохраняем блок в blocks + обновляем blockchain_state
|
* 5) Сохраняем блок в blocks + обновляем blockchain_state
|
||||||
*
|
*
|
||||||
* Важно:
|
* Важно:
|
||||||
* - Сетевой протокол AddBlock пока оставляем старые поля (globalNumber/prevGlobalHash),
|
* - Сетевой протокол AddBlock пока оставляем старые поля (globalNumber/prevGlobalHash),
|
||||||
@ -224,17 +224,27 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature", serverLastNum, serverLastHashHex);
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature", serverLastNum, serverLastHashHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7) линейная проверка (только для типов с линией)
|
// 7) line columns (only for BodyHasLine)
|
||||||
|
Integer lineCode = null;
|
||||||
Integer prevLineNumber = null;
|
Integer prevLineNumber = null;
|
||||||
byte[] prevLineHash32 = null;
|
byte[] prevLineHash32 = null;
|
||||||
Integer thisLineNumber = null;
|
Integer thisLineNumber = null;
|
||||||
|
|
||||||
if (block.body instanceof BodyHasLine bl) {
|
if (block.body instanceof BodyHasLine bl) {
|
||||||
|
lineCode = bl.lineCode();
|
||||||
prevLineNumber = bl.prevLineNumber();
|
prevLineNumber = bl.prevLineNumber();
|
||||||
prevLineHash32 = bl.prevLineHash32();
|
prevLineHash32 = bl.prevLineHash32();
|
||||||
thisLineNumber = bl.thisLineNumber();
|
thisLineNumber = bl.thisLineNumber();
|
||||||
|
|
||||||
if (prevLineNumber != null && prevLineNumber != -1) {
|
// Нормализация: -1 не пишем в БД (для совместимости со старым TextBody)
|
||||||
|
if (prevLineNumber != null && prevLineNumber == -1) {
|
||||||
|
prevLineNumber = null;
|
||||||
|
prevLineHash32 = null;
|
||||||
|
thisLineNumber = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если prevLineNumber задан — проверяем его хэш
|
||||||
|
if (prevLineNumber != null) {
|
||||||
try {
|
try {
|
||||||
byte[] dbPrevHash = blocksDAO.getHashByNumber(blockchainName, prevLineNumber);
|
byte[] dbPrevHash = blocksDAO.getHashByNumber(blockchainName, prevLineNumber);
|
||||||
if (dbPrevHash == null) {
|
if (dbPrevHash == null) {
|
||||||
@ -270,6 +280,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
be.setBlockSignature(block.getSignature64());
|
be.setBlockSignature(block.getSignature64());
|
||||||
|
|
||||||
// line columns (optional)
|
// line columns (optional)
|
||||||
|
be.setLineCode(lineCode);
|
||||||
be.setPrevLineNumber(prevLineNumber);
|
be.setPrevLineNumber(prevLineNumber);
|
||||||
be.setPrevLineHash(prevLineHash32);
|
be.setPrevLineHash(prevLineHash32);
|
||||||
be.setThisLineNumber(thisLineNumber);
|
be.setThisLineNumber(thisLineNumber);
|
||||||
@ -282,8 +293,13 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
be.setToBlockHash(t.toBlockHashBytes());
|
be.setToBlockHash(t.toBlockHashBytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
// edit helper (optional): если TEXT_EDIT — это "редактирование блока цели"
|
// edit helper (optional): если TEXT_EDIT_* — это "редактирование блока цели"
|
||||||
if ((block.type & 0xFFFF) == 1 && (block.subType & 0xFFFF) == 10 && be.getToBlockNumber() != null) {
|
int type = block.type & 0xFFFF;
|
||||||
|
int sub = block.subType & 0xFFFF;
|
||||||
|
|
||||||
|
if (type == 1
|
||||||
|
&& (sub == (MsgSubType.TEXT_EDIT_POST & 0xFFFF) || sub == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF))
|
||||||
|
&& be.getToBlockNumber() != null) {
|
||||||
be.setEditedByBlockNumber(be.getToBlockNumber());
|
be.setEditedByBlockNumber(be.getToBlockNumber());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user