SHA256
Compare commits
29
Commits
43f54c90d2
...
cf391bdd80
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
cf391bdd80 | ||
|
|
7120d6a9db | ||
|
|
4d80ab639e | ||
|
|
95bbd2852e | ||
|
|
4b0a934e51 | ||
|
|
ee5d86003b | ||
|
|
bc11b60147 | ||
|
|
801a4697ea | ||
|
|
628a970e6f | ||
|
|
566ea942dc | ||
|
|
e047c2d54e | ||
|
|
3fdd3d22e2 | ||
|
|
08e0fa9206 | ||
|
|
ed9e6ce676 | ||
|
|
a7bf07130e | ||
|
|
7c31662698 | ||
|
|
15df06a0bc | ||
|
|
47b5f8db7b | ||
|
|
0e763987aa | ||
|
|
37e8ea27d0 | ||
|
|
67d63256c2 | ||
|
|
ee185cf20a | ||
|
|
3552e05e4c | ||
|
|
735bb55b78 | ||
|
|
60f8a50a97 | ||
|
|
ffd01814b0 | ||
|
|
c511910b8f | ||
|
|
5c38f8f0d8 | ||
|
|
d512442325 |
@@ -36,6 +36,10 @@ public final class BodyRecordParser {
|
||||
case TextBody.KEY -> {
|
||||
if (st == (MsgSubType.TEXT_POST & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_EXERCISE & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_SERVICE & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_COURSE & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_ENTRYPOINT & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_REPOST & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_CHANNEL_META & 0xFFFF)) {
|
||||
yield new TextLineBody(subType, version, bodyBytes);
|
||||
@@ -46,12 +50,17 @@ public final class BodyRecordParser {
|
||||
yield new TextReplyBody(subType, version, bodyBytes);
|
||||
}
|
||||
|
||||
if (st == (MsgSubType.TEXT_RATING & 0xFFFF)) {
|
||||
yield new TextRatingBody(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 ConnectionBody.KEY -> new ConnectionBody(subType, version, bodyBytes);
|
||||
case UserParamBody.KEY -> new UserParamBody(subType, version, bodyBytes);
|
||||
case StatusActionBody.KEY -> new StatusActionBody(subType, version, bodyBytes);
|
||||
|
||||
default -> throw new IllegalArgumentException(String.format(
|
||||
"Unknown body type/version from header: type=%d ver=%d subType=%d",
|
||||
|
||||
@@ -64,17 +64,29 @@ public final class MsgSubType {
|
||||
*/
|
||||
public static final short TEXT_EDIT_REPLY = 21;
|
||||
|
||||
/** RATING — target-based отзыв на конкретный блок. */
|
||||
public static final short TEXT_RATING = 30;
|
||||
|
||||
/**
|
||||
* REPOST — репост сообщения в линии канала.
|
||||
* REPOST — отложенная будущая заготовка репоста сообщения в линии канала.
|
||||
* Имеет hasLine + target (toBlockchainName + toBlockGlobalNumber + toBlockHash32) + текст комментария.
|
||||
*/
|
||||
public static final short TEXT_REPOST = 30;
|
||||
public static final short TEXT_REPOST = 50;
|
||||
|
||||
/**
|
||||
* CHANNEL_META — скрытый технический снимок профиля канала.
|
||||
* Имеет hasLine, использует body как POST: line-поля + UTF-8 текст.
|
||||
*/
|
||||
public static final short TEXT_CHANNEL_META = 70;
|
||||
public static final short TEXT_CHANNEL_META = 90;
|
||||
|
||||
/** ENTRYPOINT — входная страница канала (line-based). */
|
||||
public static final short TEXT_ENTRYPOINT = 100;
|
||||
/** EXERCISE — упражнение/комплекс (line-based). */
|
||||
public static final short TEXT_EXERCISE = 110;
|
||||
/** SERVICE — услуга/процедура (line-based). */
|
||||
public static final short TEXT_SERVICE = 120;
|
||||
/** COURSE — курс (line-based). */
|
||||
public static final short TEXT_COURSE = 130;
|
||||
|
||||
/* ===================== REACTION (msg_type=2) ===================== */
|
||||
|
||||
@@ -144,4 +156,16 @@ public final class MsgSubType {
|
||||
|
||||
/** Параметр профиля key/value (обе строки). */
|
||||
public static final short USER_PARAM_TEXT_TEXT = 1;
|
||||
|
||||
/* ===================== STATUS_ACTION (msg_type=5) ===================== */
|
||||
|
||||
public static final short STATUS_DONE_ONCE = 10;
|
||||
public static final short STATUS_LEARNED = 20;
|
||||
public static final short STATUS_SERVICE_PASSED = 30;
|
||||
public static final short STATUS_CONFIRMED = 100;
|
||||
public static final short STATUS_INTERESTED = 110;
|
||||
public static final short STATUS_STARTED = 120;
|
||||
public static final short STATUS_IN_STUDY = 130;
|
||||
public static final short STATUS_ABANDONED = 140;
|
||||
public static final short STATUS_COMPLETED = 150;
|
||||
}
|
||||
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* StatusActionBody — type=5, ver=1.
|
||||
*
|
||||
* Все STATUS_ACTION имеют target на конкретный блок и опциональный текст-пояснение.
|
||||
*
|
||||
* Формат bodyBytes (BigEndian):
|
||||
* [1] toBlockchainNameLen (uint8)
|
||||
* [N] toBlockchainName UTF-8
|
||||
* [4] toBlockGlobalNumber
|
||||
* [32] toBlockHash32
|
||||
* [2] textLenBytes (uint16)
|
||||
* [M] text UTF-8
|
||||
*/
|
||||
public final class StatusActionBody implements BodyRecord, BodyHasTarget {
|
||||
|
||||
public static final short TYPE = 5;
|
||||
public static final short VER = 1;
|
||||
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
||||
|
||||
public final short subType;
|
||||
public final short version;
|
||||
public final String toBlockchainName;
|
||||
public final int toBlockGlobalNumber;
|
||||
public final byte[] toBlockHash32;
|
||||
public final String message;
|
||||
|
||||
public StatusActionBody(short subType, short version, byte[] bodyBytes) {
|
||||
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||||
this.subType = subType;
|
||||
this.version = version;
|
||||
|
||||
if ((version & 0xFFFF) != (VER & 0xFFFF)) {
|
||||
throw new IllegalArgumentException("StatusActionBody version must be 1, got=" + (version & 0xFFFF));
|
||||
}
|
||||
if (!isSupportedSubType(subType)) {
|
||||
throw new IllegalArgumentException("Unsupported STATUS_ACTION subType: " + (subType & 0xFFFF));
|
||||
}
|
||||
|
||||
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
||||
ensureMin(bb, 1 + 1 + 4 + 32 + 2, "STATUS_ACTION too short");
|
||||
|
||||
int nameLen = Byte.toUnsignedInt(bb.get());
|
||||
if (nameLen <= 0) throw new IllegalArgumentException("STATUS_ACTION toBlockchainNameLen is 0");
|
||||
ensureMin(bb, nameLen + 4 + 32 + 2, "STATUS_ACTION 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);
|
||||
this.message = readStrictUtf8Len16AllowEmpty(bb, "StatusActionBody text");
|
||||
|
||||
ensureNoTail(bb, "StatusActionBody");
|
||||
}
|
||||
|
||||
public StatusActionBody(short subType, String toBlockchainName, int toBlockGlobalNumber, byte[] toBlockHash32, String message) {
|
||||
Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
|
||||
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
||||
Objects.requireNonNull(message, "message == null");
|
||||
if (!isSupportedSubType(subType)) throw new IllegalArgumentException("Unsupported STATUS_ACTION subType");
|
||||
if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
|
||||
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||
if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||
|
||||
this.subType = subType;
|
||||
this.version = VER;
|
||||
this.toBlockchainName = toBlockchainName;
|
||||
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
||||
this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StatusActionBody check() {
|
||||
if (!isSupportedSubType(subType)) {
|
||||
throw new IllegalArgumentException("Bad STATUS_ACTION subType: " + (subType & 0xFFFF));
|
||||
}
|
||||
if (toBlockchainName == null || toBlockchainName.isBlank()) {
|
||||
throw new IllegalArgumentException("STATUS_ACTION toBlockchainName is blank");
|
||||
}
|
||||
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||
if (toBlockHash32 == null || toBlockHash32.length != 32) {
|
||||
throw new IllegalArgumentException("toBlockHash32 invalid");
|
||||
}
|
||||
if (message == null) throw new IllegalArgumentException("message is null");
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBytes() {
|
||||
byte[] msgUtf8 = message.getBytes(StandardCharsets.UTF_8);
|
||||
if (msgUtf8.length > 65535) throw new IllegalArgumentException("Text too long (>65535 bytes)");
|
||||
|
||||
byte[] nameUtf8 = toBlockchainName.getBytes(StandardCharsets.UTF_8);
|
||||
if (nameUtf8.length == 0 || nameUtf8.length > 255) {
|
||||
throw new IllegalArgumentException("STATUS_ACTION toBlockchainName utf8 len must be 1..255");
|
||||
}
|
||||
|
||||
ByteBuffer bb = ByteBuffer.allocate(1 + nameUtf8.length + 4 + 32 + 2 + msgUtf8.length)
|
||||
.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();
|
||||
}
|
||||
|
||||
@Override public String toBchName() { return toBlockchainName; }
|
||||
@Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
|
||||
@Override public byte[] toBlockHashBytes() { return toBlockHash32; }
|
||||
|
||||
private static boolean isSupportedSubType(short subType) {
|
||||
int st = subType & 0xFFFF;
|
||||
return st == (MsgSubType.STATUS_DONE_ONCE & 0xFFFF)
|
||||
|| st == (MsgSubType.STATUS_LEARNED & 0xFFFF)
|
||||
|| st == (MsgSubType.STATUS_SERVICE_PASSED & 0xFFFF)
|
||||
|| st == (MsgSubType.STATUS_CONFIRMED & 0xFFFF)
|
||||
|| st == (MsgSubType.STATUS_INTERESTED & 0xFFFF)
|
||||
|| st == (MsgSubType.STATUS_STARTED & 0xFFFF)
|
||||
|| st == (MsgSubType.STATUS_IN_STUDY & 0xFFFF)
|
||||
|| st == (MsgSubType.STATUS_ABANDONED & 0xFFFF)
|
||||
|| st == (MsgSubType.STATUS_COMPLETED & 0xFFFF);
|
||||
}
|
||||
|
||||
private static String readStrictUtf8Len16AllowEmpty(ByteBuffer bb, String fieldName) {
|
||||
int len = Short.toUnsignedInt(bb.getShort());
|
||||
if (len == 0) return "";
|
||||
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 {
|
||||
return decoder.decode(ByteBuffer.wrap(bytes)).toString();
|
||||
} 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());
|
||||
}
|
||||
}
|
||||
+42
-10
@@ -16,12 +16,16 @@ import java.util.Objects;
|
||||
* subType:
|
||||
* - POST (10)
|
||||
* - EDIT_POST (11)
|
||||
* - REPOST (30)
|
||||
* - CHANNEL_META (70)
|
||||
* - REPOST (50)
|
||||
* - CHANNEL_META (90)
|
||||
* - ENTRYPOINT (100)
|
||||
* - EXERCISE (110)
|
||||
* - SERVICE (120)
|
||||
* - COURSE (130)
|
||||
*
|
||||
* Формат bodyBytes (BigEndian):
|
||||
*
|
||||
* POST / CHANNEL_META:
|
||||
* POST / CHANNEL_META / ENTRYPOINT / EXERCISE / SERVICE / COURSE:
|
||||
* [4] lineCode
|
||||
* [4] prevLineNumber
|
||||
* [32] prevLineHash32
|
||||
@@ -91,7 +95,11 @@ public final class TextLineBody implements BodyRecord, BodyHasLine, BodyHasTarge
|
||||
if (st != (MsgSubType.TEXT_POST & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_EDIT_POST & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_REPOST & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_CHANNEL_META & 0xFFFF)) {
|
||||
&& st != (MsgSubType.TEXT_CHANNEL_META & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_ENTRYPOINT & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_EXERCISE & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_SERVICE & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_COURSE & 0xFFFF)) {
|
||||
throw new IllegalArgumentException("TextLineBody supports only POST/EDIT_POST/REPOST/CHANNEL_META, got subType=" + st);
|
||||
}
|
||||
|
||||
@@ -159,12 +167,16 @@ public final class TextLineBody implements BodyRecord, BodyHasLine, BodyHasTarge
|
||||
if (st != (MsgSubType.TEXT_POST & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_EDIT_POST & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_REPOST & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_CHANNEL_META & 0xFFFF)) {
|
||||
&& st != (MsgSubType.TEXT_CHANNEL_META & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_ENTRYPOINT & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_EXERCISE & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_SERVICE & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_COURSE & 0xFFFF)) {
|
||||
throw new IllegalArgumentException("TextLineBody supports only POST/EDIT_POST/REPOST/CHANNEL_META");
|
||||
}
|
||||
|
||||
if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
|
||||
if (st == (MsgSubType.TEXT_POST & 0xFFFF) && message.isBlank()) {
|
||||
if (requiresNonBlankMessage(st) && message.isBlank()) {
|
||||
throw new IllegalArgumentException("message is blank");
|
||||
}
|
||||
|
||||
@@ -211,7 +223,11 @@ public final class TextLineBody implements BodyRecord, BodyHasLine, BodyHasTarge
|
||||
if (st != (MsgSubType.TEXT_POST & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_EDIT_POST & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_REPOST & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_CHANNEL_META & 0xFFFF))
|
||||
&& st != (MsgSubType.TEXT_CHANNEL_META & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_ENTRYPOINT & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_EXERCISE & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_SERVICE & 0xFFFF)
|
||||
&& st != (MsgSubType.TEXT_COURSE & 0xFFFF))
|
||||
throw new IllegalArgumentException("Bad TextLineBody subType: " + st);
|
||||
|
||||
if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
|
||||
@@ -238,8 +254,10 @@ public final class TextLineBody implements BodyRecord, BodyHasLine, BodyHasTarge
|
||||
} else {
|
||||
if (st == (MsgSubType.TEXT_CHANNEL_META & 0xFFFF)) {
|
||||
if (message == null) throw new IllegalArgumentException("CHANNEL_META message is null");
|
||||
} else if (message == null || message.isBlank()) {
|
||||
} else if (requiresNonBlankMessage(st) && (message == null || message.isBlank())) {
|
||||
throw new IllegalArgumentException("Text message is blank");
|
||||
} else if (message == null) {
|
||||
throw new IllegalArgumentException("Text message is null");
|
||||
}
|
||||
if (toBlockchainName != null || toBlockGlobalNumber != null || toBlockHash32 != null)
|
||||
throw new IllegalArgumentException("POST/CHANNEL_META must not contain target fields");
|
||||
@@ -254,12 +272,17 @@ public final class TextLineBody implements BodyRecord, BodyHasLine, BodyHasTarge
|
||||
if (msgUtf8.length > 65535) throw new IllegalArgumentException("Text too long (>65535 bytes)");
|
||||
|
||||
int st = subType & 0xFFFF;
|
||||
if (st == (MsgSubType.TEXT_POST & 0xFFFF) && msgUtf8.length == 0) {
|
||||
if (requiresNonBlankMessage(st) && msgUtf8.length == 0) {
|
||||
throw new IllegalArgumentException("Text payload is empty");
|
||||
}
|
||||
|
||||
int cap;
|
||||
if (st == (MsgSubType.TEXT_POST & 0xFFFF) || st == (MsgSubType.TEXT_CHANNEL_META & 0xFFFF)) {
|
||||
if (st == (MsgSubType.TEXT_POST & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_CHANNEL_META & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_ENTRYPOINT & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_EXERCISE & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_SERVICE & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_COURSE & 0xFFFF)) {
|
||||
cap = (4 + 4 + 32 + 4) + 2 + msgUtf8.length;
|
||||
} else if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
|
||||
// EDIT_POST
|
||||
@@ -318,6 +341,15 @@ public final class TextLineBody implements BodyRecord, BodyHasLine, BodyHasTarge
|
||||
return (subType & 0xFFFF) == (MsgSubType.TEXT_EDIT_POST & 0xFFFF);
|
||||
}
|
||||
|
||||
private static boolean requiresNonBlankMessage(int st) {
|
||||
return st == (MsgSubType.TEXT_POST & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_REPOST & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_ENTRYPOINT & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_EXERCISE & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_SERVICE & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_COURSE & 0xFFFF);
|
||||
}
|
||||
|
||||
private static String readStrictUtf8Len16(ByteBuffer bb, String fieldName, boolean allowEmpty) {
|
||||
int len = Short.toUnsignedInt(bb.getShort());
|
||||
if (len == 0) {
|
||||
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* TextRatingBody — type=1, ver=1.
|
||||
*
|
||||
* subType:
|
||||
* - RATING (30)
|
||||
*
|
||||
* Формат bodyBytes (BigEndian):
|
||||
* [1] toBlockchainNameLen (uint8)
|
||||
* [N] toBlockchainName UTF-8
|
||||
* [4] toBlockGlobalNumber
|
||||
* [32] toBlockHash32
|
||||
* [2] textLenBytes (uint16)
|
||||
* [M] text UTF-8
|
||||
*/
|
||||
public final class TextRatingBody 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;
|
||||
public final short version;
|
||||
public final String toBlockchainName;
|
||||
public final int toBlockGlobalNumber;
|
||||
public final byte[] toBlockHash32;
|
||||
public final String message;
|
||||
|
||||
public TextRatingBody(short subType, short version, byte[] bodyBytes) {
|
||||
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||||
this.subType = subType;
|
||||
this.version = version;
|
||||
|
||||
if ((version & 0xFFFF) != (VER & 0xFFFF)) {
|
||||
throw new IllegalArgumentException("TextRatingBody version must be 1, got=" + (version & 0xFFFF));
|
||||
}
|
||||
if ((subType & 0xFFFF) != (MsgSubType.TEXT_RATING & 0xFFFF)) {
|
||||
throw new IllegalArgumentException("TextRatingBody supports only TEXT_RATING");
|
||||
}
|
||||
|
||||
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
||||
ensureMin(bb, 1 + 1 + 4 + 32 + 2, "RATING too short");
|
||||
|
||||
int nameLen = Byte.toUnsignedInt(bb.get());
|
||||
if (nameLen <= 0) throw new IllegalArgumentException("RATING toBlockchainNameLen is 0");
|
||||
ensureMin(bb, nameLen + 4 + 32 + 2, "RATING 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);
|
||||
this.message = readStrictUtf8Len16(bb, "TextRatingBody text");
|
||||
|
||||
ensureNoTail(bb, "TextRatingBody");
|
||||
}
|
||||
|
||||
public TextRatingBody(String toBlockchainName, int toBlockGlobalNumber, byte[] toBlockHash32, String message) {
|
||||
Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
|
||||
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
||||
Objects.requireNonNull(message, "message == null");
|
||||
if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
|
||||
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||
if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||
if (message.isBlank()) throw new IllegalArgumentException("message is blank");
|
||||
|
||||
this.subType = MsgSubType.TEXT_RATING;
|
||||
this.version = VER;
|
||||
this.toBlockchainName = toBlockchainName;
|
||||
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
||||
this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextRatingBody check() {
|
||||
if ((subType & 0xFFFF) != (MsgSubType.TEXT_RATING & 0xFFFF)) {
|
||||
throw new IllegalArgumentException("Bad TextRatingBody subType: " + (subType & 0xFFFF));
|
||||
}
|
||||
if (toBlockchainName == null || toBlockchainName.isBlank()) {
|
||||
throw new IllegalArgumentException("RATING toBlockchainName is blank");
|
||||
}
|
||||
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||
if (toBlockHash32 == null || toBlockHash32.length != 32) {
|
||||
throw new IllegalArgumentException("toBlockHash32 invalid");
|
||||
}
|
||||
if (message == null || message.isBlank()) throw new IllegalArgumentException("message is blank");
|
||||
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)");
|
||||
|
||||
byte[] nameUtf8 = toBlockchainName.getBytes(StandardCharsets.UTF_8);
|
||||
if (nameUtf8.length == 0 || nameUtf8.length > 255) {
|
||||
throw new IllegalArgumentException("RATING toBlockchainName utf8 len must be 1..255");
|
||||
}
|
||||
|
||||
ByteBuffer bb = ByteBuffer.allocate(1 + nameUtf8.length + 4 + 32 + 2 + msgUtf8.length)
|
||||
.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();
|
||||
}
|
||||
|
||||
@Override public String toBchName() { return toBlockchainName; }
|
||||
@Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
|
||||
@Override public byte[] toBlockHashBytes() { return toBlockHash32; }
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -32,8 +32,13 @@ public final class DatabaseInitializer {
|
||||
public static final short TEXT_EDIT_POST = 11;
|
||||
public static final short TEXT_REPLY = 20;
|
||||
public static final short TEXT_EDIT_REPLY = 21;
|
||||
public static final short TEXT_REPOST = 30;
|
||||
public static final short TEXT_CHANNEL_META = 70;
|
||||
public static final short TEXT_RATING = 30;
|
||||
public static final short TEXT_REPOST = 50;
|
||||
public static final short TEXT_CHANNEL_META = 90;
|
||||
public static final short TEXT_ENTRYPOINT = 100;
|
||||
public static final short TEXT_EXERCISE = 110;
|
||||
public static final short TEXT_SERVICE = 120;
|
||||
public static final short TEXT_COURSE = 130;
|
||||
|
||||
public static final short REACTION_LIKE = 1;
|
||||
public static final short REACTION_UNLIKE = 2;
|
||||
|
||||
@@ -30,11 +30,23 @@ public final class MsgSubType {
|
||||
/** EDIT_REPLY — редактирование исходного ответа. */
|
||||
public static final short TEXT_EDIT_REPLY = 21;
|
||||
|
||||
/** REPOST — репост сообщения в линии канала (с комментарием и target на оригинал). */
|
||||
public static final short TEXT_REPOST = 30;
|
||||
/** RATING — target-based отзыв на конкретный блок. */
|
||||
public static final short TEXT_RATING = 30;
|
||||
|
||||
/** REPOST — отложенная будущая заготовка репоста сообщения в линии канала. */
|
||||
public static final short TEXT_REPOST = 50;
|
||||
|
||||
/** CHANNEL_META — скрытый технический снимок профиля канала. */
|
||||
public static final short TEXT_CHANNEL_META = 70;
|
||||
public static final short TEXT_CHANNEL_META = 90;
|
||||
|
||||
/** ENTRYPOINT — входная страница канала (line-based). */
|
||||
public static final short TEXT_ENTRYPOINT = 100;
|
||||
/** EXERCISE — упражнение/комплекс (line-based). */
|
||||
public static final short TEXT_EXERCISE = 110;
|
||||
/** SERVICE — услуга/процедура (line-based). */
|
||||
public static final short TEXT_SERVICE = 120;
|
||||
/** COURSE — курс (line-based). */
|
||||
public static final short TEXT_COURSE = 130;
|
||||
|
||||
/* ===================== REACTION (msg_type=2) ===================== */
|
||||
|
||||
@@ -123,6 +135,18 @@ public final class MsgSubType {
|
||||
/** Параметр профиля key/value (обе строки). */
|
||||
public static final short USER_PARAM_TEXT_TEXT = 1;
|
||||
|
||||
/* ===================== STATUS_ACTION (msg_type=5) ===================== */
|
||||
|
||||
public static final short STATUS_DONE_ONCE = 10;
|
||||
public static final short STATUS_LEARNED = 20;
|
||||
public static final short STATUS_SERVICE_PASSED = 30;
|
||||
public static final short STATUS_CONFIRMED = 100;
|
||||
public static final short STATUS_INTERESTED = 110;
|
||||
public static final short STATUS_STARTED = 120;
|
||||
public static final short STATUS_IN_STUDY = 130;
|
||||
public static final short STATUS_ABANDONED = 140;
|
||||
public static final short STATUS_COMPLETED = 150;
|
||||
|
||||
/* ===================== РЕЗЕРВ НА БУДУЩЕЕ ===================== */
|
||||
// Если позже захочешь BLOCK/UNBLOCK — лучше добавить новые значения,
|
||||
// не трогая уже занятые коды.
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.util.List;
|
||||
* Возвращает по каждой активной подписке (FOLLOW) + "сам на себя":
|
||||
* - login цели (channelLogin)
|
||||
* - blockchainName цели (channelBchName)
|
||||
* - count публикаций (TEXT_POST)
|
||||
* - count публикаций (видимые line-based TEXT-сообщения канала)
|
||||
* - last publication: bytes оригинального блока (для timestamp)
|
||||
* - last publication: bytes актуального блока (edit или orig) — для текста превью
|
||||
*
|
||||
@@ -92,7 +92,7 @@ public final class SubscriptionsDAO {
|
||||
|
||||
/**
|
||||
* Получить список подписок (активные FOLLOW) + "сам на себя" и по каждой:
|
||||
* - count публикаций (TEXT_POST)
|
||||
* - count публикаций (видимые line-based TEXT-сообщения канала)
|
||||
* - последнюю публикацию (orig bytes) + её edit (если есть)
|
||||
*
|
||||
* Поведение при 0 публикаций:
|
||||
@@ -131,7 +131,7 @@ public final class SubscriptionsDAO {
|
||||
ON s.channel_login = b.login
|
||||
AND s.channel_bch_name = b.bch_name
|
||||
WHERE b.msg_type = ?
|
||||
AND b.msg_sub_type IN (?, ?)
|
||||
AND b.msg_sub_type IN (?, ?, ?, ?, ?, ?)
|
||||
GROUP BY b.login, b.bch_name
|
||||
),
|
||||
last_pub AS (
|
||||
@@ -144,7 +144,7 @@ public final class SubscriptionsDAO {
|
||||
ON s.channel_login = b.login
|
||||
AND s.channel_bch_name = b.bch_name
|
||||
WHERE b.msg_type = ?
|
||||
AND b.msg_sub_type IN (?, ?)
|
||||
AND b.msg_sub_type IN (?, ?, ?, ?, ?, ?)
|
||||
GROUP BY b.login, b.bch_name
|
||||
),
|
||||
last_pub_block AS (
|
||||
@@ -209,11 +209,19 @@ public final class SubscriptionsDAO {
|
||||
ps.setInt(i++, MSG_TYPE_TEXT);
|
||||
ps.setInt(i++, (int) MsgSubType.TEXT_POST);
|
||||
ps.setInt(i++, (int) MsgSubType.TEXT_REPOST);
|
||||
ps.setInt(i++, (int) MsgSubType.TEXT_ENTRYPOINT);
|
||||
ps.setInt(i++, (int) MsgSubType.TEXT_EXERCISE);
|
||||
ps.setInt(i++, (int) MsgSubType.TEXT_SERVICE);
|
||||
ps.setInt(i++, (int) MsgSubType.TEXT_COURSE);
|
||||
|
||||
// last_pub
|
||||
ps.setInt(i++, MSG_TYPE_TEXT);
|
||||
ps.setInt(i++, (int) MsgSubType.TEXT_POST);
|
||||
ps.setInt(i++, (int) MsgSubType.TEXT_REPOST);
|
||||
ps.setInt(i++, (int) MsgSubType.TEXT_ENTRYPOINT);
|
||||
ps.setInt(i++, (int) MsgSubType.TEXT_EXERCISE);
|
||||
ps.setInt(i++, (int) MsgSubType.TEXT_SERVICE);
|
||||
ps.setInt(i++, (int) MsgSubType.TEXT_COURSE);
|
||||
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
|
||||
@@ -53,12 +53,16 @@ BEGIN
|
||||
s.updated_at_ms,
|
||||
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||
FROM solana_user_pda_current u
|
||||
CROSS JOIN LATERAL jsonb_array_elements_text(
|
||||
CASE
|
||||
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||
ELSE u.access_servers_json::jsonb
|
||||
END
|
||||
) AS access_server(login_value)
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT login_value
|
||||
FROM jsonb_array_elements_text(
|
||||
CASE
|
||||
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||
ELSE u.access_servers_json::jsonb
|
||||
END
|
||||
) WITH ORDINALITY AS access_server(login_value, ord)
|
||||
WHERE ord <= 2
|
||||
) AS access_server
|
||||
JOIN solana_user_pda_current s
|
||||
ON LOWER(s.login) = LOWER(btrim(access_server.login_value))
|
||||
AND s.is_server = TRUE
|
||||
@@ -92,12 +96,16 @@ BEGIN
|
||||
FOR affected_user IN
|
||||
SELECT u.login
|
||||
FROM solana_user_pda_current u
|
||||
CROSS JOIN LATERAL jsonb_array_elements_text(
|
||||
CASE
|
||||
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||
ELSE u.access_servers_json::jsonb
|
||||
END
|
||||
) AS access_server(login_value)
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT login_value
|
||||
FROM jsonb_array_elements_text(
|
||||
CASE
|
||||
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||
ELSE u.access_servers_json::jsonb
|
||||
END
|
||||
) WITH ORDINALITY AS access_server(login_value, ord)
|
||||
WHERE ord <= 2
|
||||
) AS access_server
|
||||
WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login)
|
||||
LOOP
|
||||
PERFORM shine_refresh_user_access_servers_for_user(affected_user.login);
|
||||
|
||||
@@ -162,12 +162,16 @@ BEGIN
|
||||
s.updated_at_ms,
|
||||
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||
FROM solana_user_pda_current u
|
||||
CROSS JOIN LATERAL jsonb_array_elements_text(
|
||||
CASE
|
||||
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||
ELSE u.access_servers_json::jsonb
|
||||
END
|
||||
) AS access_server(login_value)
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT login_value
|
||||
FROM jsonb_array_elements_text(
|
||||
CASE
|
||||
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||
ELSE u.access_servers_json::jsonb
|
||||
END
|
||||
) WITH ORDINALITY AS access_server(login_value, ord)
|
||||
WHERE ord <= 2
|
||||
) AS access_server
|
||||
JOIN solana_user_pda_current s
|
||||
ON LOWER(s.login) = LOWER(btrim(access_server.login_value))
|
||||
AND s.is_server = TRUE
|
||||
@@ -201,12 +205,16 @@ BEGIN
|
||||
FOR affected_user IN
|
||||
SELECT u.login
|
||||
FROM solana_user_pda_current u
|
||||
CROSS JOIN LATERAL jsonb_array_elements_text(
|
||||
CASE
|
||||
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||
ELSE u.access_servers_json::jsonb
|
||||
END
|
||||
) AS access_server(login_value)
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT login_value
|
||||
FROM jsonb_array_elements_text(
|
||||
CASE
|
||||
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||
ELSE u.access_servers_json::jsonb
|
||||
END
|
||||
) WITH ORDINALITY AS access_server(login_value, ord)
|
||||
WHERE ord <= 2
|
||||
) AS access_server
|
||||
WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login)
|
||||
LOOP
|
||||
PERFORM shine_refresh_user_access_servers_for_user(affected_user.login);
|
||||
|
||||
+4
@@ -67,6 +67,7 @@ import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriend
|
||||
import server.logic.ws_protocol.JSON.handlers.channels.ChannelNamesStateBootstrapper;
|
||||
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetChannelMessages_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetMessageThread_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetPersonalDiary_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetGroupDialog_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetChannelsCounters_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.channels.Net_ListGroupChats200_Handler;
|
||||
@@ -75,6 +76,7 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelsC
|
||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelMessages_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetGroupDialog_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetMessageThread_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetPersonalDiary_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListGroupChats200_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListSubscriptionsFeed_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.connections.Net_GetUserConnectionsGraph_Handler;
|
||||
@@ -184,6 +186,7 @@ public final class JsonHandlerRegistry {
|
||||
Map.entry("GetFriendsLists", new Net_GetFriendsLists_Handler()),
|
||||
Map.entry("ListSubscriptionsFeed", new Net_ListSubscriptionsFeed_Handler()),
|
||||
Map.entry("GetChannelMessages", new Net_GetChannelMessages_Handler()),
|
||||
Map.entry("GetPersonalDiary", new Net_GetPersonalDiary_Handler()),
|
||||
Map.entry("GetMessageThread", new Net_GetMessageThread_Handler()),
|
||||
Map.entry("GetGroupDialog", new Net_GetGroupDialog_Handler()),
|
||||
Map.entry("ListGroupChats200", new Net_ListGroupChats200_Handler()),
|
||||
@@ -265,6 +268,7 @@ public final class JsonHandlerRegistry {
|
||||
Map.entry("GetFriendsLists", Net_GetFriendsLists_Request.class),
|
||||
Map.entry("ListSubscriptionsFeed", Net_ListSubscriptionsFeed_Request.class),
|
||||
Map.entry("GetChannelMessages", Net_GetChannelMessages_Request.class),
|
||||
Map.entry("GetPersonalDiary", Net_GetPersonalDiary_Request.class),
|
||||
Map.entry("GetMessageThread", Net_GetMessageThread_Request.class),
|
||||
Map.entry("GetGroupDialog", Net_GetGroupDialog_Request.class),
|
||||
Map.entry("ListGroupChats200", Net_ListGroupChats200_Request.class),
|
||||
|
||||
+9
-1
@@ -27,6 +27,7 @@ public final class SolanaUserPdaImportService {
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
private static final HttpClient HTTP = HttpClient.newHttpClient();
|
||||
private static final String MAGIC = "SHiNE";
|
||||
private static final int MAX_EFFECTIVE_ACCESS_SERVERS = 2;
|
||||
|
||||
private SolanaUserPdaImportService() {}
|
||||
|
||||
@@ -87,6 +88,7 @@ public final class SolanaUserPdaImportService {
|
||||
String serverAddress = safe(serverProfile.serverAddress());
|
||||
if (serverAddress.isBlank()) continue;
|
||||
routes.putIfAbsent(normalized, new ParsedServerRoute(normalized, serverAddress));
|
||||
if (routes.size() >= MAX_EFFECTIVE_ACCESS_SERVERS) break;
|
||||
}
|
||||
return new ArrayList<>(routes.values());
|
||||
}
|
||||
@@ -234,7 +236,13 @@ public final class SolanaUserPdaImportService {
|
||||
int n = u8(raw, c++);
|
||||
String accessServerLogin = new String(raw, c, n, StandardCharsets.UTF_8);
|
||||
c += n;
|
||||
accessServers.add(normalizeLogin(accessServerLogin));
|
||||
String normalizedAccessServerLogin = normalizeLogin(accessServerLogin);
|
||||
if (normalizedAccessServerLogin == null || accessServers.contains(normalizedAccessServerLogin)) {
|
||||
continue;
|
||||
}
|
||||
if (accessServers.size() < MAX_EFFECTIVE_ACCESS_SERVERS) {
|
||||
accessServers.add(normalizedAccessServerLogin);
|
||||
}
|
||||
}
|
||||
} else if (blockType == 50) {
|
||||
int sessionsMode = u8(raw, c++);
|
||||
|
||||
+88
@@ -6,6 +6,7 @@ import blockchain.MsgSubType;
|
||||
import blockchain.body.BodyHasLine;
|
||||
import blockchain.body.BodyHasTarget;
|
||||
import blockchain.body.CreateChannelBody;
|
||||
import blockchain.body.StatusActionBody;
|
||||
import blockchain.body.TextLineBody;
|
||||
import blockchain.body.UserParamBody;
|
||||
import org.slf4j.Logger;
|
||||
@@ -167,6 +168,9 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
case "db_error_prev_line_check" -> "Ошибка БД при проверке prevLine";
|
||||
case "channel_name_already_exists" -> "Такое название канала уже занято";
|
||||
case "repost_disabled" -> "Репосты временно отключены до будущей реализации";
|
||||
case "entrypoint_edit_forbidden" -> "TEXT_ENTRYPOINT нельзя редактировать через TEXT_EDIT_POST";
|
||||
case "status_confirmed_target_must_be_status_action" -> "STATUS_CONFIRMED должен ссылаться на STATUS_ACTION";
|
||||
case "status_action_target_not_allowed" -> "Этот STATUS_ACTION нельзя ставить на выбранный тип материала";
|
||||
case "internal_error" -> "Внутренняя ошибка сервера при записи блока";
|
||||
case "chain_resync_in_progress" -> "Цепочка сейчас пересинхронизируется";
|
||||
default -> "Ошибка: " + code;
|
||||
@@ -388,6 +392,33 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
channelMetaUpdateEntry.setMetaUpdatedAtMs(block.timestamp * 1000L);
|
||||
}
|
||||
|
||||
if ((block.type & 0xFFFF) == 1
|
||||
&& (block.subType & 0xFFFF) == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
|
||||
try {
|
||||
String editError = validateEditPostTarget(blockchainName, block);
|
||||
if (editError != null) {
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, editError, serverLastNum, serverLastHashHex);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("AddBlock: edit_post_target_check_failed (login={}, blockchainName={}, blockNumber={})",
|
||||
login, blockchainName, block.blockNumber, e);
|
||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
}
|
||||
|
||||
if ((block.type & 0xFFFF) == 5) {
|
||||
try {
|
||||
String statusError = validateStatusActionTarget(block);
|
||||
if (statusError != null) {
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, statusError, serverLastNum, serverLastHashHex);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("AddBlock: status_action_target_check_failed (login={}, blockchainName={}, blockNumber={})",
|
||||
login, blockchainName, block.blockNumber, e);
|
||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
}
|
||||
|
||||
// 4.2) запрет дырок: blockNumber строго last+1
|
||||
int expectedBlockNumber = serverLastNum + 1;
|
||||
if (block.blockNumber != expectedBlockNumber) {
|
||||
@@ -605,6 +636,63 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
String slug;
|
||||
}
|
||||
|
||||
private String validateEditPostTarget(String ownerBch, BchBlockEntry block) throws Exception {
|
||||
if (!(block.body instanceof TextLineBody editBody)) return null;
|
||||
Integer targetBlockNumber = editBody.toBlockGlobalNumber();
|
||||
byte[] targetHash = editBody.toBlockHashBytes();
|
||||
if (targetBlockNumber == null || targetHash == null || targetHash.length != 32) return "bad_block_body";
|
||||
|
||||
BlockEntry target = blocksDAO.getByNumber(ownerBch, targetBlockNumber);
|
||||
if (target == null || target.getBlockHash() == null || !Arrays.equals(target.getBlockHash(), targetHash)) {
|
||||
return null;
|
||||
}
|
||||
if (target.getMsgType() != 1) return null;
|
||||
if (target.getMsgSubType() == (MsgSubType.TEXT_ENTRYPOINT & 0xFFFF)) {
|
||||
return "entrypoint_edit_forbidden";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String validateStatusActionTarget(BchBlockEntry block) throws Exception {
|
||||
if (!(block.body instanceof StatusActionBody statusBody)) return "bad_block_body";
|
||||
String targetBch = statusBody.toBchName();
|
||||
Integer targetBlockNumber = statusBody.toBlockGlobalNumber();
|
||||
byte[] targetHash = statusBody.toBlockHashBytes();
|
||||
if (targetBch == null || targetBch.isBlank() || targetBlockNumber == null || targetHash == null || targetHash.length != 32) {
|
||||
return "bad_block_body";
|
||||
}
|
||||
|
||||
BlockEntry target = blocksDAO.getByNumber(targetBch, targetBlockNumber);
|
||||
if (target == null || target.getBlockHash() == null || !Arrays.equals(target.getBlockHash(), targetHash)) {
|
||||
return null;
|
||||
}
|
||||
int statusSubType = block.subType & 0xFFFF;
|
||||
if (statusSubType == (MsgSubType.STATUS_CONFIRMED & 0xFFFF)) {
|
||||
if (target.getMsgType() != 5) {
|
||||
return "status_confirmed_target_must_be_status_action";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (target.getMsgType() != 1) return "status_action_target_not_allowed";
|
||||
|
||||
int targetSubType = target.getMsgSubType();
|
||||
if (statusSubType == (MsgSubType.STATUS_DONE_ONCE & 0xFFFF)
|
||||
|| statusSubType == (MsgSubType.STATUS_LEARNED & 0xFFFF)) {
|
||||
return targetSubType == (MsgSubType.TEXT_EXERCISE & 0xFFFF) ? null : "status_action_target_not_allowed";
|
||||
}
|
||||
if (statusSubType == (MsgSubType.STATUS_SERVICE_PASSED & 0xFFFF)) {
|
||||
return targetSubType == (MsgSubType.TEXT_SERVICE & 0xFFFF) ? null : "status_action_target_not_allowed";
|
||||
}
|
||||
if (statusSubType == (MsgSubType.STATUS_INTERESTED & 0xFFFF)
|
||||
|| statusSubType == (MsgSubType.STATUS_STARTED & 0xFFFF)
|
||||
|| statusSubType == (MsgSubType.STATUS_IN_STUDY & 0xFFFF)
|
||||
|| statusSubType == (MsgSubType.STATUS_ABANDONED & 0xFFFF)
|
||||
|| statusSubType == (MsgSubType.STATUS_COMPLETED & 0xFFFF)) {
|
||||
return targetSubType == (MsgSubType.TEXT_COURSE & 0xFFFF) ? null : "status_action_target_not_allowed";
|
||||
}
|
||||
return "status_action_target_not_allowed";
|
||||
}
|
||||
|
||||
private ExistingChannelState loadExistingChannelState(String ownerBch, int rootBlockNumber) throws Exception {
|
||||
try (Connection c = shine.db.DbController.getInstance().getConnection();
|
||||
PreparedStatement ps = c.prepareStatement("""
|
||||
|
||||
+69
-9
@@ -22,6 +22,7 @@ final class ChannelsReadSupport {
|
||||
static final int MSG_TYPE_TEXT = 1;
|
||||
static final int MSG_TYPE_REACTION = 2;
|
||||
static final int MSG_TYPE_TECH = 0;
|
||||
static final int MSG_TYPE_STATUS_ACTION = 5;
|
||||
static final String STORIES_CHANNEL_NAME = "stories";
|
||||
static final String COMMAND_ADD = "add";
|
||||
static final String COMMAND_REMOVE = "remove";
|
||||
@@ -126,13 +127,17 @@ final class ChannelsReadSupport {
|
||||
}
|
||||
|
||||
static int countPosts(Connection c, String ownerBch, int lineCode) throws SQLException {
|
||||
String sql = "SELECT COUNT(*) AS cnt FROM blocks WHERE bch_name=? AND msg_type=? AND msg_sub_type IN (?, ?) AND line_code=?";
|
||||
String sql = "SELECT COUNT(*) AS cnt FROM blocks WHERE bch_name=? AND msg_type=? AND msg_sub_type IN (?, ?, ?, ?, ?, ?) AND line_code=?";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, ownerBch);
|
||||
ps.setInt(2, MSG_TYPE_TEXT);
|
||||
ps.setInt(3, MsgSubType.TEXT_POST);
|
||||
ps.setInt(4, MsgSubType.TEXT_REPOST);
|
||||
ps.setInt(5, lineCode);
|
||||
ps.setInt(5, MsgSubType.TEXT_ENTRYPOINT);
|
||||
ps.setInt(6, MsgSubType.TEXT_EXERCISE);
|
||||
ps.setInt(7, MsgSubType.TEXT_SERVICE);
|
||||
ps.setInt(8, MsgSubType.TEXT_COURSE);
|
||||
ps.setInt(9, lineCode);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
return rs.next() ? rs.getInt("cnt") : 0;
|
||||
}
|
||||
@@ -143,7 +148,7 @@ final class ChannelsReadSupport {
|
||||
String sql = """
|
||||
SELECT login,bch_name,block_number,block_hash,block_bytes,this_line_number
|
||||
FROM blocks
|
||||
WHERE bch_name=? AND msg_type=? AND msg_sub_type IN (?, ?) AND line_code=?
|
||||
WHERE bch_name=? AND msg_type=? AND msg_sub_type IN (?, ?, ?, ?, ?, ?) AND line_code=?
|
||||
ORDER BY block_number DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
@@ -152,7 +157,11 @@ final class ChannelsReadSupport {
|
||||
ps.setInt(2, MSG_TYPE_TEXT);
|
||||
ps.setInt(3, MsgSubType.TEXT_POST);
|
||||
ps.setInt(4, MsgSubType.TEXT_REPOST);
|
||||
ps.setInt(5, lineCode);
|
||||
ps.setInt(5, MsgSubType.TEXT_ENTRYPOINT);
|
||||
ps.setInt(6, MsgSubType.TEXT_EXERCISE);
|
||||
ps.setInt(7, MsgSubType.TEXT_SERVICE);
|
||||
ps.setInt(8, MsgSubType.TEXT_COURSE);
|
||||
ps.setInt(9, lineCode);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (!rs.next()) return null;
|
||||
PostBlock pb = new PostBlock();
|
||||
@@ -204,6 +213,10 @@ final class ChannelsReadSupport {
|
||||
ti.text = tlb.message;
|
||||
} else if (e.body instanceof TextReplyBody trb) {
|
||||
ti.text = trb.message;
|
||||
} else if (e.body instanceof blockchain.body.TextRatingBody trb) {
|
||||
ti.text = trb.message;
|
||||
} else if (e.body instanceof blockchain.body.StatusActionBody sab) {
|
||||
ti.text = sab.message;
|
||||
} else if (e.body instanceof TextBody tb) {
|
||||
ti.text = tb.message;
|
||||
}
|
||||
@@ -218,7 +231,7 @@ final class ChannelsReadSupport {
|
||||
String sql = """
|
||||
SELECT login,bch_name,block_number,block_hash,block_bytes,to_bch_name,to_block_number,to_block_hash,msg_sub_type,this_line_number
|
||||
FROM blocks
|
||||
WHERE bch_name=? AND msg_type=? AND msg_sub_type IN (?, ?) AND line_code=?
|
||||
WHERE bch_name=? AND msg_type=? AND msg_sub_type IN (?, ?, ?, ?, ?, ?) AND line_code=?
|
||||
ORDER BY block_number
|
||||
""" + order + " LIMIT ?";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
@@ -226,8 +239,12 @@ final class ChannelsReadSupport {
|
||||
ps.setInt(2, MSG_TYPE_TEXT);
|
||||
ps.setInt(3, MsgSubType.TEXT_POST);
|
||||
ps.setInt(4, MsgSubType.TEXT_REPOST);
|
||||
ps.setInt(5, lineCode);
|
||||
ps.setInt(6, limit);
|
||||
ps.setInt(5, MsgSubType.TEXT_ENTRYPOINT);
|
||||
ps.setInt(6, MsgSubType.TEXT_EXERCISE);
|
||||
ps.setInt(7, MsgSubType.TEXT_SERVICE);
|
||||
ps.setInt(8, MsgSubType.TEXT_COURSE);
|
||||
ps.setInt(9, lineCode);
|
||||
ps.setInt(10, limit);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
List<PostBlock> out = new ArrayList<>();
|
||||
while (rs.next()) {
|
||||
@@ -292,15 +309,42 @@ final class ChannelsReadSupport {
|
||||
|
||||
static int[] loadStats(Connection c, String bch, int blockNumber, byte[] blockHash) throws SQLException {
|
||||
String sql = "SELECT likes_count,replies_count FROM message_stats WHERE to_bch_name=? AND to_block_number=? AND to_block_hash=? LIMIT 1";
|
||||
int likesCount = 0;
|
||||
int repliesCount = 0;
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, bch);
|
||||
ps.setInt(2, blockNumber);
|
||||
ps.setBytes(3, blockHash);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (!rs.next()) return new int[] {0, 0};
|
||||
return new int[] {rs.getInt("likes_count"), rs.getInt("replies_count")};
|
||||
if (rs.next()) {
|
||||
likesCount = rs.getInt("likes_count");
|
||||
repliesCount = rs.getInt("replies_count");
|
||||
}
|
||||
}
|
||||
}
|
||||
String ratingsSql = """
|
||||
SELECT COUNT(*)
|
||||
FROM blocks
|
||||
WHERE msg_type = ?
|
||||
AND msg_sub_type = ?
|
||||
AND to_bch_name = ?
|
||||
AND to_block_number = ?
|
||||
AND to_block_hash = ?
|
||||
""";
|
||||
int ratingsCount = 0;
|
||||
try (PreparedStatement ps = c.prepareStatement(ratingsSql)) {
|
||||
ps.setInt(1, MSG_TYPE_TEXT);
|
||||
ps.setInt(2, MsgSubType.TEXT_RATING);
|
||||
ps.setString(3, bch);
|
||||
ps.setInt(4, blockNumber);
|
||||
ps.setBytes(5, blockHash);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
ratingsCount = rs.getInt(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new int[] {likesCount, repliesCount, ratingsCount};
|
||||
}
|
||||
|
||||
static String detectChannelDescription(Connection c, String ownerBch, int rootNumber) throws SQLException {
|
||||
@@ -589,6 +633,22 @@ final class ChannelsReadSupport {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
static boolean isChannelFeedSubType(int subType) {
|
||||
return subType == MsgSubType.TEXT_POST
|
||||
|| subType == MsgSubType.TEXT_REPOST
|
||||
|| subType == MsgSubType.TEXT_ENTRYPOINT
|
||||
|| subType == MsgSubType.TEXT_EXERCISE
|
||||
|| subType == MsgSubType.TEXT_SERVICE
|
||||
|| subType == MsgSubType.TEXT_COURSE;
|
||||
}
|
||||
|
||||
static boolean supportsEditPostVersions(int subType) {
|
||||
return subType == MsgSubType.TEXT_POST
|
||||
|| subType == MsgSubType.TEXT_EXERCISE
|
||||
|| subType == MsgSubType.TEXT_SERVICE
|
||||
|| subType == MsgSubType.TEXT_COURSE;
|
||||
}
|
||||
|
||||
static final class PostBlock {
|
||||
String login;
|
||||
String bchName;
|
||||
|
||||
+2
-1
@@ -143,7 +143,7 @@ public class Net_GetChannelMessages_Handler implements JsonMessageHandler {
|
||||
v1.setCreatedAtMs(postText.createdAtMs);
|
||||
versionsOut.add(v1);
|
||||
|
||||
if (post.msgSubType == MsgSubType.TEXT_POST) {
|
||||
if (ChannelsReadSupport.supportsEditPostVersions(post.msgSubType)) {
|
||||
List<ChannelsReadSupport.PostBlock> edits = ChannelsReadSupport.versionsForPost(c, post.bchName, post.blockNumber, post.blockHash);
|
||||
for (ChannelsReadSupport.PostBlock edit : edits) {
|
||||
ChannelsReadSupport.TextInfo editText = ChannelsReadSupport.parseTextAndTime(edit.blockBytes);
|
||||
@@ -167,6 +167,7 @@ public class Net_GetChannelMessages_Handler implements JsonMessageHandler {
|
||||
int[] stats = ChannelsReadSupport.loadStats(c, ownerBch, post.blockNumber, post.blockHash);
|
||||
item.setLikesCount(stats[0]);
|
||||
item.setRepliesCount(stats[1]);
|
||||
item.setRatingsCount(stats[2]);
|
||||
item.setLikedByMe(ChannelsReadSupport.isLikedByLogin(c, viewerLogin, post.bchName, post.blockNumber, post.blockHash));
|
||||
|
||||
items.add(item);
|
||||
|
||||
+22
-11
@@ -17,6 +17,7 @@ import shine.db.DbController;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.Comparator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
@@ -89,7 +90,7 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
|
||||
|
||||
private List<Net_GetMessageThread_Response.MessageNodeTree> loadChildren(Connection c, PostRow parent, int depthDown, int childLimit, String viewerLogin) throws Exception {
|
||||
if (depthDown <= 0) return List.of();
|
||||
List<PostRow> replies = findReplies(c, parent.bchName, parent.blockNumber, parent.blockHash, childLimit);
|
||||
List<PostRow> replies = findRepliesAndRatings(c, parent.bchName, parent.blockNumber, parent.blockHash, childLimit);
|
||||
List<Net_GetMessageThread_Response.MessageNodeTree> out = new ArrayList<>();
|
||||
for (PostRow row : replies) {
|
||||
Net_GetMessageThread_Response.MessageNodeTree t = new Net_GetMessageThread_Response.MessageNodeTree();
|
||||
@@ -100,24 +101,29 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
|
||||
return out;
|
||||
}
|
||||
|
||||
private List<PostRow> findReplies(Connection c, String toBchName, int toBlockNumber, byte[] toBlockHash, int limit) throws Exception {
|
||||
private List<PostRow> findRepliesAndRatings(Connection c, String toBchName, int toBlockNumber, byte[] toBlockHash, int limit) throws Exception {
|
||||
String sql = """
|
||||
SELECT login,bch_name,block_number,block_hash,block_bytes,to_bch_name,to_block_number,to_block_hash,line_code,msg_sub_type,this_line_number
|
||||
FROM blocks
|
||||
WHERE msg_type=1 AND msg_sub_type=?
|
||||
WHERE msg_type=1 AND msg_sub_type IN (?, ?)
|
||||
AND to_bch_name=? AND to_block_number=? AND to_block_hash=?
|
||||
ORDER BY block_number ASC
|
||||
LIMIT ?
|
||||
""";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setInt(1, MsgSubType.TEXT_REPLY);
|
||||
ps.setString(2, toBchName);
|
||||
ps.setInt(3, toBlockNumber);
|
||||
ps.setBytes(4, toBlockHash);
|
||||
ps.setInt(5, limit);
|
||||
ps.setInt(2, MsgSubType.TEXT_RATING);
|
||||
ps.setString(3, toBchName);
|
||||
ps.setInt(4, toBlockNumber);
|
||||
ps.setBytes(5, toBlockHash);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
List<PostRow> out = new ArrayList<>();
|
||||
while (rs.next()) out.add(mapRow(rs));
|
||||
out.sort(Comparator
|
||||
.comparingLong((PostRow row) -> ChannelsReadSupport.parseTextAndTime(row.blockBytes).createdAtMs)
|
||||
.thenComparing(row -> String.valueOf(row.bchName))
|
||||
.thenComparingInt(row -> row.blockNumber));
|
||||
if (out.size() > limit) {
|
||||
return new ArrayList<>(out.subList(0, limit));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -208,8 +214,12 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
|
||||
first.setCreatedAtMs(base.createdAtMs);
|
||||
versions.add(first);
|
||||
|
||||
if (row.msgSubType == MsgSubType.TEXT_REPLY || row.msgSubType == MsgSubType.TEXT_POST) {
|
||||
short editType = row.msgSubType == MsgSubType.TEXT_REPLY ? MsgSubType.TEXT_EDIT_REPLY : MsgSubType.TEXT_EDIT_POST;
|
||||
if (row.msgSubType == MsgSubType.TEXT_REPLY
|
||||
|| row.msgSubType == MsgSubType.TEXT_RATING
|
||||
|| ChannelsReadSupport.supportsEditPostVersions(row.msgSubType)) {
|
||||
short editType = (row.msgSubType == MsgSubType.TEXT_REPLY || row.msgSubType == MsgSubType.TEXT_RATING)
|
||||
? MsgSubType.TEXT_EDIT_REPLY
|
||||
: MsgSubType.TEXT_EDIT_POST;
|
||||
for (PostRow edit : findEdits(c, row.bchName, row.blockNumber, row.blockHash, editType)) {
|
||||
ChannelsReadSupport.TextInfo et = ChannelsReadSupport.parseTextAndTime(edit.blockBytes);
|
||||
Net_GetChannelMessages_Response.VersionItem v = new Net_GetChannelMessages_Response.VersionItem();
|
||||
@@ -233,6 +243,7 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
|
||||
int[] stats = ChannelsReadSupport.loadStats(c, row.bchName, row.blockNumber, row.blockHash);
|
||||
node.setLikesCount(stats[0]);
|
||||
node.setRepliesCount(stats[1]);
|
||||
node.setRatingsCount(stats[2]);
|
||||
node.setLikedByMe(ChannelsReadSupport.isLikedByLogin(c, viewerLogin, row.bchName, row.blockNumber, row.blockHash));
|
||||
if (row.lineCode != null && row.lineCode >= 0) {
|
||||
Net_GetMessageThread_Response.ChannelInfo ci = new Net_GetMessageThread_Response.ChannelInfo();
|
||||
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.channels;
|
||||
|
||||
import blockchain.BchBlockEntry;
|
||||
import blockchain.body.StatusActionBody;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelMessages_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetPersonalDiary_Request;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.DbController;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Net_GetPersonalDiary_Handler implements JsonMessageHandler {
|
||||
private static final Logger log = LoggerFactory.getLogger(Net_GetPersonalDiary_Handler.class);
|
||||
private static final String DIARY_CHANNEL_NAME = "diary";
|
||||
private static final String DIARY_DISPLAY_NAME = "Личный дневник";
|
||||
private static final String DIARY_DESCRIPTION = "История ваших действий по упражнениям, услугам и курсам";
|
||||
|
||||
@Override
|
||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||
Net_GetPersonalDiary_Request req = (Net_GetPersonalDiary_Request) baseRequest;
|
||||
String requestedLogin = String.valueOf(req.getLogin() == null ? "" : req.getLogin()).trim();
|
||||
if (requestedLogin.isBlank() && (ctx == null || ctx.getLogin() == null || ctx.getLogin().isBlank())) {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля: login");
|
||||
}
|
||||
|
||||
int limit = req.getLimit() == null ? 200 : req.getLimit();
|
||||
if (limit <= 0 || limit > 1000) {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "limit_too_large", "Некорректный limit");
|
||||
}
|
||||
boolean asc = req.getSort() == null || !"desc".equalsIgnoreCase(req.getSort());
|
||||
|
||||
try (Connection c = DbController.getInstance().getConnection()) {
|
||||
String viewerLogin = ctx != null ? String.valueOf(ctx.getLogin() == null ? "" : ctx.getLogin()).trim() : "";
|
||||
String canonicalLogin = !viewerLogin.isBlank()
|
||||
? viewerLogin
|
||||
: ChannelsReadSupport.canonicalLogin(c, requestedLogin);
|
||||
if (canonicalLogin == null || canonicalLogin.isBlank()) {
|
||||
return NetExceptionResponseFactory.error(req, 404, "user_not_found", "Пользователь не найден");
|
||||
}
|
||||
if (!viewerLogin.isBlank() && !viewerLogin.equalsIgnoreCase(canonicalLogin)) {
|
||||
return NetExceptionResponseFactory.error(req, 403, "forbidden", "Личный дневник доступен только владельцу");
|
||||
}
|
||||
|
||||
String ownerBch = loadPrimaryBlockchainName(c, canonicalLogin);
|
||||
if (ownerBch == null || ownerBch.isBlank()) {
|
||||
return NetExceptionResponseFactory.error(req, 404, "blockchain_not_found", "Не найден blockchain пользователя");
|
||||
}
|
||||
|
||||
Net_GetChannelMessages_Response resp = new Net_GetChannelMessages_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
resp.setStatus(WireCodes.Status.OK);
|
||||
|
||||
Net_GetChannelMessages_Response.Channel channel = new Net_GetChannelMessages_Response.Channel();
|
||||
channel.setOwnerLogin(canonicalLogin);
|
||||
channel.setOwnerBlockchainName(ownerBch);
|
||||
channel.setChannelName(DIARY_CHANNEL_NAME);
|
||||
channel.setDisplayName(DIARY_DISPLAY_NAME);
|
||||
channel.setChannelDescription(DIARY_DESCRIPTION);
|
||||
channel.setChannelTypeCode(900);
|
||||
channel.setChannelTypeVersion(1);
|
||||
Net_GetChannelMessages_Response.BlockRef rootRef = new Net_GetChannelMessages_Response.BlockRef();
|
||||
rootRef.setBlockNumber(0);
|
||||
rootRef.setBlockHash(ChannelsReadSupport.toHex(new byte[32]));
|
||||
channel.setChannelRoot(rootRef);
|
||||
resp.setChannel(channel);
|
||||
resp.setMetaEvents(new ArrayList<>());
|
||||
|
||||
List<Net_GetChannelMessages_Response.MessageItem> items = loadDiaryItems(c, canonicalLogin, limit, asc);
|
||||
resp.setMessages(items);
|
||||
return resp;
|
||||
} catch (Exception e) {
|
||||
log.error("GetPersonalDiary failed", e);
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "internal_error", "Внутренняя ошибка сервера");
|
||||
}
|
||||
}
|
||||
|
||||
private String loadPrimaryBlockchainName(Connection c, String canonicalLogin) throws Exception {
|
||||
try (PreparedStatement ps = c.prepareStatement("""
|
||||
SELECT blockchain_name
|
||||
FROM blockchain_state
|
||||
WHERE login = ?
|
||||
ORDER BY blockchain_name
|
||||
LIMIT 1
|
||||
""")) {
|
||||
ps.setString(1, canonicalLogin);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
return rs.next() ? rs.getString("blockchain_name") : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Net_GetChannelMessages_Response.MessageItem> loadDiaryItems(Connection c, String canonicalLogin, int limit, boolean asc) throws Exception {
|
||||
String order = asc ? "ASC" : "DESC";
|
||||
List<Net_GetChannelMessages_Response.MessageItem> out = new ArrayList<>();
|
||||
try (PreparedStatement ps = c.prepareStatement("""
|
||||
SELECT login, bch_name, block_number, block_hash, block_bytes, msg_sub_type
|
||||
FROM blocks
|
||||
WHERE login = ? AND msg_type = ?
|
||||
ORDER BY block_number
|
||||
""" + order + " LIMIT ?")) {
|
||||
ps.setString(1, canonicalLogin);
|
||||
ps.setInt(2, ChannelsReadSupport.MSG_TYPE_STATUS_ACTION);
|
||||
ps.setInt(3, limit);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
byte[] blockBytes = rs.getBytes("block_bytes");
|
||||
BchBlockEntry entry = new BchBlockEntry(blockBytes);
|
||||
if (!(entry.body instanceof StatusActionBody statusBody)) continue;
|
||||
|
||||
Net_GetChannelMessages_Response.MessageItem item = new Net_GetChannelMessages_Response.MessageItem();
|
||||
Net_GetChannelMessages_Response.BlockRef ref = new Net_GetChannelMessages_Response.BlockRef();
|
||||
ref.setBlockNumber(rs.getInt("block_number"));
|
||||
ref.setBlockHash(ChannelsReadSupport.toHex(rs.getBytes("block_hash")));
|
||||
item.setMessageRef(ref);
|
||||
item.setMsgSubType(rs.getInt("msg_sub_type"));
|
||||
item.setAuthorLogin(rs.getString("login"));
|
||||
item.setAuthorBlockchainName(rs.getString("bch_name"));
|
||||
item.setCreatedAtMs(entry.timestamp * 1000L);
|
||||
item.setLikesCount(0);
|
||||
item.setLikedByMe(false);
|
||||
item.setRepliesCount(0);
|
||||
item.setRatingsCount(0);
|
||||
item.setTargetBlockchainName(statusBody.toBchName());
|
||||
item.setTargetBlockNumber(statusBody.toBlockGlobalNumber());
|
||||
item.setTargetBlockHash(ChannelsReadSupport.toHex(statusBody.toBlockHashBytes()));
|
||||
|
||||
List<Net_GetChannelMessages_Response.VersionItem> versions = loadVersionsForDiaryItem(
|
||||
c,
|
||||
rs.getString("bch_name"),
|
||||
rs.getInt("block_number"),
|
||||
rs.getBytes("block_hash"),
|
||||
statusBody.message == null ? "" : statusBody.message,
|
||||
entry.timestamp * 1000L
|
||||
);
|
||||
item.setVersions(versions);
|
||||
item.setVersionsTotal(versions.size());
|
||||
item.setText(versions.get(versions.size() - 1).getText());
|
||||
|
||||
fillTargetDetails(c, item, statusBody.toBchName(), statusBody.toBlockGlobalNumber(), statusBody.toBlockHashBytes());
|
||||
out.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private List<Net_GetChannelMessages_Response.VersionItem> loadVersionsForDiaryItem(Connection c,
|
||||
String ownerBch,
|
||||
int originalBlockNumber,
|
||||
byte[] originalBlockHash,
|
||||
String originalText,
|
||||
long originalCreatedAtMs) throws Exception {
|
||||
List<Net_GetChannelMessages_Response.VersionItem> versions = new ArrayList<>();
|
||||
|
||||
Net_GetChannelMessages_Response.VersionItem first = new Net_GetChannelMessages_Response.VersionItem();
|
||||
first.setVersionIndex(1);
|
||||
first.setBlockNumber(originalBlockNumber);
|
||||
first.setBlockHash(ChannelsReadSupport.toHex(originalBlockHash));
|
||||
first.setText(originalText == null ? "" : originalText);
|
||||
first.setCreatedAtMs(originalCreatedAtMs);
|
||||
versions.add(first);
|
||||
|
||||
try (PreparedStatement ps = c.prepareStatement("""
|
||||
SELECT block_number, block_hash, block_bytes
|
||||
FROM blocks
|
||||
WHERE bch_name = ?
|
||||
AND msg_type = ?
|
||||
AND msg_sub_type = ?
|
||||
AND to_block_number = ?
|
||||
AND to_block_hash = ?
|
||||
AND (to_bch_name = ? OR to_bch_name IS NULL OR to_bch_name = '')
|
||||
ORDER BY block_number ASC
|
||||
""")) {
|
||||
ps.setString(1, ownerBch);
|
||||
ps.setInt(2, ChannelsReadSupport.MSG_TYPE_TEXT);
|
||||
ps.setInt(3, shine.db.MsgSubType.TEXT_EDIT_REPLY);
|
||||
ps.setInt(4, originalBlockNumber);
|
||||
ps.setBytes(5, originalBlockHash);
|
||||
ps.setString(6, ownerBch);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
ChannelsReadSupport.TextInfo editText = ChannelsReadSupport.parseTextAndTime(rs.getBytes("block_bytes"));
|
||||
Net_GetChannelMessages_Response.VersionItem item = new Net_GetChannelMessages_Response.VersionItem();
|
||||
item.setVersionIndex(versions.size() + 1);
|
||||
item.setBlockNumber(rs.getInt("block_number"));
|
||||
item.setBlockHash(ChannelsReadSupport.toHex(rs.getBytes("block_hash")));
|
||||
item.setText(editText.text);
|
||||
item.setCreatedAtMs(editText.createdAtMs);
|
||||
versions.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return versions;
|
||||
}
|
||||
|
||||
private void fillTargetDetails(Connection c,
|
||||
Net_GetChannelMessages_Response.MessageItem item,
|
||||
String targetBch,
|
||||
Integer targetBlockNumber,
|
||||
byte[] targetHash) throws Exception {
|
||||
if (targetBch == null || targetBch.isBlank() || targetBlockNumber == null || targetHash == null || targetHash.length != 32) {
|
||||
return;
|
||||
}
|
||||
try (PreparedStatement ps = c.prepareStatement("""
|
||||
SELECT login, bch_name, block_number, block_hash, block_bytes, msg_sub_type
|
||||
FROM blocks
|
||||
WHERE bch_name = ? AND block_number = ?
|
||||
LIMIT 1
|
||||
""")) {
|
||||
ps.setString(1, targetBch);
|
||||
ps.setInt(2, targetBlockNumber);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (!rs.next()) return;
|
||||
byte[] actualHash = rs.getBytes("block_hash");
|
||||
if (actualHash == null || !java.util.Arrays.equals(actualHash, targetHash)) return;
|
||||
item.setTargetMsgSubType(rs.getInt("msg_sub_type"));
|
||||
item.setTargetAuthorLogin(rs.getString("login"));
|
||||
item.setTargetAuthorBlockchainName(rs.getString("bch_name"));
|
||||
ChannelsReadSupport.TextInfo textInfo = ChannelsReadSupport.parseTextAndTime(rs.getBytes("block_bytes"));
|
||||
item.setTargetText(textInfo.text);
|
||||
item.setTargetCreatedAtMs(textInfo.createdAtMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -58,7 +58,7 @@ public class Net_MarkChannelMessagesSeen_Handler implements JsonMessageHandler {
|
||||
AND block_number = ?
|
||||
AND block_hash = ?
|
||||
AND msg_type = ?
|
||||
AND msg_sub_type = ?
|
||||
AND msg_sub_type IN (?, ?, ?, ?, ?, ?)
|
||||
%s
|
||||
LIMIT 1
|
||||
""".formatted(strictChannelMatch ? "AND line_code = ?" : "");
|
||||
@@ -98,8 +98,13 @@ public class Net_MarkChannelMessagesSeen_Handler implements JsonMessageHandler {
|
||||
existsPs.setBytes(3, ChannelsReadSupport.hexToBytes(hashHex));
|
||||
existsPs.setInt(4, ChannelsReadSupport.MSG_TYPE_TEXT);
|
||||
existsPs.setInt(5, MsgSubType.TEXT_POST);
|
||||
existsPs.setInt(6, MsgSubType.TEXT_REPOST);
|
||||
existsPs.setInt(7, MsgSubType.TEXT_ENTRYPOINT);
|
||||
existsPs.setInt(8, MsgSubType.TEXT_EXERCISE);
|
||||
existsPs.setInt(9, MsgSubType.TEXT_SERVICE);
|
||||
existsPs.setInt(10, MsgSubType.TEXT_COURSE);
|
||||
if (strictChannelMatch) {
|
||||
existsPs.setInt(6, expectedRoot);
|
||||
existsPs.setInt(11, expectedRoot);
|
||||
}
|
||||
|
||||
boolean exists;
|
||||
|
||||
+24
@@ -127,11 +127,17 @@ public class Net_GetChannelMessages_Response extends Net_Response {
|
||||
private String targetBlockchainName;
|
||||
private Integer targetBlockNumber;
|
||||
private String targetBlockHash;
|
||||
private Integer targetMsgSubType;
|
||||
private String targetText;
|
||||
private String targetAuthorLogin;
|
||||
private String targetAuthorBlockchainName;
|
||||
private Long targetCreatedAtMs;
|
||||
private long createdAtMs;
|
||||
private String text;
|
||||
private int likesCount;
|
||||
private boolean likedByMe;
|
||||
private int repliesCount;
|
||||
private int ratingsCount;
|
||||
private int versionsTotal;
|
||||
private List<VersionItem> versions = new ArrayList<>();
|
||||
|
||||
@@ -158,6 +164,21 @@ public class Net_GetChannelMessages_Response extends Net_Response {
|
||||
public String getTargetBlockHash() { return targetBlockHash; }
|
||||
public void setTargetBlockHash(String targetBlockHash) { this.targetBlockHash = targetBlockHash; }
|
||||
|
||||
public Integer getTargetMsgSubType() { return targetMsgSubType; }
|
||||
public void setTargetMsgSubType(Integer targetMsgSubType) { this.targetMsgSubType = targetMsgSubType; }
|
||||
|
||||
public String getTargetText() { return targetText; }
|
||||
public void setTargetText(String targetText) { this.targetText = targetText; }
|
||||
|
||||
public String getTargetAuthorLogin() { return targetAuthorLogin; }
|
||||
public void setTargetAuthorLogin(String targetAuthorLogin) { this.targetAuthorLogin = targetAuthorLogin; }
|
||||
|
||||
public String getTargetAuthorBlockchainName() { return targetAuthorBlockchainName; }
|
||||
public void setTargetAuthorBlockchainName(String targetAuthorBlockchainName) { this.targetAuthorBlockchainName = targetAuthorBlockchainName; }
|
||||
|
||||
public Long getTargetCreatedAtMs() { return targetCreatedAtMs; }
|
||||
public void setTargetCreatedAtMs(Long targetCreatedAtMs) { this.targetCreatedAtMs = targetCreatedAtMs; }
|
||||
|
||||
public long getCreatedAtMs() { return createdAtMs; }
|
||||
public void setCreatedAtMs(long createdAtMs) { this.createdAtMs = createdAtMs; }
|
||||
|
||||
@@ -173,6 +194,9 @@ public class Net_GetChannelMessages_Response extends Net_Response {
|
||||
public int getRepliesCount() { return repliesCount; }
|
||||
public void setRepliesCount(int repliesCount) { this.repliesCount = repliesCount; }
|
||||
|
||||
public int getRatingsCount() { return ratingsCount; }
|
||||
public void setRatingsCount(int ratingsCount) { this.ratingsCount = ratingsCount; }
|
||||
|
||||
public int getVersionsTotal() { return versionsTotal; }
|
||||
public void setVersionsTotal(int versionsTotal) { this.versionsTotal = versionsTotal; }
|
||||
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.channels.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
|
||||
public class Net_GetPersonalDiary_Request extends Net_Request {
|
||||
private String login;
|
||||
private Integer limit;
|
||||
private String sort;
|
||||
|
||||
public String getLogin() { return login; }
|
||||
public void setLogin(String login) { this.login = login; }
|
||||
|
||||
public Integer getLimit() { return limit; }
|
||||
public void setLimit(Integer limit) { this.limit = limit; }
|
||||
|
||||
public String getSort() { return sort; }
|
||||
public void setSort(String sort) { this.sort = sort; }
|
||||
}
|
||||
+22
-3
@@ -26,6 +26,7 @@ import java.util.Set;
|
||||
public class Net_CallInviteBroadcast_Handler implements JsonMessageHandler {
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
private static final int TYPE_INVITE = 100;
|
||||
private static final int TYPE_CONNECT_START = 170;
|
||||
private static final long PUSH_CALL_TTL_MS = 10_000L;
|
||||
|
||||
@Override
|
||||
@@ -38,8 +39,11 @@ public class Net_CallInviteBroadcast_Handler implements JsonMessageHandler {
|
||||
String toRequest = req.getToLogin() == null ? "" : req.getToLogin().trim();
|
||||
String callId = req.getCallId() == null ? "" : req.getCallId().trim();
|
||||
int type = req.getType() == null ? TYPE_INVITE : req.getType();
|
||||
if (toRequest.isBlank() || callId.isBlank() || type != TYPE_INVITE) {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "toLogin/callId/type=100 обязательны");
|
||||
String data = req.getData() == null ? "" : req.getData().trim();
|
||||
boolean isInvite = type == TYPE_INVITE;
|
||||
boolean isConnectStart = type == TYPE_CONNECT_START;
|
||||
if (toRequest.isBlank() || callId.isBlank() || (!isInvite && !isConnectStart)) {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "toLogin/callId/type=100|170 обязательны");
|
||||
}
|
||||
|
||||
CurrentUserEntry targetUser = CurrentUsersDAO.getInstance().getByLogin(toRequest);
|
||||
@@ -69,13 +73,28 @@ public class Net_CallInviteBroadcast_Handler implements JsonMessageHandler {
|
||||
payload.put("fromSessionId", ctx.getSessionId());
|
||||
payload.put("toLogin", to);
|
||||
payload.put("callId", callId);
|
||||
payload.put("type", TYPE_INVITE);
|
||||
payload.put("type", type);
|
||||
payload.put("timeMs", timeMs);
|
||||
if (!data.isBlank()) {
|
||||
payload.put("data", data);
|
||||
}
|
||||
|
||||
boolean sent = WsEventSender.sendEvent(targetCtx, "IncomingCallInvite", eventId, payload);
|
||||
if (sent) wsDelivered++;
|
||||
}
|
||||
|
||||
if (isConnectStart) {
|
||||
Net_CallInviteBroadcast_Response resp = new Net_CallInviteBroadcast_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
resp.setStatus(WireCodes.Status.OK);
|
||||
resp.setCallId(callId);
|
||||
resp.setDeliveredWsSessions(wsDelivered);
|
||||
resp.setDeliveredFcmSessions(0);
|
||||
resp.setDeliveredWebPushSessions(0);
|
||||
return resp;
|
||||
}
|
||||
|
||||
for (ActiveSessionEntry session : allTargetSessions) {
|
||||
String sessionId = String.valueOf(session.getSessionId() == null ? "" : session.getSessionId()).trim();
|
||||
if (!sessionId.isBlank() && activeSessionIds.contains(sessionId)) {
|
||||
|
||||
+204
-3
@@ -12,10 +12,12 @@ import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendSignal_Request;
|
||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendSignal_Response;
|
||||
import server.logic.ws_protocol.JSON.push.WebPushSender;
|
||||
import server.logic.ws_protocol.JSON.push.WsEventSender;
|
||||
import server.logic.ws_protocol.JSON.utils.AuthKeyUtils;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.ActiveSessionsDAO;
|
||||
import shine.db.dao.CurrentUsersDAO;
|
||||
import shine.db.entities.ActiveSessionEntry;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
@@ -34,6 +36,13 @@ public class Net_SendSignal_Handler implements JsonMessageHandler {
|
||||
private static final String TARGET_MODE_SINGLE = "single_session";
|
||||
private static final String TARGET_MODE_ALL = "all_sessions";
|
||||
private static final long ALLOWED_SKEW_MS = 30_000L;
|
||||
private static final long PUSH_CALL_TTL_MS = 10_000L;
|
||||
private static final String SIGNAL_CALL_INVITE = "call_invite";
|
||||
private static final String SIGNAL_CALL_ACCEPT = "call_accept";
|
||||
private static final String SIGNAL_CALL_DECLINE_BUSY = "call_decline_busy";
|
||||
private static final String SIGNAL_CALL_TIMEOUT = "call_timeout";
|
||||
private static final String SIGNAL_CALL_HANGUP = "call_hangup";
|
||||
private static final String SIGNAL_CALL_CONNECT_START = "call_connect_start";
|
||||
|
||||
@Override
|
||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
|
||||
@@ -99,6 +108,10 @@ public class Net_SendSignal_Handler implements JsonMessageHandler {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_SESSION_SIGNATURE", "Некорректная подпись session key");
|
||||
}
|
||||
|
||||
if (isCallSignalType(signalType) && clientSignatureB64.isBlank()) {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "CLIENT_SIGNATURE_REQUIRED", "Для call_* сигналов обязательна подпись client key");
|
||||
}
|
||||
|
||||
if (!clientSignatureB64.isBlank()) {
|
||||
String clientPreimage = buildClientPreimage(fromLogin, fromSessionId, toLogin, targetMode, targetSessionId, signalType, signalRequestId, timeMs, digestB64);
|
||||
if (!verifySignature(senderUser.getClientKey(), clientPreimage, clientSignatureB64, "clientKey")) {
|
||||
@@ -107,7 +120,13 @@ public class Net_SendSignal_Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
List<ConnectionContext> targets = resolveTargets(targetMode, toLogin, targetSessionId);
|
||||
if (targets.isEmpty()) {
|
||||
boolean isCallInviteAllSessions = TARGET_MODE_ALL.equals(targetMode) && SIGNAL_CALL_INVITE.equals(signalType);
|
||||
boolean isCallAcceptSingleSession = TARGET_MODE_SINGLE.equals(targetMode) && SIGNAL_CALL_ACCEPT.equals(signalType);
|
||||
boolean isCallTerminalSingleSession = TARGET_MODE_SINGLE.equals(targetMode)
|
||||
&& (SIGNAL_CALL_DECLINE_BUSY.equals(signalType)
|
||||
|| SIGNAL_CALL_TIMEOUT.equals(signalType)
|
||||
|| SIGNAL_CALL_HANGUP.equals(signalType));
|
||||
if (targets.isEmpty() && !isCallInviteAllSessions) {
|
||||
String code = TARGET_MODE_SINGLE.equals(targetMode) ? "SESSION_NOT_FOUND" : "NO_TARGET_SESSIONS";
|
||||
String msg = TARGET_MODE_SINGLE.equals(targetMode) ? "Целевая сессия не найдена" : "Нет активных сессий для доставки сигнала";
|
||||
return NetExceptionResponseFactory.error(req, 404, code, msg);
|
||||
@@ -135,16 +154,62 @@ public class Net_SendSignal_Handler implements JsonMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (deliveredSessionIds.isEmpty()) {
|
||||
int webPushDelivered = 0;
|
||||
if (isCallInviteAllSessions) {
|
||||
webPushDelivered = sendIncomingCallPushToOfflineSessions(
|
||||
toLogin,
|
||||
fromLogin,
|
||||
fromSessionId,
|
||||
signalRequestId,
|
||||
deliveredSessionIds
|
||||
);
|
||||
}
|
||||
|
||||
if (deliveredSessionIds.isEmpty() && webPushDelivered <= 0) {
|
||||
return NetExceptionResponseFactory.error(req, 404, "DELIVERY_FAILED", "Не удалось доставить сигнал ни в одну целевую сессию");
|
||||
}
|
||||
|
||||
if (isCallAcceptSingleSession) {
|
||||
notifyStopOnOtherSessions(
|
||||
fromLogin,
|
||||
fromSessionId,
|
||||
fromLogin,
|
||||
fromSessionId,
|
||||
signalRequestId,
|
||||
"accepted_on_other_device"
|
||||
);
|
||||
}
|
||||
|
||||
if (isCallTerminalSingleSession) {
|
||||
String deliveredTargetSessionId = deliveredSessionIds.isEmpty() ? targetSessionId : deliveredSessionIds.get(0);
|
||||
String reason = "terminal_call_signal_" + signalType;
|
||||
notifyStopOnOtherSessions(
|
||||
fromLogin,
|
||||
fromSessionId,
|
||||
fromLogin,
|
||||
fromSessionId,
|
||||
signalRequestId,
|
||||
reason
|
||||
);
|
||||
notifyStopOnOtherSessions(
|
||||
toLogin,
|
||||
deliveredTargetSessionId,
|
||||
fromLogin,
|
||||
fromSessionId,
|
||||
signalRequestId,
|
||||
reason
|
||||
);
|
||||
}
|
||||
|
||||
Net_SendSignal_Response resp = new Net_SendSignal_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
resp.setStatus(WireCodes.Status.OK);
|
||||
resp.setDeliveredCount(deliveredSessionIds.size());
|
||||
resp.setDeliveredCount(deliveredSessionIds.size() + webPushDelivered);
|
||||
resp.setDeliveredSessionIds(deliveredSessionIds);
|
||||
resp.setDeliveredWsSessions(deliveredSessionIds.size());
|
||||
resp.setDeliveredFcmSessions(webPushDelivered);
|
||||
resp.setDeliveredWebPushSessions(webPushDelivered);
|
||||
return resp;
|
||||
}
|
||||
|
||||
@@ -190,6 +255,19 @@ public class Net_SendSignal_Handler implements JsonMessageHandler {
|
||||
+ dataSha256B64;
|
||||
}
|
||||
|
||||
private static boolean isCallSignalType(String signalType) {
|
||||
return SIGNAL_CALL_INVITE.equals(signalType)
|
||||
|| "call_ringing".equals(signalType)
|
||||
|| SIGNAL_CALL_ACCEPT.equals(signalType)
|
||||
|| SIGNAL_CALL_DECLINE_BUSY.equals(signalType)
|
||||
|| SIGNAL_CALL_TIMEOUT.equals(signalType)
|
||||
|| SIGNAL_CALL_HANGUP.equals(signalType)
|
||||
|| SIGNAL_CALL_CONNECT_START.equals(signalType)
|
||||
|| "call_offer".equals(signalType)
|
||||
|| "call_answer".equals(signalType)
|
||||
|| "call_ice".equals(signalType);
|
||||
}
|
||||
|
||||
private static boolean verifySignature(String publicKeyValue, String preimage, String signatureB64, String fieldName) throws Exception {
|
||||
byte[] publicKey32 = AuthKeyUtils.parseEd25519PublicKey(publicKeyValue, fieldName);
|
||||
byte[] signature64 = Base64Ws.decodeLen(signatureB64, 64, "signatureB64");
|
||||
@@ -214,7 +292,130 @@ public class Net_SendSignal_Handler implements JsonMessageHandler {
|
||||
return targets;
|
||||
}
|
||||
|
||||
private int sendIncomingCallPushToOfflineSessions(
|
||||
String toLogin,
|
||||
String fromLogin,
|
||||
String fromSessionId,
|
||||
String callId,
|
||||
List<String> deliveredOnlineSessionIds
|
||||
) throws Exception {
|
||||
List<ActiveSessionEntry> persistedSessions = ActiveSessionsDAO.getInstance().getByLogin(toLogin);
|
||||
long sentAtMs = System.currentTimeMillis();
|
||||
long expiresAtMs = sentAtMs + PUSH_CALL_TTL_MS;
|
||||
int delivered = 0;
|
||||
for (ActiveSessionEntry session : persistedSessions) {
|
||||
String sessionId = safe(session.getSessionId());
|
||||
if (!sessionId.isBlank() && deliveredOnlineSessionIds.contains(sessionId)) {
|
||||
continue;
|
||||
}
|
||||
if (isBlank(session.getPushEndpoint()) || isBlank(session.getPushP256dhKey()) || isBlank(session.getPushAuthKey())) {
|
||||
continue;
|
||||
}
|
||||
String payload = "{\"kind\":\"incoming_call\""
|
||||
+ ",\"title\":\"SHiNE: входящий звонок\""
|
||||
+ ",\"text\":\"Вам звонит " + jsonEscape(fromLogin) + "\""
|
||||
+ ",\"fromLogin\":\"" + jsonEscape(fromLogin) + "\""
|
||||
+ ",\"fromSessionId\":\"" + jsonEscape(fromSessionId) + "\""
|
||||
+ ",\"targetSessionId\":\"" + jsonEscape(sessionId) + "\""
|
||||
+ ",\"toLogin\":\"" + jsonEscape(toLogin) + "\""
|
||||
+ ",\"callId\":\"" + jsonEscape(callId) + "\""
|
||||
+ ",\"sentAtMs\":" + sentAtMs
|
||||
+ ",\"expiresAtMs\":" + expiresAtMs
|
||||
+ "}";
|
||||
boolean pushed = WebPushSender.sendBase64Payload(
|
||||
session.getPushEndpoint(),
|
||||
session.getPushP256dhKey(),
|
||||
session.getPushAuthKey(),
|
||||
payload
|
||||
);
|
||||
if (pushed) {
|
||||
delivered++;
|
||||
}
|
||||
}
|
||||
return delivered;
|
||||
}
|
||||
|
||||
private void notifyStopOnOtherSessions(
|
||||
String targetLogin,
|
||||
String excludeSessionId,
|
||||
String fromLogin,
|
||||
String fromSessionId,
|
||||
String callId,
|
||||
String reason
|
||||
) throws Exception {
|
||||
if (isBlank(targetLogin) || isBlank(callId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> onlineSessionIds = new java.util.HashSet<>();
|
||||
Set<ConnectionContext> sameUserSessions = ActiveConnectionsRegistry.getInstance().getByLogin(targetLogin);
|
||||
for (ConnectionContext siblingCtx : sameUserSessions) {
|
||||
if (siblingCtx == null || siblingCtx.getWsSession() == null || !siblingCtx.getWsSession().isOpen()) continue;
|
||||
onlineSessionIds.add(safe(siblingCtx.getSessionId()));
|
||||
if (!isBlank(excludeSessionId) && excludeSessionId.equals(siblingCtx.getSessionId())) continue;
|
||||
|
||||
String siblingEventId = server.logic.ws_protocol.JSON.utils.NetIdGenerator.eventId("evt");
|
||||
ObjectNode siblingPayload = MAPPER.createObjectNode();
|
||||
siblingPayload.put("eventId", siblingEventId);
|
||||
siblingPayload.put("fromLogin", fromLogin);
|
||||
siblingPayload.put("fromSessionId", fromSessionId);
|
||||
siblingPayload.put("toLogin", targetLogin);
|
||||
siblingPayload.put("targetMode", TARGET_MODE_SINGLE);
|
||||
siblingPayload.put("targetSessionId", safe(siblingCtx.getSessionId()));
|
||||
siblingPayload.put("signalType", SIGNAL_CALL_HANGUP);
|
||||
siblingPayload.put("signalRequestId", callId);
|
||||
siblingPayload.put("data", "{\"callId\":\"" + jsonEscape(callId) + "\",\"type\":150,\"data\":\"" + jsonEscape(reason) + "\"}");
|
||||
siblingPayload.put("timeMs", System.currentTimeMillis());
|
||||
WsEventSender.sendEvent(siblingCtx, "IncomingSignal", siblingEventId, siblingPayload);
|
||||
}
|
||||
|
||||
List<ActiveSessionEntry> persistedSessions = ActiveSessionsDAO.getInstance().getByLogin(targetLogin);
|
||||
long sentAtMs = System.currentTimeMillis();
|
||||
for (ActiveSessionEntry session : persistedSessions) {
|
||||
String sessionId = safe(session.getSessionId());
|
||||
if (!isBlank(excludeSessionId) && excludeSessionId.equals(sessionId)) continue;
|
||||
if (!sessionId.isBlank() && onlineSessionIds.contains(sessionId)) continue;
|
||||
if (isBlank(session.getPushEndpoint()) || isBlank(session.getPushP256dhKey()) || isBlank(session.getPushAuthKey())) {
|
||||
continue;
|
||||
}
|
||||
String pushPayload = "{\"kind\":\"stop_call\""
|
||||
+ ",\"callId\":\"" + jsonEscape(callId) + "\""
|
||||
+ ",\"reason\":\"" + jsonEscape(reason) + "\""
|
||||
+ ",\"fromLogin\":\"" + jsonEscape(fromLogin) + "\""
|
||||
+ ",\"fromSessionId\":\"" + jsonEscape(fromSessionId) + "\""
|
||||
+ ",\"targetSessionId\":\"" + jsonEscape(sessionId) + "\""
|
||||
+ ",\"toLogin\":\"" + jsonEscape(targetLogin) + "\""
|
||||
+ ",\"sentAtMs\":" + sentAtMs
|
||||
+ "}";
|
||||
WebPushSender.sendBase64Payload(
|
||||
session.getPushEndpoint(),
|
||||
session.getPushP256dhKey(),
|
||||
session.getPushAuthKey(),
|
||||
pushPayload
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static String safe(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private static boolean isBlank(String value) {
|
||||
return value == null || value.isBlank();
|
||||
}
|
||||
|
||||
private static String jsonEscape(String s) {
|
||||
if (s == null) return "";
|
||||
StringBuilder out = new StringBuilder();
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
if (c == '\\') out.append("\\\\");
|
||||
else if (c == '"') out.append("\\\"");
|
||||
else if (c == '\n') out.append("\\n");
|
||||
else if (c == '\r') out.append("\\r");
|
||||
else if (c == '\t') out.append("\\t");
|
||||
else out.append(c);
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -6,6 +6,7 @@ public class Net_CallInviteBroadcast_Request extends Net_Request {
|
||||
private String toLogin;
|
||||
private String callId;
|
||||
private Integer type;
|
||||
private String data;
|
||||
|
||||
public String getToLogin() { return toLogin; }
|
||||
public void setToLogin(String toLogin) { this.toLogin = toLogin; }
|
||||
@@ -15,4 +16,7 @@ public class Net_CallInviteBroadcast_Request extends Net_Request {
|
||||
|
||||
public Integer getType() { return type; }
|
||||
public void setType(Integer type) { this.type = type; }
|
||||
|
||||
public String getData() { return data; }
|
||||
public void setData(String data) { this.data = data; }
|
||||
}
|
||||
|
||||
+27
@@ -8,6 +8,9 @@ import java.util.List;
|
||||
public class Net_SendSignal_Response extends Net_Response {
|
||||
private int deliveredCount;
|
||||
private List<String> deliveredSessionIds = new ArrayList<>();
|
||||
private int deliveredWsSessions;
|
||||
private int deliveredFcmSessions;
|
||||
private int deliveredWebPushSessions;
|
||||
|
||||
public int getDeliveredCount() {
|
||||
return deliveredCount;
|
||||
@@ -24,4 +27,28 @@ public class Net_SendSignal_Response extends Net_Response {
|
||||
public void setDeliveredSessionIds(List<String> deliveredSessionIds) {
|
||||
this.deliveredSessionIds = deliveredSessionIds;
|
||||
}
|
||||
|
||||
public int getDeliveredWsSessions() {
|
||||
return deliveredWsSessions;
|
||||
}
|
||||
|
||||
public void setDeliveredWsSessions(int deliveredWsSessions) {
|
||||
this.deliveredWsSessions = deliveredWsSessions;
|
||||
}
|
||||
|
||||
public int getDeliveredFcmSessions() {
|
||||
return deliveredFcmSessions;
|
||||
}
|
||||
|
||||
public void setDeliveredFcmSessions(int deliveredFcmSessions) {
|
||||
this.deliveredFcmSessions = deliveredFcmSessions;
|
||||
}
|
||||
|
||||
public int getDeliveredWebPushSessions() {
|
||||
return deliveredWebPushSessions;
|
||||
}
|
||||
|
||||
public void setDeliveredWebPushSessions(int deliveredWebPushSessions) {
|
||||
this.deliveredWebPushSessions = deliveredWebPushSessions;
|
||||
}
|
||||
}
|
||||
|
||||
+20
-12
@@ -1057,12 +1057,16 @@ public final class PostgresStorageRepository
|
||||
s.updated_at_ms,
|
||||
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||
FROM solana_user_pda_current u
|
||||
CROSS JOIN LATERAL jsonb_array_elements_text(
|
||||
CASE
|
||||
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||
ELSE u.access_servers_json::jsonb
|
||||
END
|
||||
) AS access_server(login_value)
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT login_value
|
||||
FROM jsonb_array_elements_text(
|
||||
CASE
|
||||
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||
ELSE u.access_servers_json::jsonb
|
||||
END
|
||||
) WITH ORDINALITY AS access_server(login_value, ord)
|
||||
WHERE ord <= 2
|
||||
) AS access_server
|
||||
JOIN solana_user_pda_current s
|
||||
ON LOWER(s.login) = LOWER(btrim(access_server.login_value))
|
||||
AND s.is_server = TRUE
|
||||
@@ -1100,12 +1104,16 @@ public final class PostgresStorageRepository
|
||||
FOR affected_user IN
|
||||
SELECT u.login
|
||||
FROM solana_user_pda_current u
|
||||
CROSS JOIN LATERAL jsonb_array_elements_text(
|
||||
CASE
|
||||
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||
ELSE u.access_servers_json::jsonb
|
||||
END
|
||||
) AS access_server(login_value)
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT login_value
|
||||
FROM jsonb_array_elements_text(
|
||||
CASE
|
||||
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||
ELSE u.access_servers_json::jsonb
|
||||
END
|
||||
) WITH ORDINALITY AS access_server(login_value, ord)
|
||||
WHERE ord <= 2
|
||||
) AS access_server
|
||||
WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login)
|
||||
LOOP
|
||||
PERFORM shine_refresh_user_access_servers_for_user(affected_user.login);
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# Подключение других устройств по QR и типизированные сессии
|
||||
|
||||
## Зачем
|
||||
|
||||
QR-подключение других устройств сейчас есть как заготовка, но сценарий нужно довести до устойчивого состояния. Параллельно надо аккуратно оформить типизированные сессии homeserver-ов в PDA.
|
||||
|
||||
## Что сделать
|
||||
|
||||
1. Довести QR-сценарий до стабильного подключения нового устройства.
|
||||
2. Нормально описать и хранить устройство как отдельную типизированную сессию.
|
||||
3. Согласовать это с серверной и UI-логикой.
|
||||
4. Проверить, что подключение работает одинаково на новом и повторном устройстве.
|
||||
|
||||
## Что уже есть
|
||||
|
||||
- в планах есть `сессионные homeserver-ы в PDA`;
|
||||
- в планах есть `подключение других устройств через QR`;
|
||||
- базовая заготовка уже существует, но сценарий считается нестабильным.
|
||||
|
||||
## Откуда продолжать
|
||||
|
||||
- от текущих документов в `TODO/medium/`;
|
||||
- отдельно проверить, какие поля уже есть в PDA и UI.
|
||||
|
||||
## Какие документы потом обновить
|
||||
|
||||
- `docs/Solana_Architecture/README.md`;
|
||||
- `TODO/medium/2026-06-03_подключение_других_устройств_через_qr.md`;
|
||||
- `TODO/medium/2026-06-02_сессионные_homeserver_в_pda.md`.
|
||||
@@ -1,28 +0,0 @@
|
||||
# ESP32 как личное файловое хранилище
|
||||
|
||||
## Зачем
|
||||
|
||||
Планируется использовать ESP32 как личное файловое хранилище SHiNE для переписок и вложений.
|
||||
|
||||
## Что сделать
|
||||
|
||||
1. Продумать формат хранения файлов на устройстве.
|
||||
2. Согласовать загрузку и чтение файлов между UI, сервером и устройством.
|
||||
3. Проверить, как устройство показывает статусы и ошибки.
|
||||
4. Свести это с существующим homeserver/UI-прототипом.
|
||||
|
||||
## Что уже есть
|
||||
|
||||
- в списке будущих фич уже есть отдельная задача по ESP32S3 file storage;
|
||||
- для UI homeserver уже есть отдельная документация и скетч должны держаться синхронно.
|
||||
|
||||
## Откуда продолжать
|
||||
|
||||
- от `TODO/medium/2026-05-26_0029_esp32s3_file_storage.md`;
|
||||
- от документации по ESP32 UI homeserver.
|
||||
|
||||
## Какие документы потом обновить
|
||||
|
||||
- `TODO/medium/2026-05-26_0029_esp32s3_file_storage.md`;
|
||||
- `TODO/README.md`;
|
||||
- документацию по ESP32 UI homeserver, если добавятся экраны или статусы.
|
||||
@@ -1,60 +0,0 @@
|
||||
# TODO
|
||||
|
||||
Папка для короткого списка ближайших и среднесрочных задач, которые уже обсуждались и пока отложены.
|
||||
|
||||
## Как использовать
|
||||
|
||||
- Один markdown-файл = одна задача.
|
||||
- В файле коротко фиксируем:
|
||||
- зачем это нужно;
|
||||
- что именно сделать;
|
||||
- что уже есть в коде;
|
||||
- откуда продолжать;
|
||||
- какие документы потом надо обновить.
|
||||
- Это не активная разработка. Тут только план и контекст.
|
||||
- Старую папку `docs/Future_Features/` считать архивной и больше не использовать как источник новых задач.
|
||||
|
||||
## Текущие задачи
|
||||
|
||||
- `2026-06-26_1800_корректное_завершение_за_30с.md` - дать сервису до 30 секунд на корректное завершение опасных операций перед рестартом.
|
||||
- `2026-06-26_1810_подключение_устройств_по_qr.md` - довести подключение других устройств по QR и перевести это в нормальные типизированные сессии.
|
||||
- `2026-06-26_1815_esp32_файловое_хранилище.md` - использовать ESP32 как личное файловое хранилище для переписок и вложений.
|
||||
|
||||
## Децентрализация
|
||||
|
||||
Текущий production-режим SHiNE считается односерверным. Задачи по нескольким серверам, Arweave и realtime PDA/Solana sync вынесены в `Децентрализация/` и не блокируют выкладку текущей версии на GitHub.
|
||||
|
||||
- `Децентрализация/односерверный_production_режим.md` - границы текущей production-версии с одним сервером.
|
||||
- `Децентрализация/запись_блокчейнов_в_arweave.md` - будущая запись/архивация блокчейнов в Arweave.
|
||||
- `Децентрализация/realtime_pda_solana_sync.md` - будущая онлайн-синхронизация PDA и Solana.
|
||||
- `Децентрализация/межсерверная_передача_сообщений.md` - будущая доставка сообщений между серверами.
|
||||
- `Децентрализация/межсерверные_звонки.md` - будущая маршрутизация звонков между серверами.
|
||||
- `Децентрализация/2026-06-26_1805_межсерверный_ws_и_dm_sync.md` - перенесённый старый план постоянного server-to-server WS и DM sync.
|
||||
|
||||
## Новые фишки которые надо доделать
|
||||
|
||||
- `Новые фишки которые надо доделать/Новая_контентная_модель_блокчейна/` - отложенная новая контентная модель блокчейна, не входящая в текущий односерверный production-релиз.
|
||||
|
||||
## Перенесённые планы из `docs/Future_Features/`
|
||||
|
||||
### near
|
||||
|
||||
- `near/2026-05-25_1106_telegram_agent_players.md` - разрешённые пользователи Telegram для агента, отдельные папки игроков, персональные истории и публикация краткого вопроса/ответа в общий канал.
|
||||
- `near/2026-05-25_1106_wallet_topup_solana_arweave.md` - пополнение Solana и Arweave через внешний сервис покупки с подсказкой и копированием адреса.
|
||||
|
||||
### medium
|
||||
|
||||
- `medium/2026-05-24_1140_репосты_в_каналах_и_тредах.md` - репосты в каналах и тредах.
|
||||
- `medium/2026-05-25_1106_shine_balance_wallet.md` - кошелёк и пополнение баланса сияния через блокчейн.
|
||||
- `medium/2026-05-26_0029_esp32s3_file_storage.md` - ESP32S3 как личное файловое хранилище SHiNE для файлов переписок и вложений.
|
||||
- `medium/2026-06-02_сессионные_homeserver_в_pda.md` - несколько homeserver-ов пользователя как типизированные сессии в PDA с версией записи.
|
||||
- `medium/2026-06-03_подключение_других_устройств_через_qr.md` - довести подключение других устройств через QR: сейчас заготовка есть, но сценарий работает нестабильно и его нужно будет отдельно доделать.
|
||||
- `medium/2026-07-22_переход_с_sqlite_на_postgresql.md` - завершить зачистку хвостов после перевода серверной БД с `SQLite` на `PostgreSQL`.
|
||||
|
||||
### dao_запуск
|
||||
|
||||
- `dao_запуск/2026-06-05_esp32_hardware_wallet_device_session.md` - ESP32 как аппаратный кошелёк: постоянная device-сессия на сервере, подтверждение операций на экране, делегированные сессии для браузера/телефона.
|
||||
|
||||
### far
|
||||
|
||||
- `far/2026-06-20_1639_homeserver_technical_commands_and_file_transfer.md` - технические команды для homeserver через SHiNE/WebRTC DataChannel и обмен файлами по чанкам с адресацией по `SHA-256`.
|
||||
@@ -1,114 +0,0 @@
|
||||
# Homeserver: технические команды и передача файлов через SHiNE/WebRTC
|
||||
|
||||
## Зачем нужна фича
|
||||
|
||||
Идея на дальнее будущее: дать возможность обращаться к homeserver не только как к участнику сети SHiNE, но и как к удалённой технической точке управления.
|
||||
|
||||
Цели:
|
||||
- отправлять на homeserver технические команды в текстовом виде;
|
||||
- получать текстовый ответ на команду;
|
||||
- при наличии WebRTC DataChannel передавать части файлов в обе стороны;
|
||||
- хранить полученные файлы на SD-карте homeserver;
|
||||
- использовать единый механизм доставки как через сервер SHiNE, так и напрямую через DataChannel.
|
||||
|
||||
## Горизонт
|
||||
|
||||
`far` - идея без ближайшего срока реализации. Сейчас приоритет ниже, чем запуск и стабилизация основного проекта.
|
||||
|
||||
## Что именно имеется в виду
|
||||
|
||||
### 1. Единая модель технической команды
|
||||
|
||||
Техническая команда должна иметь единый смысл независимо от транспорта доставки:
|
||||
- через любой доступный сервер SHiNE;
|
||||
- через уже установленный WebRTC DataChannel.
|
||||
|
||||
Если конкретный транспорт недоступен, ответ по нему может не прийти. Это считается нормальным поведением протокола.
|
||||
|
||||
### 2. Команда как короткоживущий подписанный сигнал
|
||||
|
||||
У команды должны быть:
|
||||
- `commandId`;
|
||||
- временная метка;
|
||||
- TTL около 10 секунд;
|
||||
- криптографическая подпись.
|
||||
|
||||
Смысл такой:
|
||||
- если команда быстро дошла, homeserver подтверждает принятие;
|
||||
- если не дошла вовремя, команда считается протухшей;
|
||||
- отправитель может безопасно послать повтор;
|
||||
- при повторе homeserver отвечает либо `команда принята`, либо `уже выполнено ранее`.
|
||||
|
||||
Это даёт дедупликацию и безопасный resend без повторного выполнения действия.
|
||||
|
||||
### 3. Текстовые технические команды
|
||||
|
||||
Базовый сценарий похож на короткий удалённый shell-протокол, но на уровне строго ограниченных команд:
|
||||
- отправил строку-команду;
|
||||
- получил строку-ответ.
|
||||
|
||||
Команды не обязаны исполнять произвольный shell. Предпочтительная модель - белый список операций с контролируемым форматом аргументов и ответа.
|
||||
|
||||
### 4. Передача файлов только при наличии DataChannel
|
||||
|
||||
Если между устройствами есть WebRTC DataChannel, через него можно передавать технические сообщения для файлового обмена.
|
||||
|
||||
Предварительная модель:
|
||||
- имя файла = `SHA-256` содержимого;
|
||||
- можно запросить диапазон байт `from..to`;
|
||||
- можно отправить диапазон байт `from..to`;
|
||||
- homeserver хранит полученные данные на SD-карте;
|
||||
- если DataChannel нет, на запрос файловой передачи возвращается ответ в духе `не могу передать, нет data channel`.
|
||||
|
||||
Фактически файл-обмен должен быть частным случаем общего протокола технических команд.
|
||||
|
||||
### 5. Установка data-соединения по явной команде
|
||||
|
||||
Нужна техническая команда уровня:
|
||||
- `установить data-соединение`.
|
||||
|
||||
Ответ:
|
||||
- либо `да`, после чего запускается обычная процедура `offer/answer/ICE`;
|
||||
- либо `нет` и причина отказа.
|
||||
|
||||
### 6. Доставка на пользовательские сессии
|
||||
|
||||
Логика должна быть совместима с общей моделью SHiNE, где технические сигналы можно отправлять на конкретные активные сессии пользователя.
|
||||
|
||||
Идея:
|
||||
- на любую активную сессию пользователя можно посылать техническую команду;
|
||||
- контакт пользователя может инициировать такую техническую коммуникацию так же, как он уже инициирует звонок или другой служебный сигнал.
|
||||
|
||||
## Что нужно будет сделать при возврате к задаче
|
||||
|
||||
- Спроектировать отдельный формат технических команд и ack-ответов.
|
||||
- Решить, будет ли это новый тип служебных сообщений в существующем протоколе блокчейн/сигналинга или отдельная ветка поверх уже имеющихся transport-операций.
|
||||
- Отдельно продумать авторизацию: кто именно из контактов и какие команды имеет право слать.
|
||||
- Ограничить набор допустимых команд, чтобы не превратить механизм в небезопасный удалённый shell.
|
||||
- Спроектировать протокол чанков файлов: размер чанка, нумерация, повторная отправка, контроль целостности, дозагрузка, завершение файла.
|
||||
- Продумать хранение на SD-карте: временные файлы, сборка чанков, проверка итогового `SHA-256`, очистка мусора.
|
||||
- Продумать поведение при отсутствии DataChannel, таймаутах и дублирующихся командах.
|
||||
- Проверить, как это лучше встраивать в текущие клиентские сессии, звонки и homeserver-логику.
|
||||
|
||||
## Вопросы для будущего уточнения
|
||||
|
||||
- Это должен быть строго служебный протокол или пользователь сможет вызывать его и вручную из UI.
|
||||
- Нужен ли доступ только к заранее разрешённым каталогам/файлам.
|
||||
- Нужна ли двусторонняя синхронизация файлов или достаточно ручных команд `запросить кусок` / `отправить кусок`.
|
||||
- Нужно ли разрешать передачу файлов через сервер SHiNE как fallback, или файл-обмен должен идти только через DataChannel.
|
||||
- Какой максимальный размер файлов и допустимый объём хранения на SD-карте.
|
||||
|
||||
## Что уже сделано
|
||||
|
||||
Пока только зафиксирована идея и базовая концепция. Реализация не начиналась.
|
||||
|
||||
## Какие документы нужно будет обновить при реализации
|
||||
|
||||
- `docs/Blockchain/README.md` и связанные файлы, если изменятся типы служебных сообщений или форматы блокчейн-команд.
|
||||
- `docs/API/` если изменится публичный серверный API или появятся новые операции.
|
||||
- `docs/Personal_Messages/Протокол_DM_v1.md` если часть маршрутизации или подтверждений будет встроена в существующую логику доставки/сессий.
|
||||
- Документацию по homeserver/ESP32, если появится пользовательская или сервисная файловая логика на устройстве.
|
||||
|
||||
## С какого места продолжать позже
|
||||
|
||||
Возвращаться к задаче только после стабилизации запуска проекта и базовых текущих функций. Начинать с проектирования протокола команд и матрицы прав доступа, а уже потом переходить к DataChannel-файлообмену.
|
||||
@@ -1,62 +0,0 @@
|
||||
# Кошелёк и пополнение баланса сияния
|
||||
|
||||
- Горизонт:
|
||||
`medium`
|
||||
- Ориентир:
|
||||
среднесрочно
|
||||
- Статус:
|
||||
`proposal`
|
||||
|
||||
## Кратко
|
||||
|
||||
Нужно добавить кошелёк для внутреннего баланса сияния и пополнение этого баланса через блокчейн-логику проекта. Задача связана с регистрацией пользователя и будущим учётом баланса.
|
||||
|
||||
## Предполагаемый сценарий
|
||||
|
||||
1. Пользователь регистрируется и получает/подключает нужные кошельки.
|
||||
2. В интерфейсе появляется баланс сияния.
|
||||
3. Пользователь открывает пополнение баланса сияния.
|
||||
4. Система создаёт или принимает блокчейн-операцию пополнения.
|
||||
5. После подтверждения баланса UI обновляет значение.
|
||||
|
||||
## Что нужно продумать
|
||||
|
||||
1. Что именно является единицей баланса сияния.
|
||||
2. Где хранится состояние баланса: в существующем блокчейне SHiNE, Solana-модуле или комбинированно.
|
||||
3. Какая операция отвечает за пополнение.
|
||||
4. Нужно ли делать отдельную регистрацию кошелька сияния или использовать существующую регистрацию пользователя.
|
||||
5. Как баланс восстанавливается после перезагрузки клиента.
|
||||
6. Какие права нужны для пополнения и списания.
|
||||
7. Нужна ли история операций баланса.
|
||||
|
||||
## Вопросы перед реализацией
|
||||
|
||||
1. Пополнение баланса сияния должно идти через основной блокчейн SHiNE или через Solana-программу.
|
||||
2. Нужна ли конвертация из SOL/AR в сияние.
|
||||
3. Кто может выпускать или начислять сияние.
|
||||
4. Нужно ли поддерживать перевод сияния между пользователями.
|
||||
5. Нужны ли лимиты, комиссии или статусы подтверждения.
|
||||
6. Какой экран должен показывать баланс: регистрация, профиль, кошелёк или отдельная страница.
|
||||
7. Нужно ли отображать неподтверждённый баланс отдельно от подтверждённого.
|
||||
|
||||
## Важное ограничение
|
||||
|
||||
Если для баланса сияния потребуется новый формат блокчейн-блока или изменение существующего формата, перед реализацией нужно отдельно предупредить пользователя и получить явное подтверждение на изменение формата блокчейна.
|
||||
|
||||
Если потребуется новый серверный API или изменение существующих `op`, перед реализацией нужно отдельно предупредить пользователя и получить явное подтверждение на изменение API.
|
||||
|
||||
## Документы, которые обновить при реализации
|
||||
|
||||
- `docs/Blockchain/`, если появятся или изменятся блоки баланса.
|
||||
- `docs/Blockchain/CHANGELOG.md`, если меняется блокчейн-формат.
|
||||
- `docs/API/`, если меняется серверный API.
|
||||
- после реализации отдельно согласовать ручную проверку.
|
||||
- Документацию Solana-регистрации, если баланс будет связан с Solana-модулем.
|
||||
|
||||
## Минимальная проверка в будущем
|
||||
|
||||
1. Новый пользователь видит корректный начальный баланс.
|
||||
2. Пополнение создаёт правильную операцию.
|
||||
3. Баланс обновляется после подтверждения.
|
||||
4. После перезагрузки UI баланс остаётся корректным.
|
||||
5. Ошибочные или повторные операции не начисляют баланс дважды.
|
||||
@@ -1,44 +0,0 @@
|
||||
# ESP32S3 как личное файловое хранилище SHiNE
|
||||
|
||||
## Горизонт
|
||||
|
||||
Среднесрочный: ближайшие недели или 1-2 месяца.
|
||||
|
||||
## Зачем нужна фича
|
||||
|
||||
Нужно проработать маленький физический сервер на ESP32S3 как персональное или доверенное файловое хранилище SHiNE.
|
||||
|
||||
Идея: при обмене сообщениями пользователи смогут использовать такой сервер для хранения своих файлов, вложений, файлов общих переписок и связанных данных.
|
||||
|
||||
## Что нужно сделать
|
||||
|
||||
- Описать роль ESP32S3-сервера в общей архитектуре ключей и сессий.
|
||||
- Определить, какие ключи может хранить такое устройство.
|
||||
- Решить, хранит ли устройство только файлы или также подписывает пользовательские операции.
|
||||
- Описать протокол загрузки, скачивания и удаления файлов.
|
||||
- Определить правила шифрования файлов до отправки на устройство.
|
||||
- Продумать индексацию файлов для личных и общих переписок.
|
||||
- Решить, как устройство авторизуется на основном сервере SHiNE.
|
||||
|
||||
## Вопросы перед реализацией
|
||||
|
||||
- ESP32S3 должен работать как полностью локальное устройство или как публично доступный мини-сервер?
|
||||
- Нужен ли внешний relay, если устройство находится за NAT?
|
||||
- Какие ограничения по размеру файла считаем допустимыми?
|
||||
- Хранит ли устройство метаданные переписок или только зашифрованные blob-файлы?
|
||||
- Как восстанавливать доступ, если устройство потеряно или заменено?
|
||||
|
||||
## Что уже сделано
|
||||
|
||||
Код не реализован. Идея зафиксирована как будущая задача после описания модели ключей.
|
||||
|
||||
## Документы, которые нужно обновить при возврате
|
||||
|
||||
- `docs/Keys/README.md`
|
||||
- `docs/Personal_Messages/Протокол_DM_v1.md`
|
||||
- `docs/API/`
|
||||
- `docs/Blockchain/`, если появятся новые блоки или команды для файлов.
|
||||
|
||||
## С какого места продолжать
|
||||
|
||||
Начать с короткого протокольного документа: роли устройства, авторизация, шифрование файлов, минимальные API-операции и сценарии восстановления.
|
||||
@@ -1,105 +0,0 @@
|
||||
# Сессионные homeserver-ы в PDA пользователя
|
||||
|
||||
- Статус:
|
||||
`future`
|
||||
|
||||
- Горизонт:
|
||||
`medium`
|
||||
|
||||
- Ориентир:
|
||||
после завершения первого этапа по пользовательским сессиям
|
||||
|
||||
- Основание:
|
||||
Идея зафиксирована после обсуждения архитектуры пользовательских сессий и внутренних homeserver-ов. Сейчас задача сознательно отложена: сначала нужно аккуратно ввести базовую модель сессий, а затем возвращаться к расширенной серверной роли.
|
||||
|
||||
## Зачем нужна фича
|
||||
|
||||
У одного пользователя может быть несколько доверенных внутренних homeserver-ов, и каждый из них должен жить как отдельная пользовательская сессия, а не как отдельная особая сущность вне общей модели.
|
||||
|
||||
Это нужно, чтобы:
|
||||
|
||||
- хранить несколько homeserver-ов у одного пользователя одновременно;
|
||||
- различать обычные клиентские сессии и серверные сессии по явному типу;
|
||||
- дать расширяемый формат записи с версией;
|
||||
- использовать единый подход для DM, звонков и внутренних команд между сессиями.
|
||||
|
||||
## Целевая идея
|
||||
|
||||
В пользовательском PDA должен появиться список записей сессий, где каждая запись содержит как минимум:
|
||||
|
||||
- `sessionType` (`u8`);
|
||||
- `sessionVersion` (`u8`);
|
||||
- `sessionName`;
|
||||
- `sessionPubKey`.
|
||||
|
||||
Предварительные значения:
|
||||
|
||||
- тип `1` - обычная пользовательская сессия;
|
||||
- тип `100` - homeserver пользователя;
|
||||
- версия `1` - первая рабочая версия формата записи сессии.
|
||||
|
||||
На текущем этапе под это уже зарезервирован отдельный блок `SessionsBlock` с `block_type = 55`, а `TrustedStateBlock` остаётся на `50`.
|
||||
|
||||
Важно: homeserver-ов у одного пользователя может быть несколько.
|
||||
|
||||
## Архитектурный принцип
|
||||
|
||||
Внутренний протокол взаимодействия должен оставаться транспортным.
|
||||
|
||||
То есть SHiNE-сервер не должен разбирать прикладной смысл внутренней нагрузки homeserver-а, а должен:
|
||||
|
||||
- доставлять сообщения между сессиями;
|
||||
- доставлять сигналы звонков между сессиями;
|
||||
- хранить и маршрутизировать адресацию;
|
||||
- не принимать на себя бизнес-логику содержимого внутренних команд.
|
||||
|
||||
## Что уже подтверждается текущим кодом
|
||||
|
||||
- Личные сообщения уже доставляются по всем сессиям целевого пользователя с отдельным учётом доставки на каждую сессию.
|
||||
- Подтверждение доставки DM уже идёт отдельно по каждой сессии.
|
||||
- Вызов звонка уже рассылается по нескольким активным сессиям пользователя.
|
||||
- Сигналы звонка уже адресуются конкретной сессии, а stop-сигналы дублируются на остальные сессии того же пользователя.
|
||||
|
||||
Иными словами, текущая серверная логика ближе к модели "сервер доставляет между сессиями", чем к модели "сервер понимает внутренний протокол homeserver-а".
|
||||
|
||||
## Что нужно сделать при возврате к задаче
|
||||
|
||||
1. Согласовать финальный бинарный формат записи сессии в PDA пользователя.
|
||||
2. Проверить, не меняет ли это уже опубликованный формат пользовательской PDA-записи.
|
||||
3. Если формат PDA меняется, заранее предупредить пользователя и получить отдельное подтверждение.
|
||||
4. Решить, где именно хранится массив сессий:
|
||||
- в основной записи пользователя;
|
||||
- в отдельной PDA-структуре расширения;
|
||||
- или в смешанной схеме с базовой записью и внешними индексами.
|
||||
5. Зафиксировать ограничения:
|
||||
- максимальное число сессий;
|
||||
- максимальную длину `sessionName`;
|
||||
- правила удаления и обновления записи;
|
||||
- правила ротации `sessionPubKey`.
|
||||
6. Продумать, как UI и сервер будут отличать тип `1` и тип `100`.
|
||||
7. Определить, какие внутренние сообщения homeserver-а останутся полностью прозрачными для SHiNE-сервера, а какие потребуют только технической маршрутизации.
|
||||
8. Добавить API/операции чтения и обновления списка сессий, если для этого не хватит существующих механизмов.
|
||||
9. После реализации обязательно обновить документацию.
|
||||
|
||||
## Что нужно обновить при реализации
|
||||
|
||||
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`
|
||||
- `docs/Solana_Architecture/README.md`
|
||||
- `docs/Инициализация_Solana_регистрации/README.md`
|
||||
- `docs/Keys/README.md`
|
||||
- `docs/Personal_Messages/Протокол_DM_v1.md`, если изменится адресация DM по типам сессий
|
||||
- `docs/API/`, если появятся новые серверные операции или изменятся ответы
|
||||
|
||||
## Что пока не делать
|
||||
|
||||
- Не включать это автоматически в основной deploy сервера.
|
||||
- Не менять сейчас Solana PDA-формат без отдельного подтверждения.
|
||||
- Не добавлять временные поля в публичный API "на всякий случай".
|
||||
|
||||
## С какого места продолжать
|
||||
|
||||
Продолжать после завершения первой части:
|
||||
|
||||
1. описать минимальный формат записи пользовательской сессии;
|
||||
2. отдельно решить, живут ли homeserver-ы в том же списке, что и обычные сессии;
|
||||
3. затем уже проектировать операции регистрации, обновления и отключения таких сессий.
|
||||
@@ -1,44 +0,0 @@
|
||||
# Подключение других устройств через QR
|
||||
|
||||
- Горизонт:
|
||||
`medium`
|
||||
- Ориентир:
|
||||
позже, не сейчас
|
||||
- Статус:
|
||||
`future`
|
||||
|
||||
## Зачем нужна фича
|
||||
|
||||
Нужно нормально довести подключение другого устройства через QR-код. Сейчас есть полуготовая заготовка, но сценарий работает нестабильно и требует отдельной доработки.
|
||||
|
||||
## Что уже есть
|
||||
|
||||
- В UI уже есть экраны:
|
||||
- `shine-UI/js/pages/connect-device-view.js`
|
||||
- `shine-UI/js/pages/device-qr-view.js`
|
||||
- Есть сервис переноса ключей через QR:
|
||||
- `shine-UI/js/services/qr-key-transfer-service.js`
|
||||
- Логика частично собрана, но её нельзя считать завершённой или надёжной.
|
||||
|
||||
## Что нужно будет сделать потом
|
||||
|
||||
1. Проверить и довести формат QR-передачи.
|
||||
2. Проверить сканирование и ручной ввод QR-текста.
|
||||
3. Проверить перенос `device`, `blockchain`, `root` ключей только по реальному наличию на исходном устройстве.
|
||||
4. Проверить, что после переноса очищается старая история нужного логина и не ломается вход.
|
||||
5. Отдельно проверить сценарий без `BarcodeDetector`.
|
||||
6. Довести экран подтверждения на втором устройстве.
|
||||
|
||||
## Что сейчас важно
|
||||
|
||||
- Не считать эту часть готовой.
|
||||
- Не возвращать её в активную разработку без отдельной команды пользователя.
|
||||
- Если вернёмся к задаче, сначала нужно понять, что именно уже работает, а что нет, и потом починить целиком.
|
||||
|
||||
## Что обновить при возврате
|
||||
|
||||
- после реализации отдельно согласовать ручную проверку
|
||||
- `shine-UI/js/pages/connect-device-view.js`
|
||||
- `shine-UI/js/pages/device-qr-view.js`
|
||||
- `shine-UI/js/services/qr-key-transfer-service.js`
|
||||
- документацию по ключам, если формат переноса меняется
|
||||
@@ -1,29 +0,0 @@
|
||||
# Перенести старые сессионные сигналы на `SendSignal`
|
||||
|
||||
## Контекст
|
||||
|
||||
В проект добавлен новый общий межсессионный transport `SendSignal`.
|
||||
|
||||
Первое текущее применение:
|
||||
|
||||
- `remote AddBlock via homeserver session`
|
||||
|
||||
Старые сценарии пока оставлены на прежнем транспорте, чтобы не ломать уже работающий код.
|
||||
|
||||
## Что перенести позже
|
||||
|
||||
1. Звонковые сигналы, которые сейчас идут через `CallSignalToSession`.
|
||||
2. Старый wallet/ESP32 обмен, где технические команды всё ещё привязаны к call-like транспорту.
|
||||
3. Остальные доверенные межсессионные команды одного пользователя.
|
||||
|
||||
## Что важно учесть при переносе
|
||||
|
||||
- не ломать обратную совместимость работающих звонков;
|
||||
- сохранить текущую маршрутизацию по `sessionId`;
|
||||
- договориться о едином `signalType`;
|
||||
- отдельно описать миграцию клиентских обработчиков событий:
|
||||
- `IncomingCallSignal` -> `IncomingSignal`
|
||||
|
||||
## С какого сценария продолжать
|
||||
|
||||
Начинать перенос со звонков, но только после отдельной ручной проверки того, что `SendSignal` стабильно отработал на `remote AddBlock`.
|
||||
@@ -1,57 +0,0 @@
|
||||
# Переход с SQLite на PostgreSQL
|
||||
|
||||
## Зачем
|
||||
|
||||
Переход runtime-сервера на `PostgreSQL` уже выполнен, но после него остались хвосты в документации, именах, комментариях и части прямых SQL-запросов.
|
||||
|
||||
Этот TODO теперь нужен не для самого перехода, а для доведения проекта до полностью консистентного состояния после ухода от `SQLite`.
|
||||
|
||||
## Что сделать
|
||||
|
||||
- Дочистить документацию, где ещё описан `SQLite` как текущий runtime.
|
||||
- Убрать или переименовать legacy-названия и комментарии, которые уже не соответствуют PostgreSQL runtime.
|
||||
- Постепенно перенести оставшиеся прямые SQL-запросы из хэндлеров в DAO/service.
|
||||
- Проверить case-insensitive сравнения, уникальные ограничения и индексы уже в чисто PostgreSQL модели.
|
||||
- Отдельно пройтись по TODO/служебным документам и убрать ссылки на удалённые SQLite-классы как на актуальный код.
|
||||
|
||||
## Что уже есть в коде
|
||||
|
||||
- Доступ к БД в основном проходит через DAO-слой, а не полностью размазан по проекту.
|
||||
- Основная серверная логика уже разделена по модулям.
|
||||
- Runtime-сервер уже работает только с `PostgreSQL`.
|
||||
- Пустая БД инициализируется автоматически через `schema_v1`.
|
||||
|
||||
## Откуда продолжать
|
||||
|
||||
- Продолжать с зачистки legacy-документации и комментариев.
|
||||
- Затем добрать оставшиеся прямые SQL-запросы вне DAO.
|
||||
- После этого можно отдельно решать вопрос косметического переименования `*V2`, `DbController` и других переходных сущностей.
|
||||
|
||||
## Что потом обновить
|
||||
|
||||
- Серверную документацию по БД и миграциям.
|
||||
- Инструкции по локальному запуску сервера.
|
||||
- Скрипты деплоя и настройки окружения.
|
||||
|
||||
## Что временно отключено и что вернуть потом
|
||||
|
||||
- В серверном runtime временно снята проверка `channelName must not contain only digits`
|
||||
в `SHiNE-server/shine-server-db/src/main/java/shine/db/channels/ChannelNameRules.java`.
|
||||
- Причина: на боевой истории уже есть блоки с числовыми именами каналов, и сервер
|
||||
должен уметь с нуля восстановить `blockchain_state` и `.bch`, подтягивая старые
|
||||
блоки от других sync-серверов.
|
||||
- Что осталось как текущее поведение:
|
||||
- UI по-прежнему не даёт создать новый канал только из цифр;
|
||||
- сервер принимает такие имена, чтобы не ломать replay старых блоков.
|
||||
- Что нужно сделать отдельным следующим шагом:
|
||||
- вернуть серверное продуктовое правило для новых каналов;
|
||||
- сделать это совместимо со старой историей, чтобы импорт/реплей существующих
|
||||
блоков не падал на старых числовых channel name.
|
||||
- Какие документы обновить при возврате:
|
||||
- `docs/libs/shine-server-bd/POSTGRES_RUNTIME_SCHEMA_V1.md`;
|
||||
- UI/серверные документы по правилам имён каналов, если появится отдельная спецификация.
|
||||
- С какого сценария продолжать:
|
||||
- повторить cold start тест на `t2`: пустая PostgreSQL schema, удалённые `.bch`,
|
||||
новый запуск, ожидание полной синхронизации от `t1`/`t3`.
|
||||
- Последняя полная рабочая точка с этим временным компромиссом:
|
||||
- ветка `migration-postgres`, коммит будет создан после этой записи.
|
||||
@@ -1,71 +0,0 @@
|
||||
# Пополнение Solana и Arweave через внешний сервис покупки
|
||||
|
||||
- Горизонт:
|
||||
`near`
|
||||
- Ориентир:
|
||||
сегодня/завтра
|
||||
- Статус:
|
||||
`proposal`
|
||||
|
||||
## Кратко
|
||||
|
||||
Нужно добавить удобное пополнение кошельков на экране регистрации/кошелька: для Solana и Arweave дать отдельные действия `Пополнить`, которые ведут на международный сервис покупки криптовалюты с карты и помогают пользователю скопировать адрес кошелька.
|
||||
|
||||
## Пользовательский сценарий
|
||||
|
||||
1. Пользователь видит адрес кошелька Solana или Arweave.
|
||||
2. Нажимает `Пополнить`.
|
||||
3. Открывается промежуточное окно с инструкцией:
|
||||
- сейчас пользователь перейдёт на страницу покупки/пополнения;
|
||||
- нужно указать или проверить адрес кошелька;
|
||||
- после оплаты нужно закрыть внешнюю страницу и вернуться назад;
|
||||
- Solana обычно приходит быстро, ориентир 10-15 секунд после подтверждения сети;
|
||||
- Arweave может идти дольше, точное время нужно уточнить по выбранному сервису.
|
||||
4. В окне есть кнопки:
|
||||
- `Скопировать адрес и перейти`;
|
||||
- `Перейти без копирования`.
|
||||
5. Для Solana и Arweave используются разные окна/инструкции и, возможно, разные внешние ссылки.
|
||||
|
||||
## Что нужно сделать
|
||||
|
||||
1. Найти текущий экран, где показываются кошельки при регистрации и пополнении.
|
||||
2. Найти текущую ссылку покупки Arweave, если она уже есть в UI.
|
||||
3. Выбрать международный сервис покупки Solana с карты, не российский.
|
||||
4. Проверить, поддерживает ли сервис deep link с предзаполненным адресом кошелька.
|
||||
5. Если deep link невозможен, реализовать промежуточное окно с копированием адреса.
|
||||
6. Добавить отдельные действия для Solana и Arweave.
|
||||
7. Сделать текст инструкции коротким и понятным.
|
||||
8. Проверить, что адрес копируется в буфер обмена в браузере.
|
||||
9. Проверить мобильный сценарий и desktop-сценарий.
|
||||
|
||||
## Вопросы перед реализацией
|
||||
|
||||
1. Какой сервис покупки Solana использовать: тот же провайдер, что для Arweave, или другой международный on-ramp.
|
||||
2. Нужно ли разрешать покупку только SOL или также USDC/SPL-токены на Solana.
|
||||
3. Где именно показывать кнопку `Пополнить`: только регистрация, настройки кошелька или оба места.
|
||||
4. Нужно ли показывать предупреждение о комиссиях и стороннем сервисе.
|
||||
5. Нужно ли открывать внешнюю страницу в новой вкладке или в текущем окне.
|
||||
6. Нужно ли логировать факт нажатия `Пополнить` на сервере.
|
||||
7. Какой точный текст использовать для времени прихода Arweave.
|
||||
|
||||
## Риски и ограничения
|
||||
|
||||
- On-ramp-сервисы меняют ссылки и параметры, поэтому deep link нужно проверять перед реализацией.
|
||||
- Clipboard API может требовать HTTPS и пользовательский жест.
|
||||
- Нельзя обещать точное время поступления средств: лучше писать ориентир и зависимость от сети/провайдера.
|
||||
- Внешний сервис может быть недоступен в отдельных странах или для отдельных карт.
|
||||
|
||||
## Документы, которые обновить при реализации
|
||||
|
||||
- Документацию UI/кошельков, если такая есть.
|
||||
- после реализации отдельно согласовать ручную проверку.
|
||||
- `docs/API/`, только если появится новый серверный API или логирование.
|
||||
|
||||
## Минимальная проверка
|
||||
|
||||
1. На Solana-кошельке открывается правильное окно пополнения.
|
||||
2. Кнопка `Скопировать адрес и перейти` копирует Solana-адрес и открывает внешний сервис.
|
||||
3. Кнопка `Перейти без копирования` открывает внешний сервис без копирования.
|
||||
4. Аналогичный сценарий работает для Arweave.
|
||||
5. На мобильном экране текст и кнопки не перекрываются.
|
||||
6. Возврат назад в приложение не ломает состояние регистрации/кошелька.
|
||||
@@ -1,33 +0,0 @@
|
||||
# Убрать временную очистку `signed_messages_v2` в миграции БД v11
|
||||
|
||||
## Зачем это нужно
|
||||
|
||||
При переходе на новый DM-протокол `SHiNE_DM v1` была добавлена временная миграция БД `v11`, которая при первом старте на старой базе полностью очищает:
|
||||
|
||||
- `signed_messages_v2`
|
||||
- `signed_message_session_delivery`
|
||||
|
||||
Это сделано как защитный reset, потому что старые DM-строки и backlog могли быть несовместимы с новым форматом, новой логикой tombstone и новым клиентским E2EE-разбором.
|
||||
|
||||
## Что именно потом сделать
|
||||
|
||||
- найти историческое место, где была добавлена временная миграция `migrateToV11()`, и убрать её остатки из runtime-логики/документации;
|
||||
- удалить helper `clearLegacySignedMessagesForDmV11(...)`;
|
||||
- поднять версию схемы дальше обычным образом уже без destructive-cleanup;
|
||||
- при необходимости заменить это на нормальную точечную миграцию старых DM-записей или совсем убрать поддержку старой истории.
|
||||
|
||||
## Что уже есть в коде
|
||||
|
||||
- `LATEST_SCHEMA_VERSION = 11`;
|
||||
- при миграции в `v11` выполняется полная очистка DM-таблиц;
|
||||
- в коде прямо оставлен комментарий, что это временная мера.
|
||||
|
||||
## Откуда продолжать
|
||||
|
||||
Продолжать от коммита, в котором была добавлена миграция `v11` для очистки DM-таблиц после перехода на `SHiNE_DM v1`.
|
||||
|
||||
## Какие документы потом обновить
|
||||
|
||||
- `docs/Personal_Messages/Протокол_DM_v1.md`, если изменится стратегия миграции старой истории;
|
||||
- `docs/Personal_Messages/Формат_DM_v1.md`, если появится отдельное правило совместимости/конвертации;
|
||||
- при необходимости `docs/API/12_Direct_Messages_Push_Calls_API.md`, если затронется поведение backlog/доставки.
|
||||
@@ -0,0 +1,89 @@
|
||||
# ESP32 и wallet-extension: перейти на единый `SendSignal` и обязательную подпись `client key`
|
||||
|
||||
## Зачем
|
||||
|
||||
Сейчас в проекте уже есть универсальный transport `SendSignal`, который умеет работать и в режим `all_sessions`, и в режим `single_session`.
|
||||
|
||||
При этом в коде всё ещё живут старые call-like транспорты:
|
||||
|
||||
- `CallInviteBroadcast`
|
||||
- `CallSignalToSession`
|
||||
|
||||
Для web-call логики их можно постепенно убрать в пользу одного `SendSignal`.
|
||||
|
||||
Отдельный хвост остался в ESP32 homeserver/wallet-сценариях:
|
||||
|
||||
- в ESP32-скетче технический ответ wallet RPC всё ещё уходит через `CallSignalToSession`;
|
||||
- в ESP32 send-signal сценарии сейчас допускается режим только с `sessionSignatureB64`, без обязательной подписи `client key`;
|
||||
- такое поведение выбивается из общей модели безопасности и создаёт лишнюю специальную ветку.
|
||||
|
||||
## Что уже есть в коде
|
||||
|
||||
- универсальный серверный transport `SendSignal` уже существует и поддерживает:
|
||||
- `single_session`;
|
||||
- `all_sessions`.
|
||||
- web UI уже использует `SendSignal` в части межсессионных сценариев.
|
||||
- звонки пока ещё используют старые call-specific методы.
|
||||
- ESP32 homeserver main использует:
|
||||
- `CallSignalToSession` для wallet RPC response;
|
||||
- `SendSignal` для части технических ответов.
|
||||
- в ESP32 есть код генерации:
|
||||
- `sessionSignatureB64`;
|
||||
- `clientSignatureB64`;
|
||||
но для некоторых send-signal вызовов `client key` сейчас не обязателен.
|
||||
|
||||
## Что сделать
|
||||
|
||||
1. Перевести ESP32 сценарии со старого `CallSignalToSession` на `SendSignal(single_session)`.
|
||||
2. Убрать из ESP32-special flow странное исключение, где можно жить без `client key`.
|
||||
3. Переделать wallet-extension / связанный ESP32-клиент так, чтобы он тоже хранил `client key` и подписывал им `SendSignal`, как обычные клиенты SHiNE.
|
||||
4. Сделать `SendSignal` контрактно обязательным по подписи `client key`, а не только `session key`.
|
||||
5. После этого зачистить legacy call-like транспорт там, где он больше не нужен.
|
||||
6. Отдельно проверить, не осталось ли старых обработчиков, завязанных на:
|
||||
- `IncomingCallSignal`;
|
||||
- `IncomingCallInvite`;
|
||||
там, где должен работать общий `IncomingSignal`.
|
||||
|
||||
## Отдельно про шифрование
|
||||
|
||||
Нужно продумать будущее расширение: чтобы через `SendSignal` можно было передавать не только подписанные, но и зашифрованные payload.
|
||||
|
||||
Предварительный вывод по текущей архитектуре:
|
||||
|
||||
- для сервера это почти не должно требовать специальной логики;
|
||||
- сервер в `SendSignal` в основном:
|
||||
- валидирует подписи;
|
||||
- маршрутизирует событие в нужные сессии;
|
||||
- пересылает `data` дальше.
|
||||
|
||||
Если UI/ESP32 начнут передавать уже клиентски зашифрованный `data`, сервер должен это пережить без серьёзных изменений, пока:
|
||||
|
||||
- формат `data` остаётся строковым/JSON-совместимым;
|
||||
- сервер продолжает считать `SHA-256` от фактической передаваемой строки;
|
||||
- подписи строятся по уже зашифрованному payload.
|
||||
|
||||
То есть возможное E2E-шифрование `SendSignal` в будущем скорее относится к клиентским участникам протокола, а не к серверной маршрутизации.
|
||||
|
||||
## Что важно учесть при миграции
|
||||
|
||||
- не ломать текущие звонки и текущий wallet RPC сценарий одним большим рывком;
|
||||
- сначала перевести ESP32 и wallet-extension на единый `SendSignal`;
|
||||
- только потом убирать старые call-specific методы;
|
||||
- не оставлять “особый ESP32-режим” без `client key`;
|
||||
- проверить, что у расширения/ESP32 есть безопасное хранение `client key` и понятный сценарий восстановления/перепривязки.
|
||||
|
||||
## Откуда продолжать
|
||||
|
||||
- от `TODO/medium/2026-06-28_send_signal_перенос_старых_сигналов.md`;
|
||||
- от текущего ESP32 homeserver-скетча:
|
||||
- `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/main-device/shine_homeserver_main/shine_homeserver_main.ino`
|
||||
- от web UI auth/call transport логики:
|
||||
- `shine-UI/js/services/auth-service.js`
|
||||
- `shine-UI/js/services/call-service.js`
|
||||
|
||||
## Какие документы потом обновить
|
||||
|
||||
- `TODO/medium/2026-06-28_send_signal_перенос_старых_сигналов.md`;
|
||||
- `TODO/README.md`;
|
||||
- документацию по ESP32 homeserver/wallet flow, если появится отдельный утверждённый transport contract;
|
||||
- документацию по межсессионным сигналам, если будет зафиксирован единый список `signalType`.
|
||||
+8
@@ -1,5 +1,13 @@
|
||||
# Восстановить полную логику `shine_login_guard`
|
||||
|
||||
|
||||
Тоесть сделать что бы нормально проверялись логины пользователей
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Статус: отложено.
|
||||
|
||||
## Зачем это нужно
|
||||
@@ -0,0 +1 @@
|
||||
Передачу билетов владельцами со счёта на счёт
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
Баланс в салане
|
||||
|
||||
баланс в Арвив / и турбо
|
||||
|
||||
Балан лимит МБ / оно же сияния токены SHN
|
||||
@@ -0,0 +1,3 @@
|
||||
Сделать поддержку нескольких залогиненных аккаунтов враз тоесть что бы можно было менять акаунт под которым заходить
|
||||
- подумать о деталях, а так хорошая тема - и тестировать удобнее станет
|
||||
-
|
||||
-10
@@ -27,13 +27,3 @@
|
||||
|
||||
- `BlockchainTmpRecoveryOnStartup` и `BlockchainResyncRecoveryOnStartup` уже умеют добирать незавершённые хвосты после старта.
|
||||
- `AddBlock` уже стал crash-safe через `tmp_bch` / `write_check` / `write_pending`.
|
||||
|
||||
## Откуда продолжать
|
||||
|
||||
- начать с `systemd`-юнита и базового shutdown-hook в сервере;
|
||||
- затем проверить, что текущие операции реально завершаются в отведённые 30 секунд.
|
||||
|
||||
## Какие документы потом обновить
|
||||
|
||||
- `deploy/`;
|
||||
- `docs/Blockchain/sync-between-servers.md`, если изменится поведение остановки/восстановления.
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
Там были какието разные апи для ошибок звонка
|
||||
и для старта тестовых соединений
|
||||
|
||||
убрать короче лишнее
|
||||
|
||||
|
||||
и сделать номальное апи что бы клиент мог
|
||||
высылать уведомления об ошибках на сервер
|
||||
предлогал отправить уведомление
|
||||
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
Сделать что бы если сервера с исходящими сообщениями не могут доставить на входящие то
|
||||
- делать повторные попытки через время
|
||||
- если так и не получилось и никужа не доставлено уведомлять пользователя
|
||||
|
||||
- Так же как вариант собирать подписи что полученно входящее сообщение с серверов пользователя полчателя
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
Как_вариант_можно_сделать_hameserver_как_хранилище_файлов_пользователя
|
||||
И всё это можно сделать внутри ESP32
|
||||
|
||||
Хотя не понятно надо ли так делать - потому что вроде удобно,
|
||||
но тем не менее и сложно как то объяснить такой функционал людям
|
||||
@@ -1,43 +0,0 @@
|
||||
# Постоянный server-to-server WS и DM sync
|
||||
|
||||
## Зачем
|
||||
|
||||
Текущий production-режим SHiNE рассчитан на один основной сервер. Межсерверная синхронизация относится к будущей децентрализации и не должна блокировать выкладку односерверной production-версии.
|
||||
|
||||
Сейчас синхронизация между серверами работает в основном как periodic sync и one-shot push. Для нормальной репликации в будущем ещё нужен постоянный межсерверный канал:
|
||||
|
||||
- живое подключение к партнёру;
|
||||
- push новых блоков;
|
||||
- push DM;
|
||||
- ACK на доставку;
|
||||
- backoff/reconnect;
|
||||
- стартовый backfill.
|
||||
|
||||
## Что сделать
|
||||
|
||||
1. Поднять постоянное WebSocket-соединение между партнёрскими серверами.
|
||||
2. Сделать push новых блоков сразу после `AddBlock`.
|
||||
3. Сделать push DM-блоков между серверами.
|
||||
4. Добавить ACK и повторную отправку при сбое.
|
||||
5. Ввести стартовый обмен курсорами и добор хвоста.
|
||||
|
||||
## Что уже есть
|
||||
|
||||
- `ListBlockchainHeads`;
|
||||
- `GetBlockchainBlock`;
|
||||
- `GetSyncUserProfile`;
|
||||
- базовый periodic sync;
|
||||
- базовый backfill хвоста;
|
||||
- базовый full resync при divergence.
|
||||
|
||||
## Откуда продолжать
|
||||
|
||||
- от текущего `sync_servers` bootstrap и `PeriodicBlockchainSyncService`;
|
||||
- дальше выделить отдельный межсерверный transport layer.
|
||||
|
||||
## Какие документы потом обновить
|
||||
|
||||
- `docs/Blockchain/sync-between-servers.md`;
|
||||
- `docs/Personal_Messages/Протокол_DM_v1.md`;
|
||||
- `docs/Personal_Messages/Формат_DM_v1.md`;
|
||||
- `docs/API/`.
|
||||
@@ -1,16 +0,0 @@
|
||||
# Децентрализация
|
||||
|
||||
Папка для задач, которые нужны для будущего режима с несколькими серверами, Solana/PDA-синхронизацией и внешним хранением данных.
|
||||
|
||||
## Текущий статус
|
||||
|
||||
Сейчас production-режим SHiNE считается односерверным: один сервер обслуживает пользователей, сообщения, звонки и запись данных. Задачи из этой папки не являются блокерами для выкладки текущего репозитория на GitHub и запуска одного production-сервера.
|
||||
|
||||
## Задачи
|
||||
|
||||
- `односерверный_production_режим.md` - зафиксировать границы текущей production-версии.
|
||||
- `запись_блокчейнов_в_arweave.md` - вынести долговременную запись блокчейнов в Arweave.
|
||||
- `realtime_pda_solana_sync.md` - сделать онлайн-синхронизацию PDA/Solana в реальном времени.
|
||||
- `межсерверная_передача_сообщений.md` - реализовать доставку сообщений между серверами.
|
||||
- `межсерверные_звонки.md` - реализовать маршрутизацию звонков между серверами.
|
||||
- `2026-06-26_1805_межсерверный_ws_и_dm_sync.md` - старый план постоянного server-to-server WS и DM sync, перенесённый в контекст децентрализации.
|
||||
@@ -1,30 +0,0 @@
|
||||
# Realtime-синхронизация PDA и Solana
|
||||
|
||||
## Зачем
|
||||
|
||||
В будущем PDA-записи и Solana-состояние должны автоматически и быстро синхронизироваться с серверным состоянием, чтобы данные пользователей, homeserver-сессии и связанные записи не расходились.
|
||||
|
||||
## Что сделать
|
||||
|
||||
1. Определить, какие серверные события должны обновлять PDA.
|
||||
2. Добавить очередь/воркер для надёжной отправки изменений в Solana.
|
||||
3. Добавить периодическую сверку серверного состояния с PDA.
|
||||
4. Добавить обработку ошибок, повторов и конфликтов версий.
|
||||
5. Добавить мониторинг задержек и неуспешных Solana-транзакций.
|
||||
|
||||
## Что учесть
|
||||
|
||||
- Solana/Anchor-модуль находится в `shine-solana/shine/` и ведётся отдельно от основного server/UI deploy.
|
||||
- Перед изменениями внутри Solana-модуля нужно читать `shine-solana/shine/AGENTS.md`.
|
||||
- Основная инструкция по Solana-регистрации находится в `docs/Инициализация_Solana_регистрации/README.md`.
|
||||
- Формат пользовательской PDA-записи описан в `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`.
|
||||
|
||||
## Документы, которые потом нужно обновить
|
||||
|
||||
- `docs/Инициализация_Solana_регистрации/README.md`;
|
||||
- `docs/Solana_Architecture/README.md`;
|
||||
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`, если меняется формат PDA.
|
||||
|
||||
## Статус
|
||||
|
||||
Отложено до этапа децентрализации.
|
||||
@@ -1,29 +0,0 @@
|
||||
# Запись блокчейнов в Arweave
|
||||
|
||||
## Зачем
|
||||
|
||||
Для будущей децентрализации нужно долговременное внешнее хранение блокчейнов, чтобы данные не зависели только от одного серверного диска.
|
||||
|
||||
## Что сделать
|
||||
|
||||
1. Определить, какие блокчейны и какие диапазоны блоков записываются в Arweave.
|
||||
2. Зафиксировать формат пачки блоков, метаданных, ссылок и контрольных хэшей.
|
||||
3. Добавить безопасный механизм публикации без хранения приватного JWK в git.
|
||||
4. Добавить проверку уже загруженных диапазонов, чтобы не плодить дубли.
|
||||
5. Описать восстановление блокчейна из Arweave при потере локальных данных.
|
||||
|
||||
## Важные ограничения
|
||||
|
||||
- Любое изменение формата блокчейна требует отдельного предупреждения и явного подтверждения пользователя.
|
||||
- Добавление данных в блокчейн должно выполняться только через `AddBlock`.
|
||||
- Секреты Arweave нельзя хранить в репозитории.
|
||||
|
||||
## Документы, которые потом нужно обновить
|
||||
|
||||
- `docs/Blockchain/README.md`;
|
||||
- `docs/Blockchain/CHANGELOG.md`;
|
||||
- документы deploy/секретов в `deploy/`, если появятся новые параметры.
|
||||
|
||||
## Статус
|
||||
|
||||
Отложено до этапа децентрализации.
|
||||
@@ -1,30 +0,0 @@
|
||||
# Межсерверная передача сообщений
|
||||
|
||||
## Зачем
|
||||
|
||||
Когда у SHiNE появится несколько серверов, пользователи на разных серверах должны получать личные сообщения без ручной синхронизации и без привязки к одному центральному узлу.
|
||||
|
||||
## Что сделать
|
||||
|
||||
1. Определить протокол server-to-server доставки DM.
|
||||
2. Добавить маршрутизацию получателя по серверу, user id, публичному ключу или PDA.
|
||||
3. Добавить ACK, повторы, дедупликацию и backfill пропущенных сообщений.
|
||||
4. Разделить realtime-доставку и восстановление истории.
|
||||
5. Описать поведение при недоступности удалённого сервера.
|
||||
|
||||
## Что учесть
|
||||
|
||||
- Логика DM должна соответствовать документам в `docs/Personal_Messages/`.
|
||||
- При изменении формата signed DM-блока или правил доставки нужно обновлять протокол и байтовый формат DM.
|
||||
- Если появятся новые server API/WebSocket операции, нужно обновить `docs/API/`.
|
||||
|
||||
## Документы, которые потом нужно обновить
|
||||
|
||||
- `docs/Personal_Messages/Протокол_DM_v1.md`;
|
||||
- `docs/Personal_Messages/Формат_DM_v1.md`;
|
||||
- `docs/API/`;
|
||||
- `docs/API/09_Operations_Index.md`, если добавляются новые `op`.
|
||||
|
||||
## Статус
|
||||
|
||||
Отложено до этапа децентрализации.
|
||||
@@ -1,29 +0,0 @@
|
||||
# Межсерверные звонки
|
||||
|
||||
## Зачем
|
||||
|
||||
В будущем пользователи на разных серверах должны иметь возможность устанавливать звонки так же, как пользователи одного сервера.
|
||||
|
||||
## Что сделать
|
||||
|
||||
1. Определить протокол межсерверной сигнализации звонков.
|
||||
2. Добавить маршрутизацию offer/answer/ICE-кандидатов между серверами.
|
||||
3. Добавить обработку статусов занятости, отказа, таймаута и ошибок маршрута.
|
||||
4. Добавить диагностику доставки сигналов между серверами.
|
||||
5. Проверить совместимость с текущими логами `CallDeliveryReport`.
|
||||
|
||||
## Что учесть
|
||||
|
||||
- Специальная диагностика установки звонков идёт через `CallDeliveryReport`.
|
||||
- На production важно сохранять поля `reason`, `failureStage`, `pcConnectionState`, `pcIceConnectionState`, `routeLabel`, `configuredTurnHosts*`, `reachableTurnHosts*`.
|
||||
- Межсерверные звонки не должны ломать текущий односерверный сценарий.
|
||||
|
||||
## Документы, которые потом нужно обновить
|
||||
|
||||
- `docs/API/`, если добавляются или меняются операции сигнализации;
|
||||
- документы по звонкам/диагностике, если они будут выделены отдельно;
|
||||
- deploy-документы, если появятся новые параметры TURN/server-to-server маршрутизации.
|
||||
|
||||
## Статус
|
||||
|
||||
Отложено до этапа децентрализации.
|
||||
@@ -1,23 +0,0 @@
|
||||
# Односерверный production-режим
|
||||
|
||||
## Зачем
|
||||
|
||||
Перед выкладкой репозитория на GitHub и запуском production нужно явно зафиксировать, что текущая стабильная версия работает как один основной сервер.
|
||||
|
||||
## Что считаем текущей нормой
|
||||
|
||||
- Один production-сервер обслуживает пользователей, сообщения, звонки и серверные данные.
|
||||
- Децентрализованные сценарии не считаются обязательными для первого production-релиза.
|
||||
- Межсерверная доставка сообщений, межсерверные звонки, realtime PDA/Solana sync и запись блокчейнов в Arweave вынесены в отдельные будущие задачи.
|
||||
- Код и документация текущего production не должны создавать ожидание, что несколько серверов уже работают как единая realtime-сеть.
|
||||
|
||||
## Что сделать перед возвратом к децентрализации
|
||||
|
||||
1. Проверить актуальные документы по API, blockchain, DM и deploy.
|
||||
2. Выделить минимальный протокол server-to-server взаимодействия.
|
||||
3. Решить, какие данные остаются локальными, какие реплицируются между серверами, а какие записываются во внешнее долговременное хранилище.
|
||||
4. После изменения API, blockchain-форматов или DM-протокола обновить соответствующие документы по правилам проекта.
|
||||
|
||||
## Статус
|
||||
|
||||
Отложено. Текущий production работает как один сервер.
|
||||
@@ -0,0 +1,6 @@
|
||||
Сделать все эти: друг, близкий друг, и статусы типо сияет, точно сияет, реальный человек и т.д.
|
||||
|
||||
и как вариант не реальный человек тк
|
||||
- украли акаунт
|
||||
- сразу был нереальным мошенником
|
||||
- умер
|
||||
-275
@@ -1,275 +0,0 @@
|
||||
# Новая логика контента в блокчейне SHiNE
|
||||
|
||||
## Зачем это нужно
|
||||
|
||||
Сейчас блокчейн SHiNE хорошо умеет хранить обычные сообщения, ответы, лайки и связи между людьми.
|
||||
|
||||
Новая модель добавляет поверх этого более понятный смысл контента:
|
||||
|
||||
- обычный текст;
|
||||
- упражнение;
|
||||
- услуга / процедура;
|
||||
- курс;
|
||||
- стартовая страница канала (`entrypoint`).
|
||||
|
||||
Это нужно для того, чтобы канал стал не просто лентой постов, а полноценным пространством знаний, практик, услуг и сообществ.
|
||||
|
||||
## Что меняется для людей
|
||||
|
||||
### 1. В канале появятся понятные виды материалов
|
||||
|
||||
Сообщение можно будет создать не только как обычный текст, но и как:
|
||||
|
||||
- упражнение;
|
||||
- услугу / процедуру;
|
||||
- курс;
|
||||
- стартовую страницу канала.
|
||||
|
||||
Смысл в том, что приложение и сервер будут понимать, что это за материал, а не просто показывать любой текст одинаково.
|
||||
|
||||
### 2. У канала будет стартовая страница
|
||||
|
||||
У канала появится отдельное стартовое сообщение `entrypoint`.
|
||||
|
||||
Это не курс и не оглавление, а именно главная точка входа в канал:
|
||||
|
||||
- короткое объяснение, о чём канал;
|
||||
- описание структуры;
|
||||
- ссылки на нужные материалы;
|
||||
- удобное начало для новых людей.
|
||||
|
||||
У канала в каждый момент времени будет только одна актуальная стартовая страница.
|
||||
Если её исправляют, то сохраняется история версий.
|
||||
Если её удаляют, для интерфейса считается, что стартовой страницы у канала сейчас нет.
|
||||
|
||||
### 3. Курс, упражнение и услуга / процедура будут отличаться по смыслу
|
||||
|
||||
Это важно для логики и статистики.
|
||||
|
||||
- `Упражнение` — то, что человек может делать много раз.
|
||||
- `Услуга / процедура` — то, что тоже можно проходить много раз, но обычно с участием другого человека.
|
||||
- `Курс` — то, что можно начать, закончить или бросить.
|
||||
|
||||
За счёт этого сервер сможет честно считать активность, а интерфейс сможет показывать человеку именно те действия, которые подходят к данному типу материала.
|
||||
|
||||
### 4. Появятся статусные действия
|
||||
|
||||
На контент можно будет не только ответить или поставить лайк, но и отметить свой путь:
|
||||
|
||||
- сделал один раз;
|
||||
- заинтересовался и рассматривает;
|
||||
- начал;
|
||||
- закончил / освоил / знаю;
|
||||
- бросил.
|
||||
|
||||
При этом:
|
||||
|
||||
- для упражнений и услуг / процедур будет отдельно считаться, сколько раз человек сделал / прошёл;
|
||||
- для упражнений и курсов будет храниться текущий статус.
|
||||
|
||||
Текущий статус определяется просто:
|
||||
|
||||
- последнее статусное действие и считается актуальным.
|
||||
|
||||
Например:
|
||||
|
||||
- если последнее действие “заинтересовался и рассматривает”, значит человек присматривается, но ещё не начал;
|
||||
- если последнее действие `started`, значит материал сейчас в процессе;
|
||||
- если последнее действие `abandoned`, значит человек бросил;
|
||||
- если последнее действие `completed`, значит для системы он завершил / освоил материал.
|
||||
|
||||
### 5. К действиям можно добавлять живой текст
|
||||
|
||||
Практически любое статусное действие можно будет сопровождать коротким комментарием.
|
||||
|
||||
Например:
|
||||
|
||||
- “Начал изучать, потому что давно хотел разобраться”;
|
||||
- “Бросил, пока нет времени”;
|
||||
- “Прошёл процедуру, стало заметно легче”.
|
||||
|
||||
Это важно, потому что сам блокчейн будет хранить не только формальный статус, но и живую человеческую причину или заметку.
|
||||
|
||||
### 6. Появится подтверждение статуса другими людьми
|
||||
|
||||
Отдельный человек сможет подтвердить чей-то статус.
|
||||
|
||||
Примеры:
|
||||
|
||||
- подтвердить, что человек действительно занимался;
|
||||
- подтвердить, что он реально прошёл услугу;
|
||||
- подтвердить, что он освоил материал.
|
||||
|
||||
Подтверждение — это не замена статуса, а отдельное мнение / свидетельство со стороны.
|
||||
|
||||
### 7. Появится отдельный тип «мнение»
|
||||
|
||||
На любое сообщение можно будет ответить не только обычным ответом, но и специальным типом ответа: `мнение`.
|
||||
|
||||
Это по сути тоже текстовый ответ, но с отдельным смыслом:
|
||||
|
||||
- это отзыв;
|
||||
- это оценка;
|
||||
- это мнение о материале;
|
||||
- это явная метка для будущего анализа нейронками.
|
||||
|
||||
То есть:
|
||||
|
||||
- обычный ответ нужен для разговора;
|
||||
- `мнение` нужно для отзыва, оценки и анализа реакции людей.
|
||||
|
||||
## Что остаётся как раньше
|
||||
|
||||
### Комментарии
|
||||
|
||||
Обычные ответы на сообщения остаются.
|
||||
То есть обсуждение материалов не ломается и не меняется концептуально.
|
||||
|
||||
### Лайки контента
|
||||
|
||||
Лайк на сообщение, курс, упражнение или услугу остаётся обычной реакцией на конкретный блок.
|
||||
|
||||
### Лайк пользователю
|
||||
|
||||
Лайк пользователю не будет считаться реакцией на сообщение.
|
||||
Он относится к графу связей между людьми.
|
||||
|
||||
Это удобно, потому что:
|
||||
|
||||
- лайк человека — это отношение к человеку;
|
||||
- лайк материала — это отношение к контенту.
|
||||
|
||||
## Сообщество вокруг канала
|
||||
|
||||
Канал сможет работать не только как лента, но и как сообщество.
|
||||
|
||||
Для этого появятся простые действия:
|
||||
|
||||
- заявка на вступление;
|
||||
- самостоятельный выход;
|
||||
- принятие;
|
||||
- исключение.
|
||||
|
||||
Сервер сможет понимать:
|
||||
|
||||
- кто только подал заявку;
|
||||
- кто уже принят;
|
||||
- кто вышел;
|
||||
- кто был исключён.
|
||||
|
||||
## Личный канал и лента достижений
|
||||
|
||||
У каждого человека по смыслу появляется два важных пространства:
|
||||
|
||||
- канал его обычных постов;
|
||||
- отдельная лента его тренировок и достижений.
|
||||
|
||||
В обычном канале человек сможет:
|
||||
|
||||
- писать посты;
|
||||
- делиться мыслями;
|
||||
- публиковать материалы;
|
||||
- обсуждать темы как раньше.
|
||||
|
||||
А в ленте достижений будут видны его реальные действия:
|
||||
|
||||
- какие упражнения он делал;
|
||||
- какие услуги / процедуры проходил;
|
||||
- какие курсы его заинтересовали;
|
||||
- какие курсы он начал;
|
||||
- какие курсы он закончил;
|
||||
- что он бросил.
|
||||
|
||||
То есть блокчейн SHiNE сможет хранить не только слова человека, но и его путь, активность и историю практики.
|
||||
|
||||
## Что смогут делать авторы контента
|
||||
|
||||
Создатели контента в своих каналах смогут публиковать не только обычные посты, но и:
|
||||
|
||||
- упражнения;
|
||||
- курсы;
|
||||
- стартовую страницу канала;
|
||||
- услуги / процедуры, которые они оказывают.
|
||||
|
||||
Это превращает канал в сочетание:
|
||||
|
||||
- блога;
|
||||
- базы знаний;
|
||||
- пространства обучения;
|
||||
- каталога услуг и практик.
|
||||
|
||||
## Что увидит человек в интерфейсе
|
||||
|
||||
На специальных сообщениях в UI можно будет показывать отдельные кнопки действий.
|
||||
|
||||
Например:
|
||||
|
||||
- `Выполнил упражнение`
|
||||
- `Прошёл процедуру`
|
||||
- `Заинтересовало`
|
||||
- `Начал курс`
|
||||
- `Закончил курс`
|
||||
|
||||
То есть материал можно будет не просто прочитать, а сразу отметить реальное действие.
|
||||
|
||||
Также при ответе на любое сообщение можно будет выбрать:
|
||||
|
||||
- обычный ответ;
|
||||
- `мнение / отзыв`.
|
||||
|
||||
## Как будет работать лента достижений
|
||||
|
||||
Если кто-то зайдёт в твою ленту достижений, он сможет:
|
||||
|
||||
- прочитать, что ты делал;
|
||||
- оставить мнение / отзыв;
|
||||
- подтвердить, что это действительно было.
|
||||
|
||||
Это даёт основу для мягкой “сертификации” внутри SHiNE.
|
||||
|
||||
Например:
|
||||
|
||||
- человек прошёл курс и получил подтверждения;
|
||||
- человек прошёл процедуру и получил отзыв;
|
||||
- человек регулярно делает упражнения, и это видно в его истории.
|
||||
|
||||
Так постепенно у пользователя появляется не только лента постов, но и лента достижений, подтверждений и репутации.
|
||||
|
||||
## Ссылки внутри SHiNE
|
||||
|
||||
Для переходов между материалами вводятся простые внутренние адреса:
|
||||
|
||||
- обычная ссылка: `SHiNE/alice-001/157`
|
||||
- особополная ссылка: `SHiNE/alice-001/157/ХЭШ`
|
||||
|
||||
Первая форма — основная и каноническая.
|
||||
Вторая нужна там, где хочется добавить ещё и точную проверку по хэшу.
|
||||
|
||||
## Что это даёт в итоге
|
||||
|
||||
После внедрения новая блокчейн-логика позволит:
|
||||
|
||||
- строить каналы как структурированные пространства, а не просто как поток постов;
|
||||
- выделять упражнения, услуги и курсы как отдельные сущности;
|
||||
- показывать стартовую страницу канала;
|
||||
- хранить путь человека по материалу;
|
||||
- хранить отдельную ленту его действий и достижений;
|
||||
- считать активность и статусы;
|
||||
- подтверждать результаты другими людьми;
|
||||
- развивать сообщество вокруг канала.
|
||||
|
||||
И самое важное: всё это можно добавить как расширение уже существующего блокчейна SHiNE, не разрушая старую модель сообщений.
|
||||
|
||||
## Отдельный вопрос для будущего
|
||||
|
||||
Отзывы о людях как о людях — полезная идея, но её стоит дополнительно обдумать.
|
||||
|
||||
Например, на вкладке связей в будущем можно:
|
||||
|
||||
- писать человеку отзыв;
|
||||
- смотреть все отзывы о человеке;
|
||||
- выводить сначала отзывы близких друзей, родственников, друзей и контактов, а уже потом остальные.
|
||||
|
||||
Но этот слой нужно делать осторожно, чтобы он не стал слишком жёстким или неприятным для людей.
|
||||
|
||||
Поэтому отзывы о людях как отдельная социальная механика требуют дополнительного обсуждения и проектирования.
|
||||
-670
@@ -1,670 +0,0 @@
|
||||
# ТЗ: новая контентная модель блокчейна SHiNE
|
||||
|
||||
## Статус документа
|
||||
|
||||
Этот документ описывает предлагаемые новые типы блоков и правила их обработки.
|
||||
|
||||
Цель:
|
||||
|
||||
- добавить новую семантику контента;
|
||||
- не ломать существующие блоки `type=0..4`;
|
||||
- внедрить всё как расширение блокчейна за счёт новых форматов.
|
||||
|
||||
Документ является проектным ТЗ на реализацию в сервере, БД, API чтения и UI.
|
||||
|
||||
## 1. Базовые принципы
|
||||
|
||||
### 1.1. Совместимость
|
||||
|
||||
Старые типы не меняются:
|
||||
|
||||
- `type=0` — TECH
|
||||
- `type=1` — TEXT
|
||||
- `type=2` — REACTION
|
||||
- `type=3` — CONNECTION
|
||||
- `type=4` — USER_PARAM
|
||||
|
||||
Новые сущности и действия добавляются только как новые `type` и новые `body`.
|
||||
|
||||
Это означает:
|
||||
|
||||
- старые блоки продолжают читаться как раньше;
|
||||
- старые `TEXT_POST`, `TEXT_REPLY`, `REACTION_LIKE` и остальные форматы не ломаются;
|
||||
- существующий блокчейн остаётся валидным;
|
||||
- новый функционал появляется только там, где клиент и сервер умеют его понимать.
|
||||
|
||||
### 1.2. Общая стратегия
|
||||
|
||||
Новая модель делится на четыре слоя:
|
||||
|
||||
1. контентные сущности;
|
||||
2. текстовые отзывы и мнения;
|
||||
3. статусные действия пользователей;
|
||||
4. community-события вокруг канала.
|
||||
|
||||
### 1.3. Редактирование и удаление
|
||||
|
||||
Для новых контентных сущностей сохраняется действующий принцип SHiNE:
|
||||
|
||||
- редактирование всегда ссылается на оригинальный блок;
|
||||
- тип сущности edit не меняет;
|
||||
- удаление выполняется через `edit` с пустым текстом;
|
||||
- отдельный `DELETE`-подтип не вводится.
|
||||
|
||||
Это правило особенно важно для:
|
||||
|
||||
- `plain_text`
|
||||
- `exercise`
|
||||
- `service`
|
||||
- `course`
|
||||
- `entrypoint`
|
||||
|
||||
В пользовательских текстах и UI желательно использовать русские названия:
|
||||
|
||||
- обычный текст;
|
||||
- упражнение;
|
||||
- услуга / процедура;
|
||||
- курс;
|
||||
- стартовое сообщение канала.
|
||||
|
||||
## 2. Канонические внутренние ссылки
|
||||
|
||||
В новой модели поддерживаются только две формы внутренней ссылки:
|
||||
|
||||
- каноническая: `SHiNE/<blockchainName>/<blockNumber>`
|
||||
- особополная: `SHiNE/<blockchainName>/<blockNumber>/<blockHash>`
|
||||
|
||||
Примеры:
|
||||
|
||||
- `SHiNE/alice-001/157`
|
||||
- `SHiNE/alice-001/157/abcd1234...`
|
||||
|
||||
Правила:
|
||||
|
||||
- канонической считается именно короткая форма без хэша;
|
||||
- форма с хэшем используется как усиленный вариант для точной проверки;
|
||||
- внутри UI и серверной логики ссылка должна приводиться как минимум к паре:
|
||||
- `blockchainName`
|
||||
- `blockNumber`
|
||||
- если хэш присутствует, он участвует в дополнительной валидации ссылки.
|
||||
|
||||
## 3. Новые контентные сущности
|
||||
|
||||
## 3.1. Новый `type=5` — `CONTENT`
|
||||
|
||||
Назначение:
|
||||
|
||||
- хранение новых смысловых материалов канала;
|
||||
- сохранение линии канала;
|
||||
- поддержка edit-версий и логического удаления.
|
||||
|
||||
### 3.1.1. Подтипы `CONTENT`
|
||||
|
||||
- `subType=10` — `CONTENT_PLAIN`
|
||||
- `subType=11` — `CONTENT_EDIT_PLAIN`
|
||||
- `subType=20` — `CONTENT_EXERCISE`
|
||||
- `subType=21` — `CONTENT_EDIT_EXERCISE`
|
||||
- `subType=30` — `CONTENT_SERVICE`
|
||||
- `subType=31` — `CONTENT_EDIT_SERVICE`
|
||||
- `subType=40` — `CONTENT_COURSE`
|
||||
- `subType=41` — `CONTENT_EDIT_COURSE`
|
||||
- `subType=50` — `CONTENT_ENTRYPOINT`
|
||||
- `subType=51` — `CONTENT_EDIT_ENTRYPOINT`
|
||||
|
||||
### 3.1.2. Семантика подтипов
|
||||
|
||||
- `CONTENT_PLAIN` — обычный текст нового поколения.
|
||||
- `CONTENT_EXERCISE` — упражнение, которое можно выполнять многократно.
|
||||
- `CONTENT_SERVICE` — услуга / процедура, которую можно проходить многократно.
|
||||
- `CONTENT_COURSE` — курс / оглавление.
|
||||
- `CONTENT_ENTRYPOINT` — стартовое сообщение канала.
|
||||
|
||||
### 3.1.3. Почему `entrypoint` отдельный тип
|
||||
|
||||
`entrypoint` не считается курсом.
|
||||
|
||||
Это отдельная сущность, потому что:
|
||||
|
||||
- она описывает вход в канал;
|
||||
- по ней нельзя делать `started / completed / abandoned`;
|
||||
- у канала в каждый момент времени должна быть только одна актуальная стартовая страница.
|
||||
|
||||
### 3.1.4. Ограничение на `entrypoint`
|
||||
|
||||
Для одного канала допускается только один исходный блок `CONTENT_ENTRYPOINT`.
|
||||
|
||||
Правила:
|
||||
|
||||
- если entrypoint уже существует, создать второй нельзя;
|
||||
- изменять можно только через `CONTENT_EDIT_ENTRYPOINT`;
|
||||
- если entrypoint логически удалён, UI должен считать, что стартовой страницы больше нет;
|
||||
- исторический блок при этом остаётся в цепочке.
|
||||
|
||||
### 3.1.5. Формат body для `CONTENT_*`
|
||||
|
||||
Для `version=1` рекомендуется использовать формат, максимально совместимый по логике с текущими `TEXT_POST` / `TEXT_EDIT_POST`.
|
||||
|
||||
#### Создающие блоки
|
||||
|
||||
Для:
|
||||
|
||||
- `CONTENT_PLAIN`
|
||||
- `CONTENT_EXERCISE`
|
||||
- `CONTENT_SERVICE`
|
||||
- `CONTENT_COURSE`
|
||||
- `CONTENT_ENTRYPOINT`
|
||||
|
||||
body:
|
||||
|
||||
```text
|
||||
ContentLineBody_v1
|
||||
- lineCode: int32
|
||||
- prevLineNumber: int32
|
||||
- prevLineHash32: [32]
|
||||
- thisLineNumber: int32
|
||||
- textLenBytes: uint16
|
||||
- text UTF-8
|
||||
```
|
||||
|
||||
#### Edit-блоки
|
||||
|
||||
Для:
|
||||
|
||||
- `CONTENT_EDIT_PLAIN`
|
||||
- `CONTENT_EDIT_EXERCISE`
|
||||
- `CONTENT_EDIT_SERVICE`
|
||||
- `CONTENT_EDIT_COURSE`
|
||||
- `CONTENT_EDIT_ENTRYPOINT`
|
||||
|
||||
body:
|
||||
|
||||
```text
|
||||
ContentEditBody_v1
|
||||
- lineCode: int32
|
||||
- prevLineNumber: int32
|
||||
- prevLineHash32: [32]
|
||||
- thisLineNumber: int32
|
||||
- toBlockGlobalNumber: int32
|
||||
- toBlockHash32: [32]
|
||||
- textLenBytes: uint16
|
||||
- text UTF-8
|
||||
```
|
||||
|
||||
Правила:
|
||||
|
||||
- edit всегда ссылается на оригинальный блок соответствующего типа;
|
||||
- `toBlockchainName` в edit не хранится;
|
||||
- `textLen=0` означает логическое удаление содержимого;
|
||||
- тип исходной сущности edit не меняет.
|
||||
|
||||
### 3.1.6. Что считается комментарием
|
||||
|
||||
Комментарии не требуют нового формата.
|
||||
|
||||
Для обсуждения новых контентных сущностей продолжают использоваться уже существующие:
|
||||
|
||||
- `TEXT_REPLY`
|
||||
- `TEXT_EDIT_REPLY`
|
||||
|
||||
Это позволяет не ломать старую reply-механику и reuse текущую модель тредов.
|
||||
|
||||
## 4. Текстовые отзывы
|
||||
|
||||
## 4.1. Новый `type=6` — `TEXT_RATING`
|
||||
|
||||
Назначение:
|
||||
|
||||
- текстовая оценка / отзыв на объект;
|
||||
- без числовой шкалы;
|
||||
- с возможностью редактирования и логического удаления.
|
||||
|
||||
Смысл `TEXT_RATING`:
|
||||
|
||||
- это текст;
|
||||
- это специальный отзыв / мнение / оценка;
|
||||
- это явный сигнал, что перед нами не просто комментарий, а осмысленный отзыв;
|
||||
- в будущем это поле можно отдельно анализировать нейронками.
|
||||
|
||||
### 4.1.1. Подтипы
|
||||
|
||||
- `subType=10` — `TEXT_RATING_POST`
|
||||
- `subType=11` — `TEXT_RATING_EDIT`
|
||||
|
||||
### 4.1.2. Где разрешён `TEXT_RATING_POST`
|
||||
|
||||
Разрешён на target:
|
||||
|
||||
- `HEADER` пользователя;
|
||||
- контентный блок `type=5`;
|
||||
- при необходимости в будущем — на другие target-блоки по отдельному решению.
|
||||
|
||||
Сейчас в данном ТЗ:
|
||||
|
||||
- отзыв / оценка на пользователя — да;
|
||||
- отзыв / оценка на контент — да;
|
||||
- отзыв / лайк на канал целиком — не вводится, только оставляется как будущая возможность.
|
||||
|
||||
### 4.1.3. Где и как используется `TEXT_RATING_POST`
|
||||
|
||||
`TEXT_RATING_POST` можно создавать:
|
||||
|
||||
- как отзыв на контентный блок;
|
||||
- как отзыв на пользователя через target на `HEADER`;
|
||||
- как специальный ответ вместо обычного комментария.
|
||||
|
||||
Практическое правило для UI:
|
||||
|
||||
- при ответе на любое сообщение пользователь может выбрать:
|
||||
- обычный ответ;
|
||||
- `мнение / отзыв`.
|
||||
|
||||
### 4.1.4. Формат body
|
||||
|
||||
#### Создание
|
||||
|
||||
```text
|
||||
TextRatingBody_v1
|
||||
- toBlockchainNameLen: uint8
|
||||
- toBlockchainName UTF-8
|
||||
- toBlockGlobalNumber: int32
|
||||
- toBlockHash32: [32]
|
||||
- textLenBytes: uint16
|
||||
- text UTF-8
|
||||
```
|
||||
|
||||
#### Редактирование
|
||||
|
||||
```text
|
||||
TextRatingEditBody_v1
|
||||
- toBlockGlobalNumber: int32
|
||||
- toBlockHash32: [32]
|
||||
- textLenBytes: uint16
|
||||
- text UTF-8
|
||||
```
|
||||
|
||||
Правила:
|
||||
|
||||
- edit ссылается на оригинальный `TEXT_RATING_POST`;
|
||||
- пустой текст в edit означает логическое удаление отзыва.
|
||||
|
||||
## 5. Статусные действия и накопительные события
|
||||
|
||||
## 5.1. Новый `type=7` — `STATUS_ACTION`
|
||||
|
||||
Назначение:
|
||||
|
||||
- хранение действий пользователя по отношению к контенту;
|
||||
- вычисление текущего статуса;
|
||||
- накопительный учёт повторных прохождений;
|
||||
- подтверждение статусов другими людьми.
|
||||
|
||||
### 5.1.1. Подтипы
|
||||
|
||||
- `subType=10` — `STATUS_DONE_ONCE`
|
||||
- `subType=20` — `STATUS_INTERESTED`
|
||||
- `subType=30` — `STATUS_STARTED`
|
||||
- `subType=40` — `STATUS_COMPLETED`
|
||||
- `subType=50` — `STATUS_ABANDONED`
|
||||
- `subType=60` — `STATUS_CONFIRMED`
|
||||
|
||||
### 5.1.2. Матрица допустимости по контенту
|
||||
|
||||
`STATUS_DONE_ONCE` разрешён только для:
|
||||
|
||||
- `CONTENT_EXERCISE`
|
||||
- `CONTENT_SERVICE`
|
||||
|
||||
`STATUS_INTERESTED`, `STATUS_STARTED`, `STATUS_COMPLETED`, `STATUS_ABANDONED` разрешены только для:
|
||||
|
||||
- `CONTENT_EXERCISE`
|
||||
- `CONTENT_COURSE`
|
||||
|
||||
`CONTENT_ENTRYPOINT` не поддерживает:
|
||||
|
||||
- `interested`
|
||||
- `started`
|
||||
- `completed`
|
||||
- `abandoned`
|
||||
|
||||
### 5.1.3. Как считать текущее состояние
|
||||
|
||||
Для пары:
|
||||
|
||||
- `actorLogin`
|
||||
- `targetBlock`
|
||||
|
||||
актуальным статусом считается последнее по времени статусное событие из набора:
|
||||
|
||||
- `STATUS_INTERESTED`
|
||||
- `STATUS_STARTED`
|
||||
- `STATUS_COMPLETED`
|
||||
- `STATUS_ABANDONED`
|
||||
|
||||
Следствия:
|
||||
|
||||
- у одного пользователя по одному объекту в каждый момент времени только один актуальный статус;
|
||||
- если последним пришёл `interested`, статус считается “заинтересовался / рассматривает, но ещё не начал”;
|
||||
- если последним пришёл `started`, статус считается “в процессе”;
|
||||
- если последним пришёл `completed`, статус считается “завершён / освоен / знаю”;
|
||||
- если последним пришёл `abandoned`, статус считается “брошен”.
|
||||
|
||||
### 5.1.4. Как считать количество прохождений
|
||||
|
||||
`STATUS_DONE_ONCE` не меняет текущий статус.
|
||||
|
||||
Он считается отдельно как накопительное событие.
|
||||
|
||||
Сервер должен уметь считать:
|
||||
|
||||
- сколько раз пользователь сделал упражнение;
|
||||
- сколько раз пользователь прошёл услугу / процедуру.
|
||||
|
||||
### 5.1.5. Дополнительный текст действия
|
||||
|
||||
Каждое действие `STATUS_*` может содержать дополнительный текст-комментарий.
|
||||
|
||||
Примеры:
|
||||
|
||||
- как именно делал упражнение;
|
||||
- чем заинтересовал курс;
|
||||
- с какими мыслями начал курс;
|
||||
- почему бросил;
|
||||
- что именно подтверждает подтверждающий человек.
|
||||
|
||||
### 5.1.6. Подтверждение статуса
|
||||
|
||||
`STATUS_CONFIRMED` разрешён только на target-статусы:
|
||||
|
||||
- `STATUS_DONE_ONCE`
|
||||
- `STATUS_INTERESTED`
|
||||
- `STATUS_STARTED`
|
||||
- `STATUS_COMPLETED`
|
||||
- `STATUS_ABANDONED`
|
||||
|
||||
Это значит:
|
||||
|
||||
- подтверждение не ставится прямо на курс или упражнение;
|
||||
- подтверждение ставится на конкретный статусный блок другого человека.
|
||||
|
||||
Подтверждение:
|
||||
|
||||
- не меняет основной статус автора;
|
||||
- не меняет счётчик `done_once`;
|
||||
- хранится как отдельное мнение / свидетельство.
|
||||
|
||||
### 5.1.7. Формат body
|
||||
|
||||
Для `STATUS_DONE_ONCE`, `STATUS_INTERESTED`, `STATUS_STARTED`, `STATUS_COMPLETED`, `STATUS_ABANDONED`:
|
||||
|
||||
```text
|
||||
StatusActionBody_v1
|
||||
- toBlockchainNameLen: uint8
|
||||
- toBlockchainName UTF-8
|
||||
- toBlockGlobalNumber: int32
|
||||
- toBlockHash32: [32]
|
||||
- noteLenBytes: uint16
|
||||
- note UTF-8
|
||||
```
|
||||
|
||||
Для `STATUS_CONFIRMED`:
|
||||
|
||||
```text
|
||||
StatusConfirmBody_v1
|
||||
- toBlockchainNameLen: uint8
|
||||
- toBlockchainName UTF-8
|
||||
- toBlockGlobalNumber: int32
|
||||
- toBlockHash32: [32]
|
||||
- noteLenBytes: uint16
|
||||
- note UTF-8
|
||||
```
|
||||
|
||||
На уровне бинарного формата тело можно оставить одинаковым.
|
||||
Различие задаётся `subType` и правилами валидации target.
|
||||
|
||||
## 6. Community-события
|
||||
|
||||
## 6.1. Новый `type=8` — `COMMUNITY_EVENT`
|
||||
|
||||
Назначение:
|
||||
|
||||
- заявки в сообщество;
|
||||
- выход из сообщества;
|
||||
- принятие;
|
||||
- исключение.
|
||||
|
||||
### 6.1.1. Подтипы
|
||||
|
||||
- `subType=10` — `COMMUNITY_JOIN_REQUEST`
|
||||
- `subType=20` — `COMMUNITY_LEAVE`
|
||||
- `subType=30` — `COMMUNITY_ACCEPT`
|
||||
- `subType=40` — `COMMUNITY_REMOVE`
|
||||
|
||||
### 6.1.2. Базовая логика
|
||||
|
||||
`COMMUNITY_JOIN_REQUEST`
|
||||
|
||||
- создаёт пользователь;
|
||||
- target — `CONTENT_ENTRYPOINT` канала;
|
||||
- может содержать текст заявки.
|
||||
|
||||
`COMMUNITY_LEAVE`
|
||||
|
||||
- создаёт сам участник;
|
||||
- target — `CONTENT_ENTRYPOINT` канала;
|
||||
- подтверждение не требуется;
|
||||
- может содержать текст.
|
||||
|
||||
`COMMUNITY_ACCEPT`
|
||||
|
||||
- создаёт владелец канала;
|
||||
- target — конкретный блок `COMMUNITY_JOIN_REQUEST`;
|
||||
- может содержать текст.
|
||||
|
||||
`COMMUNITY_REMOVE`
|
||||
|
||||
- создаёт владелец канала;
|
||||
- target — `CONTENT_ENTRYPOINT` канала;
|
||||
- body дополнительно хранит `subjectLogin`, кого исключили;
|
||||
- может содержать текст.
|
||||
|
||||
### 6.1.3. Текущее членство
|
||||
|
||||
Пользователь считается текущим участником сообщества, если:
|
||||
|
||||
- у него есть хотя бы одно принятие в это сообщество;
|
||||
- после этого принятия нет более позднего:
|
||||
- `COMMUNITY_LEAVE`
|
||||
- `COMMUNITY_REMOVE`
|
||||
|
||||
Заявка сама по себе членство не создаёт.
|
||||
|
||||
### 6.1.4. Формат body
|
||||
|
||||
Для `JOIN_REQUEST` и `LEAVE`:
|
||||
|
||||
```text
|
||||
CommunityActionBody_v1
|
||||
- toBlockchainNameLen: uint8
|
||||
- toBlockchainName UTF-8
|
||||
- toBlockGlobalNumber: int32
|
||||
- toBlockHash32: [32]
|
||||
- noteLenBytes: uint16
|
||||
- note UTF-8
|
||||
```
|
||||
|
||||
Для `ACCEPT`:
|
||||
|
||||
```text
|
||||
CommunityAcceptBody_v1
|
||||
- toBlockchainNameLen: uint8
|
||||
- toBlockchainName UTF-8
|
||||
- toBlockGlobalNumber: int32
|
||||
- toBlockHash32: [32]
|
||||
- noteLenBytes: uint16
|
||||
- note UTF-8
|
||||
```
|
||||
|
||||
Для `REMOVE`:
|
||||
|
||||
```text
|
||||
CommunityRemoveBody_v1
|
||||
- toBlockchainNameLen: uint8
|
||||
- toBlockchainName UTF-8
|
||||
- toBlockGlobalNumber: int32
|
||||
- toBlockHash32: [32]
|
||||
- subjectLoginLen: uint8
|
||||
- subjectLogin ASCII
|
||||
- noteLenBytes: uint16
|
||||
- note UTF-8
|
||||
```
|
||||
|
||||
## 7. Что остаётся на старых типах
|
||||
|
||||
### 7.0. Обычный канал и лента достижений
|
||||
|
||||
На уровне продукта рекомендуется различать:
|
||||
|
||||
- обычный канал постов пользователя;
|
||||
- отдельную ленту его действий и достижений.
|
||||
|
||||
В обычном канале пользователь:
|
||||
|
||||
- пишет посты;
|
||||
- публикует материалы;
|
||||
- общается и обсуждает.
|
||||
|
||||
В ленте достижений видны события:
|
||||
|
||||
- какие упражнения он делал;
|
||||
- какие услуги / процедуры проходил;
|
||||
- какие курсы его заинтересовали;
|
||||
- какие курсы он начал;
|
||||
- какие курсы он завершил;
|
||||
- что он бросил.
|
||||
|
||||
В данном ТЗ эта модель фиксируется как продуктовая логика.
|
||||
Конкретный способ хранения можно реализовать:
|
||||
|
||||
- либо отдельным специальным каналом;
|
||||
- либо отдельным режимом чтения по статусным блокам.
|
||||
|
||||
### 7.1. Лайк пользователю
|
||||
|
||||
Лайк пользователю не вводится как `REACTION`.
|
||||
|
||||
Он остаётся в слое социальных связей:
|
||||
|
||||
- через `CONNECTION`
|
||||
- как будущий отдельный подтип связи
|
||||
|
||||
В этом ТЗ сам новый подтип связи не описывается детально.
|
||||
Нужно только зафиксировать правило:
|
||||
|
||||
- лайк человека относится к графу связей, а не к реакции на блок.
|
||||
|
||||
### 7.2. Лайк контента
|
||||
|
||||
Лайк на:
|
||||
|
||||
- `CONTENT_PLAIN`
|
||||
- `CONTENT_EXERCISE`
|
||||
- `CONTENT_SERVICE`
|
||||
- `CONTENT_COURSE`
|
||||
- `CONTENT_ENTRYPOINT`
|
||||
|
||||
может использовать уже существующий:
|
||||
|
||||
- `REACTION_LIKE`
|
||||
- `REACTION_UNLIKE`
|
||||
|
||||
Отдельный новый формат для лайка контента не нужен.
|
||||
|
||||
### 7.3. Канал целиком
|
||||
|
||||
В текущем ТЗ не вводятся:
|
||||
|
||||
- отзыв на канал целиком;
|
||||
- лайк канала целиком.
|
||||
|
||||
Это оставляется как будущая возможность.
|
||||
|
||||
### 7.4. Отзывы о людях
|
||||
|
||||
Отзывы о человеке как о человеке в текущем ТЗ допустимы через `TEXT_RATING` на `HEADER`.
|
||||
|
||||
Но продуктовую модель их показа нужно отдельно продумать.
|
||||
|
||||
Направление для будущего:
|
||||
|
||||
- просмотр отзывов о человеке на вкладке связей;
|
||||
- приоритетный вывод отзывов от близких друзей, родственников, друзей и контактов;
|
||||
- затем вывод остальных отзывов.
|
||||
|
||||
Эта тема полезна, но требует дополнительной осторожной проработки с точки зрения UX и социальных рисков.
|
||||
|
||||
## 8. Требования к серверу
|
||||
|
||||
Сервер после внедрения должен уметь:
|
||||
|
||||
1. Валидировать новые `type=5..8`.
|
||||
2. Хранить новые блоки без ломки старого чтения.
|
||||
3. Определять текущий статус пользователя по объекту:
|
||||
- `interested`
|
||||
- `started`
|
||||
- `completed`
|
||||
- `abandoned`
|
||||
4. Считать накопительные события `done_once` для:
|
||||
- `exercise`
|
||||
- `service`
|
||||
5. Считать подтверждения статусов.
|
||||
6. Определять единственный актуальный `entrypoint` канала.
|
||||
7. Определять текущее членство в сообществе канала.
|
||||
8. Поддерживать внутренние ссылки вида:
|
||||
- `SHiNE/<blockchainName>/<blockNumber>`
|
||||
- `SHiNE/<blockchainName>/<blockNumber>/<blockHash>`
|
||||
|
||||
## 9. Требования к UI
|
||||
|
||||
UI после внедрения должен уметь:
|
||||
|
||||
1. Показывать разные карточки для:
|
||||
- текста
|
||||
- упражнения
|
||||
- услуги
|
||||
- курса
|
||||
- entrypoint
|
||||
2. Показывать стартовую страницу канала, если `entrypoint` существует.
|
||||
3. Не показывать entrypoint, если он логически удалён.
|
||||
4. Давать человеку только допустимые действия по типу материала.
|
||||
5. Показывать:
|
||||
- текущий статус;
|
||||
- количество `done_once`;
|
||||
- подтверждения статуса.
|
||||
6. Показывать отдельные действия-кнопки на специальных блоках, например:
|
||||
- `Выполнил упражнение`
|
||||
- `Прошёл процедуру`
|
||||
- `Заинтересовало`
|
||||
- `Начал курс`
|
||||
- `Закончил курс`
|
||||
7. При ответе на сообщение давать выбор:
|
||||
- обычный ответ;
|
||||
- `мнение / отзыв`.
|
||||
8. Открывать внутренние ссылки SHiNE.
|
||||
|
||||
## 10. Вывод по совместимости
|
||||
|
||||
Предлагаемая модель реализуема без слома старого блокчейна.
|
||||
|
||||
Причина:
|
||||
|
||||
- старые `type=0..4` не меняются;
|
||||
- новые сущности вводятся только как новые `type=5..8`;
|
||||
- существующие `reply`, `like`, `edit`, `HEADER`, `CREATE_CHANNEL` и `CONNECTION` продолжают работать как раньше;
|
||||
- старые клиенты смогут игнорировать новые типы как неизвестные;
|
||||
- новые клиенты смогут постепенно включать поддержку нового функционала.
|
||||
|
||||
Итог:
|
||||
|
||||
- это расширение формата блокчейна;
|
||||
- это не миграция со сломом старых блоков;
|
||||
- это можно внедрять поэтапно.
|
||||
@@ -0,0 +1,2 @@
|
||||
Доделть мелочи
|
||||
и разместить проект нормально на гитхаб
|
||||
@@ -0,0 +1,7 @@
|
||||
# Межсерверные звонки
|
||||
|
||||
Доделать Звонки что бы работало как сигнал о том что вызов идёт.
|
||||
|
||||
и
|
||||
Пользователи на разных серверах должны иметь возможность устанавливать звонки так же, как пользователи одного сервера.
|
||||
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
client.version=1.5.4
|
||||
server.version=1.4.5
|
||||
client.version=1.6.0
|
||||
server.version=1.5.0
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
backup.schema.version=1
|
||||
backup.full.version=2
|
||||
last.full.backup.date=2026-07-10
|
||||
backup.schema.version=2
|
||||
backup.full.version=3
|
||||
last.full.backup.date=2026-08-08
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
cld9-012186
|
||||
p702072.kvmvps
|
||||
---
|
||||
Linux cld9-012186 6.8.0-124-generic #124-Ubuntu SMP PREEMPT_DYNAMIC Tue May 26 13:00:45 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux
|
||||
Linux p702072.kvmvps 5.15.0-187-generic #197-Ubuntu SMP Fri Jul 17 19:17:01 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux
|
||||
---
|
||||
Filesystem Size Used Avail Use% Mounted on
|
||||
tmpfs 392M 1.2M 391M 1% /run
|
||||
/dev/vda2 32G 14G 17G 46% /
|
||||
tmpfs 2.0G 0 2.0G 0% /dev/shm
|
||||
tmpfs 197M 1.2M 196M 1% /run
|
||||
/dev/sda1 40G 6.7G 31G 18% /
|
||||
tmpfs 982M 0 982M 0% /dev/shm
|
||||
tmpfs 5.0M 0 5.0M 0% /run/lock
|
||||
tmpfs 392M 12K 392M 1% /run/user/1002
|
||||
tmpfs 197M 0 197M 0% /run/user/1000
|
||||
---
|
||||
NAME FSTYPE FSVER LABEL UUID FSAVAIL FSUSE% MOUNTPOINTS
|
||||
sr0 iso9660 Joliet Extension cidata 2026-06-02-12-22-36-00
|
||||
sr1
|
||||
vda
|
||||
├─vda1
|
||||
└─vda2 ext4 1.0 c422dce2-e6a3-4ce4-a9c1-17ab14e9193a 16.1G 44% /
|
||||
NAME FSTYPE FSVER LABEL UUID FSAVAIL FSUSE% MOUNTPOINTS
|
||||
sda
|
||||
└─sda1 ext4 1.0 42e8f44f-2dd9-4fc6-8485-6581d6528df5 30.6G 17% /
|
||||
sr0
|
||||
|
||||
@@ -1,13 +1,48 @@
|
||||
UNIT FILE STATE PRESET
|
||||
agent-memory.service enabled enabled
|
||||
caddy.service enabled enabled
|
||||
containerd.service enabled enabled
|
||||
coturn.service enabled enabled
|
||||
docker.service enabled enabled
|
||||
elaira-agent.service enabled enabled
|
||||
hermes-dashboard.service enabled enabled
|
||||
hermes-gateway.service enabled enabled
|
||||
shine-server.service enabled enabled
|
||||
ubuntu-fan.service enabled enabled
|
||||
UNIT FILE STATE VENDOR PRESET
|
||||
apparmor.service enabled enabled
|
||||
blk-availability.service enabled enabled
|
||||
caddy.service enabled enabled
|
||||
console-setup.service enabled enabled
|
||||
containerd.service enabled enabled
|
||||
coturn.service enabled enabled
|
||||
cron.service enabled enabled
|
||||
dmesg.service enabled enabled
|
||||
docker.service enabled enabled
|
||||
e2scrub_reap.service enabled enabled
|
||||
finalrd.service enabled enabled
|
||||
getty@.service enabled enabled
|
||||
gpu-manager.service enabled enabled
|
||||
grub-common.service enabled enabled
|
||||
grub-initrd-fallback.service enabled enabled
|
||||
guestfs-firstboot.service enabled enabled
|
||||
irqbalance.service enabled enabled
|
||||
keyboard-setup.service enabled enabled
|
||||
lvm2-monitor.service enabled enabled
|
||||
lxd-agent.service enabled enabled
|
||||
ModemManager.service enabled enabled
|
||||
multipathd.service enabled enabled
|
||||
networkd-dispatcher.service enabled enabled
|
||||
open-iscsi.service enabled enabled
|
||||
open-vm-tools.service enabled enabled
|
||||
pollinate.service enabled enabled
|
||||
rsyslog.service enabled enabled
|
||||
secureboot-db.service enabled enabled
|
||||
setvtrgb.service enabled enabled
|
||||
shine-server.service enabled enabled
|
||||
snap.lxd.activate.service enabled enabled
|
||||
ssh.service enabled enabled
|
||||
systemd-networkd-wait-online.service enabled disabled
|
||||
systemd-networkd.service enabled enabled
|
||||
systemd-pstore.service enabled enabled
|
||||
systemd-resolved.service enabled enabled
|
||||
systemd-timesyncd.service enabled enabled
|
||||
thermald.service enabled enabled
|
||||
ua-reboot-cmds.service enabled enabled
|
||||
ubuntu-advantage.service enabled enabled
|
||||
ubuntu-fan.service enabled enabled
|
||||
udisks2.service enabled enabled
|
||||
ufw.service enabled enabled
|
||||
unattended-upgrades.service enabled enabled
|
||||
vgauth.service enabled enabled
|
||||
|
||||
10 unit files listed.
|
||||
45 unit files listed.
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
UNIT LOAD ACTIVE SUB DESCRIPTION
|
||||
agent-memory.service loaded active running agent-memory service
|
||||
caddy.service loaded active running Caddy
|
||||
containerd.service loaded active running containerd container runtime
|
||||
coturn.service loaded active running coTURN STUN/TURN Server
|
||||
cron.service loaded active running Regular background program processing daemon
|
||||
dbus.service loaded active running D-Bus System Message Bus
|
||||
docker.service loaded active running Docker Application Container Engine
|
||||
elaira-agent.service loaded active running Elaira self-hosted agent
|
||||
getty@tty1.service loaded active running Getty on tty1
|
||||
hermes-gateway.service loaded active running Hermes Agent Gateway - Messaging Platform Integration
|
||||
irqbalance.service loaded active running irqbalance daemon
|
||||
ModemManager.service loaded active running Modem Manager
|
||||
multipathd.service loaded active running Device-Mapper Multipath Device Controller
|
||||
networkd-dispatcher.service loaded active running Dispatcher daemon for systemd-networkd
|
||||
packagekit.service loaded active running PackageKit Daemon
|
||||
polkit.service loaded active running Authorization Manager
|
||||
qemu-guest-agent.service loaded active running QEMU Guest Agent
|
||||
rsyslog.service loaded active running System Logging Service
|
||||
shine-server.service loaded active running SHiNE Server
|
||||
shine-server.service loaded active running SHiNE Server (shineup.me)
|
||||
ssh.service loaded active running OpenBSD Secure Shell server
|
||||
systemd-hostnamed.service loaded active running Hostname Service
|
||||
systemd-journald.service loaded active running Journal Service
|
||||
@@ -26,10 +26,9 @@
|
||||
udisks2.service loaded active running Disk Manager
|
||||
unattended-upgrades.service loaded active running Unattended Upgrades Shutdown
|
||||
upower.service loaded active running Daemon for power management
|
||||
user@1002.service loaded active running User Manager for UID 1002
|
||||
|
||||
Legend: LOAD → Reflects whether the unit definition was properly loaded.
|
||||
ACTIVE → The high-level unit activation state, i.e. generalization of SUB.
|
||||
SUB → The low-level unit activation state, values depend on unit type.
|
||||
user@1000.service loaded active running User Manager for UID 1000
|
||||
|
||||
LOAD = Reflects whether the unit definition was properly loaded.
|
||||
ACTIVE = The high-level unit activation state, i.e. generalization of SUB.
|
||||
SUB = The low-level unit activation state, values depend on unit type.
|
||||
28 loaded units listed.
|
||||
|
||||
@@ -1,32 +1,12 @@
|
||||
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
|
||||
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=323411,fd=15))
|
||||
LISTEN 0 1024 127.0.0.1:3479 0.0.0.0:* users:(("turnserver",pid=1689866,fd=37))
|
||||
LISTEN 0 1024 127.0.0.1:3479 0.0.0.0:* users:(("turnserver",pid=1689866,fd=15))
|
||||
LISTEN 0 1024 127.0.0.1:3478 0.0.0.0:* users:(("turnserver",pid=1689866,fd=34))
|
||||
LISTEN 0 1024 127.0.0.1:3478 0.0.0.0:* users:(("turnserver",pid=1689866,fd=13))
|
||||
LISTEN 0 1024 172.17.0.1:3478 0.0.0.0:* users:(("turnserver",pid=1689866,fd=58))
|
||||
LISTEN 0 1024 172.17.0.1:3478 0.0.0.0:* users:(("turnserver",pid=1689866,fd=21))
|
||||
LISTEN 0 1024 172.17.0.1:3479 0.0.0.0:* users:(("turnserver",pid=1689866,fd=60))
|
||||
LISTEN 0 1024 172.17.0.1:3479 0.0.0.0:* users:(("turnserver",pid=1689866,fd=23))
|
||||
LISTEN 0 2048 0.0.0.0:8000 0.0.0.0:* users:(("uvicorn",pid=2056697,fd=6))
|
||||
LISTEN 0 4096 127.0.0.1:2019 0.0.0.0:* users:(("caddy",pid=8096,fd=14))
|
||||
LISTEN 0 4096 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=323411,fd=17))
|
||||
LISTEN 0 4096 0.0.0.0:2222 0.0.0.0:* users:(("docker-proxy",pid=8410,fd=7))
|
||||
LISTEN 0 4096 127.0.0.1:45999 0.0.0.0:* users:(("containerd",pid=2121968,fd=15))
|
||||
LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:* users:(("docker-proxy",pid=8433,fd=7))
|
||||
LISTEN 0 2048 127.0.0.1:9119 0.0.0.0:* users:(("hermes",pid=1913356,fd=7))
|
||||
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=323588,fd=3),("systemd",pid=1,fd=130))
|
||||
LISTEN 0 1024 185.229.109.118:3478 0.0.0.0:* users:(("turnserver",pid=1689866,fd=40))
|
||||
LISTEN 0 1024 185.229.109.118:3478 0.0.0.0:* users:(("turnserver",pid=1689866,fd=17))
|
||||
LISTEN 0 1024 185.229.109.118:3479 0.0.0.0:* users:(("turnserver",pid=1689866,fd=56))
|
||||
LISTEN 0 1024 185.229.109.118:3479 0.0.0.0:* users:(("turnserver",pid=1689866,fd=19))
|
||||
LISTEN 0 100 *:8018 *:* users:(("java",pid=9688,fd=13))
|
||||
LISTEN 0 50 *:7070 *:* users:(("java",pid=1732508,fd=16))
|
||||
LISTEN 0 4096 [::]:2222 [::]:* users:(("docker-proxy",pid=8417,fd=7))
|
||||
LISTEN 0 4096 [::]:22 [::]:* users:(("sshd",pid=323588,fd=4),("systemd",pid=1,fd=132))
|
||||
LISTEN 0 4096 *:80 *:* users:(("caddy",pid=8096,fd=16))
|
||||
LISTEN 0 4096 *:443 *:* users:(("caddy",pid=8096,fd=15))
|
||||
LISTEN 0 1024 [::1]:3478 [::]:* users:(("turnserver",pid=1689866,fd=25))
|
||||
LISTEN 0 1024 [::1]:3478 [::]:* users:(("turnserver",pid=1689866,fd=62))
|
||||
LISTEN 0 1024 [::1]:3479 [::]:* users:(("turnserver",pid=1689866,fd=27))
|
||||
LISTEN 0 1024 [::1]:3479 [::]:* users:(("turnserver",pid=1689866,fd=64))
|
||||
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
|
||||
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=839,fd=3))
|
||||
LISTEN 0 1024 0.0.0.0:3478 0.0.0.0:* users:(("turnserver",pid=522,fd=34))
|
||||
LISTEN 0 1024 0.0.0.0:3478 0.0.0.0:* users:(("turnserver",pid=522,fd=33))
|
||||
LISTEN 0 4096 127.0.0.1:2019 0.0.0.0:* users:(("caddy",pid=804,fd=4))
|
||||
LISTEN 0 4096 127.0.0.1:42113 0.0.0.0:* users:(("containerd",pid=536,fd=16))
|
||||
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=466,fd=14))
|
||||
LISTEN 0 4096 127.0.0.1:5432 0.0.0.0:* users:(("docker-proxy",pid=1240,fd=7))
|
||||
LISTEN 0 4096 *:80 *:* users:(("caddy",pid=804,fd=9))
|
||||
LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=839,fd=4))
|
||||
LISTEN 0 4096 *:443 *:* users:(("caddy",pid=804,fd=7))
|
||||
LISTEN 0 50 *:7070 *:* users:(("java",pid=1289,fd=19))
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
NAMES IMAGE STATUS PORTS
|
||||
gitea gitea/gitea:1.22.6 Up 5 weeks 127.0.0.1:3000->3000/tcp, 0.0.0.0:2222->22/tcp, [::]:2222->22/tcp
|
||||
NAMES IMAGE STATUS PORTS
|
||||
shine-postgres postgres:18 Up 19 hours 127.0.0.1:5432->5432/tcp
|
||||
|
||||
@@ -1,8 +1 @@
|
||||
4.0K /home/player/hosts.codex.test
|
||||
8.0K /home/player/Work_AGENTS
|
||||
804K /home/player/sites
|
||||
33M /home/player/elaira-agent
|
||||
36M /home/player/agent-memory
|
||||
46M /home/player/gitea
|
||||
183M /home/player/SHiNE
|
||||
1.7G /home/player/hermes
|
||||
85M /home/player/SHiNE
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
4.0K /var/local
|
||||
4.0K /var/mail
|
||||
4.0K /var/opt
|
||||
4.0K /var/snap
|
||||
16K /var/spool
|
||||
88K /var/tmp
|
||||
3.2M /var/backups
|
||||
143M /var/cache
|
||||
1.2G /var/lib
|
||||
1.5G /var/log
|
||||
72K /var/tmp
|
||||
1.9M /var/backups
|
||||
319M /var/cache
|
||||
1.1G /var/log
|
||||
1.5G /var/lib
|
||||
2.8G /var
|
||||
|
||||
@@ -1 +1 @@
|
||||
2026-07-10T08:27:36Z
|
||||
2026-08-08T20:05:20Z
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
openmindsoft.io, www.openmindsoft.io {
|
||||
encode zstd gzip
|
||||
root * /home/player/sites/OpenMindSoft.io
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
{
|
||||
auto_https disable_redirects
|
||||
}
|
||||
|
||||
shineup.me {
|
||||
@@ -46,33 +43,3 @@ shineup.me {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
git.shineup.me {
|
||||
encode zstd gzip
|
||||
reverse_proxy 127.0.0.1:3000
|
||||
}
|
||||
|
||||
test-solana-tickets.shineup.me, test-solana-tickets.shiningpeople.ru {
|
||||
encode zstd gzip
|
||||
root * /home/player/sites/test-solana-tickets.shineup.me
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
header -Etag
|
||||
header {
|
||||
Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
|
||||
Pragma "no-cache"
|
||||
Expires "0"
|
||||
}
|
||||
}
|
||||
|
||||
hermes.shineup.me {
|
||||
encode zstd gzip
|
||||
basicauth {
|
||||
player $2a$14$a41XVsBhgxgKN2MVS5Vt3Otu4C6mmv2FRo1gYDjvPDEYwYkPGnj1e
|
||||
}
|
||||
reverse_proxy 127.0.0.1:9119 {
|
||||
header_up Host 127.0.0.1:9119
|
||||
header_up Origin http://127.0.0.1:9119
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
missing: /etc/systemd/system/agent-memory.service
|
||||
@@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=SHiNE Server
|
||||
Description=SHiNE Server (shineup.me)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
@@ -7,7 +7,7 @@ Type=simple
|
||||
User=player
|
||||
Group=player
|
||||
WorkingDirectory=/home/player/SHiNE/shine-server
|
||||
ExecStart=/usr/bin/java -Dserver.1port=7070 -jar /home/player/SHiNE/shine-server/shine-server.jar
|
||||
ExecStart=/usr/bin/java -Dserver.port=7070 -jar /home/player/SHiNE/shine-server/shine-server.jar
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
|
||||
@@ -1,709 +1,17 @@
|
||||
# Coturn TURN SERVER configuration file
|
||||
#
|
||||
# Boolean values note: where boolean value is supposed to be used,
|
||||
# you can use '0', 'off', 'no', 'false', 'f' as 'false,
|
||||
# and you can use '1', 'on', 'yes', 'true', 't' as 'true'
|
||||
# If the value is missed, then it means 'true'.
|
||||
#
|
||||
|
||||
# Listener interface device (optional, Linux only).
|
||||
# NOT RECOMMENDED.
|
||||
#
|
||||
#listening-device=eth0
|
||||
|
||||
# TURN listener port for UDP and TCP (Default: 3478).
|
||||
# Note: actually, TLS & DTLS sessions can connect to the
|
||||
# "plain" TCP & UDP port(s), too - if allowed by configuration.
|
||||
#
|
||||
#listening-port=3478
|
||||
|
||||
# TURN listener port for TLS (Default: 5349).
|
||||
# Note: actually, "plain" TCP & UDP sessions can connect to the TLS & DTLS
|
||||
# port(s), too - if allowed by configuration. The TURN server
|
||||
# "automatically" recognizes the type of traffic. Actually, two listening
|
||||
# endpoints (the "plain" one and the "tls" one) are equivalent in terms of
|
||||
# functionality; but we keep both endpoints to satisfy the RFC 5766 specs.
|
||||
# For secure TCP connections, we currently support SSL version 3 and
|
||||
# TLS version 1.0, 1.1 and 1.2.
|
||||
# For secure UDP connections, we support DTLS version 1.
|
||||
#
|
||||
#tls-listening-port=5349
|
||||
|
||||
# Alternative listening port for UDP and TCP listeners;
|
||||
# default (or zero) value means "listening port plus one".
|
||||
# This is needed for RFC 5780 support
|
||||
# (STUN extension specs, NAT behavior discovery). The TURN Server
|
||||
# supports RFC 5780 only if it is started with more than one
|
||||
# listening IP address of the same family (IPv4 or IPv6).
|
||||
# RFC 5780 is supported only by UDP protocol, other protocols
|
||||
# are listening to that endpoint only for "symmetry".
|
||||
#
|
||||
#alt-listening-port=0
|
||||
|
||||
# Alternative listening port for TLS and DTLS protocols.
|
||||
# Default (or zero) value means "TLS listening port plus one".
|
||||
#
|
||||
#alt-tls-listening-port=0
|
||||
|
||||
# Listener IP address of relay server. Multiple listeners can be specified.
|
||||
# If no IP(s) specified in the config file or in the command line options,
|
||||
# then all IPv4 and IPv6 system IPs will be used for listening.
|
||||
#
|
||||
#listening-ip=172.17.19.101
|
||||
#listening-ip=10.207.21.238
|
||||
#listening-ip=2607:f0d0:1002:51::4
|
||||
|
||||
# Auxiliary STUN/TURN server listening endpoint.
|
||||
# Aux servers have almost full TURN and STUN functionality.
|
||||
# The (minor) limitations are:
|
||||
#
|
||||
# 1) Auxiliary servers do not have alternative ports and
|
||||
# they do not support STUN RFC 5780 functionality (CHANGE REQUEST).
|
||||
#
|
||||
# 2) Auxiliary servers also are never returning ALTERNATIVE-SERVER reply.
|
||||
#
|
||||
# Valid formats are 1.2.3.4:5555 for IPv4 and [1:2::3:4]:5555 for IPv6.
|
||||
#
|
||||
# There may be multiple aux-server options, each will be used for listening
|
||||
# to client requests.
|
||||
#
|
||||
#aux-server=172.17.19.110:33478
|
||||
#aux-server=[2607:f0d0:1002:51::4]:33478
|
||||
|
||||
# (recommended for older Linuxes only)
|
||||
# Automatically balance UDP traffic over auxiliary servers (if configured).
|
||||
# The load balancing is using the ALTERNATE-SERVER mechanism.
|
||||
# The TURN client must support 300 ALTERNATE-SERVER response for this
|
||||
# functionality.
|
||||
#
|
||||
#udp-self-balance
|
||||
|
||||
# Relay interface device for relay sockets (optional, Linux only).
|
||||
# NOT RECOMMENDED.
|
||||
#
|
||||
#relay-device=eth1
|
||||
|
||||
# Relay address (the local IP address that will be used to relay the
|
||||
# packets to the peer).
|
||||
# Multiple relay addresses may be used.
|
||||
# The same IP(s) can be used as both listening IP(s) and relay IP(s).
|
||||
#
|
||||
# If no relay IP(s) specified, then the turnserver will apply the default
|
||||
# policy: it will decide itself which relay addresses to be used, and it
|
||||
# will always be using the client socket IP address as the relay IP address
|
||||
# of the TURN session (if the requested relay address family is the same
|
||||
# as the family of the client socket).
|
||||
#
|
||||
#relay-ip=172.17.19.105
|
||||
#relay-ip=2607:f0d0:1002:51::5
|
||||
|
||||
# For Amazon EC2 users:
|
||||
#
|
||||
# TURN Server public/private address mapping, if the server is behind NAT.
|
||||
# In that situation, if a -X is used in form "-X <ip>" then that ip will be reported
|
||||
# as relay IP address of all allocations. This scenario works only in a simple case
|
||||
# when one single relay address is be used, and no RFC5780 functionality is required.
|
||||
# That single relay address must be mapped by NAT to the 'external' IP.
|
||||
# The "external-ip" value, if not empty, is returned in XOR-RELAYED-ADDRESS field.
|
||||
# For that 'external' IP, NAT must forward ports directly (relayed port 12345
|
||||
# must be always mapped to the same 'external' port 12345).
|
||||
#
|
||||
# In more complex case when more than one IP address is involved,
|
||||
# that option must be used several times, each entry must
|
||||
# have form "-X <public-ip/private-ip>", to map all involved addresses.
|
||||
# RFC5780 NAT discovery STUN functionality will work correctly,
|
||||
# if the addresses are mapped properly, even when the TURN server itself
|
||||
# is behind A NAT.
|
||||
#
|
||||
# By default, this value is empty, and no address mapping is used.
|
||||
#
|
||||
#external-ip=60.70.80.91
|
||||
#
|
||||
#OR:
|
||||
#
|
||||
#external-ip=60.70.80.91/172.17.19.101
|
||||
#external-ip=60.70.80.92/172.17.19.102
|
||||
|
||||
|
||||
# Number of the relay threads to handle the established connections
|
||||
# (in addition to authentication thread and the listener thread).
|
||||
# If explicitly set to 0 then application runs relay process in a
|
||||
# single thread, in the same thread with the listener process
|
||||
# (the authentication thread will still be a separate thread).
|
||||
#
|
||||
# If this parameter is not set, then the default OS-dependent
|
||||
# thread pattern algorithm will be employed. Usually the default
|
||||
# algorithm is the most optimal, so you have to change this option
|
||||
# only if you want to make some fine tweaks.
|
||||
#
|
||||
# In the older systems (Linux kernel before 3.9),
|
||||
# the number of UDP threads is always one thread per network listening
|
||||
# endpoint - including the auxiliary endpoints - unless 0 (zero) or
|
||||
# 1 (one) value is set.
|
||||
#
|
||||
#relay-threads=0
|
||||
|
||||
# Lower and upper bounds of the UDP relay endpoints:
|
||||
# (default values are 49152 and 65535)
|
||||
#
|
||||
#min-port=49152
|
||||
#max-port=65535
|
||||
|
||||
# Uncomment to run TURN server in 'normal' 'moderate' verbose mode.
|
||||
# By default the verbose mode is off.
|
||||
#verbose
|
||||
|
||||
# Uncomment to run TURN server in 'extra' verbose mode.
|
||||
# This mode is very annoying and produces lots of output.
|
||||
# Not recommended under any normal circumstances.
|
||||
#
|
||||
#Verbose
|
||||
|
||||
# Uncomment to use fingerprints in the TURN messages.
|
||||
# By default the fingerprints are off.
|
||||
#
|
||||
#fingerprint
|
||||
|
||||
# Uncomment to use long-term credential mechanism.
|
||||
# By default no credentials mechanism is used (any user allowed).
|
||||
#
|
||||
#lt-cred-mech
|
||||
|
||||
# This option is opposite to lt-cred-mech.
|
||||
# (TURN Server with no-auth option allows anonymous access).
|
||||
# If neither option is defined, and no users are defined,
|
||||
# then no-auth is default. If at least one user is defined,
|
||||
# in this file or in command line or in usersdb file, then
|
||||
# lt-cred-mech is default.
|
||||
#
|
||||
#no-auth
|
||||
|
||||
# TURN REST API flag.
|
||||
# (Time Limited Long Term Credential)
|
||||
# Flag that sets a special authorization option that is based upon authentication secret.
|
||||
#
|
||||
# This feature's purpose is to support "TURN Server REST API", see
|
||||
# "TURN REST API" link in the project's page
|
||||
# https://github.com/coturn/coturn/
|
||||
#
|
||||
# This option is used with timestamp:
|
||||
#
|
||||
# usercombo -> "timestamp:userid"
|
||||
# turn user -> usercombo
|
||||
# turn password -> base64(hmac(secret key, usercombo))
|
||||
#
|
||||
# This allows TURN credentials to be accounted for a specific user id.
|
||||
# If you don't have a suitable id, the timestamp alone can be used.
|
||||
# This option is just turning on secret-based authentication.
|
||||
# The actual value of the secret is defined either by option static-auth-secret,
|
||||
# or can be found in the turn_secret table in the database (see below).
|
||||
#
|
||||
# Read more about it:
|
||||
# - https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
|
||||
# - https://www.ietf.org/proceedings/87/slides/slides-87-behave-10.pdf
|
||||
#
|
||||
# Be aware that use-auth-secret overrides some part of lt-cred-mech.
|
||||
# Notice that this feature depends internally on lt-cred-mech, so if you set
|
||||
# use-auth-secret then it enables internally automatically lt-cred-mech option
|
||||
# like if you enable both.
|
||||
#
|
||||
# You can use only one of the to auth mechanisms in the same time because,
|
||||
# both mechanism use the username and password validation in different way.
|
||||
#
|
||||
# This way be aware that you can't use both auth mechnaism in the same time!
|
||||
# Use in config either the lt-cred-mech or the use-auth-secret
|
||||
# to avoid any confusion.
|
||||
#
|
||||
listening-port=3478
|
||||
fingerprint
|
||||
lt-cred-mech
|
||||
use-auth-secret
|
||||
|
||||
# 'Static' authentication secret value (a string) for TURN REST API only.
|
||||
# If not set, then the turn server
|
||||
# will try to use the 'dynamic' value in turn_secret table
|
||||
# in user database (if present). The database-stored value can be changed on-the-fly
|
||||
# by a separate program, so this is why that other mode is 'dynamic'.
|
||||
#
|
||||
#static-auth-secret=north
|
||||
|
||||
# Server name used for
|
||||
# the oAuth authentication purposes.
|
||||
# The default value is the realm name.
|
||||
#
|
||||
#server-name=blackdow.carleon.gov
|
||||
|
||||
# Flag that allows oAuth authentication.
|
||||
#
|
||||
#oauth
|
||||
|
||||
# 'Static' user accounts for long term credentials mechanism, only.
|
||||
# This option cannot be used with TURN REST API.
|
||||
# 'Static' user accounts are NOT dynamically checked by the turnserver process,
|
||||
# so that they can NOT be changed while the turnserver is running.
|
||||
#
|
||||
#user=username1:key1
|
||||
#user=username2:key2
|
||||
# OR:
|
||||
#user=username1:password1
|
||||
#user=username2:password2
|
||||
#
|
||||
# Keys must be generated by turnadmin utility. The key value depends
|
||||
# on user name, realm, and password:
|
||||
#
|
||||
# Example:
|
||||
# $ turnadmin -k -u ninefingers -r north.gov -p youhavetoberealistic
|
||||
# Output: 0xbc807ee29df3c9ffa736523fb2c4e8ee
|
||||
# ('0x' in the beginning of the key is what differentiates the key from
|
||||
# password. If it has 0x then it is a key, otherwise it is a password).
|
||||
#
|
||||
# The corresponding user account entry in the config file will be:
|
||||
#
|
||||
#user=ninefingers:0xbc807ee29df3c9ffa736523fb2c4e8ee
|
||||
# Or, equivalently, with open clear password (less secure):
|
||||
#user=ninefingers:youhavetoberealistic
|
||||
#
|
||||
|
||||
# SQLite database file name.
|
||||
#
|
||||
# Default file name is /var/db/turndb or /usr/local/var/db/turndb or
|
||||
# /var/lib/turn/turndb.
|
||||
#
|
||||
#userdb=/var/db/turndb
|
||||
|
||||
# PostgreSQL database connection string in the case that we are using PostgreSQL
|
||||
# as the user database.
|
||||
# This database can be used for long-term credential mechanism
|
||||
# and it can store the secret value for secret-based timed authentication in TURN RESP API.
|
||||
# See http://www.postgresql.org/docs/8.4/static/libpq-connect.html for 8.x PostgreSQL
|
||||
# versions connection string format, see
|
||||
# http://www.postgresql.org/docs/9.2/static/libpq-connect.html#LIBPQ-CONNSTRING
|
||||
# for 9.x and newer connection string formats.
|
||||
#
|
||||
#psql-userdb="host=<host> dbname=<database-name> user=<database-user> password=<database-user-password> connect_timeout=30"
|
||||
|
||||
# MySQL database connection string in the case that we are using MySQL
|
||||
# as the user database.
|
||||
# This database can be used for long-term credential mechanism
|
||||
# and it can store the secret value for secret-based timed authentication in TURN RESP API.
|
||||
#
|
||||
# Optional connection string parameters for the secure communications (SSL):
|
||||
# ca, capath, cert, key, cipher
|
||||
# (see http://dev.mysql.com/doc/refman/5.1/en/ssl-options.html for the
|
||||
# command options description).
|
||||
#
|
||||
# Use string format as below (space separated parameters, all optional):
|
||||
#
|
||||
#mysql-userdb="host=<host> dbname=<database-name> user=<database-user> password=<database-user-password> port=<port> connect_timeout=<seconds> read_timeout=<seconds>"
|
||||
|
||||
# If you want to use in the MySQL connection string the password in encrypted format,
|
||||
# then set in this option the MySQL password encryption secret key file.
|
||||
#
|
||||
# Warning: If this option is set, then mysql password must be set in "mysql-userdb" in encrypted format!
|
||||
# If you want to use cleartext password then do not set this option!
|
||||
#
|
||||
# This is the file path which contain secret key of aes encryption while using password encryption.
|
||||
#
|
||||
#secret-key-file=/path/
|
||||
|
||||
# MongoDB database connection string in the case that we are using MongoDB
|
||||
# as the user database.
|
||||
# This database can be used for long-term credential mechanism
|
||||
# and it can store the secret value for secret-based timed authentication in TURN RESP API.
|
||||
# Use string format is described at http://hergert.me/docs/mongo-c-driver/mongoc_uri.html
|
||||
#
|
||||
#mongo-userdb="mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]"
|
||||
|
||||
# Redis database connection string in the case that we are using Redis
|
||||
# as the user database.
|
||||
# This database can be used for long-term credential mechanism
|
||||
# and it can store the secret value for secret-based timed authentication in TURN RESP API.
|
||||
# Use string format as below (space separated parameters, all optional):
|
||||
#
|
||||
#redis-userdb="ip=<ip-address> dbname=<database-number> password=<database-user-password> port=<port> connect_timeout=<seconds>"
|
||||
|
||||
# Redis status and statistics database connection string, if used (default - empty, no Redis stats DB used).
|
||||
# This database keeps allocations status information, and it can be also used for publishing
|
||||
# and delivering traffic and allocation event notifications.
|
||||
# The connection string has the same parameters as redis-userdb connection string.
|
||||
# Use string format as below (space separated parameters, all optional):
|
||||
#
|
||||
#redis-statsdb="ip=<ip-address> dbname=<database-number> password=<database-user-password> port=<port> connect_timeout=<seconds>"
|
||||
|
||||
# The default realm to be used for the users when no explicit
|
||||
# origin/realm relationship was found in the database, or if the TURN
|
||||
# server is not using any database (just the commands-line settings
|
||||
# and the userdb file). Must be used with long-term credentials
|
||||
# mechanism or with TURN REST API.
|
||||
#
|
||||
# Note: If default realm is not specified at all, then realm falls back to the host domain name.
|
||||
# If domain name is empty string, or '(None)', then it is initialized to am empty string.
|
||||
#
|
||||
#realm=mycompany.org
|
||||
|
||||
# The flag that sets the origin consistency
|
||||
# check: across the session, all requests must have the same
|
||||
# main ORIGIN attribute value (if the ORIGIN was
|
||||
# initially used by the session).
|
||||
#
|
||||
#check-origin-consistency
|
||||
|
||||
# Per-user allocation quota.
|
||||
# default value is 0 (no quota, unlimited number of sessions per user).
|
||||
# This option can also be set through the database, for a particular realm.
|
||||
#
|
||||
#user-quota=0
|
||||
|
||||
# Total allocation quota.
|
||||
# default value is 0 (no quota).
|
||||
# This option can also be set through the database, for a particular realm.
|
||||
#
|
||||
#total-quota=0
|
||||
|
||||
# Max bytes-per-second bandwidth a TURN session is allowed to handle
|
||||
# (input and output network streams are treated separately). Anything above
|
||||
# that limit will be dropped or temporary suppressed (within
|
||||
# the available buffer limits).
|
||||
# This option can also be set through the database, for a particular realm.
|
||||
#
|
||||
#max-bps=0
|
||||
|
||||
#
|
||||
# Maximum server capacity.
|
||||
# Total bytes-per-second bandwidth the TURN server is allowed to allocate
|
||||
# for the sessions, combined (input and output network streams are treated separately).
|
||||
#
|
||||
# bps-capacity=0
|
||||
|
||||
# Uncomment if no UDP client listener is desired.
|
||||
# By default UDP client listener is always started.
|
||||
#
|
||||
#no-udp
|
||||
|
||||
# Uncomment if no TCP client listener is desired.
|
||||
# By default TCP client listener is always started.
|
||||
#
|
||||
#no-tcp
|
||||
|
||||
# Uncomment if no TLS client listener is desired.
|
||||
# By default TLS client listener is always started.
|
||||
#
|
||||
#no-tls
|
||||
|
||||
# Uncomment if no DTLS client listener is desired.
|
||||
# By default DTLS client listener is always started.
|
||||
#
|
||||
#no-dtls
|
||||
|
||||
# Uncomment if no UDP relay endpoints are allowed.
|
||||
# By default UDP relay endpoints are enabled (like in RFC 5766).
|
||||
#
|
||||
#no-udp-relay
|
||||
|
||||
# Uncomment if no TCP relay endpoints are allowed.
|
||||
# By default TCP relay endpoints are enabled (like in RFC 6062).
|
||||
#
|
||||
#no-tcp-relay
|
||||
|
||||
# Uncomment if extra security is desired,
|
||||
# with nonce value having limited lifetime.
|
||||
# By default, the nonce value is unique for a session,
|
||||
# and has unlimited lifetime.
|
||||
# Set this option to limit the nonce lifetime.
|
||||
# It defaults to 600 secs (10 min) if no value is provided. After that delay,
|
||||
# the client will get 438 error and will have to re-authenticate itself.
|
||||
#
|
||||
#stale-nonce=600
|
||||
|
||||
# Uncomment if you want to set the maximum allocation
|
||||
# time before it has to be refreshed.
|
||||
# Default is 3600s.
|
||||
#
|
||||
#max-allocate-lifetime=3600
|
||||
|
||||
|
||||
# Uncomment to set the lifetime for the channel.
|
||||
# Default value is 600 secs (10 minutes).
|
||||
# This value MUST not be changed for production purposes.
|
||||
#
|
||||
#channel-lifetime=600
|
||||
|
||||
# Uncomment to set the permission lifetime.
|
||||
# Default to 300 secs (5 minutes).
|
||||
# In production this value MUST not be changed,
|
||||
# however it can be useful for test purposes.
|
||||
#
|
||||
#permission-lifetime=300
|
||||
|
||||
# Certificate file.
|
||||
# Use an absolute path or path relative to the
|
||||
# configuration file.
|
||||
#
|
||||
#cert=/usr/local/etc/turn_server_cert.pem
|
||||
|
||||
# Private key file.
|
||||
# Use an absolute path or path relative to the
|
||||
# configuration file.
|
||||
# Use PEM file format.
|
||||
#
|
||||
#pkey=/usr/local/etc/turn_server_pkey.pem
|
||||
|
||||
# Private key file password, if it is in encoded format.
|
||||
# This option has no default value.
|
||||
#
|
||||
#pkey-pwd=...
|
||||
|
||||
# Allowed OpenSSL cipher list for TLS/DTLS connections.
|
||||
# Default value is "DEFAULT".
|
||||
#
|
||||
#cipher-list="DEFAULT"
|
||||
|
||||
# CA file in OpenSSL format.
|
||||
# Forces TURN server to verify the client SSL certificates.
|
||||
# By default it is not set: there is no default value and the client
|
||||
# certificate is not checked.
|
||||
#
|
||||
# Example:
|
||||
#CA-file=/etc/ssh/id_rsa.cert
|
||||
|
||||
# Curve name for EC ciphers, if supported by OpenSSL
|
||||
# library (TLS and DTLS). The default value is prime256v1,
|
||||
# if pre-OpenSSL 1.0.2 is used. With OpenSSL 1.0.2+,
|
||||
# an optimal curve will be automatically calculated, if not defined
|
||||
# by this option.
|
||||
#
|
||||
#ec-curve-name=prime256v1
|
||||
|
||||
# Use 566 bits predefined DH TLS key. Default size of the key is 1066.
|
||||
#
|
||||
#dh566
|
||||
|
||||
# Use 2066 bits predefined DH TLS key. Default size of the key is 1066.
|
||||
#
|
||||
#dh2066
|
||||
|
||||
# Use custom DH TLS key, stored in PEM format in the file.
|
||||
# Flags --dh566 and --dh2066 are ignored when the DH key is taken from a file.
|
||||
#
|
||||
#dh-file=<DH-PEM-file-name>
|
||||
|
||||
# Flag to prevent stdout log messages.
|
||||
# By default, all log messages are going to both stdout and to
|
||||
# the configured log file. With this option everything will be
|
||||
# going to the configured log only (unless the log file itself is stdout).
|
||||
#
|
||||
#no-stdout-log
|
||||
|
||||
# Option to set the log file name.
|
||||
# By default, the turnserver tries to open a log file in
|
||||
# /var/log, /var/tmp, /tmp and current directories directories
|
||||
# (which open operation succeeds first that file will be used).
|
||||
# With this option you can set the definite log file name.
|
||||
# The special names are "stdout" and "-" - they will force everything
|
||||
# to the stdout. Also, the "syslog" name will force everything to
|
||||
# the system log (syslog).
|
||||
# In the runtime, the logfile can be reset with the SIGHUP signal
|
||||
# to the turnserver process.
|
||||
#
|
||||
#log-file=/var/tmp/turn.log
|
||||
|
||||
# Option to redirect all log output into system log (syslog).
|
||||
#
|
||||
syslog
|
||||
|
||||
# This flag means that no log file rollover will be used, and the log file
|
||||
# name will be constructed as-is, without PID and date appendage.
|
||||
# This option can be used, for example, together with the logrotate tool.
|
||||
#
|
||||
#simple-log
|
||||
|
||||
# Option to set the "redirection" mode. The value of this option
|
||||
# will be the address of the alternate server for UDP & TCP service in form of
|
||||
# <ip>[:<port>]. The server will send this value in the attribute
|
||||
# ALTERNATE-SERVER, with error 300, on ALLOCATE request, to the client.
|
||||
# Client will receive only values with the same address family
|
||||
# as the client network endpoint address family.
|
||||
# See RFC 5389 and RFC 5766 for ALTERNATE-SERVER functionality description.
|
||||
# The client must use the obtained value for subsequent TURN communications.
|
||||
# If more than one --alternate-server options are provided, then the functionality
|
||||
# can be more accurately described as "load-balancing" than a mere "redirection".
|
||||
# If the port number is omitted, then the default port
|
||||
# number 3478 for the UDP/TCP protocols will be used.
|
||||
# Colon (:) characters in IPv6 addresses may conflict with the syntax of
|
||||
# the option. To alleviate this conflict, literal IPv6 addresses are enclosed
|
||||
# in square brackets in such resource identifiers, for example:
|
||||
# [2001:db8:85a3:8d3:1319:8a2e:370:7348]:3478 .
|
||||
# Multiple alternate servers can be set. They will be used in the
|
||||
# round-robin manner. All servers in the pool are considered of equal weight and
|
||||
# the load will be distributed equally. For example, if we have 4 alternate servers,
|
||||
# then each server will receive 25% of ALLOCATE requests. A alternate TURN server
|
||||
# address can be used more than one time with the alternate-server option, so this
|
||||
# can emulate "weighting" of the servers.
|
||||
#
|
||||
# Examples:
|
||||
#alternate-server=1.2.3.4:5678
|
||||
#alternate-server=11.22.33.44:56789
|
||||
#alternate-server=5.6.7.8
|
||||
#alternate-server=[2001:db8:85a3:8d3:1319:8a2e:370:7348]:3478
|
||||
|
||||
# Option to set alternative server for TLS & DTLS services in form of
|
||||
# <ip>:<port>. If the port number is omitted, then the default port
|
||||
# number 5349 for the TLS/DTLS protocols will be used. See the previous
|
||||
# option for the functionality description.
|
||||
#
|
||||
# Examples:
|
||||
#tls-alternate-server=1.2.3.4:5678
|
||||
#tls-alternate-server=11.22.33.44:56789
|
||||
#tls-alternate-server=[2001:db8:85a3:8d3:1319:8a2e:370:7348]:3478
|
||||
|
||||
# Option to suppress TURN functionality, only STUN requests will be processed.
|
||||
# Run as STUN server only, all TURN requests will be ignored.
|
||||
# By default, this option is NOT set.
|
||||
#
|
||||
#stun-only
|
||||
|
||||
# Option to suppress STUN functionality, only TURN requests will be processed.
|
||||
# Run as TURN server only, all STUN requests will be ignored.
|
||||
# By default, this option is NOT set.
|
||||
#
|
||||
#no-stun
|
||||
|
||||
# This is the timestamp/username separator symbol (character) in TURN REST API.
|
||||
# The default value is ':'.
|
||||
# rest-api-separator=:
|
||||
|
||||
# Flag that can be used to allow peers on the loopback addresses (127.x.x.x and ::1).
|
||||
# This is an extra security measure.
|
||||
#
|
||||
# (To avoid any security issue that allowing loopback access may raise,
|
||||
# the no-loopback-peers option is replaced by allow-loopback-peers.)
|
||||
#
|
||||
# Allow it only for testing in a development environment!
|
||||
# In production it adds a possible security vulnerability, so for security reasons
|
||||
# it is not allowed using it together with empty cli-password.
|
||||
#
|
||||
#allow-loopback-peers
|
||||
|
||||
# Flag that can be used to disallow peers on well-known broadcast addresses (224.0.0.0 and above, and FFXX:*).
|
||||
# This is an extra security measure.
|
||||
#
|
||||
#no-multicast-peers
|
||||
|
||||
# Option to set the max time, in seconds, allowed for full allocation establishment.
|
||||
# Default is 60 seconds.
|
||||
#
|
||||
#max-allocate-timeout=60
|
||||
|
||||
# Option to allow or ban specific ip addresses or ranges of ip addresses.
|
||||
# If an ip address is specified as both allowed and denied, then the ip address is
|
||||
# considered to be allowed. This is useful when you wish to ban a range of ip
|
||||
# addresses, except for a few specific ips within that range.
|
||||
#
|
||||
# This can be used when you do not want users of the turn server to be able to access
|
||||
# machines reachable by the turn server, but would otherwise be unreachable from the
|
||||
# internet (e.g. when the turn server is sitting behind a NAT)
|
||||
#
|
||||
# Examples:
|
||||
# denied-peer-ip=83.166.64.0-83.166.95.255
|
||||
# allowed-peer-ip=83.166.68.45
|
||||
|
||||
# File name to store the pid of the process.
|
||||
# Default is /var/run/turnserver.pid (if superuser account is used) or
|
||||
# /var/tmp/turnserver.pid .
|
||||
#
|
||||
#pidfile="/var/run/turnserver.pid"
|
||||
|
||||
# Require authentication of the STUN Binding request.
|
||||
# By default, the clients are allowed anonymous access to the STUN Binding functionality.
|
||||
#
|
||||
#secure-stun
|
||||
|
||||
# Mobility with ICE (MICE) specs support.
|
||||
#
|
||||
#mobility
|
||||
|
||||
# Allocate Address Family according
|
||||
# If enabled then TURN server allocates address family according the TURN
|
||||
# Client <=> Server communication address family.
|
||||
# (By default coTURN works according RFC 6156.)
|
||||
# !!Warning: Enabling this option breaks RFC6156 section-4.2 (violates use default IPv4)!!
|
||||
#
|
||||
#keep-address-family
|
||||
|
||||
|
||||
# User name to run the process. After the initialization, the turnserver process
|
||||
# will make an attempt to change the current user ID to that user.
|
||||
#
|
||||
#proc-user=<user-name>
|
||||
|
||||
# Group name to run the process. After the initialization, the turnserver process
|
||||
# will make an attempt to change the current group ID to that group.
|
||||
#
|
||||
#proc-group=<group-name>
|
||||
|
||||
# Turn OFF the CLI support.
|
||||
# By default it is always ON.
|
||||
# See also options cli-ip and cli-port.
|
||||
#
|
||||
#no-cli
|
||||
|
||||
#Local system IP address to be used for CLI server endpoint. Default value
|
||||
# is 127.0.0.1.
|
||||
#
|
||||
#cli-ip=127.0.0.1
|
||||
|
||||
# CLI server port. Default is 5766.
|
||||
#
|
||||
#cli-port=5766
|
||||
|
||||
# CLI access password. Default is empty (no password).
|
||||
# For the security reasons, it is recommended to use the encrypted
|
||||
# for of the password (see the -P command in the turnadmin utility).
|
||||
#
|
||||
# Secure form for password 'qwerty':
|
||||
#
|
||||
#cli-password=$5$79a316b350311570$81df9cfb9af7f5e5a76eada31e7097b663a0670f99a3c07ded3f1c8e59c5658a
|
||||
#
|
||||
# Or unsecure form for the same password:
|
||||
#
|
||||
#cli-password=qwerty
|
||||
|
||||
# Enable Web-admin support on https. By default it is Disabled.
|
||||
# If it is enabled it also enables a http a simple static banner page
|
||||
# with a small reminder that the admin page is available only on https.
|
||||
#
|
||||
#web-admin
|
||||
|
||||
# Local system IP address to be used for Web-admin server endpoint. Default value is 127.0.0.1.
|
||||
#
|
||||
#web-admin-ip=127.0.0.1
|
||||
|
||||
# Web-admin server port. Default is 8080.
|
||||
#
|
||||
#web-admin-port=8080
|
||||
|
||||
# Web-admin server listen on STUN/TURN worker threads
|
||||
# By default it is disabled for security resons! (Not recommended in any production environment!)
|
||||
#
|
||||
#web-admin-listen-on-workers
|
||||
|
||||
# Server relay. NON-STANDARD AND DANGEROUS OPTION.
|
||||
# Only for those applications when we want to run
|
||||
# server applications on the relay endpoints.
|
||||
# This option eliminates the IP permissions check on
|
||||
# the packets incoming to the relay endpoints.
|
||||
#
|
||||
#server-relay
|
||||
|
||||
# Maximum number of output sessions in ps CLI command.
|
||||
# This value can be changed on-the-fly in CLI. The default value is 256.
|
||||
#
|
||||
#cli-max-output-sessions
|
||||
|
||||
# Set network engine type for the process (for internal purposes).
|
||||
#
|
||||
#ne=[1|2|3]
|
||||
|
||||
# Do not allow an TLS/DTLS version of protocol
|
||||
#
|
||||
#no-tlsv1
|
||||
#no-tlsv1_1
|
||||
#no-tlsv1_2
|
||||
static-auth-secret=<secret-on-server-only>
|
||||
static-auth-secret=def6d444734d380d2f67a9d345b1debf985eaba0973c343e392c060d97c30106
|
||||
realm=turn1.shineup.me
|
||||
total-quota=200
|
||||
stale-nonce=600
|
||||
no-multicast-peers
|
||||
no-loopback-peers
|
||||
no-cli
|
||||
simple-log
|
||||
external-ip=178.208.64.62
|
||||
listening-ip=0.0.0.0
|
||||
relay-ip=178.208.64.62
|
||||
min-port=49000
|
||||
max-port=53999
|
||||
|
||||
@@ -5,9 +5,21 @@ REMOTE_HOST="${REMOTE_HOST:-player@shineup.me}"
|
||||
SCHEME_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
CAP_DIR="${SCHEME_DIR}/captures"
|
||||
CFG_DIR="${SCHEME_DIR}/configs"
|
||||
RSYNC_REMOTE_SUDO=(--rsync-path="sudo -n rsync")
|
||||
|
||||
mkdir -p "${CAP_DIR}" "${CFG_DIR}/systemd"
|
||||
|
||||
copy_optional_file() {
|
||||
local remote_path="$1"
|
||||
local local_path="$2"
|
||||
if ssh "${REMOTE_HOST}" "test -f '$remote_path'"; then
|
||||
rsync -a "${RSYNC_REMOTE_SUDO[@]}" "${REMOTE_HOST}:${remote_path}" "${local_path}"
|
||||
else
|
||||
mkdir -p "$(dirname "${local_path}")"
|
||||
echo "missing: ${remote_path}" > "${local_path}.MISSING.txt"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "[1/3] Обновляю инвентарь"
|
||||
ssh "${REMOTE_HOST}" 'hostnamectl --static; echo ---; uname -a; echo ---; df -h; echo ---; lsblk -f' > "${CAP_DIR}/01_host_disk.txt"
|
||||
ssh "${REMOTE_HOST}" 'systemctl list-unit-files --type=service --state=enabled --no-pager' > "${CAP_DIR}/02_enabled_services.txt"
|
||||
@@ -18,10 +30,14 @@ ssh "${REMOTE_HOST}" 'du -sh /home/player/* 2>/dev/null | sort -h' > "${CAP_DIR}
|
||||
ssh "${REMOTE_HOST}" 'sudo -n du -xhd1 /var 2>/dev/null | sort -h' > "${CAP_DIR}/07_var_sizes.txt"
|
||||
|
||||
echo "[2/3] Обновляю ключевые конфиги"
|
||||
rsync -a "${REMOTE_HOST}:/home/player/SHiNE/caddy/Caddyfile" "${CFG_DIR}/Caddyfile"
|
||||
rsync -a "${REMOTE_HOST}:/etc/turnserver.conf" "${CFG_DIR}/turnserver.conf"
|
||||
rsync -a "${REMOTE_HOST}:/etc/systemd/system/shine-server.service" "${CFG_DIR}/systemd/"
|
||||
rsync -a "${REMOTE_HOST}:/etc/systemd/system/agent-memory.service" "${CFG_DIR}/systemd/"
|
||||
if ssh "${REMOTE_HOST}" 'test -f /etc/caddy/Caddyfile'; then
|
||||
rsync -a "${RSYNC_REMOTE_SUDO[@]}" "${REMOTE_HOST}:/etc/caddy/Caddyfile" "${CFG_DIR}/Caddyfile"
|
||||
else
|
||||
rsync -a "${REMOTE_HOST}:/home/player/SHiNE/caddy/Caddyfile" "${CFG_DIR}/Caddyfile"
|
||||
fi
|
||||
copy_optional_file "/etc/turnserver.conf" "${CFG_DIR}/turnserver.conf"
|
||||
copy_optional_file "/etc/systemd/system/shine-server.service" "${CFG_DIR}/systemd/shine-server.service"
|
||||
copy_optional_file "/etc/systemd/system/agent-memory.service" "${CFG_DIR}/systemd/agent-memory.service"
|
||||
|
||||
echo "[3/3] Метка обновления"
|
||||
date -u +%Y-%m-%dT%H:%M:%SZ > "${CAP_DIR}/UPDATED_AT_UTC.txt"
|
||||
|
||||
@@ -88,6 +88,9 @@
|
||||
- `limit_exceeded`
|
||||
- `chain_resync_in_progress` — цепочка временно заблокирована полным resync
|
||||
- `repost_disabled` — репосты временно отключены до будущей реализации
|
||||
- `entrypoint_edit_forbidden` — `TEXT_ENTRYPOINT` нельзя редактировать через `TEXT_EDIT_POST`
|
||||
- `status_confirmed_target_must_be_status_action` — `STATUS_CONFIRMED` должен ссылаться на статусный блок
|
||||
- `status_action_target_not_allowed` — выбранный `STATUS_ACTION` нельзя ставить на этот тип материала
|
||||
- `bad_channel_meta_line`, `channel_not_found`, `bad_channel_meta_*`, `channel_meta_*_too_long` — ошибки `TEXT_CHANNEL_META`
|
||||
- `internal_error`
|
||||
|
||||
@@ -104,8 +107,13 @@
|
||||
- `TEXT_EDIT_POST (11)`
|
||||
- `TEXT_REPLY (20)`
|
||||
- `TEXT_EDIT_REPLY (21)`
|
||||
- `TEXT_REPOST (30)` — формат зарезервирован, но новые блоки временно отклоняются с `repost_disabled`
|
||||
- `TEXT_CHANNEL_META (70)` — скрытый технический снимок профиля канала
|
||||
- `TEXT_RATING (30)` — target-based отзыв на конкретный блок
|
||||
- `TEXT_REPOST (50)` — формат зарезервирован, но новые блоки временно отклоняются с `repost_disabled`
|
||||
- `TEXT_CHANNEL_META (90)` — скрытый технический снимок профиля канала
|
||||
- `TEXT_ENTRYPOINT (100)` — входная страница канала
|
||||
- `TEXT_EXERCISE (110)` — line-based материал упражнения
|
||||
- `TEXT_SERVICE (120)` — line-based материал услуги / процедуры
|
||||
- `TEXT_COURSE (130)` — line-based материал курса
|
||||
|
||||
3. **REACTION (type=2)**
|
||||
- `REACTION_LIKE (1)`
|
||||
@@ -135,6 +143,17 @@
|
||||
5. **USER_PARAM (type=4)**
|
||||
- `USER_PARAM_TEXT_TEXT (1)`
|
||||
|
||||
6. **STATUS_ACTION (type=5)**
|
||||
- `STATUS_DONE_ONCE (10)`
|
||||
- `STATUS_LEARNED (20)`
|
||||
- `STATUS_SERVICE_PASSED (30)`
|
||||
- `STATUS_CONFIRMED (100)`
|
||||
- `STATUS_INTERESTED (110)`
|
||||
- `STATUS_STARTED (120)`
|
||||
- `STATUS_IN_STUDY (130)`
|
||||
- `STATUS_ABANDONED (140)`
|
||||
- `STATUS_COMPLETED (150)`
|
||||
|
||||
## 6. Практические payload-форматы для каналов и вложений
|
||||
|
||||
`AddBlock` не имеет отдельных JSON-полей для вложений, аватаров или человекочитаемого имени канала. Клиент собирает бинарный блок нужного типа, а новые данные кладёт в текстовые поля тела блока по правилам blockchain-формата.
|
||||
@@ -173,7 +192,7 @@
|
||||
|
||||
### Изменение профиля канала
|
||||
|
||||
Последующие изменения аватара, человекочитаемого имени или описания канала пишутся отдельным скрытым `TEXT_CHANNEL_META (subType=70)`.
|
||||
Последующие изменения аватара, человекочитаемого имени или описания канала пишутся отдельным скрытым `TEXT_CHANNEL_META (subType=90)`.
|
||||
|
||||
Текстовое содержимое body использует тот же формат полного снимка профиля:
|
||||
|
||||
|
||||
@@ -297,6 +297,7 @@
|
||||
Первый целевой сценарий:
|
||||
|
||||
- `remote AddBlock via homeserver session`
|
||||
- межсессионная доставка звонковых `call_*` сигналов между устройствами пользователя
|
||||
|
||||
То есть телефон без локального `blockchain.key` может:
|
||||
|
||||
@@ -336,6 +337,12 @@
|
||||
- запрос должен быть подписан и `session key`, и `client key`;
|
||||
- в будущем для отдельных wallet-сценариев `clientSignatureB64` может быть пустой.
|
||||
|
||||
Для звонковых сигналов `signalType = call_*` правило строже:
|
||||
|
||||
- `clientSignatureB64` обязателен;
|
||||
- сервер отклоняет такой `SendSignal`, если `client key` подпись не передана;
|
||||
- это правило действует и для `single_session`, и для `all_sessions`.
|
||||
|
||||
### Запрос в одну сессию
|
||||
|
||||
```json
|
||||
@@ -366,11 +373,20 @@
|
||||
"ok": true,
|
||||
"payload": {
|
||||
"deliveredCount": 1,
|
||||
"deliveredSessionIds": ["sess-hs-001"]
|
||||
"deliveredSessionIds": ["sess-hs-001"],
|
||||
"deliveredWsSessions": 1,
|
||||
"deliveredFcmSessions": 0,
|
||||
"deliveredWebPushSessions": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Для `call_invite` с `targetMode = "all_sessions"` сервер может дополнительно отправить offline web-push в те сессии адресата, которые сейчас не подключены по WebSocket. В таком случае:
|
||||
|
||||
- `deliveredWsSessions` — сколько сессий получили `IncomingSignal` по WebSocket;
|
||||
- `deliveredWebPushSessions` — сколько offline-сессий получили web-push;
|
||||
- `deliveredFcmSessions` пока дублирует тот же счётчик push-доставки и зарезервирован под отдельные push-каналы.
|
||||
|
||||
### Событие на принимающей стороне
|
||||
|
||||
```json
|
||||
@@ -428,6 +444,7 @@
|
||||
- `404 / USER_NOT_FOUND` — логин адресата не найден.
|
||||
- `400 / BAD_DATA` — сервер не смог обработать `data`.
|
||||
- `400 / BAD_SESSION_SIGNATURE` — некорректная подпись `session key`.
|
||||
- `400 / CLIENT_SIGNATURE_REQUIRED` — для `call_*` сигнала не передана обязательная подпись `client key`.
|
||||
- `400 / BAD_CLIENT_SIGNATURE` — некорректная подпись `client key`.
|
||||
- `404 / SESSION_NOT_FOUND` — при `single_session` целевая сессия не найдена или не онлайн.
|
||||
- `404 / NO_TARGET_SESSIONS` — при `all_sessions` у пользователя сейчас нет активных онлайн-сессий.
|
||||
|
||||
@@ -14,11 +14,13 @@
|
||||
3. `GetMessageThread` — отдает дерево обсуждения вокруг конкретного сообщения:
|
||||
предки, фокус-сообщение, потомки.
|
||||
|
||||
4. `GetChannelsCounters` — отдает счетчики разделов каналов для пользователя.
|
||||
4. `GetPersonalDiary` — отдает виртуальную ленту `Личный дневник`, собранную из `STATUS_ACTION` текущего пользователя.
|
||||
|
||||
5. `ListGroupChats200` — отдает список групповых чатов типа `200`.
|
||||
5. `GetChannelsCounters` — отдает счетчики разделов каналов для пользователя.
|
||||
|
||||
6. `GetGroupDialog` — отдает сообщения конкретного группового чата типа `200`.
|
||||
6. `ListGroupChats200` — отдает список групповых чатов типа `200`.
|
||||
|
||||
7. `GetGroupDialog` — отдает сообщения конкретного группового чата типа `200`.
|
||||
|
||||
> На первом этапе мы **не используем курсоры** (`nextCursor`) и загружаем полные списки.
|
||||
|
||||
@@ -192,6 +194,7 @@
|
||||
"text": "текущая версия",
|
||||
"likesCount": 12,
|
||||
"repliesCount": 3,
|
||||
"ratingsCount": 2,
|
||||
"versionsTotal": 4,
|
||||
"versions": [
|
||||
{ "versionIndex": 1, "blockNumber": 140, "blockHash": "...", "text": "v1", "createdAtMs": 1760000000000 },
|
||||
@@ -248,6 +251,36 @@
|
||||
- `rawBlockB64` — сырой `block_bytes` текущего блока в Base64.
|
||||
- Поле `rawBlockB64` присутствует у узлов во всех частях ответа `GetMessageThread`: `focus`, `ancestors[]`, `descendants[]`.
|
||||
- В `GetChannelMessages` поле `rawBlockB64` **не добавляется** (лента канала без сырого блока, чтобы не раздувать ответ).
|
||||
- И в `GetChannelMessages`, и в `GetMessageThread` каждое сообщение теперь содержит:
|
||||
- `repliesCount` — число дочерних сообщений типа `TEXT_REPLY`;
|
||||
- `ratingsCount` — число дочерних сообщений типа `TEXT_RATING`.
|
||||
- В `descendants[]` операции `GetMessageThread` возвращаются оба типа дочерних текстовых сообщений:
|
||||
- `TEXT_REPLY`;
|
||||
- `TEXT_RATING`.
|
||||
Они идут в одной общей ветке обсуждения и сортируются по времени создания.
|
||||
|
||||
---
|
||||
|
||||
## 4) GetPersonalDiary
|
||||
|
||||
Возвращает виртуальный канал `Личный дневник` для самого пользователя.
|
||||
|
||||
- Вызов доступен только владельцу дневника.
|
||||
- Сообщения в ответе строятся из блоков `STATUS_ACTION`.
|
||||
- Поля `targetMsgSubType`, `targetText`, `targetAuthorLogin`, `targetAuthorBlockchainName`, `targetCreatedAtMs` описывают исходный материал, к которому относится действие.
|
||||
|
||||
### Request
|
||||
```json
|
||||
{
|
||||
"op": "GetPersonalDiary",
|
||||
"requestId": "req-4",
|
||||
"payload": {
|
||||
"login": "Alice",
|
||||
"limit": 200,
|
||||
"sort": "asc"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
| `ListSubscriptionsFeed` | `06_Channels_Read_API.md` | лента каналов/подписок |
|
||||
| `GetChannelMessages` | `06_Channels_Read_API.md` | сообщения канала |
|
||||
| `GetMessageThread` | `06_Channels_Read_API.md` | тред сообщения |
|
||||
| `GetPersonalDiary` | `06_Channels_Read_API.md` | виртуальный канал `Личный дневник` из STATUS_ACTION |
|
||||
| `GetChannelsCounters` | `06_Channels_Read_API.md` | счетчики разделов каналов |
|
||||
| `ListGroupChats200` | `06_Channels_Read_API.md` | список групповых чатов типа `200` |
|
||||
| `GetGroupDialog` | `06_Channels_Read_API.md` | сообщения группового чата типа `200` |
|
||||
|
||||
@@ -11,14 +11,16 @@
|
||||
- `12_REACTION_Blocks.md` — реакции (`type=2`).
|
||||
- `13_CONNECTION_Blocks.md` — связи/подписки (`type=3`).
|
||||
- `14_USER_PARAM_Blocks.md` — пользовательские параметры (`type=4`).
|
||||
- `15_STATUS_ACTION_Blocks.md` — статусные действия (`type=5`).
|
||||
|
||||
## Быстрая карта типов
|
||||
|
||||
- `type=0` — TECH: HEADER, CREATE_CHANNEL.
|
||||
- `type=1` — TEXT: POST/EDIT_POST/REPLY/EDIT_REPLY/REPOST.
|
||||
- `type=1` — TEXT: POST/EDIT_POST/REPLY/EDIT_REPLY/RATING/REPOST/CHANNEL_META/ENTRYPOINT/EXERCISE/SERVICE/COURSE.
|
||||
- `type=2` — REACTION: LIKE/UNLIKE.
|
||||
- `type=3` — CONNECTION: FRIEND/CONTACT/FOLLOW/SPOUSE/PARENT/CHILD/SIBLING и обратные операции.
|
||||
- `type=4` — USER_PARAM: key/value-параметры пользователя.
|
||||
- `type=5` — STATUS_ACTION: DONE_ONCE/LEARNED/SERVICE_PASSED/CONFIRMED/INTERESTED/STARTED/IN_STUDY/ABANDONED/COMPLETED.
|
||||
|
||||
## Примечание
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
Описание, человекочитаемое имя и аватар канала меняются только через скрытый технический блок:
|
||||
|
||||
- `msg_type=1`
|
||||
- `subType=70`
|
||||
- `subType=90`
|
||||
- `TEXT_CHANNEL_META`
|
||||
|
||||
Спецификация: [16_TEXT_Channel_Meta.md](./16_TEXT_Channel_Meta.md).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# TEXT блоки (`type=1`, `version=1`)
|
||||
|
||||
TEXT-тип хранит сообщения и редактирования.
|
||||
TEXT-тип хранит сообщения, материалы и редактирования.
|
||||
|
||||
## Подтипы
|
||||
|
||||
@@ -21,24 +21,52 @@ TEXT-тип хранит сообщения и редактирования.
|
||||
- target на исходный REPLY + новый текст.
|
||||
- допускается пустой `text` для логического удаления сообщения (без физического удаления блока).
|
||||
|
||||
5. `subType=30` — `TEXT_REPOST`
|
||||
5. `subType=30` — `TEXT_RATING`
|
||||
- target-based отзыв на конкретный блок;
|
||||
- содержит target (`toBlockchainName`, `toBlockGlobalNumber`, `toBlockHash32`) + текст отзыва;
|
||||
- не является сообщением линии канала.
|
||||
|
||||
6. `subType=50` — `TEXT_REPOST`
|
||||
- репост сообщения в линию канала;
|
||||
- содержит line-поля + target на оригинальное сообщение + текст комментария;
|
||||
- на текущем этапе продуктовой логики репост не редактируется (версии не накапливаются);
|
||||
- временно отключён для записи через `AddBlock` до будущей реализации репостов.
|
||||
|
||||
6. `subType=70` — `TEXT_CHANNEL_META`
|
||||
7. `subType=90` — `TEXT_CHANNEL_META`
|
||||
- скрытый технический снимок профиля канала;
|
||||
- содержит line-поля + текст с тегами `SHiNE:title`/`SHiNE:avatar` и описанием;
|
||||
- не отображается как обычное сообщение ленты;
|
||||
- применяется сервером к текущему состоянию канала.
|
||||
|
||||
8. `subType=100` — `TEXT_ENTRYPOINT`
|
||||
- входная страница канала;
|
||||
- line-based сообщение с тем же body, что у `TEXT_POST`;
|
||||
- не редактируется через `TEXT_EDIT_POST`: новая версия создаётся новым `TEXT_ENTRYPOINT`.
|
||||
|
||||
9. `subType=110` — `TEXT_EXERCISE`
|
||||
- line-based материал упражнения;
|
||||
- использует тот же body, что у `TEXT_POST`.
|
||||
|
||||
10. `subType=120` — `TEXT_SERVICE`
|
||||
- line-based материал услуги / процедуры;
|
||||
- использует тот же body, что у `TEXT_POST`.
|
||||
|
||||
11. `subType=130` — `TEXT_COURSE`
|
||||
- line-based материал курса;
|
||||
- использует тот же body, что у `TEXT_POST`.
|
||||
|
||||
Подробная спецификация: [16_TEXT_Channel_Meta.md](./16_TEXT_Channel_Meta.md).
|
||||
|
||||
## Правило для edit
|
||||
|
||||
`EDIT_POST` и `EDIT_REPLY` должны ссылаться на **оригинальный** блок, а не на предыдущий edit.
|
||||
|
||||
Важно:
|
||||
|
||||
- `TEXT_EDIT_POST` — технический edit для line-based сообщений канала;
|
||||
- `TEXT_EDIT_REPLY` — технический edit для reply-сообщений;
|
||||
- `TEXT_ENTRYPOINT` через `TEXT_EDIT_POST` не редактируется.
|
||||
|
||||
## Пустой text в edit
|
||||
|
||||
- Для `TEXT_EDIT_POST` и `TEXT_EDIT_REPLY` допустим `textLen=0`.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# STATUS_ACTION блоки (`type=5`, `version=1`)
|
||||
|
||||
`STATUS_ACTION` хранит статусные действия пользователя по отношению к конкретному материалу.
|
||||
|
||||
## Подтипы
|
||||
|
||||
1. `subType=10` — `STATUS_DONE_ONCE`
|
||||
- выполнил упражнение один раз.
|
||||
|
||||
2. `subType=20` — `STATUS_LEARNED`
|
||||
- изучил упражнение / комплекс.
|
||||
|
||||
3. `subType=30` — `STATUS_SERVICE_PASSED`
|
||||
- прошёл услугу / процедуру.
|
||||
|
||||
4. `subType=100` — `STATUS_CONFIRMED`
|
||||
- подтвердил чужой status-блок.
|
||||
|
||||
5. `subType=110` — `STATUS_INTERESTED`
|
||||
- заинтересовался курсом.
|
||||
|
||||
6. `subType=120` — `STATUS_STARTED`
|
||||
- начал курс.
|
||||
|
||||
7. `subType=130` — `STATUS_IN_STUDY`
|
||||
- находится в процессе полноценного изучения курса.
|
||||
|
||||
8. `subType=140` — `STATUS_ABANDONED`
|
||||
- бросил курс.
|
||||
|
||||
9. `subType=150` — `STATUS_COMPLETED`
|
||||
- завершил курс.
|
||||
|
||||
## Формат body
|
||||
|
||||
Все `STATUS_ACTION` используют один и тот же бинарный body-формат:
|
||||
|
||||
```text
|
||||
[1] toBlockchainNameLen (uint8)
|
||||
[N] toBlockchainName UTF-8
|
||||
[4] toBlockGlobalNumber
|
||||
[32] toBlockHash32
|
||||
[2] textLenBytes (uint16)
|
||||
[M] text UTF-8
|
||||
```
|
||||
|
||||
Где:
|
||||
|
||||
- `toBlockchainName` — блокчейн, в котором находится целевой материал;
|
||||
- `toBlockGlobalNumber` — номер целевого блока;
|
||||
- `toBlockHash32` — хэш целевого блока;
|
||||
- `text` — опциональное пояснение пользователя к статусу.
|
||||
|
||||
## Правила
|
||||
|
||||
- Каждый `STATUS_ACTION` всегда является target-based сообщением.
|
||||
- `STATUS_ACTION` не является line-based сообщением канала.
|
||||
- Текст может быть пустым: основной смысл задаётся самим `subType`.
|
||||
- Базовые статусы пользователь ставит сам за себя.
|
||||
- `STATUS_CONFIRMED` ставится другим человеком на конкретный status-блок.
|
||||
- Допустимые target-типы:
|
||||
- `STATUS_DONE_ONCE` и `STATUS_LEARNED` только для `TEXT_EXERCISE`;
|
||||
- `STATUS_SERVICE_PASSED` только для `TEXT_SERVICE`;
|
||||
- `STATUS_INTERESTED`, `STATUS_STARTED`, `STATUS_IN_STUDY`, `STATUS_ABANDONED`, `STATUS_COMPLETED` только для `TEXT_COURSE`.
|
||||
|
||||
## Что не поддерживается
|
||||
|
||||
- Отдельного `EDIT` для `STATUS_ACTION` нет.
|
||||
- Если нужно изменить смысл статуса, пишется новое статусное событие.
|
||||
@@ -5,7 +5,7 @@
|
||||
## Блок
|
||||
|
||||
- `msg_type = 1`
|
||||
- `subType = 70`
|
||||
- `subType = 90`
|
||||
- `msgVersion = 1`
|
||||
- body использует тот же бинарный формат line-текста, что и `TEXT_POST`: line-поля + `textLen` + UTF-8 текст.
|
||||
|
||||
|
||||
@@ -1,5 +1,36 @@
|
||||
# История изменений документации блокчейна
|
||||
|
||||
## 2026-08-09 23:30:06 +0400
|
||||
- Базовый коммит-ориентир: `ee185cf`.
|
||||
- Нумерация `STATUS_ACTION` уточнена под дневник действий:
|
||||
- `10` — `STATUS_DONE_ONCE`;
|
||||
- `20` — `STATUS_LEARNED`;
|
||||
- `30` — `STATUS_SERVICE_PASSED`;
|
||||
- `100` — `STATUS_CONFIRMED`;
|
||||
- `110/120/130/140/150` — курсные статусы `INTERESTED/STARTED/IN_STUDY/ABANDONED/COMPLETED`.
|
||||
- Зафиксированы допустимые связи `STATUS_ACTION -> target`:
|
||||
- упражнение: `DONE_ONCE`, `LEARNED`;
|
||||
- услуга: `SERVICE_PASSED`;
|
||||
- курс: `INTERESTED`, `STARTED`, `IN_STUDY`, `ABANDONED`, `COMPLETED`.
|
||||
- Добавлен серверный read API `GetPersonalDiary` для виртуальной ленты личного дневника из `STATUS_ACTION`.
|
||||
|
||||
## 2026-08-09 19:40:00 +0400
|
||||
- Базовый коммит-ориентир: `3552e05`.
|
||||
- Уточнено серверное чтение каналов и тредов для `TEXT_RATING`:
|
||||
- `GetChannelMessages` и `GetMessageThread` теперь отдают отдельное поле `ratingsCount`;
|
||||
- `GetMessageThread` включает `TEXT_RATING` в общее дерево потомков вместе с `TEXT_REPLY`;
|
||||
- в `docs/API/06_Channels_Read_API.md` зафиксировано, что потомки треда возвращаются вперемешку по времени создания.
|
||||
|
||||
## 2026-08-09 18:55:16 +0400
|
||||
- Базовый коммит-ориентир: `43f54c9`.
|
||||
- Для первой итерации новых контентных типов обновлена карта `TEXT`-подтипов:
|
||||
- `TEXT_RATING` добавлен как `subType=30` и трактуется как target-based отзыв на конкретный блок;
|
||||
- `TEXT_REPOST` перенесён на `subType=50` и оставлен как отложенная заготовка;
|
||||
- `TEXT_CHANNEL_META` перенесён на `subType=90`;
|
||||
- добавлены line-based `TEXT_ENTRYPOINT (100)`, `TEXT_EXERCISE (110)`, `TEXT_SERVICE (120)`, `TEXT_COURSE (130)`.
|
||||
- Добавлен новый верхнеуровневый тип `STATUS_ACTION (type=5)` с подтипами `10/20/30/40/50/60/70/80`.
|
||||
- `CHANNEL_MEMBERSHIP` в текущую реализацию не включён и ведётся отдельно как отложенная тема.
|
||||
|
||||
## 2026-08-04 12:00:00 +0400
|
||||
- Базовый коммит-ориентир: `391b18a`.
|
||||
- Формат вложений расширен до `SHiNE:attach v=2` для опционального второго preview-файла: добавлены поля `previewAr` и `previewSha256` при сохранении совместимости со старыми `v=1`.
|
||||
|
||||
@@ -17,15 +17,17 @@
|
||||
Социальные связи (`msg_type=3`).
|
||||
7. [14_USER_PARAM_Blocks.md](./14_USER_PARAM_Blocks.md)
|
||||
Параметры пользователя (`msg_type=4`).
|
||||
8. [15_TEXT_Attachments.md](./15_TEXT_Attachments.md)
|
||||
8. [15_STATUS_ACTION_Blocks.md](./15_STATUS_ACTION_Blocks.md)
|
||||
Статусные действия пользователя (`msg_type=5`).
|
||||
9. [16_TEXT_Attachments.md](./16_TEXT_Attachments.md)
|
||||
Вложения в TEXT-сообщениях через `SHiNE:attach v=1/v=2`, включая опциональные `previewAr/previewSha256` для видео и крупных изображений.
|
||||
9. [16_TEXT_Channel_Meta.md](./16_TEXT_Channel_Meta.md)
|
||||
10. [16_TEXT_Channel_Meta.md](./16_TEXT_Channel_Meta.md)
|
||||
Скрытый `TEXT_CHANNEL_META` для профиля канала.
|
||||
10. [01_Channel_Types_and_CreateChannel.md](./01_Channel_Types_and_CreateChannel.md)
|
||||
11. [01_Channel_Types_and_CreateChannel.md](./01_Channel_Types_and_CreateChannel.md)
|
||||
Типы каналов и формат `CreateChannelBody`.
|
||||
11. [02_Channel_Commands.md](./02_Channel_Commands.md)
|
||||
12. [02_Channel_Commands.md](./02_Channel_Commands.md)
|
||||
Команды в текстовых сообщениях каналов.
|
||||
12. [CHANGELOG.md](./CHANGELOG.md)
|
||||
13. [CHANGELOG.md](./CHANGELOG.md)
|
||||
Журнал изменений документации.
|
||||
|
||||
## Смежная документация
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# Figma
|
||||
|
||||
Эта папка хранит рабочие инструкции по переносу экранов SHiNE в Figma и по обратному переносу изменений из Figma в код.
|
||||
|
||||
## Что здесь лежит
|
||||
|
||||
- `README.md` — точка входа и краткий регламент.
|
||||
- `TRANSFER_UI_SCREENS.md` — подробная инструкция по переносу экранов UI в Figma и обратно.
|
||||
|
||||
## Когда читать
|
||||
|
||||
Читать перед любыми задачами вида:
|
||||
- перенести экран из `shine-UI` в Figma;
|
||||
- собрать новый Figma-файл для экранов SHiNE;
|
||||
- перенести изменения из Figma обратно в код;
|
||||
- уточнить, каким способом переносить экраны: по одному или пачкой.
|
||||
|
||||
## Ключевое правило
|
||||
|
||||
Для экранов SHiNE безопасный рабочий способ на текущий момент:
|
||||
- переносить экраны в Figma по одному;
|
||||
- не пытаться сразу переносить длинный auth-flow пачкой;
|
||||
- после каждого переноса визуально проверять результат в самой Figma;
|
||||
- только после удачного одного экрана переходить к следующему.
|
||||
|
||||
## Про Miro
|
||||
|
||||
Отдельной папки `Miro` пока нет.
|
||||
|
||||
Причина:
|
||||
- практики по Miro в проекте пока мало;
|
||||
- устойчивого процесса ещё нет;
|
||||
- как только появится стабильный сценарий работы с Miro, его нужно будет оформить аналогично Figma.
|
||||
@@ -1,222 +0,0 @@
|
||||
# Перенос экранов UI в Figma и обратно
|
||||
|
||||
## Зачем нужен этот документ
|
||||
|
||||
Этот документ фиксирует практический опыт, который уже был получен на переносе стартового экрана, экрана регистрации и дальнейших попытках.
|
||||
|
||||
Главная цель:
|
||||
- чтобы агент не повторял неудачные попытки;
|
||||
- чтобы переносы делались одинаково;
|
||||
- чтобы изменения из Figma можно было уверенно переносить назад в `shine-UI`.
|
||||
|
||||
## Где находится основной UI
|
||||
|
||||
- основной клиентский UI: `shine-UI/`
|
||||
- маршруты и список pre-auth экранов: `shine-UI/js/router.js`
|
||||
- экраны: `shine-UI/js/pages/`
|
||||
- общие стили: `shine-UI/styles/main.css`, `shine-UI/styles/layout.css`, `shine-UI/styles/components.css`
|
||||
|
||||
## Что считать успешным переносом в Figma
|
||||
|
||||
Успешный перенос экрана в Figma — это не просто фон и прямоугольники.
|
||||
|
||||
Нужно, чтобы:
|
||||
- были видны все ключевые текстовые элементы;
|
||||
- кнопки были перенесены как отдельные элементы;
|
||||
- поля ввода были явно видны;
|
||||
- экран был узнаваем визуально;
|
||||
- пользователь мог вручную подправить макет в Figma;
|
||||
- после правок можно было понять, что именно переносить обратно в код.
|
||||
|
||||
## Текущий рабочий способ
|
||||
|
||||
На текущем проекте лучший практический способ такой:
|
||||
|
||||
1. Переносить только один экран за раз.
|
||||
2. Сначала читать конкретный `js/pages/<screen>.js`.
|
||||
3. Затем читать связанные стили из `styles/components.css` и `styles/layout.css`.
|
||||
4. После этого вручную собирать экран в Figma как отдельный frame с явными элементами.
|
||||
5. Проверять в Figma, что не получился только фон без текста и контролов.
|
||||
6. Только после успешной проверки переходить к следующему экрану.
|
||||
|
||||
## Почему нельзя переносить пачкой
|
||||
|
||||
Был получен негативный опыт:
|
||||
- при переносе сразу многих экранов в Figma часть экранов отображалась как фон без нормальных надписей и элементов;
|
||||
- длинные экраны с большим количеством текста и форм разваливались;
|
||||
- автогенерация давала внешний вид, непригодный для ручной доработки.
|
||||
|
||||
Поэтому правило такое:
|
||||
- auth-flow, регистрация, вход, onboarding — переносить по одному экрану;
|
||||
- после каждого экрана ждать визуального подтверждения пользователя;
|
||||
- не объединять 5-10 экранов в один проход без отдельного разрешения и без промежуточной проверки.
|
||||
|
||||
## Рекомендуемый порядок переноса в Figma
|
||||
|
||||
### Вперёд: код -> Figma
|
||||
|
||||
1. Определить точный экран.
|
||||
2. Найти файл экрана в `shine-UI/js/pages/`.
|
||||
3. Найти используемые CSS-классы через поиск по файлу экрана.
|
||||
4. Вытащить:
|
||||
- тексты;
|
||||
- состав кнопок;
|
||||
- поля ввода;
|
||||
- карточки;
|
||||
- блоки статуса;
|
||||
- последовательность секций.
|
||||
5. Если экран длинный, всё равно переносить его как один frame, но собирать блоками сверху вниз.
|
||||
6. В Figma создавать отдельный экран рядом с уже существующими экранами, а не смешивать всё в одну кучу.
|
||||
7. После создания экрана проверить метаданные/скриншот Figma, если инструмент это позволяет.
|
||||
|
||||
### Назад: Figma -> код
|
||||
|
||||
1. Снять актуальный скриншот изменённого Figma-экрана.
|
||||
2. Получить метаданные узла, если это помогает понять структуру.
|
||||
3. Сравнить Figma с текущим кодом экрана.
|
||||
4. Переносить обратно в код только реальные изменения:
|
||||
- порядок блоков;
|
||||
- тексты;
|
||||
- размеры/отступы;
|
||||
- наличие или отсутствие карточек;
|
||||
- подписи кнопок;
|
||||
- видимость блоков.
|
||||
5. Не придумывать новые UX-решения без отдельного подтверждения пользователя, если их нет в Figma.
|
||||
6. После правок проверять экран локально или как минимум по коду и зависимостям.
|
||||
|
||||
## Что переносить вручную
|
||||
|
||||
Вручную, а не автогенерацией, нужно переносить:
|
||||
- экраны регистрации;
|
||||
- экраны входа;
|
||||
- длинные формы;
|
||||
- экраны с несколькими карточками;
|
||||
- экраны с длинными объясняющими текстами;
|
||||
- экраны, где важен порядок блоков.
|
||||
|
||||
Причина:
|
||||
- именно они чаще всего ломаются при слишком автоматическом переносе.
|
||||
|
||||
## Какие ошибки уже были
|
||||
|
||||
### Ошибка 1. Перенос пачкой
|
||||
|
||||
Проблема:
|
||||
- несколько экранов были добавлены сразу;
|
||||
- пользователь увидел, что на экранах в Figma «какая-то ерунда».
|
||||
|
||||
Вывод:
|
||||
- переносить по одному.
|
||||
|
||||
### Ошибка 2. Видно только фон
|
||||
|
||||
Проблема:
|
||||
- frame создавался, фон и свечения были видны;
|
||||
- тексты и элементы либо не появлялись, либо получались непригодными.
|
||||
|
||||
Вывод:
|
||||
- при сложных экранах собирать элементы вручную и явно.
|
||||
|
||||
### Ошибка 3. Слишком вольная реконструкция
|
||||
|
||||
Проблема:
|
||||
- экран формально был перенесён, но визуально не соответствовал ожиданию пользователя.
|
||||
|
||||
Вывод:
|
||||
- для SHiNE важнее узнаваемый и редактируемый экран, чем «формально похожий» экран.
|
||||
|
||||
## Обязательные проверки после переноса в Figma
|
||||
|
||||
После каждого нового экрана агент должен проверить:
|
||||
- виден ли заголовок;
|
||||
- видны ли кнопки;
|
||||
- видны ли поля ввода;
|
||||
- не исчезли ли длинные тексты;
|
||||
- не сломан ли порядок секций;
|
||||
- не оказался ли на холсте только фон и пустые прямоугольники.
|
||||
|
||||
Если хотя бы один пункт не выполнен:
|
||||
- не считать перенос завершённым;
|
||||
- либо переделать экран сразу;
|
||||
- либо остановиться и показать пользователю только после исправления.
|
||||
|
||||
## Правила для длинных экранов
|
||||
|
||||
Если экран длинный, например регистрация:
|
||||
- высота frame может быть больше стандартной мобильной высоты;
|
||||
- секции должны идти в правильном вертикальном порядке;
|
||||
- отдельные карточки должны быть вынесены в отдельные блоки;
|
||||
- тексты лучше упрощённо располагать вручную, чем терять их совсем.
|
||||
|
||||
## Правила для экрана регистрации
|
||||
|
||||
Экран `register-view` особенно чувствительный.
|
||||
|
||||
При переносе нужно отдельно учитывать:
|
||||
- заголовок и стрелку назад;
|
||||
- поля логина и пароля;
|
||||
- строку статуса длины пароля;
|
||||
- строку статуса проверки логина;
|
||||
- кнопку проверки логина;
|
||||
- отдельную карточку первого сервера;
|
||||
- отдельную карточку FAQ;
|
||||
- нижние кнопки `Назад` и `Далее`.
|
||||
|
||||
## Правила для экрана входа
|
||||
|
||||
Для экранов входа важно не смешивать:
|
||||
- экран выбора способа входа;
|
||||
- вход по логину/паролю;
|
||||
- вход через другое устройство;
|
||||
- вход по QR.
|
||||
|
||||
Каждый из них переносить отдельно.
|
||||
|
||||
## Что делать после правок пользователя в Figma
|
||||
|
||||
Если пользователь изменил экран в Figma:
|
||||
|
||||
1. Считать Figma источником визуальной правки.
|
||||
2. Сначала понять, что именно изменено:
|
||||
- тексты;
|
||||
- порядок блоков;
|
||||
- наличие блоков;
|
||||
- размеры;
|
||||
- отступы;
|
||||
- логика flow.
|
||||
3. Переносить эти изменения назад в код минимально необходимыми правками.
|
||||
4. Если из Figma следует уже не только визуальная, но и UX-логическая правка, отдельно проверить, что она согласована пользователем.
|
||||
|
||||
## Когда нужно отдельно согласовать ручную проверку
|
||||
|
||||
Если после изменения по Figma:
|
||||
- поменялась логика flow;
|
||||
- поменялась регистрация/вход;
|
||||
- нужен реальный прогон на test2;
|
||||
- затронута интеграция с Solana;
|
||||
|
||||
тогда нужно отдельно согласовать ручную проверку с пользователем.
|
||||
|
||||
## Что пока не оформлено для Miro
|
||||
|
||||
По Miro пока нет устойчивого процесса.
|
||||
|
||||
Из того, что уже понятно:
|
||||
- пока не стоит обещать такой же отлаженный перенос, как для Figma;
|
||||
- сначала нужно накопить хотя бы 2-3 реальных сценария работы;
|
||||
- только после этого оформлять отдельную папку и регламент.
|
||||
|
||||
## Краткая памятка для агента
|
||||
|
||||
Если задача звучит как:
|
||||
- «перенеси экран в Figma»;
|
||||
- «добавь экран в Figma»;
|
||||
- «я поправил экран в Figma, перенеси назад»;
|
||||
|
||||
то агент должен:
|
||||
|
||||
1. Прочитать этот документ.
|
||||
2. Работать по одному экрану.
|
||||
3. Не переносить auth-flow пачкой.
|
||||
4. Проверять результат после каждого экрана.
|
||||
5. При переносе обратно в код не гадать, а опираться на Figma-правки.
|
||||
@@ -1,230 +0,0 @@
|
||||
# Личные сообщения (DM) — v0.5 устаревшая спецификация
|
||||
|
||||
## Статус документа
|
||||
|
||||
Этот документ устарел и сохранён только как историческое описание ранее реализованной схемы DM.
|
||||
|
||||
Aктуальная целевая спецификация:
|
||||
|
||||
- `docs/Personal_Messages/Протокол_DM_v1.md`
|
||||
|
||||
Что в этом документе считать устаревшим:
|
||||
|
||||
- трактовку `encryptedBody` как фактически одинакового содержимого пары;
|
||||
- отсутствие нормального E2EE-шифрования DM;
|
||||
- старую модель удаления через пустую ревизию без терминального tombstone;
|
||||
- старую трактовку обновления только как общей пары без отдельной будущей модели перешифровки;
|
||||
- все упоминания legacy-формата read-receipt как части целевой архитектуры следующего этапа.
|
||||
|
||||
## Текущее состояние
|
||||
|
||||
Сейчас в проекте реализованы:
|
||||
|
||||
- новый формат контентных личных сообщений `SHiNE_DM`;
|
||||
- ревизии сообщений через `revisionTimeMs`;
|
||||
- редактирование сообщения через повторную отправку той же логической пары;
|
||||
- удаление сообщения через пустую ревизию;
|
||||
- `upsert` последней версии сообщения на сервере.
|
||||
|
||||
Сейчас в проекте **не реализованы**:
|
||||
|
||||
- вложения в DM;
|
||||
- upload/download файлов для DM;
|
||||
- UI-кнопка прикрепления файла;
|
||||
- серверное хранение файловых связей для DM.
|
||||
|
||||
Черновик будущих вложений вынесен отдельно:
|
||||
|
||||
- `docs/Personal_Messages/Черновик_будущих_DM_вложений.md`
|
||||
|
||||
## Общая схема
|
||||
|
||||
Личное сообщение по-прежнему отправляется парой signed-блоков:
|
||||
|
||||
- `type=1` — входящий блок для получателя;
|
||||
- `type=2` — исходящая копия для отправителя.
|
||||
|
||||
Read-receipt пока остаются в legacy-формате:
|
||||
|
||||
- `type=3` — входящее подтверждение прочтения;
|
||||
- `type=4` — исходящая копия подтверждения.
|
||||
|
||||
Ключи сообщения:
|
||||
|
||||
- `baseKey = fromLogin|toLogin|timeMs|nonce`
|
||||
- `messageKey = baseKey|messageType`
|
||||
|
||||
Логический идентификатор письма задаётся парой:
|
||||
|
||||
- `timeMs`
|
||||
- `nonce`
|
||||
|
||||
Эти поля не меняются при редактировании или удалении. Меняется только:
|
||||
|
||||
- `revisionTimeMs`
|
||||
- содержимое `encryptedBody`
|
||||
|
||||
Сервер хранит только последнюю версию записи для каждого `messageKey`.
|
||||
|
||||
## Формат контентного DM: `SHiNE_DM`
|
||||
|
||||
Префикс бинарного блока:
|
||||
|
||||
- `SHiNE_DM`
|
||||
|
||||
Поля идут в big-endian порядке:
|
||||
|
||||
1. `formatVersionMajor` (`u8`) = `1`
|
||||
2. `formatVersionMinor` (`u8`) = `0`
|
||||
3. `toLoginLen` (`u8`) + `toLogin` (ASCII, `1..60`)
|
||||
4. `fromLoginLen` (`u8`) + `fromLogin` (ASCII, `1..60`)
|
||||
5. `timeMs` (`u64`)
|
||||
6. `nonce` (`u32`)
|
||||
7. `messageType` (`u16`) — только `1` или `2`
|
||||
8. `revisionTimeMs` (`u64`)
|
||||
9. `attachmentsCount` (`u8`)
|
||||
10. `encryptedBodyLen` (`u32`)
|
||||
11. `encryptedBody` (`bytes`)
|
||||
12. `signature` (`64 bytes`, Ed25519)
|
||||
|
||||
### Ограничения
|
||||
|
||||
- `attachmentsCount` сейчас всегда должен быть `0`
|
||||
- `encryptedBodyLen` сейчас ограничен сервером до `16384` байт
|
||||
- `revisionTimeMs` не может быть отрицательным
|
||||
|
||||
Если приходит `attachmentsCount != 0`, сервер отклоняет такой DM как:
|
||||
|
||||
- `ATTACHMENTS_DISABLED`
|
||||
|
||||
## Legacy read-receipt: `SHiNE_dm2`
|
||||
|
||||
Подтверждения прочтения `type=3/4` пока используют старый контейнер `SHiNE_dm2`:
|
||||
|
||||
1. `toLoginLen` (`u8`) + `toLogin`
|
||||
2. `fromLoginLen` (`u8`) + `fromLogin`
|
||||
3. `timeMs` (`u64`)
|
||||
4. `nonce` (`u32`)
|
||||
5. `messageType` (`u16`) — `3` или `4`
|
||||
6. `payloadLen` (`u16`)
|
||||
7. `payloadBytes`
|
||||
8. `signature`
|
||||
|
||||
## Редактирование
|
||||
|
||||
Редактирование делается новой отправкой той же логической пары сообщения:
|
||||
|
||||
- `timeMs` и `nonce` остаются теми же;
|
||||
- `messageType` остаётся `1/2`;
|
||||
- `revisionTimeMs` становится больше;
|
||||
- `encryptedBody` содержит новую версию текста.
|
||||
|
||||
Если на сервер приходит более старая ревизия, она игнорируется.
|
||||
|
||||
Если приходит та же ревизия и тот же бинарный блок, сервер тоже её не применяет повторно.
|
||||
|
||||
## Удаление
|
||||
|
||||
Удаление личного сообщения делается как новая ревизия того же сообщения:
|
||||
|
||||
- `timeMs` и `nonce` остаются прежними;
|
||||
- `revisionTimeMs` увеличивается;
|
||||
- `attachmentsCount = 0`;
|
||||
- `encryptedBodyLen = 0`;
|
||||
- `encryptedBody` пустой.
|
||||
|
||||
В UI такое сообщение не показывается.
|
||||
|
||||
На сервере это не отдельный тип сообщения, а просто последняя пустая ревизия того же `messageKey`.
|
||||
|
||||
## Поведение сервера
|
||||
|
||||
Для контентных DM сервер:
|
||||
|
||||
1. принимает пару signed-блоков `type=1/2`;
|
||||
2. валидирует формат, подпись и совпадение ключевых полей пары;
|
||||
3. проверяет, что для обеих сторон пары совпадают:
|
||||
- `fromLogin`
|
||||
- `toLogin`
|
||||
- `timeMs`
|
||||
- `nonce`
|
||||
- `revisionTimeMs`
|
||||
- `encryptedBody`
|
||||
4. делает `upsert` последней версии в `signed_messages_v2`;
|
||||
5. сбрасывает pending-доставку по сессиям для новой ревизии;
|
||||
6. рассылает актуальную версию адресатам через `SignedMessageArrived`.
|
||||
|
||||
История старых ревизий сейчас не хранится отдельно: в таблице остаётся только последняя версия по каждому `messageKey`.
|
||||
|
||||
## Хранение в БД
|
||||
|
||||
Основная таблица:
|
||||
|
||||
- `signed_messages_v2`
|
||||
|
||||
Для контентных DM в ней используются:
|
||||
|
||||
- `message_key`
|
||||
- `base_key`
|
||||
- `target_login`
|
||||
- `from_login`
|
||||
- `to_login`
|
||||
- `time_ms`
|
||||
- `nonce`
|
||||
- `message_type`
|
||||
- `revision_time_ms`
|
||||
- `raw_block`
|
||||
- `created_at_ms`
|
||||
|
||||
Отдельных таблиц файлов для DM сейчас нет.
|
||||
|
||||
## События и доставка
|
||||
|
||||
Запрос на отправку по WebSocket остаётся прежним:
|
||||
|
||||
- `SendMessagePair`
|
||||
- `ReceiveOutcomingMessage` как алиас
|
||||
|
||||
Клиент отправляет:
|
||||
|
||||
- `incomingBlobB64`
|
||||
- `outgoingBlobB64`
|
||||
|
||||
Событие в активные сессии:
|
||||
|
||||
- `SignedMessageArrived`
|
||||
|
||||
Если пришла новая ревизия того же сообщения, `messageKey` остаётся прежним, а внутри `blobB64` будет более новый `revisionTimeMs`.
|
||||
|
||||
Подтверждение доставки в сессию:
|
||||
|
||||
- `AckSessionDelivery`
|
||||
|
||||
WebPush и локальные уведомления сейчас работают так:
|
||||
|
||||
- для активной онлайн-сессии приоритет у доставки по WebSocket через `SignedMessageArrived`;
|
||||
- если целевая сессия не онлайн по WebSocket, сервер может отправить WebPush с `kind=new_message`;
|
||||
- если вкладка/приложение живы, но страница скрыта (`document.visibilityState !== visible`), UI дополнительно пытается показать системное уведомление через `service worker`;
|
||||
- для активной видимой страницы UI проигрывает короткий локальный сигнал на каждое новое входящее DM, если браузер ранее разрешил аудио-контекст после пользовательского жеста;
|
||||
- для скрытой, но живой страницы UI также делает `best effort` сигнал через `vibrate()` и более длинный локальный звук;
|
||||
- эти локальные сигналы не гарантируются браузером: на мобильных устройствах они зависят от политики Chrome/Android/iOS.
|
||||
|
||||
## Правила UI
|
||||
|
||||
UI сейчас работает так:
|
||||
|
||||
- показывает только текст `encryptedBody`;
|
||||
- умеет обновлять уже существующее сообщение по тому же `messageKey`;
|
||||
- не показывает удалённые сообщения;
|
||||
- позволяет владельцу сообщения вызвать меню `Скопировать как текст / Прочесть / Изменить / Удалить`;
|
||||
- при редактировании показывает над полем ввода полоску `Редактируем сообщение: ...` с кнопкой отмены;
|
||||
- после редактирования показывает под временем отдельную строку `изменено: <дата время>`;
|
||||
- на видимом экране чата/приложения проигрывает короткий локальный звук на новое входящее DM;
|
||||
- при входящем DM для скрытой, но ещё живой страницы пытается поднять системное уведомление через `service worker`;
|
||||
- не показывает и не принимает вложения.
|
||||
|
||||
## Что обязательно помнить
|
||||
|
||||
- вложения в DM сейчас отключены на уровне протокола и UI;
|
||||
- любые старые описания `/f/...`, `/upload` и файловых таблиц для DM больше не актуальны;
|
||||
- если позже вложения вернутся, их формат и серверная логика могут быть другими.
|
||||
+2
-1
@@ -1 +1,2 @@
|
||||
Данная документация местами устарела и не соответствует реальному коду
|
||||
Данная документация местами устарела и не соответствует реальному коду
|
||||
Особенно в отношениии работы с Блокчейном
|
||||
@@ -1,12 +0,0 @@
|
||||
shine-server-bd — это библиотека реалезующая всю работу с БД:
|
||||
|
||||
хранит пользователей/сессии/параметры/кэш IP→гео и данные блокчейна (состояние + блоки), предоставляя единый PostgreSQL runtime-контроллер соединений, набор DAO под каждую таблицу (Singleton, методы с Connection для транзакций и без Connection — сами открывают/закрывают), и простые entity-модели как контейнеры данных для маппинга ResultSet↔Java.
|
||||
|
||||
Логика структуры классов (в двух словах):
|
||||
|
||||
shine.db.DbController / shine.db.PostgresDbController — вход в runtime БД: читает `db.url/db.user/db.password`, подключается только к PostgreSQL и выдаёт новые `Connection`.
|
||||
shine.db.DatabaseInitializer — проверяет наличие `db_schema_version` и при пустой БД автоматически накатывает `postgres/schema_v1.sql`.
|
||||
|
||||
|
||||
shine.db.entities.* — POJO-модели строк таблиц (без логики, только поля/геттеры/сеттеры + иногда удобные методы вроде getClientKeyByte()).
|
||||
shine.db.dao.* — DAO по таблицам: ActiveSessionsDAO, CurrentUsersDAO, UserParamsDAO, IpGeoCacheDAO, BlockchainStateDAO, BlocksDAO, SignedMessagesDAO; плюс сервисные DAO под recovery/resync.
|
||||
@@ -1,91 +0,0 @@
|
||||
# PostgreSQL runtime schema v1
|
||||
|
||||
Дата фиксации: `2026-07-24`
|
||||
|
||||
## Назначение
|
||||
|
||||
Это целевая серверная runtime-схема PostgreSQL для SHiNE без опоры на SQLite.
|
||||
|
||||
Схема `v1` нужна как стартовая точка большого механического переноса DAO и runtime-запросов
|
||||
с существующей SQLite-логики на PostgreSQL.
|
||||
|
||||
## Ключевые решения
|
||||
|
||||
- Источник истины по пользователям: `solana_user_pda_current`.
|
||||
- Legacy-таблицы старого runtime для пользователей и личных сообщений в новой схеме не создаются.
|
||||
- Основная таблица серверных личных сообщений: `signed_messages`.
|
||||
- Таблица `blockchain_state` сохраняется как runtime-state таблица сервера:
|
||||
она не является identity-слоем и не мигрируется как legacy SQLite data.
|
||||
- Триггеры по `blocks` сохраняются и переписываются под PostgreSQL.
|
||||
|
||||
## Таблицы sync-модуля Solana users
|
||||
|
||||
- `solana_sync_state`
|
||||
- `solana_sync_tx_history`
|
||||
- `solana_user_pda_current`
|
||||
- `solana_user_pda_history`
|
||||
|
||||
## Таблицы server runtime
|
||||
|
||||
- `db_schema_version`
|
||||
- `active_sessions`
|
||||
- `esp_pairing_settings`
|
||||
- `esp_pairing_requests`
|
||||
- `users_params`
|
||||
- `ip_geo_cache`
|
||||
- `test_free_avatar_uploads`
|
||||
- `sync_servers`
|
||||
- `blockchain_state`
|
||||
- `blocks`
|
||||
- `connections_state`
|
||||
- `message_stats`
|
||||
- `reactions_state`
|
||||
- `channel_names_state`
|
||||
- `chat200_state`
|
||||
- `chat200_members_state`
|
||||
- `user_push_tokens`
|
||||
- `signed_direct_message_replay`
|
||||
- `signed_direct_messages_history`
|
||||
- `signed_messages`
|
||||
- `signed_message_session_delivery`
|
||||
|
||||
## Триггеры
|
||||
|
||||
Схема `v1` уже включает PostgreSQL-версии триггеров:
|
||||
|
||||
- `trg_blocks_line_integrity_bi`
|
||||
- `trg_blocks_connection_state_ai`
|
||||
- `trg_blocks_message_stats_like_ai`
|
||||
- `trg_blocks_message_stats_reply_ai`
|
||||
- `trg_blocks_edit_apply_ai`
|
||||
|
||||
## Что не входит в v1
|
||||
|
||||
- полная зачистка legacy-документации, старых названий и TODO-хвостов;
|
||||
- переименование Java DAO/классов `*V2` в runtime-коде;
|
||||
- перенос прямых SQL-запросов из хэндлеров в DAO/service;
|
||||
- переключение всего runtime-кода на новый `DbProvider`.
|
||||
|
||||
Это отдельные механические шаги поверх уже утверждённой схемы.
|
||||
|
||||
## Совместимость со старыми блоками каналов
|
||||
|
||||
В runtime-сервере сознательно нет жёсткой серверной проверки
|
||||
`channelName must not contain only digits`.
|
||||
|
||||
Причина: в уже существующей истории блокчейна есть каналы с числовыми именами,
|
||||
и при холодном восстановлении сервера с пустой БД и без `.bch` такие блоки должны
|
||||
успешно переигрываться от других sync-серверов.
|
||||
|
||||
Сейчас правило "новый публичный канал не должен состоять только из цифр" остаётся
|
||||
на уровне UI/продуктовых требований и должно быть позже возвращено на сервере
|
||||
отдельным совместимым способом, который не ломает replay исторических блоков.
|
||||
|
||||
## Инициализация пустой БД
|
||||
|
||||
Если сервер подключается к PostgreSQL через `db.url=jdbc:postgresql:...` и в выбранной БД ещё нет таблицы `db_schema_version`,
|
||||
он сам автоматически накатывает `schema_v1.sql` из classpath-ресурса:
|
||||
|
||||
- ресурс: `shine-server-db/src/main/resources/postgres/schema_v1.sql`
|
||||
- признак пустой схемы: отсутствует `db_schema_version`
|
||||
- стартовая версия схемы: `1`
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user