diff --git a/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/BodyRecordParser.java b/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/BodyRecordParser.java index 21103a16..35cf71b0 100644 --- a/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/BodyRecordParser.java +++ b/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/BodyRecordParser.java @@ -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", diff --git a/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/MsgSubType.java b/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/MsgSubType.java index f5b72745..eb780c99 100644 --- a/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/MsgSubType.java +++ b/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/MsgSubType.java @@ -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; } diff --git a/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/body/StatusActionBody.java b/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/body/StatusActionBody.java new file mode 100644 index 00000000..3a3bd0b9 --- /dev/null +++ b/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/body/StatusActionBody.java @@ -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()); + } +} diff --git a/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/body/TextLineBody.java b/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/body/TextLineBody.java index 2e392f12..8f3b4917 100644 --- a/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/body/TextLineBody.java +++ b/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/body/TextLineBody.java @@ -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) { diff --git a/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/body/TextRatingBody.java b/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/body/TextRatingBody.java new file mode 100644 index 00000000..270a3904 --- /dev/null +++ b/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/body/TextRatingBody.java @@ -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()); + } +} diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java index 7bccfda8..793c49a1 100644 --- a/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java +++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java @@ -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; diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/MsgSubType.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/MsgSubType.java index 6952cd20..3cd7456c 100644 --- a/SHiNE-server/shine-server-db/src/main/java/shine/db/MsgSubType.java +++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/MsgSubType.java @@ -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 — лучше добавить новые значения, // не трогая уже занятые коды. diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/SubscriptionsDAO.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/SubscriptionsDAO.java index cd089db4..83882403 100644 --- a/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/SubscriptionsDAO.java +++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/dao/SubscriptionsDAO.java @@ -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()) { diff --git a/SHiNE-server/shine-server-db/src/main/resources/postgres/migration_v2.sql b/SHiNE-server/shine-server-db/src/main/resources/postgres/migration_v2.sql index 172ea9f3..d0934849 100644 --- a/SHiNE-server/shine-server-db/src/main/resources/postgres/migration_v2.sql +++ b/SHiNE-server/shine-server-db/src/main/resources/postgres/migration_v2.sql @@ -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); diff --git a/SHiNE-server/shine-server-db/src/main/resources/postgres/schema_v1.sql b/SHiNE-server/shine-server-db/src/main/resources/postgres/schema_v1.sql index e514899d..5778d7fe 100644 --- a/SHiNE-server/shine-server-db/src/main/resources/postgres/schema_v1.sql +++ b/SHiNE-server/shine-server-db/src/main/resources/postgres/schema_v1.sql @@ -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); diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java index 787efd20..65719bf4 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java @@ -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), diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java index 4f58614a..e97fa70f 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/SolanaUserPdaImportService.java @@ -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++); diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java index 5b3e6d3f..6843ef11 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java @@ -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(""" diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java index 35fbfc29..0c59bfd1 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java @@ -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 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; diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetChannelMessages_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetChannelMessages_Handler.java index 9a882ea4..c3b063cd 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetChannelMessages_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetChannelMessages_Handler.java @@ -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 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); diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetMessageThread_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetMessageThread_Handler.java index 243ea507..d86af517 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetMessageThread_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetMessageThread_Handler.java @@ -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 loadChildren(Connection c, PostRow parent, int depthDown, int childLimit, String viewerLogin) throws Exception { if (depthDown <= 0) return List.of(); - List replies = findReplies(c, parent.bchName, parent.blockNumber, parent.blockHash, childLimit); + List replies = findRepliesAndRatings(c, parent.bchName, parent.blockNumber, parent.blockHash, childLimit); List 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 findReplies(Connection c, String toBchName, int toBlockNumber, byte[] toBlockHash, int limit) throws Exception { + private List 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 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(); diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetPersonalDiary_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetPersonalDiary_Handler.java new file mode 100644 index 00000000..2b2d6021 --- /dev/null +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetPersonalDiary_Handler.java @@ -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 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 loadDiaryItems(Connection c, String canonicalLogin, int limit, boolean asc) throws Exception { + String order = asc ? "ASC" : "DESC"; + List 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 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 loadVersionsForDiaryItem(Connection c, + String ownerBch, + int originalBlockNumber, + byte[] originalBlockHash, + String originalText, + long originalCreatedAtMs) throws Exception { + List 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); + } + } + } +} diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_MarkChannelMessagesSeen_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_MarkChannelMessagesSeen_Handler.java index a41b9811..4f1c167c 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_MarkChannelMessagesSeen_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_MarkChannelMessagesSeen_Handler.java @@ -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; diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetChannelMessages_Response.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetChannelMessages_Response.java index 9c1a80ff..7d5f0a92 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetChannelMessages_Response.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetChannelMessages_Response.java @@ -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 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; } diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetPersonalDiary_Request.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetPersonalDiary_Request.java new file mode 100644 index 00000000..67d63e4d --- /dev/null +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetPersonalDiary_Request.java @@ -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; } +} diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_CallInviteBroadcast_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_CallInviteBroadcast_Handler.java index 359529e0..c21602af 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_CallInviteBroadcast_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_CallInviteBroadcast_Handler.java @@ -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)) { diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_SendSignal_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_SendSignal_Handler.java index 7c24e117..7cf84e7e 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_SendSignal_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_SendSignal_Handler.java @@ -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 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 deliveredOnlineSessionIds + ) throws Exception { + List 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 onlineSessionIds = new java.util.HashSet<>(); + Set 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 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(); + } } diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/entyties/Net_CallInviteBroadcast_Request.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/entyties/Net_CallInviteBroadcast_Request.java index 139c5fa0..3703a33c 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/entyties/Net_CallInviteBroadcast_Request.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/entyties/Net_CallInviteBroadcast_Request.java @@ -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; } } diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/entyties/Net_SendSignal_Response.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/entyties/Net_SendSignal_Response.java index 53b807df..8842212f 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/entyties/Net_SendSignal_Response.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/entyties/Net_SendSignal_Response.java @@ -8,6 +8,9 @@ import java.util.List; public class Net_SendSignal_Response extends Net_Response { private int deliveredCount; private List 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 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; + } } diff --git a/SHiNE-server/shine-server-solana-users-sync/src/main/java/sync/storage/postgres/PostgresStorageRepository.java b/SHiNE-server/shine-server-solana-users-sync/src/main/java/sync/storage/postgres/PostgresStorageRepository.java index cdbe7d27..b1a2c145 100644 --- a/SHiNE-server/shine-server-solana-users-sync/src/main/java/sync/storage/postgres/PostgresStorageRepository.java +++ b/SHiNE-server/shine-server-solana-users-sync/src/main/java/sync/storage/postgres/PostgresStorageRepository.java @@ -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); diff --git a/TODO/2026-06-26_1810_подключение_устройств_по_qr.md b/TODO/2026-06-26_1810_подключение_устройств_по_qr.md deleted file mode 100644 index 2e8419c2..00000000 --- a/TODO/2026-06-26_1810_подключение_устройств_по_qr.md +++ /dev/null @@ -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`. diff --git a/TODO/2026-06-26_1815_esp32_файловое_хранилище.md b/TODO/2026-06-26_1815_esp32_файловое_хранилище.md deleted file mode 100644 index a1aadca2..00000000 --- a/TODO/2026-06-26_1815_esp32_файловое_хранилище.md +++ /dev/null @@ -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, если добавятся экраны или статусы. diff --git a/TODO/README.md b/TODO/README.md deleted file mode 100644 index 6a13506f..00000000 --- a/TODO/README.md +++ /dev/null @@ -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`. diff --git a/TODO/far/2026-06-20_1639_homeserver_technical_commands_and_file_transfer.md b/TODO/far/2026-06-20_1639_homeserver_technical_commands_and_file_transfer.md deleted file mode 100644 index 3b328cb8..00000000 --- a/TODO/far/2026-06-20_1639_homeserver_technical_commands_and_file_transfer.md +++ /dev/null @@ -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-файлообмену. diff --git a/TODO/medium/2026-05-25_1106_shine_balance_wallet.md b/TODO/medium/2026-05-25_1106_shine_balance_wallet.md deleted file mode 100644 index cb735c8b..00000000 --- a/TODO/medium/2026-05-25_1106_shine_balance_wallet.md +++ /dev/null @@ -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. Ошибочные или повторные операции не начисляют баланс дважды. diff --git a/TODO/medium/2026-05-26_0029_esp32s3_file_storage.md b/TODO/medium/2026-05-26_0029_esp32s3_file_storage.md deleted file mode 100644 index a9b7f457..00000000 --- a/TODO/medium/2026-05-26_0029_esp32s3_file_storage.md +++ /dev/null @@ -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-операции и сценарии восстановления. diff --git a/TODO/medium/2026-06-02_сессионные_homeserver_в_pda.md b/TODO/medium/2026-06-02_сессионные_homeserver_в_pda.md deleted file mode 100644 index 4b1af4c2..00000000 --- a/TODO/medium/2026-06-02_сессионные_homeserver_в_pda.md +++ /dev/null @@ -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. затем уже проектировать операции регистрации, обновления и отключения таких сессий. diff --git a/TODO/medium/2026-06-03_подключение_других_устройств_через_qr.md b/TODO/medium/2026-06-03_подключение_других_устройств_через_qr.md deleted file mode 100644 index 12d561b6..00000000 --- a/TODO/medium/2026-06-03_подключение_других_устройств_через_qr.md +++ /dev/null @@ -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` -- документацию по ключам, если формат переноса меняется diff --git a/TODO/medium/2026-06-28_send_signal_перенос_старых_сигналов.md b/TODO/medium/2026-06-28_send_signal_перенос_старых_сигналов.md deleted file mode 100644 index 2f695b6e..00000000 --- a/TODO/medium/2026-06-28_send_signal_перенос_старых_сигналов.md +++ /dev/null @@ -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`. diff --git a/TODO/medium/2026-07-22_переход_с_sqlite_на_postgresql.md b/TODO/medium/2026-07-22_переход_с_sqlite_на_postgresql.md deleted file mode 100644 index 72c097a1..00000000 --- a/TODO/medium/2026-07-22_переход_с_sqlite_на_postgresql.md +++ /dev/null @@ -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`, коммит будет создан после этой записи. diff --git a/TODO/near/2026-05-25_1106_wallet_topup_solana_arweave.md b/TODO/near/2026-05-25_1106_wallet_topup_solana_arweave.md deleted file mode 100644 index 4de664eb..00000000 --- a/TODO/near/2026-05-25_1106_wallet_topup_solana_arweave.md +++ /dev/null @@ -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. Возврат назад в приложение не ломает состояние регистрации/кошелька. diff --git a/TODO/near/2026-07-07_дм_v11_убрать_временную_очистку_signed_messages.md b/TODO/near/2026-07-07_дм_v11_убрать_временную_очистку_signed_messages.md deleted file mode 100644 index 2b04642a..00000000 --- a/TODO/near/2026-07-07_дм_v11_убрать_временную_очистку_signed_messages.md +++ /dev/null @@ -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/доставки. diff --git a/TODO/Близкое/ESP32/В_клшельке_и_esp32_переделать_на_sendsignal_и_client_key.md b/TODO/Близкое/ESP32/В_клшельке_и_esp32_переделать_на_sendsignal_и_client_key.md new file mode 100644 index 00000000..f8e56e6d --- /dev/null +++ b/TODO/Близкое/ESP32/В_клшельке_и_esp32_переделать_на_sendsignal_и_client_key.md @@ -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`. diff --git a/TODO/near/2026-07-01_2205_восстановить_полную_логику_shine_login_guard.md b/TODO/Близкое/Solana_смарт_контракты/Восстановить_полную_логику_shine_login_guard.md similarity index 98% rename from TODO/near/2026-07-01_2205_восстановить_полную_логику_shine_login_guard.md rename to TODO/Близкое/Solana_смарт_контракты/Восстановить_полную_логику_shine_login_guard.md index 70f6d862..283d410d 100644 --- a/TODO/near/2026-07-01_2205_восстановить_полную_логику_shine_login_guard.md +++ b/TODO/Близкое/Solana_смарт_контракты/Восстановить_полную_логику_shine_login_guard.md @@ -1,5 +1,13 @@ # Восстановить полную логику `shine_login_guard` + +Тоесть сделать что бы нормально проверялись логины пользователей + + + + + + Статус: отложено. ## Зачем это нужно diff --git a/TODO/Близкое/Solana_смарт_контракты/Навести_порядок_с_адремаси_програм_и_счетов_в_Солане.md b/TODO/Близкое/Solana_смарт_контракты/Навести_порядок_с_адремаси_програм_и_счетов_в_Солане.md new file mode 100644 index 00000000..473a0f4c diff --git a/TODO/Близкое/Solana_смарт_контракты/Передать_все_права_обновления_ДАО_И_сделат_сервиспроверки_актуальных_голосований_в_ДАО.md b/TODO/Близкое/Solana_смарт_контракты/Передать_все_права_обновления_ДАО_И_сделат_сервиспроверки_актуальных_голосований_в_ДАО.md new file mode 100644 index 00000000..473a0f4c diff --git a/TODO/Близкое/Solana_смарт_контракты/Сделать_полноценные_инвестиционные_инструменты.md b/TODO/Близкое/Solana_смарт_контракты/Сделать_полноценные_инвестиционные_инструменты.md new file mode 100644 index 00000000..3b475e87 --- /dev/null +++ b/TODO/Близкое/Solana_смарт_контракты/Сделать_полноценные_инвестиционные_инструменты.md @@ -0,0 +1 @@ +Передачу билетов владельцами со счёта на счёт diff --git a/TODO/Децентрализация/ИТХ/README.md b/TODO/Близкое/Децентрализация/ИТХ/README.md similarity index 100% rename from TODO/Децентрализация/ИТХ/README.md rename to TODO/Близкое/Децентрализация/ИТХ/README.md diff --git a/TODO/Децентрализация/ИТХ/Спецификация_ИТХ_v1.md b/TODO/Близкое/Децентрализация/ИТХ/Спецификация_ИТХ_v1.md similarity index 100% rename from TODO/Децентрализация/ИТХ/Спецификация_ИТХ_v1.md rename to TODO/Близкое/Децентрализация/ИТХ/Спецификация_ИТХ_v1.md diff --git a/TODO/Близкое/Еовести порядок с кошельками и отображением баланса_ тот минимум что нужен для работы сияния.md b/TODO/Близкое/Еовести порядок с кошельками и отображением баланса_ тот минимум что нужен для работы сияния.md new file mode 100644 index 00000000..7164762a --- /dev/null +++ b/TODO/Близкое/Еовести порядок с кошельками и отображением баланса_ тот минимум что нужен для работы сияния.md @@ -0,0 +1,5 @@ +Баланс в салане + +баланс в Арвив / и турбо + +Балан лимит МБ / оно же сияния токены SHN \ No newline at end of file diff --git a/TODO/Близкое/Сделать поддержку нескольких залогиненных аккаунтов враз.md b/TODO/Близкое/Сделать поддержку нескольких залогиненных аккаунтов враз.md new file mode 100644 index 00000000..3e242690 --- /dev/null +++ b/TODO/Близкое/Сделать поддержку нескольких залогиненных аккаунтов враз.md @@ -0,0 +1,3 @@ +Сделать поддержку нескольких залогиненных аккаунтов враз тоесть что бы можно было менять акаунт под которым заходить + - подумать о деталях, а так хорошая тема - и тестировать удобнее станет + - \ No newline at end of file diff --git a/TODO/2026-06-26_1800_корректное_завершение_за_30с.md b/TODO/Близкое/Сделать_возможность_корректного_завершения_работы_сервера_за_30с_чтобы_все_задачи_доделались.md similarity index 74% rename from TODO/2026-06-26_1800_корректное_завершение_за_30с.md rename to TODO/Близкое/Сделать_возможность_корректного_завершения_работы_сервера_за_30с_чтобы_все_задачи_доделались.md index c525498b..da6a618a 100644 --- a/TODO/2026-06-26_1800_корректное_завершение_за_30с.md +++ b/TODO/Близкое/Сделать_возможность_корректного_завершения_работы_сервера_за_30с_чтобы_все_задачи_доделались.md @@ -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`, если изменится поведение остановки/восстановления. diff --git a/TODO/Близкое/Убрать разные тестовые штуки для звонков и сделать отправку ошибки на сервер.md b/TODO/Близкое/Убрать разные тестовые штуки для звонков и сделать отправку ошибки на сервер.md new file mode 100644 index 00000000..cd98505b --- /dev/null +++ b/TODO/Близкое/Убрать разные тестовые штуки для звонков и сделать отправку ошибки на сервер.md @@ -0,0 +1,11 @@ +Там были какието разные апи для ошибок звонка +и для старта тестовых соединений + +убрать короче лишнее + + +и сделать номальное апи что бы клиент мог + высылать уведомления об ошибках на сервер + предлогал отправить уведомление + + diff --git a/TODO/medium/2026-05-24_1140_репосты_в_каналах_и_тредах.md b/TODO/В_отдалённом_Будущем/В будущем_сделать_репосты_в_каналах_и_тредах.md similarity index 100% rename from TODO/medium/2026-05-24_1140_репосты_в_каналах_и_тредах.md rename to TODO/В_отдалённом_Будущем/В будущем_сделать_репосты_в_каналах_и_тредах.md diff --git a/TODO/В_отдалённом_Будущем/ДЦ_личную_переписку_дороботать_для_максимальной_надёжности.md b/TODO/В_отдалённом_Будущем/ДЦ_личную_переписку_дороботать_для_максимальной_надёжности.md new file mode 100644 index 00000000..8dbaaaf8 --- /dev/null +++ b/TODO/В_отдалённом_Будущем/ДЦ_личную_переписку_дороботать_для_максимальной_надёжности.md @@ -0,0 +1,5 @@ +Сделать что бы если сервера с исходящими сообщениями не могут доставить на входящие то + - делать повторные попытки через время + - если так и не получилось и никужа не доставлено уведомлять пользователя + + - Так же как вариант собирать подписи что полученно входящее сообщение с серверов пользователя полчателя diff --git a/TODO/В_отдалённом_Будущем/Предложения_к_рассмотрению_в_будущее/Как_вариант_можно_сделать_hameserver_как_хранилище_файлов_пользователя.md b/TODO/В_отдалённом_Будущем/Предложения_к_рассмотрению_в_будущее/Как_вариант_можно_сделать_hameserver_как_хранилище_файлов_пользователя.md new file mode 100644 index 00000000..ddfd59ee --- /dev/null +++ b/TODO/В_отдалённом_Будущем/Предложения_к_рассмотрению_в_будущее/Как_вариант_можно_сделать_hameserver_как_хранилище_файлов_пользователя.md @@ -0,0 +1,5 @@ +Как_вариант_можно_сделать_hameserver_как_хранилище_файлов_пользователя +И всё это можно сделать внутри ESP32 + +Хотя не понятно надо ли так делать - потому что вроде удобно, +но тем не менее и сложно как то объяснить такой функционал людям diff --git a/TODO/Децентрализация/2026-06-26_1805_межсерверный_ws_и_dm_sync.md b/TODO/Децентрализация/2026-06-26_1805_межсерверный_ws_и_dm_sync.md deleted file mode 100644 index 362db242..00000000 --- a/TODO/Децентрализация/2026-06-26_1805_межсерверный_ws_и_dm_sync.md +++ /dev/null @@ -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/`. diff --git a/TODO/Децентрализация/README.md b/TODO/Децентрализация/README.md deleted file mode 100644 index 2236815c..00000000 --- a/TODO/Децентрализация/README.md +++ /dev/null @@ -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, перенесённый в контекст децентрализации. diff --git a/TODO/Децентрализация/realtime_pda_solana_sync.md b/TODO/Децентрализация/realtime_pda_solana_sync.md deleted file mode 100644 index cf036690..00000000 --- a/TODO/Децентрализация/realtime_pda_solana_sync.md +++ /dev/null @@ -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. - -## Статус - -Отложено до этапа децентрализации. diff --git a/TODO/Децентрализация/запись_блокчейнов_в_arweave.md b/TODO/Децентрализация/запись_блокчейнов_в_arweave.md deleted file mode 100644 index 1d702707..00000000 --- a/TODO/Децентрализация/запись_блокчейнов_в_arweave.md +++ /dev/null @@ -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/`, если появятся новые параметры. - -## Статус - -Отложено до этапа децентрализации. diff --git a/TODO/Децентрализация/межсерверная_передача_сообщений.md b/TODO/Децентрализация/межсерверная_передача_сообщений.md deleted file mode 100644 index 6f45c7b0..00000000 --- a/TODO/Децентрализация/межсерверная_передача_сообщений.md +++ /dev/null @@ -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`. - -## Статус - -Отложено до этапа децентрализации. diff --git a/TODO/Децентрализация/межсерверные_звонки.md b/TODO/Децентрализация/межсерверные_звонки.md deleted file mode 100644 index 0bae8762..00000000 --- a/TODO/Децентрализация/межсерверные_звонки.md +++ /dev/null @@ -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 маршрутизации. - -## Статус - -Отложено до этапа децентрализации. diff --git a/TODO/Децентрализация/односерверный_production_режим.md b/TODO/Децентрализация/односерверный_production_режим.md deleted file mode 100644 index c0cb9718..00000000 --- a/TODO/Децентрализация/односерверный_production_режим.md +++ /dev/null @@ -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 работает как один сервер. diff --git a/TODO/Доделать в блокчейн - подтверждения и добавление участников в канал.md b/TODO/Доделать в блокчейн - подтверждения и добавление участников в канал.md new file mode 100644 index 00000000..473a0f4c diff --git a/TODO/Новые типы связей пользователей.md b/TODO/Новые типы связей пользователей.md new file mode 100644 index 00000000..f444897d --- /dev/null +++ b/TODO/Новые типы связей пользователей.md @@ -0,0 +1,6 @@ +Сделать все эти: друг, близкий друг, и статусы типо сияет, точно сияет, реальный человек и т.д. + +и как вариант не реальный человек тк + - украли акаунт + - сразу был нереальным мошенником + - умер \ No newline at end of file diff --git a/TODO/Новые фишки которые надо доделать/Новая_контентная_модель_блокчейна/01_Презентация_для_людей.md b/TODO/Новые фишки которые надо доделать/Новая_контентная_модель_блокчейна/01_Презентация_для_людей.md deleted file mode 100644 index 5a1d35a2..00000000 --- a/TODO/Новые фишки которые надо доделать/Новая_контентная_модель_блокчейна/01_Презентация_для_людей.md +++ /dev/null @@ -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, не разрушая старую модель сообщений. - -## Отдельный вопрос для будущего - -Отзывы о людях как о людях — полезная идея, но её стоит дополнительно обдумать. - -Например, на вкладке связей в будущем можно: - -- писать человеку отзыв; -- смотреть все отзывы о человеке; -- выводить сначала отзывы близких друзей, родственников, друзей и контактов, а уже потом остальные. - -Но этот слой нужно делать осторожно, чтобы он не стал слишком жёстким или неприятным для людей. - -Поэтому отзывы о людях как отдельная социальная механика требуют дополнительного обсуждения и проектирования. diff --git a/TODO/Новые фишки которые надо доделать/Новая_контентная_модель_блокчейна/02_ТЗ_новых_типов_блоков.md b/TODO/Новые фишки которые надо доделать/Новая_контентная_модель_блокчейна/02_ТЗ_новых_типов_блоков.md deleted file mode 100644 index 6fafb893..00000000 --- a/TODO/Новые фишки которые надо доделать/Новая_контентная_модель_блокчейна/02_ТЗ_новых_типов_блоков.md +++ /dev/null @@ -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//` -- особополная: `SHiNE///` - -Примеры: - -- `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//` - - `SHiNE///` - -## 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` продолжают работать как раньше; -- старые клиенты смогут игнорировать новые типы как неизвестные; -- новые клиенты смогут постепенно включать поддержку нового функционала. - -Итог: - -- это расширение формата блокчейна; -- это не миграция со сломом старых блоков; -- это можно внедрять поэтапно. diff --git a/TODO/Поделить сервер на две части что бы работал с VPS.md b/TODO/Поделить сервер на две части что бы работал с VPS.md new file mode 100644 index 00000000..473a0f4c diff --git a/TODO/Разместить на гитхаб с AGPLv3 лицензией.md b/TODO/Разместить на гитхаб с AGPLv3 лицензией.md new file mode 100644 index 00000000..9e6f0a02 --- /dev/null +++ b/TODO/Разместить на гитхаб с AGPLv3 лицензией.md @@ -0,0 +1,2 @@ +Доделть мелочи +и разместить проект нормально на гитхаб \ No newline at end of file diff --git a/TODO/межсерверные_звонки.md b/TODO/межсерверные_звонки.md new file mode 100644 index 00000000..0b8a3af3 --- /dev/null +++ b/TODO/межсерверные_звонки.md @@ -0,0 +1,7 @@ +# Межсерверные звонки + +Доделать Звонки что бы работало как сигнал о том что вызов идёт. + +и +Пользователи на разных серверах должны иметь возможность устанавливать звонки так же, как пользователи одного сервера. + diff --git a/VERSION.properties b/VERSION.properties index 64d6dd3f..7042c4e0 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.5.4 -server.version=1.4.5 +client.version=1.5.36 +server.version=1.4.11 diff --git a/deploy/backup/backup-version.properties b/deploy/backup/backup-version.properties index 4cb102d0..72dd5eb9 100644 --- a/deploy/backup/backup-version.properties +++ b/deploy/backup/backup-version.properties @@ -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 diff --git a/deploy/backup/scheme/shineup.me/captures/01_host_disk.txt b/deploy/backup/scheme/shineup.me/captures/01_host_disk.txt index c0980e47..d4094a65 100644 --- a/deploy/backup/scheme/shineup.me/captures/01_host_disk.txt +++ b/deploy/backup/scheme/shineup.me/captures/01_host_disk.txt @@ -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 diff --git a/deploy/backup/scheme/shineup.me/captures/02_enabled_services.txt b/deploy/backup/scheme/shineup.me/captures/02_enabled_services.txt index d10ea2d8..b3e8d9ff 100644 --- a/deploy/backup/scheme/shineup.me/captures/02_enabled_services.txt +++ b/deploy/backup/scheme/shineup.me/captures/02_enabled_services.txt @@ -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. diff --git a/deploy/backup/scheme/shineup.me/captures/03_running_services.txt b/deploy/backup/scheme/shineup.me/captures/03_running_services.txt index bfef0583..9b8ffe75 100644 --- a/deploy/backup/scheme/shineup.me/captures/03_running_services.txt +++ b/deploy/backup/scheme/shineup.me/captures/03_running_services.txt @@ -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. diff --git a/deploy/backup/scheme/shineup.me/captures/04_listen_ports.txt b/deploy/backup/scheme/shineup.me/captures/04_listen_ports.txt index da6b5b3b..32c79855 100644 --- a/deploy/backup/scheme/shineup.me/captures/04_listen_ports.txt +++ b/deploy/backup/scheme/shineup.me/captures/04_listen_ports.txt @@ -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)) diff --git a/deploy/backup/scheme/shineup.me/captures/05_docker_ps.txt b/deploy/backup/scheme/shineup.me/captures/05_docker_ps.txt index d3af29ef..785befd4 100644 --- a/deploy/backup/scheme/shineup.me/captures/05_docker_ps.txt +++ b/deploy/backup/scheme/shineup.me/captures/05_docker_ps.txt @@ -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 diff --git a/deploy/backup/scheme/shineup.me/captures/06_home_player_sizes.txt b/deploy/backup/scheme/shineup.me/captures/06_home_player_sizes.txt index 49f99b01..f6af8b4d 100644 --- a/deploy/backup/scheme/shineup.me/captures/06_home_player_sizes.txt +++ b/deploy/backup/scheme/shineup.me/captures/06_home_player_sizes.txt @@ -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 diff --git a/deploy/backup/scheme/shineup.me/captures/07_var_sizes.txt b/deploy/backup/scheme/shineup.me/captures/07_var_sizes.txt index 5dc0c1f5..619833f2 100644 --- a/deploy/backup/scheme/shineup.me/captures/07_var_sizes.txt +++ b/deploy/backup/scheme/shineup.me/captures/07_var_sizes.txt @@ -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 diff --git a/deploy/backup/scheme/shineup.me/captures/UPDATED_AT_UTC.txt b/deploy/backup/scheme/shineup.me/captures/UPDATED_AT_UTC.txt index 30b0fd41..7135beff 100644 --- a/deploy/backup/scheme/shineup.me/captures/UPDATED_AT_UTC.txt +++ b/deploy/backup/scheme/shineup.me/captures/UPDATED_AT_UTC.txt @@ -1 +1 @@ -2026-07-10T08:27:36Z +2026-08-08T20:05:20Z diff --git a/deploy/backup/scheme/shineup.me/configs/Caddyfile b/deploy/backup/scheme/shineup.me/configs/Caddyfile index b1829b0b..055b2c6e 100644 --- a/deploy/backup/scheme/shineup.me/configs/Caddyfile +++ b/deploy/backup/scheme/shineup.me/configs/Caddyfile @@ -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 - } -} diff --git a/deploy/backup/scheme/shineup.me/configs/systemd/agent-memory.service.MISSING.txt b/deploy/backup/scheme/shineup.me/configs/systemd/agent-memory.service.MISSING.txt new file mode 100644 index 00000000..0632d271 --- /dev/null +++ b/deploy/backup/scheme/shineup.me/configs/systemd/agent-memory.service.MISSING.txt @@ -0,0 +1 @@ +missing: /etc/systemd/system/agent-memory.service diff --git a/deploy/backup/scheme/shineup.me/configs/systemd/shine-server.service b/deploy/backup/scheme/shineup.me/configs/systemd/shine-server.service index 693bd670..d44da2f6 100644 --- a/deploy/backup/scheme/shineup.me/configs/systemd/shine-server.service +++ b/deploy/backup/scheme/shineup.me/configs/systemd/shine-server.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 diff --git a/deploy/backup/scheme/shineup.me/configs/turnserver.conf b/deploy/backup/scheme/shineup.me/configs/turnserver.conf index 4ef9e9f9..b6ee829d 100644 --- a/deploy/backup/scheme/shineup.me/configs/turnserver.conf +++ b/deploy/backup/scheme/shineup.me/configs/turnserver.conf @@ -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 " 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 ", 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= dbname= 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= dbname= user= password= port= connect_timeout= read_timeout=" - -# 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= dbname= password= port= connect_timeout=" - -# 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= dbname= password= port= connect_timeout=" - -# 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= - -# 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 -# [:]. 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 -# :. 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= - -# 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= - -# 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= +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 diff --git a/deploy/backup/scheme/shineup.me/scripts/refresh_scheme.sh b/deploy/backup/scheme/shineup.me/scripts/refresh_scheme.sh index 23ecdf5f..ac4f944e 100755 --- a/deploy/backup/scheme/shineup.me/scripts/refresh_scheme.sh +++ b/deploy/backup/scheme/shineup.me/scripts/refresh_scheme.sh @@ -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" diff --git a/docs/API/04_Add_Block_to_Blockchain_API.md b/docs/API/04_Add_Block_to_Blockchain_API.md index c156605f..88c6857e 100644 --- a/docs/API/04_Add_Block_to_Blockchain_API.md +++ b/docs/API/04_Add_Block_to_Blockchain_API.md @@ -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 использует тот же формат полного снимка профиля: diff --git a/docs/API/05_Technical_Requests_API.md b/docs/API/05_Technical_Requests_API.md index d458d04a..84f51356 100644 --- a/docs/API/05_Technical_Requests_API.md +++ b/docs/API/05_Technical_Requests_API.md @@ -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` у пользователя сейчас нет активных онлайн-сессий. diff --git a/docs/API/06_Channels_Read_API.md b/docs/API/06_Channels_Read_API.md index ad6c30d3..b2dc3d93 100644 --- a/docs/API/06_Channels_Read_API.md +++ b/docs/API/06_Channels_Read_API.md @@ -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" + } +} +``` --- diff --git a/docs/API/09_Operations_Index.md b/docs/API/09_Operations_Index.md index 69a01682..95826e81 100644 --- a/docs/API/09_Operations_Index.md +++ b/docs/API/09_Operations_Index.md @@ -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` | diff --git a/docs/Blockchain/00_Blockchain_Formats_and_Block_Types.md b/docs/Blockchain/00_Blockchain_Formats_and_Block_Types.md index 52d25999..d4336ffa 100644 --- a/docs/Blockchain/00_Blockchain_Formats_and_Block_Types.md +++ b/docs/Blockchain/00_Blockchain_Formats_and_Block_Types.md @@ -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. ## Примечание diff --git a/docs/Blockchain/02_Channel_Commands.md b/docs/Blockchain/02_Channel_Commands.md index 417040f9..823da7cd 100644 --- a/docs/Blockchain/02_Channel_Commands.md +++ b/docs/Blockchain/02_Channel_Commands.md @@ -9,7 +9,7 @@ Описание, человекочитаемое имя и аватар канала меняются только через скрытый технический блок: - `msg_type=1` -- `subType=70` +- `subType=90` - `TEXT_CHANNEL_META` Спецификация: [16_TEXT_Channel_Meta.md](./16_TEXT_Channel_Meta.md). diff --git a/docs/Blockchain/11_TEXT_Blocks.md b/docs/Blockchain/11_TEXT_Blocks.md index 63d68d65..c629eb85 100644 --- a/docs/Blockchain/11_TEXT_Blocks.md +++ b/docs/Blockchain/11_TEXT_Blocks.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`. diff --git a/docs/Blockchain/15_STATUS_ACTION_Blocks.md b/docs/Blockchain/15_STATUS_ACTION_Blocks.md new file mode 100644 index 00000000..1a330569 --- /dev/null +++ b/docs/Blockchain/15_STATUS_ACTION_Blocks.md @@ -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` нет. +- Если нужно изменить смысл статуса, пишется новое статусное событие. diff --git a/docs/Blockchain/16_TEXT_Channel_Meta.md b/docs/Blockchain/16_TEXT_Channel_Meta.md index 51220e23..211b1832 100644 --- a/docs/Blockchain/16_TEXT_Channel_Meta.md +++ b/docs/Blockchain/16_TEXT_Channel_Meta.md @@ -5,7 +5,7 @@ ## Блок - `msg_type = 1` -- `subType = 70` +- `subType = 90` - `msgVersion = 1` - body использует тот же бинарный формат line-текста, что и `TEXT_POST`: line-поля + `textLen` + UTF-8 текст. diff --git a/docs/Blockchain/CHANGELOG.md b/docs/Blockchain/CHANGELOG.md index e9b5a317..e88c277e 100644 --- a/docs/Blockchain/CHANGELOG.md +++ b/docs/Blockchain/CHANGELOG.md @@ -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`. diff --git a/docs/Blockchain/README.md b/docs/Blockchain/README.md index 438eb4ff..822b3430 100644 --- a/docs/Blockchain/README.md +++ b/docs/Blockchain/README.md @@ -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) Журнал изменений документации. ## Смежная документация diff --git a/docs/Figma/README.md b/docs/Figma/README.md deleted file mode 100644 index fc6c0835..00000000 --- a/docs/Figma/README.md +++ /dev/null @@ -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. diff --git a/docs/Figma/TRANSFER_UI_SCREENS.md b/docs/Figma/TRANSFER_UI_SCREENS.md deleted file mode 100644 index d680d1c4..00000000 --- a/docs/Figma/TRANSFER_UI_SCREENS.md +++ /dev/null @@ -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/.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-правки. diff --git a/docs/Personal_Messages/Спецификация_DM_v0.5_устаревшая.md b/docs/Personal_Messages/Спецификация_DM_v0.5_устаревшая.md deleted file mode 100644 index de98104f..00000000 --- a/docs/Personal_Messages/Спецификация_DM_v0.5_устаревшая.md +++ /dev/null @@ -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 больше не актуальны; -- если позже вложения вернутся, их формат и серверная логика могут быть другими. diff --git a/docs/libs/Agetn.ms b/docs/libs/Agetn.ms index 172c0e03..0e96d022 100644 --- a/docs/libs/Agetn.ms +++ b/docs/libs/Agetn.ms @@ -1 +1,2 @@ -Данная документация местами устарела и не соответствует реальному коду \ No newline at end of file +Данная документация местами устарела и не соответствует реальному коду +Особенно в отношениии работы с Блокчейном \ No newline at end of file diff --git a/docs/libs/shine-server-bd/DOC.md b/docs/libs/shine-server-bd/DOC.md deleted file mode 100644 index ad2c2483..00000000 --- a/docs/libs/shine-server-bd/DOC.md +++ /dev/null @@ -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. diff --git a/docs/libs/shine-server-bd/POSTGRES_RUNTIME_SCHEMA_V1.md b/docs/libs/shine-server-bd/POSTGRES_RUNTIME_SCHEMA_V1.md deleted file mode 100644 index 1f7e787a..00000000 --- a/docs/libs/shine-server-bd/POSTGRES_RUNTIME_SCHEMA_V1.md +++ /dev/null @@ -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` diff --git a/docs/audit/Solana-audit-2-by-Claude-11июня2026.md b/docs/старый_аудит_смартконтрактов_в_claud_fable/Solana-audit-2-by-Claude-11июня2026.md similarity index 100% rename from docs/audit/Solana-audit-2-by-Claude-11июня2026.md rename to docs/старый_аудит_смартконтрактов_в_claud_fable/Solana-audit-2-by-Claude-11июня2026.md diff --git a/docs/audit/Solana-audit-3-by-Claude-12июня2026.md b/docs/старый_аудит_смартконтрактов_в_claud_fable/Solana-audit-3-by-Claude-12июня2026.md similarity index 100% rename from docs/audit/Solana-audit-3-by-Claude-12июня2026.md rename to docs/старый_аудит_смартконтрактов_в_claud_fable/Solana-audit-3-by-Claude-12июня2026.md diff --git a/docs/audit/Solana-audit-by-Claude-File5-9июня2026.md b/docs/старый_аудит_смартконтрактов_в_claud_fable/Solana-audit-by-Claude-File5-9июня2026.md similarity index 100% rename from docs/audit/Solana-audit-by-Claude-File5-9июня2026.md rename to docs/старый_аудит_смартконтрактов_в_claud_fable/Solana-audit-by-Claude-File5-9июня2026.md diff --git a/shine-UI/img/shine-logo-transparent-final.png b/shine-UI/img/shine-logo-transparent-final.png index be1831a5..a4b44ec9 100644 Binary files a/shine-UI/img/shine-logo-transparent-final.png and b/shine-UI/img/shine-logo-transparent-final.png differ diff --git a/shine-UI/img/shine-logo-transparent-final_big.png b/shine-UI/img/shine-logo-transparent-final_big.png new file mode 100644 index 00000000..be1831a5 Binary files /dev/null and b/shine-UI/img/shine-logo-transparent-final_big.png differ diff --git a/shine-UI/index.html b/shine-UI/index.html index 7a10f904..fea3b7e3 100644 --- a/shine-UI/index.html +++ b/shine-UI/index.html @@ -2,7 +2,10 @@ - + @@ -37,7 +40,9 @@ window.__SHINE_BUILD_HASH__ = '20260806223040';
Сияние
+
+
diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index 82e141ee..7b93e921 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -10,6 +10,7 @@ import { handleIncomingCallInvite, handleIncomingCallPush, handleIncomingCallSignal, + handleIncomingCallSignalViaSendSignal, handleStopCallPush, setCallDebugReporter, startDebugConnectionAsInitiator, @@ -150,6 +151,8 @@ const routes = { }; const screenEl = document.getElementById('app-screen'); +const topbarEl = document.getElementById('topbar-slot'); +const composerEl = document.getElementById('composer-slot'); const toolbarEl = document.getElementById('toolbar-slot'); const appShellEl = document.querySelector('.app-shell'); const initialSplashEl = document.getElementById('initial-splash'); @@ -179,6 +182,8 @@ let uiVersionPeriodicIntervalId = null; let hiddenDmAudioContext = null; let hiddenDmAudioUnlocked = false; let initialConnectionCompleted = false; +let orientationLockInFlight = false; +let currentChromeCleanup = null; const CALL_PUSH_PENDING_ACTION_KEY = 'shine-ui-call-push-pending-action-v1'; const GUEST_ALLOWED_PAGES = new Set([ 'start-view', @@ -202,6 +207,90 @@ initPwaInstallPromptHandling(); initCallUiOverlay(); setCallDebugReporter((payload) => authService.reportClientDebug(payload)); +function setShellMetricVar(name, valuePx) { + if (!appShellEl) return; + appShellEl.style.setProperty(name, `${Math.max(0, Math.ceil(Number(valuePx || 0)))}px`); +} + +function setKeyboardOffsetPx(valuePx = 0) { + setShellMetricVar('--keyboard-offset', valuePx); +} + +function attachSlotHeightObserver(slotEl, cssVarName) { + if (!slotEl || typeof ResizeObserver !== 'function') return null; + const sync = () => { + const visible = !slotEl.hidden && slotEl.childElementCount > 0; + setShellMetricVar(cssVarName, visible ? slotEl.offsetHeight : 0); + }; + const observer = new ResizeObserver(sync); + observer.observe(slotEl); + sync(); + return { observer, sync }; +} + +const topbarHeightObserver = attachSlotHeightObserver(topbarEl, '--topbar-height'); +const composerHeightObserver = attachSlotHeightObserver(composerEl, '--composer-height'); +const toolbarHeightObserver = attachSlotHeightObserver(toolbarEl, '--toolbar-height'); + +function clearSlot(slotEl, cssVarName) { + if (!slotEl) return; + slotEl.innerHTML = ''; + slotEl.hidden = true; + setShellMetricVar(cssVarName, 0); +} + +function mountSlot(slotEl, cssVarName, node) { + if (!slotEl) return; + slotEl.innerHTML = ''; + if (node instanceof Node) { + slotEl.append(node); + slotEl.hidden = false; + } else { + slotEl.hidden = true; + } + setShellMetricVar(cssVarName, !slotEl.hidden ? slotEl.offsetHeight : 0); +} + +function createChromeController(showAppChrome) { + let topbarNode = null; + let composerNode = null; + let disposed = false; + + const apply = () => { + if (disposed) return; + if (!showAppChrome) { + clearSlot(topbarEl, '--topbar-height'); + clearSlot(composerEl, '--composer-height'); + return; + } + mountSlot(topbarEl, '--topbar-height', topbarNode); + mountSlot(composerEl, '--composer-height', composerNode); + topbarHeightObserver?.sync?.(); + composerHeightObserver?.sync?.(); + }; + + return { + setTopbar(node = null) { + topbarNode = node instanceof Node ? node : null; + apply(); + }, + setComposer(node = null) { + composerNode = node instanceof Node ? node : null; + apply(); + }, + clear() { + topbarNode = null; + composerNode = null; + apply(); + }, + dispose() { + disposed = true; + clearSlot(topbarEl, '--topbar-height'); + clearSlot(composerEl, '--composer-height'); + }, + }; +} + async function unlockHiddenDmAudio() { try { const Ctx = window.AudioContext || window.webkitAudioContext; @@ -257,6 +346,23 @@ async function playDmSignal({ extended = false } = {}) { } } +async function tryLockPortraitOrientation() { + if (orientationLockInFlight) return false; + + const orientationApi = window.screen?.orientation; + if (!orientationApi?.lock) return false; + + orientationLockInFlight = true; + try { + await orientationApi.lock('portrait'); + return true; + } catch { + return false; + } finally { + orientationLockInFlight = false; + } +} + async function notifyHiddenIncomingMessage(fromLogin, text) { const body = String(text || '').trim() || `Вам пришло сообщение от ${fromLogin}`; const title = `Сообщение от ${fromLogin}`; @@ -889,7 +995,14 @@ function renderPageFailureFallback(pageId, error) { screenEl.append(wrap); screenEl.classList.toggle('no-app-chrome', false); + if (typeof currentChromeCleanup === 'function') { + currentChromeCleanup(); + currentChromeCleanup = null; + } + clearSlot(topbarEl, '--topbar-height'); + clearSlot(composerEl, '--composer-height'); toolbarEl.innerHTML = ''; + toolbarHeightObserver?.sync?.(); refreshConnectionUi(); } @@ -913,10 +1026,17 @@ function renderApp() { currentCleanup(); currentCleanup = null; } + if (typeof currentChromeCleanup === 'function') { + currentChromeCleanup(); + currentChromeCleanup = null; + } try { screenEl.innerHTML = ''; - const screen = page.render({ route, navigate }); + const showAppChrome = page.pageMeta?.showAppChrome !== false; + const chrome = createChromeController(showAppChrome); + currentChromeCleanup = () => chrome.dispose(); + const screen = page.render({ route, navigate, chrome }); if (!(screen instanceof Node)) { throw new Error('Page render returned invalid node'); } @@ -924,7 +1044,6 @@ function renderApp() { screenEl.append(screen); currentCleanup = typeof screen.cleanup === 'function' ? screen.cleanup : null; - const showAppChrome = page.pageMeta?.showAppChrome !== false; screenEl.classList.toggle('no-app-chrome', !showAppChrome); screenEl.classList.toggle('preauth-flow', PRE_AUTH_PAGES.includes(pageId)); @@ -932,6 +1051,7 @@ function renderApp() { if (showAppChrome) { toolbarEl.append(renderToolbar(page.pageMeta.id, navigate)); } + toolbarHeightObserver?.sync?.(); refreshConnectionUi(); } catch (error) { console.error('[renderApp] controlled fallback', error); @@ -949,6 +1069,7 @@ function refreshToolbarOnly() { if (showAppChrome) { toolbarEl.append(renderToolbar(page.pageMeta.id, navigate)); } + toolbarHeightObserver?.sync?.(); refreshConnectionUi(); } @@ -1027,6 +1148,7 @@ async function ensureSessionRuntimeStarted() { async function init() { consumeCallPushActionFromUrlIfAny(); + void tryLockPortraitOrientation(); if (state.session.isLocalDemo) { setConnectionStatus('connected'); @@ -1210,13 +1332,14 @@ async function init() { if (added && isIncomingForCurrent && !payload.backlog) { const parsedText = parseDmTechBlocks(text); const displayText = String(parsedText.displayText || ''); + const suppressHiddenNotification = parsedText.callSummary?.status === 'completed'; if (document.visibilityState === 'visible') { void playDmSignal({ extended: false }); - } else if (Notification.permission === 'granted') { + } else if (Notification.permission === 'granted' && !suppressHiddenNotification) { try { void notifyHiddenIncomingMessage(fromLogin, displayText || ''); } catch {} - } else { + } else if (!suppressHiddenNotification) { void playDmSignal({ extended: true }); } } @@ -1313,6 +1436,10 @@ async function init() { try { await handleIncomingCallSignal(evt); } catch {} }); + authService.onEvent('IncomingSignal', async (evt) => { + try { await handleIncomingCallSignalViaSendSignal(evt); } catch {} + }); + authService.onEvent('DebugConnectPrepareResponder', async (evt) => { try { const p = evt?.payload || {}; @@ -1405,8 +1532,12 @@ async function init() { }, { passive: true }); document.addEventListener('visibilitychange', () => { if (document.visibilityState !== 'visible') return; + void tryLockPortraitOrientation(); void checkConnectionHealth(); }); + window.addEventListener('orientationchange', () => { + void tryLockPortraitOrientation(); + }); } init(); diff --git a/shine-UI/js/components/arweave-attachment-manager.js b/shine-UI/js/components/arweave-attachment-manager.js index c22207ed..72f5529d 100644 --- a/shine-UI/js/components/arweave-attachment-manager.js +++ b/shine-UI/js/components/arweave-attachment-manager.js @@ -286,7 +286,7 @@ export function openArweaveAttachmentManager({ historyOnly = false, mode = 'attachment', historyPurpose = '', - uploadTransport = 'arweave', + uploadTransport = 'turbo', turboKeySource = 'client', } = {}) { const cleanLogin = String(login || '').trim(); @@ -303,7 +303,7 @@ export function openArweaveAttachmentManager({ let turboKeyChoices = []; let selectedWalletId = readLastWalletId(cleanLogin) || 'derived-client-key'; let selectedTurboKeySource = readLastTurboKeySource(cleanLogin, turboKeySource); - let selectedUploadTransport = String(uploadTransport || '').trim().toLowerCase() === 'turbo' ? 'turbo' : 'arweave'; + let selectedUploadTransport = String(uploadTransport || '').trim().toLowerCase() === 'arweave' ? 'arweave' : 'turbo'; let selectedFile = null; let selectedSha256 = ''; let selectedPreviewEnabled = false; @@ -592,6 +592,7 @@ export function openArweaveAttachmentManager({