SHA256
Compare commits
105
Commits
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
d8e0c77951 | ||
|
|
015fade4c0 | ||
|
|
da14b51e80 | ||
|
|
7a5ac01d1c | ||
|
|
5e6d64e965 | ||
|
|
c425fa41aa | ||
|
|
781157299f | ||
|
|
0f30317bb4 | ||
|
|
1f1bc0a7b9 | ||
|
|
60c8a6608a | ||
|
|
86adaf8c6b | ||
|
|
731688d16e | ||
|
|
63b66c48d8 | ||
|
|
ea19e511c0 | ||
|
|
f5e401cff8 | ||
|
|
bc8c7f318b | ||
|
|
585750007d | ||
|
|
6e80ac976a | ||
|
|
c70f18fcf4 | ||
|
|
d3e6aa2be2 | ||
|
|
b7a869c514 | ||
|
|
19362b950a | ||
|
|
0a4c31fb36 | ||
|
|
fef7694b48 | ||
|
|
60206e21df | ||
|
|
b9b77c66ce | ||
|
|
745a0e39d7 | ||
|
|
23bdadab56 | ||
|
|
47a4844ce8 | ||
|
|
8741be6cba | ||
|
|
4656a03ea9 | ||
|
|
5763eb828e | ||
|
|
6a5c20a165 | ||
|
|
fac166f186 | ||
|
|
091291bfa2 | ||
|
|
288fde67e8 | ||
|
|
ce30b77328 | ||
|
|
8c4e404f23 | ||
|
|
e8a4713f6e | ||
|
|
b4b23afc10 | ||
|
|
8f32e82d14 | ||
|
|
cf391bdd80 | ||
|
|
7120d6a9db | ||
|
|
4d80ab639e | ||
|
|
95bbd2852e | ||
|
|
4b0a934e51 | ||
|
|
ee5d86003b | ||
|
|
bc11b60147 | ||
|
|
801a4697ea | ||
|
|
628a970e6f | ||
|
|
566ea942dc | ||
|
|
e047c2d54e | ||
|
|
3fdd3d22e2 | ||
|
|
08e0fa9206 | ||
|
|
ed9e6ce676 | ||
|
|
a7bf07130e | ||
|
|
7c31662698 | ||
|
|
15df06a0bc | ||
|
|
47b5f8db7b | ||
|
|
0e763987aa | ||
|
|
37e8ea27d0 | ||
|
|
67d63256c2 | ||
|
|
ee185cf20a | ||
|
|
3552e05e4c | ||
|
|
735bb55b78 | ||
|
|
60f8a50a97 | ||
|
|
ffd01814b0 | ||
|
|
c511910b8f | ||
|
|
5c38f8f0d8 | ||
|
|
d512442325 | ||
|
|
43f54c90d2 | ||
|
|
57a99b478a | ||
|
|
3f402fbde7 | ||
|
|
98140c1f71 | ||
|
|
f00de85e38 | ||
|
|
b686b12c6a | ||
|
|
e4b903b13c | ||
|
|
72f576a70e | ||
|
|
c4f2c7be63 | ||
|
|
75b75fd223 | ||
|
|
391b18a5ac | ||
|
|
8de0f8e87c | ||
|
|
adb0b76559 | ||
|
|
b6df25c02c | ||
|
|
620186db1c | ||
|
|
21fac8046e | ||
|
|
de7c35f587 | ||
|
|
a23c979045 | ||
|
|
13da7287b1 | ||
|
|
e8398df9fc | ||
|
|
8303682bd7 | ||
|
|
8125d1a124 | ||
|
|
110ff5e12e | ||
|
|
24afcba20a | ||
|
|
d5f41d12e8 | ||
|
|
d640dd66e2 | ||
|
|
05bd9f57d6 | ||
|
|
60ab476ec7 | ||
|
|
bcae285ab3 | ||
|
|
cd67176b41 | ||
|
|
98eba54621 | ||
|
|
c530627078 | ||
|
|
f961947e1e | ||
|
|
e9e6628b21 | ||
|
|
c7684d6fe1 |
+11
-1
@@ -36,7 +36,12 @@ public final class BodyRecordParser {
|
|||||||
case TextBody.KEY -> {
|
case TextBody.KEY -> {
|
||||||
if (st == (MsgSubType.TEXT_POST & 0xFFFF)
|
if (st == (MsgSubType.TEXT_POST & 0xFFFF)
|
||||||
|| st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)
|
|| st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)
|
||||||
|| st == (MsgSubType.TEXT_REPOST & 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);
|
yield new TextLineBody(subType, version, bodyBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,12 +50,17 @@ public final class BodyRecordParser {
|
|||||||
yield new TextReplyBody(subType, version, bodyBytes);
|
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);
|
throw new IllegalArgumentException("Unknown TEXT subType for type=1 ver=1: subType=" + st);
|
||||||
}
|
}
|
||||||
|
|
||||||
case ReactionBody.KEY -> new ReactionBody(subType, version, bodyBytes);
|
case ReactionBody.KEY -> new ReactionBody(subType, version, bodyBytes);
|
||||||
case ConnectionBody.KEY -> new ConnectionBody(subType, version, bodyBytes);
|
case ConnectionBody.KEY -> new ConnectionBody(subType, version, bodyBytes);
|
||||||
case UserParamBody.KEY -> new UserParamBody(subType, version, bodyBytes);
|
case UserParamBody.KEY -> new UserParamBody(subType, version, bodyBytes);
|
||||||
|
case StatusActionBody.KEY -> new StatusActionBody(subType, version, bodyBytes);
|
||||||
|
|
||||||
default -> throw new IllegalArgumentException(String.format(
|
default -> throw new IllegalArgumentException(String.format(
|
||||||
"Unknown body type/version from header: type=%d ver=%d subType=%d",
|
"Unknown body type/version from header: type=%d ver=%d subType=%d",
|
||||||
|
|||||||
@@ -64,11 +64,29 @@ public final class MsgSubType {
|
|||||||
*/
|
*/
|
||||||
public static final short TEXT_EDIT_REPLY = 21;
|
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) + текст комментария.
|
* Имеет 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 = 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) ===================== */
|
/* ===================== REACTION (msg_type=2) ===================== */
|
||||||
|
|
||||||
@@ -80,14 +98,9 @@ public final class MsgSubType {
|
|||||||
/* ===================== CONNECTION (msg_type=3) ===================== */
|
/* ===================== CONNECTION (msg_type=3) ===================== */
|
||||||
|
|
||||||
/** Добавить в близкие друзья (close friend). */
|
/** Добавить в близкие друзья (close friend). */
|
||||||
public static final short CONNECTION_FRIEND = 10;
|
public static final short CONNECTION_CLOSE_FRIEND = 10;
|
||||||
/** Удалить из близких друзей (close friend). */
|
/** Удалить из близких друзей (close friend). */
|
||||||
public static final short CONNECTION_UNFRIEND = 11;
|
public static final short CONNECTION_UNCLOSE_FRIEND = 11;
|
||||||
|
|
||||||
/** Alias: добавить в close friend (то же значение, что CONNECTION_FRIEND). */
|
|
||||||
public static final short CONNECTION_CLOSE_FRIEND = CONNECTION_FRIEND;
|
|
||||||
/** Alias: удалить из close friend (то же значение, что CONNECTION_UNFRIEND). */
|
|
||||||
public static final short CONNECTION_UNCLOSE_FRIEND = CONNECTION_UNFRIEND;
|
|
||||||
|
|
||||||
/** Добавить в контакты. */
|
/** Добавить в контакты. */
|
||||||
public static final short CONNECTION_CONTACT = 20;
|
public static final short CONNECTION_CONTACT = 20;
|
||||||
@@ -138,4 +151,16 @@ public final class MsgSubType {
|
|||||||
|
|
||||||
/** Параметр профиля key/value (обе строки). */
|
/** Параметр профиля key/value (обе строки). */
|
||||||
public static final short USER_PARAM_TEXT_TEXT = 1;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -66,7 +66,7 @@ import java.util.Objects;
|
|||||||
* toBlockHash32=hash32(CREATE_CHANNEL)
|
* toBlockHash32=hash32(CREATE_CHANNEL)
|
||||||
*
|
*
|
||||||
* 3) ЗАПРЕТЫ ВАЛИДАЦИИ (желательно на сервере/в БД):
|
* 3) ЗАПРЕТЫ ВАЛИДАЦИИ (желательно на сервере/в БД):
|
||||||
* - CONNECTION_FRIEND/CONTACT не могут ссылаться на не-HEADER (toBlockNumber != 0 запрещено).
|
* - CONNECTION_CLOSE_FRIEND/CONTACT не могут ссылаться на не-HEADER (toBlockNumber != 0 запрещено).
|
||||||
* - FOLLOW на канал "X" не может ссылаться на произвольный пост внутри канала:
|
* - FOLLOW на канал "X" не может ссылаться на произвольный пост внутри канала:
|
||||||
* разрешено ТОЛЬКО на ROOT (HEADER или CREATE_CHANNEL).
|
* разрешено ТОЛЬКО на ROOT (HEADER или CREATE_CHANNEL).
|
||||||
*
|
*
|
||||||
@@ -183,8 +183,8 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasL
|
|||||||
|
|
||||||
private static boolean isValidSubType(short st) {
|
private static boolean isValidSubType(short st) {
|
||||||
int v = st & 0xFFFF;
|
int v = st & 0xFFFF;
|
||||||
return v == (MsgSubType.CONNECTION_FRIEND & 0xFFFF)
|
return v == (MsgSubType.CONNECTION_CLOSE_FRIEND & 0xFFFF)
|
||||||
|| v == (MsgSubType.CONNECTION_UNFRIEND & 0xFFFF)
|
|| v == (MsgSubType.CONNECTION_UNCLOSE_FRIEND & 0xFFFF)
|
||||||
|| v == (MsgSubType.CONNECTION_CONTACT & 0xFFFF)
|
|| v == (MsgSubType.CONNECTION_CONTACT & 0xFFFF)
|
||||||
|| v == (MsgSubType.CONNECTION_UNCONTACT & 0xFFFF)
|
|| v == (MsgSubType.CONNECTION_UNCONTACT & 0xFFFF)
|
||||||
|| v == (MsgSubType.CONNECTION_FOLLOW & 0xFFFF)
|
|| v == (MsgSubType.CONNECTION_FOLLOW & 0xFFFF)
|
||||||
|
|||||||
+6
-7
@@ -19,7 +19,7 @@ import java.util.Objects;
|
|||||||
* [1] channelNameLen
|
* [1] channelNameLen
|
||||||
* [N] channelName UTF-8
|
* [N] channelName UTF-8
|
||||||
* [2] channelDescriptionLen
|
* [2] channelDescriptionLen
|
||||||
* [M] channelDescription UTF-8 (0..200 bytes)
|
* [M] channelDescription UTF-8 (0..2048 bytes)
|
||||||
* [2] channelTypeCode (uint16)
|
* [2] channelTypeCode (uint16)
|
||||||
* [2] channelTypeVersion (uint16)
|
* [2] channelTypeVersion (uint16)
|
||||||
*/
|
*/
|
||||||
@@ -38,7 +38,7 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
|
|||||||
|
|
||||||
private static final byte[] ZERO32 = new byte[32];
|
private static final byte[] ZERO32 = new byte[32];
|
||||||
private static final int MAX_NAME_LENGTH = 32;
|
private static final int MAX_NAME_LENGTH = 32;
|
||||||
private static final int MAX_DESCRIPTION_UTF8_LEN = 200;
|
public static final int MAX_DESCRIPTION_UTF8_LEN = 2048;
|
||||||
|
|
||||||
public final short subType;
|
public final short subType;
|
||||||
public final short version;
|
public final short version;
|
||||||
@@ -88,7 +88,7 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
|
|||||||
|
|
||||||
int descriptionLen = Short.toUnsignedInt(bb.getShort());
|
int descriptionLen = Short.toUnsignedInt(bb.getShort());
|
||||||
if (descriptionLen > MAX_DESCRIPTION_UTF8_LEN) {
|
if (descriptionLen > MAX_DESCRIPTION_UTF8_LEN) {
|
||||||
throw new IllegalArgumentException("channelDescription utf8 len must be <=200");
|
throw new IllegalArgumentException("channelDescription utf8 len must be <=2048");
|
||||||
}
|
}
|
||||||
if (bb.remaining() != descriptionLen + 4) {
|
if (bb.remaining() != descriptionLen + 4) {
|
||||||
throw new IllegalArgumentException("CreateChannelBody tail mismatch: remaining=" + bb.remaining() + " descriptionLen=" + descriptionLen);
|
throw new IllegalArgumentException("CreateChannelBody tail mismatch: remaining=" + bb.remaining() + " descriptionLen=" + descriptionLen);
|
||||||
@@ -145,7 +145,7 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
|
|||||||
String normalizedDescription = normalizeDescription(channelDescription);
|
String normalizedDescription = normalizeDescription(channelDescription);
|
||||||
byte[] descUtf8 = normalizedDescription.getBytes(StandardCharsets.UTF_8);
|
byte[] descUtf8 = normalizedDescription.getBytes(StandardCharsets.UTF_8);
|
||||||
if (descUtf8.length > MAX_DESCRIPTION_UTF8_LEN) {
|
if (descUtf8.length > MAX_DESCRIPTION_UTF8_LEN) {
|
||||||
throw new IllegalArgumentException("channelDescription utf8 len must be <=200");
|
throw new IllegalArgumentException("channelDescription utf8 len must be <=2048");
|
||||||
}
|
}
|
||||||
|
|
||||||
int typeCode = Short.toUnsignedInt(channelTypeCode);
|
int typeCode = Short.toUnsignedInt(channelTypeCode);
|
||||||
@@ -166,7 +166,7 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
|
|||||||
|
|
||||||
private static String normalizeDescription(String value) {
|
private static String normalizeDescription(String value) {
|
||||||
if (value == null) return "";
|
if (value == null) return "";
|
||||||
return value.trim().replaceAll("\\s+", " ");
|
return value.trim().replace("\r\n", "\n").replace('\r', '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -177,7 +177,7 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
|
|||||||
}
|
}
|
||||||
byte[] descriptionUtf8 = normalizeDescription(channelDescription).getBytes(StandardCharsets.UTF_8);
|
byte[] descriptionUtf8 = normalizeDescription(channelDescription).getBytes(StandardCharsets.UTF_8);
|
||||||
if (descriptionUtf8.length > MAX_DESCRIPTION_UTF8_LEN) {
|
if (descriptionUtf8.length > MAX_DESCRIPTION_UTF8_LEN) {
|
||||||
throw new IllegalArgumentException("channelDescription utf8 len must be <=200");
|
throw new IllegalArgumentException("channelDescription utf8 len must be <=2048");
|
||||||
}
|
}
|
||||||
|
|
||||||
int cap = 4 + (4 + 32 + 4) + 1 + nameUtf8.length + 2 + descriptionUtf8.length + 4;
|
int cap = 4 + (4 + 32 + 4) + 1 + nameUtf8.length + 2 + descriptionUtf8.length + 4;
|
||||||
@@ -204,4 +204,3 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine {
|
|||||||
@Override
|
@Override
|
||||||
public int lineSeq() { return thisLineNumber; }
|
public int lineSeq() { return thisLineNumber; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+166
@@ -0,0 +1,166 @@
|
|||||||
|
package blockchain.body;
|
||||||
|
|
||||||
|
import blockchain.MsgSubType;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.charset.CharacterCodingException;
|
||||||
|
import java.nio.charset.CodingErrorAction;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StatusActionBody — type=5, ver=1.
|
||||||
|
*
|
||||||
|
* Все STATUS_ACTION имеют target на конкретный блок и опциональный текст-пояснение.
|
||||||
|
*
|
||||||
|
* Формат bodyBytes (BigEndian):
|
||||||
|
* [1] toBlockchainNameLen (uint8)
|
||||||
|
* [N] toBlockchainName UTF-8
|
||||||
|
* [4] toBlockGlobalNumber
|
||||||
|
* [32] toBlockHash32
|
||||||
|
* [2] textLenBytes (uint16)
|
||||||
|
* [M] text UTF-8
|
||||||
|
*/
|
||||||
|
public final class StatusActionBody implements BodyRecord, BodyHasTarget {
|
||||||
|
|
||||||
|
public static final short TYPE = 5;
|
||||||
|
public static final short VER = 1;
|
||||||
|
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
||||||
|
|
||||||
|
public final short subType;
|
||||||
|
public final short version;
|
||||||
|
public final String toBlockchainName;
|
||||||
|
public final int toBlockGlobalNumber;
|
||||||
|
public final byte[] toBlockHash32;
|
||||||
|
public final String message;
|
||||||
|
|
||||||
|
public StatusActionBody(short subType, short version, byte[] bodyBytes) {
|
||||||
|
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||||||
|
this.subType = subType;
|
||||||
|
this.version = version;
|
||||||
|
|
||||||
|
if ((version & 0xFFFF) != (VER & 0xFFFF)) {
|
||||||
|
throw new IllegalArgumentException("StatusActionBody version must be 1, got=" + (version & 0xFFFF));
|
||||||
|
}
|
||||||
|
if (!isSupportedSubType(subType)) {
|
||||||
|
throw new IllegalArgumentException("Unsupported STATUS_ACTION subType: " + (subType & 0xFFFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
ensureMin(bb, 1 + 1 + 4 + 32 + 2, "STATUS_ACTION too short");
|
||||||
|
|
||||||
|
int nameLen = Byte.toUnsignedInt(bb.get());
|
||||||
|
if (nameLen <= 0) throw new IllegalArgumentException("STATUS_ACTION toBlockchainNameLen is 0");
|
||||||
|
ensureMin(bb, nameLen + 4 + 32 + 2, "STATUS_ACTION payload too short");
|
||||||
|
|
||||||
|
byte[] nameBytes = new byte[nameLen];
|
||||||
|
bb.get(nameBytes);
|
||||||
|
this.toBlockchainName = new String(nameBytes, StandardCharsets.UTF_8);
|
||||||
|
this.toBlockGlobalNumber = bb.getInt();
|
||||||
|
this.toBlockHash32 = new byte[32];
|
||||||
|
bb.get(this.toBlockHash32);
|
||||||
|
this.message = readStrictUtf8Len16AllowEmpty(bb, "StatusActionBody text");
|
||||||
|
|
||||||
|
ensureNoTail(bb, "StatusActionBody");
|
||||||
|
}
|
||||||
|
|
||||||
|
public StatusActionBody(short subType, String toBlockchainName, int toBlockGlobalNumber, byte[] toBlockHash32, String message) {
|
||||||
|
Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
|
||||||
|
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
||||||
|
Objects.requireNonNull(message, "message == null");
|
||||||
|
if (!isSupportedSubType(subType)) throw new IllegalArgumentException("Unsupported STATUS_ACTION subType");
|
||||||
|
if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
|
||||||
|
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||||
|
if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||||
|
|
||||||
|
this.subType = subType;
|
||||||
|
this.version = VER;
|
||||||
|
this.toBlockchainName = toBlockchainName;
|
||||||
|
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
||||||
|
this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StatusActionBody check() {
|
||||||
|
if (!isSupportedSubType(subType)) {
|
||||||
|
throw new IllegalArgumentException("Bad STATUS_ACTION subType: " + (subType & 0xFFFF));
|
||||||
|
}
|
||||||
|
if (toBlockchainName == null || toBlockchainName.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("STATUS_ACTION toBlockchainName is blank");
|
||||||
|
}
|
||||||
|
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||||
|
if (toBlockHash32 == null || toBlockHash32.length != 32) {
|
||||||
|
throw new IllegalArgumentException("toBlockHash32 invalid");
|
||||||
|
}
|
||||||
|
if (message == null) throw new IllegalArgumentException("message is null");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] toBytes() {
|
||||||
|
byte[] msgUtf8 = message.getBytes(StandardCharsets.UTF_8);
|
||||||
|
if (msgUtf8.length > 65535) throw new IllegalArgumentException("Text too long (>65535 bytes)");
|
||||||
|
|
||||||
|
byte[] nameUtf8 = toBlockchainName.getBytes(StandardCharsets.UTF_8);
|
||||||
|
if (nameUtf8.length == 0 || nameUtf8.length > 255) {
|
||||||
|
throw new IllegalArgumentException("STATUS_ACTION toBlockchainName utf8 len must be 1..255");
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer bb = ByteBuffer.allocate(1 + nameUtf8.length + 4 + 32 + 2 + msgUtf8.length)
|
||||||
|
.order(ByteOrder.BIG_ENDIAN);
|
||||||
|
bb.put((byte) nameUtf8.length);
|
||||||
|
bb.put(nameUtf8);
|
||||||
|
bb.putInt(toBlockGlobalNumber);
|
||||||
|
bb.put(toBlockHash32);
|
||||||
|
bb.putShort((short) msgUtf8.length);
|
||||||
|
bb.put(msgUtf8);
|
||||||
|
return bb.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public String toBchName() { return toBlockchainName; }
|
||||||
|
@Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
|
||||||
|
@Override public byte[] toBlockHashBytes() { return toBlockHash32; }
|
||||||
|
|
||||||
|
private static boolean isSupportedSubType(short subType) {
|
||||||
|
int st = subType & 0xFFFF;
|
||||||
|
return st == (MsgSubType.STATUS_DONE_ONCE & 0xFFFF)
|
||||||
|
|| st == (MsgSubType.STATUS_LEARNED & 0xFFFF)
|
||||||
|
|| st == (MsgSubType.STATUS_SERVICE_PASSED & 0xFFFF)
|
||||||
|
|| st == (MsgSubType.STATUS_CONFIRMED & 0xFFFF)
|
||||||
|
|| st == (MsgSubType.STATUS_INTERESTED & 0xFFFF)
|
||||||
|
|| st == (MsgSubType.STATUS_STARTED & 0xFFFF)
|
||||||
|
|| st == (MsgSubType.STATUS_IN_STUDY & 0xFFFF)
|
||||||
|
|| st == (MsgSubType.STATUS_ABANDONED & 0xFFFF)
|
||||||
|
|| st == (MsgSubType.STATUS_COMPLETED & 0xFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readStrictUtf8Len16AllowEmpty(ByteBuffer bb, String fieldName) {
|
||||||
|
int len = Short.toUnsignedInt(bb.getShort());
|
||||||
|
if (len == 0) return "";
|
||||||
|
if (bb.remaining() < len) throw new IllegalArgumentException(fieldName + " payload too short (len=" + len + ")");
|
||||||
|
|
||||||
|
byte[] bytes = new byte[len];
|
||||||
|
bb.get(bytes);
|
||||||
|
|
||||||
|
var decoder = StandardCharsets.UTF_8.newDecoder()
|
||||||
|
.onMalformedInput(CodingErrorAction.REPORT)
|
||||||
|
.onUnmappableCharacter(CodingErrorAction.REPORT);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return decoder.decode(ByteBuffer.wrap(bytes)).toString();
|
||||||
|
} catch (CharacterCodingException e) {
|
||||||
|
throw new IllegalArgumentException(fieldName + " is not valid UTF-8", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ensureMin(ByteBuffer bb, int need, String msg) {
|
||||||
|
if (bb.remaining() < need) throw new IllegalArgumentException(msg + " (need=" + need + ", remaining=" + bb.remaining() + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ensureNoTail(ByteBuffer bb, String ctx) {
|
||||||
|
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes for " + ctx + ", remaining=" + bb.remaining());
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
-13
@@ -16,11 +16,16 @@ import java.util.Objects;
|
|||||||
* subType:
|
* subType:
|
||||||
* - POST (10)
|
* - POST (10)
|
||||||
* - EDIT_POST (11)
|
* - EDIT_POST (11)
|
||||||
* - REPOST (30)
|
* - REPOST (50)
|
||||||
|
* - CHANNEL_META (90)
|
||||||
|
* - ENTRYPOINT (100)
|
||||||
|
* - EXERCISE (110)
|
||||||
|
* - SERVICE (120)
|
||||||
|
* - COURSE (130)
|
||||||
*
|
*
|
||||||
* Формат bodyBytes (BigEndian):
|
* Формат bodyBytes (BigEndian):
|
||||||
*
|
*
|
||||||
* POST:
|
* POST / CHANNEL_META / ENTRYPOINT / EXERCISE / SERVICE / COURSE:
|
||||||
* [4] lineCode
|
* [4] lineCode
|
||||||
* [4] prevLineNumber
|
* [4] prevLineNumber
|
||||||
* [32] prevLineHash32
|
* [32] prevLineHash32
|
||||||
@@ -89,8 +94,13 @@ public final class TextLineBody implements BodyRecord, BodyHasLine, BodyHasTarge
|
|||||||
int st = this.subType & 0xFFFF;
|
int st = this.subType & 0xFFFF;
|
||||||
if (st != (MsgSubType.TEXT_POST & 0xFFFF)
|
if (st != (MsgSubType.TEXT_POST & 0xFFFF)
|
||||||
&& st != (MsgSubType.TEXT_EDIT_POST & 0xFFFF)
|
&& st != (MsgSubType.TEXT_EDIT_POST & 0xFFFF)
|
||||||
&& st != (MsgSubType.TEXT_REPOST & 0xFFFF)) {
|
&& st != (MsgSubType.TEXT_REPOST & 0xFFFF)
|
||||||
throw new IllegalArgumentException("TextLineBody supports only POST/EDIT_POST/REPOST, got subType=" + st);
|
&& 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
||||||
@@ -133,7 +143,8 @@ public final class TextLineBody implements BodyRecord, BodyHasLine, BodyHasTarge
|
|||||||
this.toBlockHash32 = null;
|
this.toBlockHash32 = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.message = readStrictUtf8Len16(bb, "TextLineBody text", st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF));
|
this.message = readStrictUtf8Len16(bb, "TextLineBody text",
|
||||||
|
st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF) || st == (MsgSubType.TEXT_CHANNEL_META & 0xFFFF));
|
||||||
|
|
||||||
ensureNoTail(bb, "TextLineBody");
|
ensureNoTail(bb, "TextLineBody");
|
||||||
}
|
}
|
||||||
@@ -155,12 +166,17 @@ public final class TextLineBody implements BodyRecord, BodyHasLine, BodyHasTarge
|
|||||||
int st = subType & 0xFFFF;
|
int st = subType & 0xFFFF;
|
||||||
if (st != (MsgSubType.TEXT_POST & 0xFFFF)
|
if (st != (MsgSubType.TEXT_POST & 0xFFFF)
|
||||||
&& st != (MsgSubType.TEXT_EDIT_POST & 0xFFFF)
|
&& st != (MsgSubType.TEXT_EDIT_POST & 0xFFFF)
|
||||||
&& st != (MsgSubType.TEXT_REPOST & 0xFFFF)) {
|
&& st != (MsgSubType.TEXT_REPOST & 0xFFFF)
|
||||||
throw new IllegalArgumentException("TextLineBody supports only POST/EDIT_POST/REPOST");
|
&& 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 (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");
|
throw new IllegalArgumentException("message is blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +222,12 @@ public final class TextLineBody implements BodyRecord, BodyHasLine, BodyHasTarge
|
|||||||
int st = subType & 0xFFFF;
|
int st = subType & 0xFFFF;
|
||||||
if (st != (MsgSubType.TEXT_POST & 0xFFFF)
|
if (st != (MsgSubType.TEXT_POST & 0xFFFF)
|
||||||
&& st != (MsgSubType.TEXT_EDIT_POST & 0xFFFF)
|
&& st != (MsgSubType.TEXT_EDIT_POST & 0xFFFF)
|
||||||
&& st != (MsgSubType.TEXT_REPOST & 0xFFFF))
|
&& st != (MsgSubType.TEXT_REPOST & 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);
|
throw new IllegalArgumentException("Bad TextLineBody subType: " + st);
|
||||||
|
|
||||||
if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
|
if (lineCode < 0) throw new IllegalArgumentException("lineCode < 0");
|
||||||
@@ -231,10 +252,15 @@ public final class TextLineBody implements BodyRecord, BodyHasLine, BodyHasTarge
|
|||||||
if (toBlockHash32 == null || toBlockHash32.length != 32)
|
if (toBlockHash32 == null || toBlockHash32.length != 32)
|
||||||
throw new IllegalArgumentException("REPOST toBlockHash32 invalid");
|
throw new IllegalArgumentException("REPOST toBlockHash32 invalid");
|
||||||
} else {
|
} else {
|
||||||
if (message == null || message.isBlank())
|
if (st == (MsgSubType.TEXT_CHANNEL_META & 0xFFFF)) {
|
||||||
|
if (message == null) throw new IllegalArgumentException("CHANNEL_META message is null");
|
||||||
|
} else if (requiresNonBlankMessage(st) && (message == null || message.isBlank())) {
|
||||||
throw new IllegalArgumentException("Text message is blank");
|
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)
|
if (toBlockchainName != null || toBlockGlobalNumber != null || toBlockHash32 != null)
|
||||||
throw new IllegalArgumentException("POST must not contain target fields");
|
throw new IllegalArgumentException("POST/CHANNEL_META must not contain target fields");
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
@@ -246,12 +272,17 @@ public final class TextLineBody implements BodyRecord, BodyHasLine, BodyHasTarge
|
|||||||
if (msgUtf8.length > 65535) throw new IllegalArgumentException("Text too long (>65535 bytes)");
|
if (msgUtf8.length > 65535) throw new IllegalArgumentException("Text too long (>65535 bytes)");
|
||||||
|
|
||||||
int st = subType & 0xFFFF;
|
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");
|
throw new IllegalArgumentException("Text payload is empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
int cap;
|
int cap;
|
||||||
if (st == (MsgSubType.TEXT_POST & 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;
|
cap = (4 + 4 + 32 + 4) + 2 + msgUtf8.length;
|
||||||
} else if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
|
} else if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
|
||||||
// EDIT_POST
|
// EDIT_POST
|
||||||
@@ -310,6 +341,15 @@ public final class TextLineBody implements BodyRecord, BodyHasLine, BodyHasTarge
|
|||||||
return (subType & 0xFFFF) == (MsgSubType.TEXT_EDIT_POST & 0xFFFF);
|
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) {
|
private static String readStrictUtf8Len16(ByteBuffer bb, String fieldName, boolean allowEmpty) {
|
||||||
int len = Short.toUnsignedInt(bb.getShort());
|
int len = Short.toUnsignedInt(bb.getShort());
|
||||||
if (len == 0) {
|
if (len == 0) {
|
||||||
|
|||||||
+157
@@ -0,0 +1,157 @@
|
|||||||
|
package blockchain.body;
|
||||||
|
|
||||||
|
import blockchain.MsgSubType;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.charset.CharacterCodingException;
|
||||||
|
import java.nio.charset.CodingErrorAction;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TextRatingBody — type=1, ver=1.
|
||||||
|
*
|
||||||
|
* subType:
|
||||||
|
* - RATING (30)
|
||||||
|
*
|
||||||
|
* Формат bodyBytes (BigEndian):
|
||||||
|
* [1] toBlockchainNameLen (uint8)
|
||||||
|
* [N] toBlockchainName UTF-8
|
||||||
|
* [4] toBlockGlobalNumber
|
||||||
|
* [32] toBlockHash32
|
||||||
|
* [2] textLenBytes (uint16)
|
||||||
|
* [M] text UTF-8
|
||||||
|
*/
|
||||||
|
public final class TextRatingBody implements BodyRecord, BodyHasTarget {
|
||||||
|
|
||||||
|
public static final short TYPE = 1;
|
||||||
|
public static final short VER = 1;
|
||||||
|
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
||||||
|
|
||||||
|
public final short subType;
|
||||||
|
public final short version;
|
||||||
|
public final String toBlockchainName;
|
||||||
|
public final int toBlockGlobalNumber;
|
||||||
|
public final byte[] toBlockHash32;
|
||||||
|
public final String message;
|
||||||
|
|
||||||
|
public TextRatingBody(short subType, short version, byte[] bodyBytes) {
|
||||||
|
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||||||
|
this.subType = subType;
|
||||||
|
this.version = version;
|
||||||
|
|
||||||
|
if ((version & 0xFFFF) != (VER & 0xFFFF)) {
|
||||||
|
throw new IllegalArgumentException("TextRatingBody version must be 1, got=" + (version & 0xFFFF));
|
||||||
|
}
|
||||||
|
if ((subType & 0xFFFF) != (MsgSubType.TEXT_RATING & 0xFFFF)) {
|
||||||
|
throw new IllegalArgumentException("TextRatingBody supports only TEXT_RATING");
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
ensureMin(bb, 1 + 1 + 4 + 32 + 2, "RATING too short");
|
||||||
|
|
||||||
|
int nameLen = Byte.toUnsignedInt(bb.get());
|
||||||
|
if (nameLen <= 0) throw new IllegalArgumentException("RATING toBlockchainNameLen is 0");
|
||||||
|
ensureMin(bb, nameLen + 4 + 32 + 2, "RATING payload too short");
|
||||||
|
|
||||||
|
byte[] nameBytes = new byte[nameLen];
|
||||||
|
bb.get(nameBytes);
|
||||||
|
this.toBlockchainName = new String(nameBytes, StandardCharsets.UTF_8);
|
||||||
|
this.toBlockGlobalNumber = bb.getInt();
|
||||||
|
this.toBlockHash32 = new byte[32];
|
||||||
|
bb.get(this.toBlockHash32);
|
||||||
|
this.message = readStrictUtf8Len16(bb, "TextRatingBody text");
|
||||||
|
|
||||||
|
ensureNoTail(bb, "TextRatingBody");
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextRatingBody(String toBlockchainName, int toBlockGlobalNumber, byte[] toBlockHash32, String message) {
|
||||||
|
Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
|
||||||
|
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
||||||
|
Objects.requireNonNull(message, "message == null");
|
||||||
|
if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
|
||||||
|
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||||
|
if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||||
|
if (message.isBlank()) throw new IllegalArgumentException("message is blank");
|
||||||
|
|
||||||
|
this.subType = MsgSubType.TEXT_RATING;
|
||||||
|
this.version = VER;
|
||||||
|
this.toBlockchainName = toBlockchainName;
|
||||||
|
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
||||||
|
this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TextRatingBody check() {
|
||||||
|
if ((subType & 0xFFFF) != (MsgSubType.TEXT_RATING & 0xFFFF)) {
|
||||||
|
throw new IllegalArgumentException("Bad TextRatingBody subType: " + (subType & 0xFFFF));
|
||||||
|
}
|
||||||
|
if (toBlockchainName == null || toBlockchainName.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("RATING toBlockchainName is blank");
|
||||||
|
}
|
||||||
|
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||||
|
if (toBlockHash32 == null || toBlockHash32.length != 32) {
|
||||||
|
throw new IllegalArgumentException("toBlockHash32 invalid");
|
||||||
|
}
|
||||||
|
if (message == null || message.isBlank()) throw new IllegalArgumentException("message is blank");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] toBytes() {
|
||||||
|
byte[] msgUtf8 = message.getBytes(StandardCharsets.UTF_8);
|
||||||
|
if (msgUtf8.length == 0) throw new IllegalArgumentException("Text payload is empty");
|
||||||
|
if (msgUtf8.length > 65535) throw new IllegalArgumentException("Text too long (>65535 bytes)");
|
||||||
|
|
||||||
|
byte[] nameUtf8 = toBlockchainName.getBytes(StandardCharsets.UTF_8);
|
||||||
|
if (nameUtf8.length == 0 || nameUtf8.length > 255) {
|
||||||
|
throw new IllegalArgumentException("RATING toBlockchainName utf8 len must be 1..255");
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer bb = ByteBuffer.allocate(1 + nameUtf8.length + 4 + 32 + 2 + msgUtf8.length)
|
||||||
|
.order(ByteOrder.BIG_ENDIAN);
|
||||||
|
bb.put((byte) nameUtf8.length);
|
||||||
|
bb.put(nameUtf8);
|
||||||
|
bb.putInt(toBlockGlobalNumber);
|
||||||
|
bb.put(toBlockHash32);
|
||||||
|
bb.putShort((short) msgUtf8.length);
|
||||||
|
bb.put(msgUtf8);
|
||||||
|
return bb.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public String toBchName() { return toBlockchainName; }
|
||||||
|
@Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
|
||||||
|
@Override public byte[] toBlockHashBytes() { return toBlockHash32; }
|
||||||
|
|
||||||
|
private static String readStrictUtf8Len16(ByteBuffer bb, String fieldName) {
|
||||||
|
int len = Short.toUnsignedInt(bb.getShort());
|
||||||
|
if (len == 0) throw new IllegalArgumentException(fieldName + " is empty");
|
||||||
|
if (bb.remaining() < len) throw new IllegalArgumentException(fieldName + " payload too short (len=" + len + ")");
|
||||||
|
|
||||||
|
byte[] bytes = new byte[len];
|
||||||
|
bb.get(bytes);
|
||||||
|
|
||||||
|
var decoder = StandardCharsets.UTF_8.newDecoder()
|
||||||
|
.onMalformedInput(CodingErrorAction.REPORT)
|
||||||
|
.onUnmappableCharacter(CodingErrorAction.REPORT);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String s = decoder.decode(ByteBuffer.wrap(bytes)).toString();
|
||||||
|
if (s.isBlank()) throw new IllegalArgumentException(fieldName + " is blank");
|
||||||
|
return s;
|
||||||
|
} catch (CharacterCodingException e) {
|
||||||
|
throw new IllegalArgumentException(fieldName + " is not valid UTF-8", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ensureMin(ByteBuffer bb, int need, String msg) {
|
||||||
|
if (bb.remaining() < need) throw new IllegalArgumentException(msg + " (need=" + need + ", remaining=" + bb.remaining() + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ensureNoTail(ByteBuffer bb, String ctx) {
|
||||||
|
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes for " + ctx + ", remaining=" + bb.remaining());
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-1
@@ -14,6 +14,9 @@ public final class ShineSignatureConstants {
|
|||||||
/** Подписываемые данные параметра пользователя: prefix + login + param + time_ms + value */
|
/** Подписываемые данные параметра пользователя: prefix + login + param + time_ms + value */
|
||||||
public static final String USER_PARAMETER_PREFIX = "SHiNe/UserParameter:";
|
public static final String USER_PARAMETER_PREFIX = "SHiNe/UserParameter:";
|
||||||
|
|
||||||
|
/** Подписываемые данные пользовательских настроек: prefix + login + type + key + time_ms + value_text + value_num */
|
||||||
|
public static final String USER_SETTINGS_PREFIX = "SHiNe/UserSettings:";
|
||||||
|
|
||||||
/** TAG в HeaderBody (genesis). ASCII "SHiNe". */
|
/** TAG в HeaderBody (genesis). ASCII "SHiNe". */
|
||||||
public static final String BLOCKCHAIN_HEADER_TAG = "SHiNe";
|
public static final String BLOCKCHAIN_HEADER_TAG = "SHiNe";
|
||||||
|
|
||||||
@@ -31,4 +34,4 @@ public final class ShineSignatureConstants {
|
|||||||
|
|
||||||
/** Длина публичного ключа Ed25519. */
|
/** Длина публичного ключа Ed25519. */
|
||||||
public static final int ED25519_PUBLIC_KEY32_LEN = 32;
|
public static final int ED25519_PUBLIC_KEY32_LEN = 32;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,29 @@ public final class DatabaseInitializer {
|
|||||||
public static final int SCHEMA_VERSION_1 = 1;
|
public static final int SCHEMA_VERSION_1 = 1;
|
||||||
public static final int SCHEMA_VERSION_2 = 2;
|
public static final int SCHEMA_VERSION_2 = 2;
|
||||||
public static final int SCHEMA_VERSION_3 = 3;
|
public static final int SCHEMA_VERSION_3 = 3;
|
||||||
|
public static final int SCHEMA_VERSION_4 = 4;
|
||||||
|
public static final int SCHEMA_VERSION_5 = 5;
|
||||||
|
public static final int SCHEMA_VERSION_6 = 6;
|
||||||
|
public static final int SCHEMA_VERSION_7 = 7;
|
||||||
|
public static final int SCHEMA_VERSION_8 = 8;
|
||||||
|
public static final int SCHEMA_VERSION_9 = 9;
|
||||||
|
public static final int SCHEMA_VERSION_10 = 10;
|
||||||
|
public static final int SCHEMA_VERSION_11 = 11;
|
||||||
|
public static final int SCHEMA_VERSION_12 = 12;
|
||||||
|
public static final int SCHEMA_VERSION_13 = 13;
|
||||||
public static final String POSTGRES_SCHEMA_RESOURCE = "postgres/schema_v1.sql";
|
public static final String POSTGRES_SCHEMA_RESOURCE = "postgres/schema_v1.sql";
|
||||||
public static final String POSTGRES_MIGRATION_V2_RESOURCE = "postgres/migration_v2.sql";
|
public static final String POSTGRES_MIGRATION_V2_RESOURCE = "postgres/migration_v2.sql";
|
||||||
public static final String POSTGRES_MIGRATION_V3_RESOURCE = "postgres/migration_v3.sql";
|
public static final String POSTGRES_MIGRATION_V3_RESOURCE = "postgres/migration_v3.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V4_RESOURCE = "postgres/migration_v4.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V5_RESOURCE = "postgres/migration_v5.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V6_RESOURCE = "postgres/migration_v6.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V7_RESOURCE = "postgres/migration_v7.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V8_RESOURCE = "postgres/migration_v8.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V9_RESOURCE = "postgres/migration_v9.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V10_RESOURCE = "postgres/migration_v10.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V11_RESOURCE = "postgres/migration_v11.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V12_RESOURCE = "postgres/migration_v12.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V13_RESOURCE = "postgres/migration_v13.sql";
|
||||||
|
|
||||||
private DatabaseInitializer() {}
|
private DatabaseInitializer() {}
|
||||||
|
|
||||||
@@ -30,15 +50,19 @@ public final class DatabaseInitializer {
|
|||||||
public static final short TEXT_EDIT_POST = 11;
|
public static final short TEXT_EDIT_POST = 11;
|
||||||
public static final short TEXT_REPLY = 20;
|
public static final short TEXT_REPLY = 20;
|
||||||
public static final short TEXT_EDIT_REPLY = 21;
|
public static final short TEXT_EDIT_REPLY = 21;
|
||||||
public static final short TEXT_REPOST = 30;
|
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_LIKE = 1;
|
||||||
public static final short REACTION_UNLIKE = 2;
|
public static final short REACTION_UNLIKE = 2;
|
||||||
|
|
||||||
public static final short CONNECTION_FRIEND = 10;
|
public static final short CONNECTION_CLOSE_FRIEND = 10;
|
||||||
public static final short CONNECTION_UNFRIEND = 11;
|
public static final short CONNECTION_UNCLOSE_FRIEND = 11;
|
||||||
public static final short CONNECTION_CLOSE_FRIEND = CONNECTION_FRIEND;
|
|
||||||
public static final short CONNECTION_UNCLOSE_FRIEND = CONNECTION_UNFRIEND;
|
|
||||||
|
|
||||||
public static final short CONNECTION_CONTACT = 20;
|
public static final short CONNECTION_CONTACT = 20;
|
||||||
public static final short CONNECTION_UNCONTACT = 21;
|
public static final short CONNECTION_UNCONTACT = 21;
|
||||||
@@ -88,6 +112,47 @@ public final class DatabaseInitializer {
|
|||||||
}
|
}
|
||||||
if (currentVersion < SCHEMA_VERSION_3) {
|
if (currentVersion < SCHEMA_VERSION_3) {
|
||||||
runSqlScript(conn, POSTGRES_MIGRATION_V3_RESOURCE);
|
runSqlScript(conn, POSTGRES_MIGRATION_V3_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_3;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_4) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V4_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_4;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_5) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V5_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_5;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_6) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V6_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_6;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_7) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V7_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_7;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_8) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V8_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_8;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_9) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V9_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_9;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_10) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V10_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_10;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_11) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V11_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_11;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_12) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V12_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_12;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_13) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V13_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_13;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package shine.db;
|
package shine.db;
|
||||||
|
|
||||||
import shine.db.connection.DbProvider;
|
import shine.db.connection.DbProvider;
|
||||||
|
import shine.db.dao.DmDialogStateDAO;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
@@ -15,6 +16,7 @@ public final class DbController implements DbProvider {
|
|||||||
private static volatile DbController instance;
|
private static volatile DbController instance;
|
||||||
|
|
||||||
private final PostgresDbController delegate;
|
private final PostgresDbController delegate;
|
||||||
|
private volatile boolean dmDialogStateBootstrapped;
|
||||||
|
|
||||||
private DbController() {
|
private DbController() {
|
||||||
this.delegate = PostgresDbController.getInstance();
|
this.delegate = PostgresDbController.getInstance();
|
||||||
@@ -24,7 +26,9 @@ public final class DbController implements DbProvider {
|
|||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
synchronized (DbController.class) {
|
synchronized (DbController.class) {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
instance = new DbController();
|
DbController created = new DbController();
|
||||||
|
instance = created;
|
||||||
|
created.bootstrapDmDialogStateIfNeeded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,4 +44,18 @@ public final class DbController implements DbProvider {
|
|||||||
public void close() {
|
public void close() {
|
||||||
delegate.close();
|
delegate.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void bootstrapDmDialogStateIfNeeded() {
|
||||||
|
if (dmDialogStateBootstrapped) return;
|
||||||
|
synchronized (this) {
|
||||||
|
if (dmDialogStateBootstrapped) return;
|
||||||
|
try (Connection connection = delegate.getConnection()) {
|
||||||
|
DmDialogStateDAO.getInstance().bootstrapIfEmpty(connection);
|
||||||
|
dmDialogStateBootstrapped = true;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
instance = null;
|
||||||
|
throw new RuntimeException("DM dialog state bootstrap failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,23 @@ public final class MsgSubType {
|
|||||||
/** EDIT_REPLY — редактирование исходного ответа. */
|
/** EDIT_REPLY — редактирование исходного ответа. */
|
||||||
public static final short TEXT_EDIT_REPLY = 21;
|
public static final short TEXT_EDIT_REPLY = 21;
|
||||||
|
|
||||||
/** REPOST — репост сообщения в линии канала (с комментарием и target на оригинал). */
|
/** RATING — target-based отзыв на конкретный блок. */
|
||||||
public static final short TEXT_REPOST = 30;
|
public static final short TEXT_RATING = 30;
|
||||||
|
|
||||||
|
/** REPOST — отложенная будущая заготовка репоста сообщения в линии канала. */
|
||||||
|
public static final short TEXT_REPOST = 50;
|
||||||
|
|
||||||
|
/** CHANNEL_META — скрытый технический снимок профиля канала. */
|
||||||
|
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) ===================== */
|
/* ===================== REACTION (msg_type=2) ===================== */
|
||||||
|
|
||||||
@@ -43,23 +58,17 @@ public final class MsgSubType {
|
|||||||
/* ===================== CONNECTION (msg_type=3) ===================== */
|
/* ===================== CONNECTION (msg_type=3) ===================== */
|
||||||
/**
|
/**
|
||||||
* Совпадает с ConnectionBody:
|
* Совпадает с ConnectionBody:
|
||||||
* SET: CLOSE_FRIEND(=FRIEND)=10, CONTACT=20, FOLLOW=30, SPOUSE=40, PARENT=50, CHILD=52, SIBLING=54,
|
* SET: CLOSE_FRIEND=10, CONTACT=20, FOLLOW=30, SPOUSE=40, PARENT=50, CHILD=52, SIBLING=54,
|
||||||
* KNOWN_PERSON=60, SHINE_CONFIRMED=70, SHINE_SEEN=74
|
* KNOWN_PERSON=60, SHINE_CONFIRMED=70, SHINE_SEEN=74
|
||||||
* UNSET: UNCLOSE_FRIEND(=UNFRIEND)=11, UNCONTACT=21, UNFOLLOW=31, UNSPOUSE=41, UNPARENT=51, UNCHILD=53, UNSIBLING=55,
|
* UNSET: UNCLOSE_FRIEND=11, UNCONTACT=21, UNFOLLOW=31, UNSPOUSE=41, UNPARENT=51, UNCHILD=53, UNSIBLING=55,
|
||||||
* UNKNOWN_PERSON=61, SHINE_UNCONFIRMED=71, SHINE_UNSEEN=75
|
* UNKNOWN_PERSON=61, SHINE_UNCONFIRMED=71, SHINE_UNSEEN=75
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Добавить в близкие друзья (close friend). */
|
/** Добавить в близкие друзья (close friend). */
|
||||||
public static final short CONNECTION_FRIEND = 10;
|
public static final short CONNECTION_CLOSE_FRIEND = 10;
|
||||||
|
|
||||||
/** Удалить из близких друзей (close friend). */
|
/** Удалить из близких друзей (close friend). */
|
||||||
public static final short CONNECTION_UNFRIEND = 11;
|
public static final short CONNECTION_UNCLOSE_FRIEND = 11;
|
||||||
|
|
||||||
/** Alias: добавить в close friend (то же значение, что CONNECTION_FRIEND). */
|
|
||||||
public static final short CONNECTION_CLOSE_FRIEND = CONNECTION_FRIEND;
|
|
||||||
|
|
||||||
/** Alias: удалить из close friend (то же значение, что CONNECTION_UNFRIEND). */
|
|
||||||
public static final short CONNECTION_UNCLOSE_FRIEND = CONNECTION_UNFRIEND;
|
|
||||||
|
|
||||||
/** Добавить в контакты. */
|
/** Добавить в контакты. */
|
||||||
public static final short CONNECTION_CONTACT = 20;
|
public static final short CONNECTION_CONTACT = 20;
|
||||||
@@ -120,6 +129,18 @@ public final class MsgSubType {
|
|||||||
/** Параметр профиля key/value (обе строки). */
|
/** Параметр профиля key/value (обе строки). */
|
||||||
public static final short USER_PARAM_TEXT_TEXT = 1;
|
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 — лучше добавить новые значения,
|
// Если позже захочешь BLOCK/UNBLOCK — лучше добавить новые значения,
|
||||||
// не трогая уже занятые коды.
|
// не трогая уже занятые коды.
|
||||||
|
|||||||
+6
-6
@@ -185,10 +185,10 @@ public final class BlockchainResyncCleanupDAO {
|
|||||||
private int decreaseForeignLikesCount(Connection c, String blockchainName) throws SQLException {
|
private int decreaseForeignLikesCount(Connection c, String blockchainName) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
UPDATE message_stats
|
UPDATE message_stats
|
||||||
SET likes_count = MAX(
|
SET likes_count = GREATEST(
|
||||||
0,
|
0,
|
||||||
likes_count - (
|
likes_count - COALESCE((
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)::int
|
||||||
FROM reactions_state rs
|
FROM reactions_state rs
|
||||||
WHERE rs.from_bch_name = ?
|
WHERE rs.from_bch_name = ?
|
||||||
AND rs.reaction_type = ?
|
AND rs.reaction_type = ?
|
||||||
@@ -198,7 +198,7 @@ public final class BlockchainResyncCleanupDAO {
|
|||||||
AND rs.to_block_number = message_stats.to_block_number
|
AND rs.to_block_number = message_stats.to_block_number
|
||||||
AND rs.to_block_hash = message_stats.to_block_hash
|
AND rs.to_block_hash = message_stats.to_block_hash
|
||||||
AND rs.to_bch_name <> ?
|
AND rs.to_bch_name <> ?
|
||||||
)
|
), 0)
|
||||||
)
|
)
|
||||||
WHERE EXISTS (
|
WHERE EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
@@ -237,10 +237,10 @@ public final class BlockchainResyncCleanupDAO {
|
|||||||
private int decreaseForeignRepliesCount(Connection c, String blockchainName) throws SQLException {
|
private int decreaseForeignRepliesCount(Connection c, String blockchainName) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
UPDATE message_stats
|
UPDATE message_stats
|
||||||
SET replies_count = MAX(
|
SET replies_count = GREATEST(
|
||||||
0,
|
0,
|
||||||
replies_count - COALESCE((
|
replies_count - COALESCE((
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)::int
|
||||||
FROM blocks b
|
FROM blocks b
|
||||||
WHERE b.bch_name = ?
|
WHERE b.bch_name = ?
|
||||||
AND b.msg_type = 1
|
AND b.msg_type = 1
|
||||||
|
|||||||
@@ -65,8 +65,12 @@ public final class ChannelNameStateDAO {
|
|||||||
channel_type_version,
|
channel_type_version,
|
||||||
channel_root_block_number,
|
channel_root_block_number,
|
||||||
channel_root_block_hash,
|
channel_root_block_hash,
|
||||||
created_at_ms
|
created_at_ms,
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
ava_ar,
|
||||||
|
ava_sha256,
|
||||||
|
ava_size,
|
||||||
|
meta_updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""";
|
""";
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, entry.getSlug());
|
ps.setString(1, entry.getSlug());
|
||||||
@@ -79,6 +83,34 @@ public final class ChannelNameStateDAO {
|
|||||||
ps.setInt(8, entry.getChannelRootBlockNumber());
|
ps.setInt(8, entry.getChannelRootBlockNumber());
|
||||||
ps.setBytes(9, entry.getChannelRootBlockHash());
|
ps.setBytes(9, entry.getChannelRootBlockHash());
|
||||||
ps.setLong(10, entry.getCreatedAtMs());
|
ps.setLong(10, entry.getCreatedAtMs());
|
||||||
|
ps.setString(11, entry.getAvaAr() == null ? "" : entry.getAvaAr());
|
||||||
|
ps.setString(12, entry.getAvaSha256() == null ? "" : entry.getAvaSha256());
|
||||||
|
ps.setLong(13, Math.max(0L, entry.getAvaSize()));
|
||||||
|
ps.setLong(14, Math.max(0L, entry.getMetaUpdatedAtMs()));
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateMeta(Connection c, ChannelNameStateEntry entry) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
UPDATE channel_names_state
|
||||||
|
SET display_name = ?,
|
||||||
|
channel_description = ?,
|
||||||
|
ava_ar = ?,
|
||||||
|
ava_sha256 = ?,
|
||||||
|
ava_size = ?,
|
||||||
|
meta_updated_at_ms = ?
|
||||||
|
WHERE owner_bch_name = ? AND channel_root_block_number = ?
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, entry.getDisplayName() == null ? "" : entry.getDisplayName());
|
||||||
|
ps.setString(2, entry.getChannelDescription() == null ? "" : entry.getChannelDescription());
|
||||||
|
ps.setString(3, entry.getAvaAr() == null ? "" : entry.getAvaAr());
|
||||||
|
ps.setString(4, entry.getAvaSha256() == null ? "" : entry.getAvaSha256());
|
||||||
|
ps.setLong(5, Math.max(0L, entry.getAvaSize()));
|
||||||
|
ps.setLong(6, Math.max(0L, entry.getMetaUpdatedAtMs()));
|
||||||
|
ps.setString(7, entry.getOwnerBlockchainName());
|
||||||
|
ps.setInt(8, entry.getChannelRootBlockNumber());
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,36 @@ public final class ConnectionsStateDAO {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasOutgoingByRelTypeCanonical(Connection c, String loginAnyCase, String peerLoginAnyCase, int relType) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT 1
|
||||||
|
FROM connections_state cs
|
||||||
|
LEFT JOIN %s
|
||||||
|
ON LOWER(u_login.login) = LOWER(cs.to_login)
|
||||||
|
LEFT JOIN %s
|
||||||
|
ON LOWER(u_bch.blockchain_name) = LOWER(cs.to_bch_name)
|
||||||
|
WHERE LOWER(cs.login) = LOWER(?)
|
||||||
|
AND cs.rel_type = ?
|
||||||
|
AND (
|
||||||
|
LOWER(cs.to_login) = LOWER(?)
|
||||||
|
OR LOWER(COALESCE(u_login.login, u_bch.login, cs.to_login)) = LOWER(?)
|
||||||
|
)
|
||||||
|
LIMIT 1
|
||||||
|
""".formatted(
|
||||||
|
CurrentUsersSql.usersSubquery("u_login"),
|
||||||
|
CurrentUsersSql.usersSubquery("u_bch")
|
||||||
|
);
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, loginAnyCase);
|
||||||
|
ps.setInt(2, relType);
|
||||||
|
ps.setString(3, peerLoginAnyCase);
|
||||||
|
ps.setString(4, peerLoginAnyCase);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
return rs.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Incoming: список логинов (канонических), кто поставил relType пользователю login.
|
* Incoming: список логинов (канонических), кто поставил relType пользователю login.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public final class CurrentUsersDAO {
|
|||||||
String sql = """
|
String sql = """
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM %s
|
FROM %s
|
||||||
WHERE LOWER(login) = LOWER(?)
|
WHERE normalized_login = LOWER(BTRIM(?))
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""".formatted(CurrentUsersSql.usersSubquery("su"));
|
""".formatted(CurrentUsersSql.usersSubquery("su"));
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ public final class CurrentUsersDAO {
|
|||||||
blockchain_key,
|
blockchain_key,
|
||||||
client_key
|
client_key
|
||||||
FROM %s
|
FROM %s
|
||||||
WHERE LOWER(login) = LOWER(?)
|
WHERE normalized_login = LOWER(BTRIM(?))
|
||||||
""".formatted(CurrentUsersSql.usersSubquery("su"));
|
""".formatted(CurrentUsersSql.usersSubquery("su"));
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
@@ -167,7 +167,7 @@ public final class CurrentUsersDAO {
|
|||||||
blockchain_key,
|
blockchain_key,
|
||||||
client_key
|
client_key
|
||||||
FROM %s
|
FROM %s
|
||||||
WHERE LOWER(login) LIKE ?
|
WHERE normalized_login LIKE LOWER(BTRIM(?))
|
||||||
AND (? IS NULL OR is_server = ?)
|
AND (? IS NULL OR is_server = ?)
|
||||||
ORDER BY login
|
ORDER BY login
|
||||||
LIMIT 5
|
LIMIT 5
|
||||||
@@ -176,7 +176,7 @@ public final class CurrentUsersDAO {
|
|||||||
List<CurrentUserEntry> result = new ArrayList<>();
|
List<CurrentUserEntry> result = new ArrayList<>();
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, prefix.toLowerCase() + "%");
|
ps.setString(1, prefix.trim() + "%");
|
||||||
if (isServer == null) {
|
if (isServer == null) {
|
||||||
ps.setNull(2, Types.BOOLEAN);
|
ps.setNull(2, Types.BOOLEAN);
|
||||||
ps.setNull(3, Types.BOOLEAN);
|
ps.setNull(3, Types.BOOLEAN);
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
package shine.db.dao;
|
|
||||||
|
|
||||||
import shine.db.DbController;
|
|
||||||
import shine.db.entities.DirectMessageEntry;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.sql.PreparedStatement;
|
|
||||||
|
|
||||||
public final class DirectMessagesDAO {
|
|
||||||
private static volatile DirectMessagesDAO instance;
|
|
||||||
private final DbController db = DbController.getInstance();
|
|
||||||
|
|
||||||
private DirectMessagesDAO() {}
|
|
||||||
|
|
||||||
public static DirectMessagesDAO getInstance() {
|
|
||||||
if (instance == null) {
|
|
||||||
synchronized (DirectMessagesDAO.class) {
|
|
||||||
if (instance == null) instance = new DirectMessagesDAO();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void insert(DirectMessageEntry entry) throws Exception {
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
String sql = """
|
|
||||||
INSERT INTO direct_messages (
|
|
||||||
message_id, from_login, to_login, text, created_at_ms
|
|
||||||
) VALUES (?, ?, ?, ?, ?)
|
|
||||||
""";
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
|
||||||
ps.setString(1, entry.getMessageId());
|
|
||||||
ps.setString(2, entry.getFromLogin());
|
|
||||||
ps.setString(3, entry.getToLogin());
|
|
||||||
ps.setString(4, entry.getText());
|
|
||||||
ps.setLong(5, entry.getCreatedAtMs());
|
|
||||||
ps.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean existsFromTo(String fromLogin, String toLogin) throws Exception {
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
String sql = "SELECT 1 FROM direct_messages WHERE from_login = ? AND to_login = ? LIMIT 1";
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
|
||||||
ps.setString(1, fromLogin);
|
|
||||||
ps.setString(2, toLogin);
|
|
||||||
return ps.executeQuery().next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,518 @@
|
|||||||
|
package shine.db.dao;
|
||||||
|
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.entities.DmDeliveryStateEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Types;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/** Хранилище изменяемого состояния межсерверной доставки DM-пары. */
|
||||||
|
public final class DmDeliveryStateDAO {
|
||||||
|
private static volatile DmDeliveryStateDAO instance;
|
||||||
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
|
private DmDeliveryStateDAO() {}
|
||||||
|
|
||||||
|
public static DmDeliveryStateDAO getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (DmDeliveryStateDAO.class) {
|
||||||
|
if (instance == null) instance = new DmDeliveryStateDAO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DmDeliveryStateEntry upsertPair(
|
||||||
|
String outgoingMessageKey,
|
||||||
|
String eventId,
|
||||||
|
String baseKey,
|
||||||
|
String fromLogin,
|
||||||
|
String toLogin,
|
||||||
|
String incomingMessageKey,
|
||||||
|
long createdAtMs,
|
||||||
|
long expiresAtMs,
|
||||||
|
int initialState,
|
||||||
|
String deliveredServerLogin,
|
||||||
|
String routesHash,
|
||||||
|
boolean assistImmediately
|
||||||
|
) throws SQLException {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
int safeState = normalizeState(initialState, deliveredServerLogin);
|
||||||
|
if (expiresAtMs <= now && safeState == DmDeliveryStateEntry.PENDING_NONE) {
|
||||||
|
safeState = DmDeliveryStateEntry.FAILED_FINAL;
|
||||||
|
}
|
||||||
|
Long nextAttemptAt = safeState == DmDeliveryStateEntry.ACCEPTED
|
||||||
|
? now
|
||||||
|
: null;
|
||||||
|
String safeDeliveredLogin = safeState == DmDeliveryStateEntry.DELIVERED_ONE
|
||||||
|
? normalize(deliveredServerLogin)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO dm_delivery_state (
|
||||||
|
outgoing_message_key, event_id, base_key, from_login, to_login,
|
||||||
|
incoming_message_key, created_at_ms, delivery_expires_at_ms,
|
||||||
|
delivery_state, delivered_server_login, recipient_routes_hash,
|
||||||
|
attempt_index, next_attempt_at_ms, last_attempt_at_ms, last_error, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, NULL, NULL, ?)
|
||||||
|
ON CONFLICT (outgoing_message_key) DO UPDATE SET
|
||||||
|
event_id = EXCLUDED.event_id,
|
||||||
|
base_key = EXCLUDED.base_key,
|
||||||
|
from_login = EXCLUDED.from_login,
|
||||||
|
to_login = EXCLUDED.to_login,
|
||||||
|
incoming_message_key = EXCLUDED.incoming_message_key,
|
||||||
|
created_at_ms = CASE
|
||||||
|
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.created_at_ms
|
||||||
|
ELSE EXCLUDED.created_at_ms
|
||||||
|
END,
|
||||||
|
delivery_expires_at_ms = CASE
|
||||||
|
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.delivery_expires_at_ms
|
||||||
|
ELSE EXCLUDED.delivery_expires_at_ms
|
||||||
|
END,
|
||||||
|
delivery_state = CASE
|
||||||
|
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.delivery_state
|
||||||
|
ELSE EXCLUDED.delivery_state
|
||||||
|
END,
|
||||||
|
delivered_server_login = CASE
|
||||||
|
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.delivered_server_login
|
||||||
|
ELSE EXCLUDED.delivered_server_login
|
||||||
|
END,
|
||||||
|
recipient_routes_hash = CASE
|
||||||
|
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN COALESCE(dm_delivery_state.recipient_routes_hash, EXCLUDED.recipient_routes_hash)
|
||||||
|
ELSE EXCLUDED.recipient_routes_hash
|
||||||
|
END,
|
||||||
|
attempt_index = CASE
|
||||||
|
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.attempt_index
|
||||||
|
ELSE 0
|
||||||
|
END,
|
||||||
|
next_attempt_at_ms = CASE
|
||||||
|
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.next_attempt_at_ms
|
||||||
|
ELSE EXCLUDED.next_attempt_at_ms
|
||||||
|
END,
|
||||||
|
last_attempt_at_ms = CASE
|
||||||
|
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.last_attempt_at_ms
|
||||||
|
ELSE NULL
|
||||||
|
END,
|
||||||
|
last_error = CASE
|
||||||
|
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.last_error
|
||||||
|
ELSE NULL
|
||||||
|
END,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, outgoingMessageKey);
|
||||||
|
ps.setString(2, eventId);
|
||||||
|
ps.setString(3, baseKey);
|
||||||
|
ps.setString(4, normalize(fromLogin));
|
||||||
|
ps.setString(5, normalize(toLogin));
|
||||||
|
ps.setString(6, incomingMessageKey);
|
||||||
|
ps.setLong(7, createdAtMs);
|
||||||
|
ps.setLong(8, expiresAtMs);
|
||||||
|
ps.setInt(9, safeState);
|
||||||
|
setNullableString(ps, 10, safeDeliveredLogin);
|
||||||
|
setNullableString(ps, 11, normalizeBlank(routesHash));
|
||||||
|
setNullableLong(ps, 12, nextAttemptAt);
|
||||||
|
ps.setLong(13, now);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DmDeliveryStateEntry result;
|
||||||
|
if (initialState != DmDeliveryStateEntry.PENDING_NONE || !isBlank(deliveredServerLogin)) {
|
||||||
|
result = mergeRemote(eventId, initialState, deliveredServerLogin, routesHash, now);
|
||||||
|
} else {
|
||||||
|
result = getByEventId(eventId);
|
||||||
|
}
|
||||||
|
if (result != null && result.getDeliveryExpiresAtMs() <= now
|
||||||
|
&& result.getDeliveryState() == DmDeliveryStateEntry.PENDING_NONE) {
|
||||||
|
return finishAtExpiry(eventId, now);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DmDeliveryStateEntry getByEventId(String eventId) throws SQLException {
|
||||||
|
if (isBlank(eventId)) return null;
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(selectColumns() + " WHERE event_id = ?")) {
|
||||||
|
ps.setString(1, eventId.trim());
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
return rs.next() ? mapRow(rs) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DmDeliveryStateEntry getByOutgoingMessageKey(String outgoingMessageKey) throws SQLException {
|
||||||
|
if (isBlank(outgoingMessageKey)) return null;
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(selectColumns() + " WHERE outgoing_message_key = ?")) {
|
||||||
|
ps.setString(1, outgoingMessageKey.trim());
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
return rs.next() ? mapRow(rs) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, DmDeliveryStateEntry> listByOutgoingMessageKeys(List<String> keys) throws SQLException {
|
||||||
|
Map<String, DmDeliveryStateEntry> out = new HashMap<>();
|
||||||
|
if (keys == null || keys.isEmpty()) return out;
|
||||||
|
String placeholders = String.join(",", java.util.Collections.nCopies(keys.size(), "?"));
|
||||||
|
String sql = selectColumns() + " WHERE outgoing_message_key IN (" + placeholders + ")";
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
for (int i = 0; i < keys.size(); i++) ps.setString(i + 1, keys.get(i));
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
DmDeliveryStateEntry row = mapRow(rs);
|
||||||
|
out.put(row.getOutgoingMessageKey(), row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DmDeliveryStateEntry> listDue(long nowMs, int limit) throws SQLException {
|
||||||
|
String sql = selectColumns() + """
|
||||||
|
WHERE delivery_state = 0
|
||||||
|
AND next_attempt_at_ms IS NOT NULL
|
||||||
|
AND next_attempt_at_ms <= ?
|
||||||
|
ORDER BY next_attempt_at_ms ASC, created_at_ms ASC
|
||||||
|
LIMIT ?
|
||||||
|
""";
|
||||||
|
List<DmDeliveryStateEntry> out = new ArrayList<>();
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setLong(1, nowMs);
|
||||||
|
ps.setInt(2, Math.max(1, limit));
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) out.add(mapRow(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean claimAttempt(DmDeliveryStateEntry row, long nowMs, Long nextAttemptAtMs) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
UPDATE dm_delivery_state
|
||||||
|
SET attempt_index = attempt_index + 1,
|
||||||
|
next_attempt_at_ms = ?,
|
||||||
|
last_attempt_at_ms = ?,
|
||||||
|
last_error = NULL,
|
||||||
|
updated_at_ms = ?
|
||||||
|
WHERE outgoing_message_key = ?
|
||||||
|
AND event_id = ?
|
||||||
|
AND attempt_index = ?
|
||||||
|
AND delivery_state = 0
|
||||||
|
AND next_attempt_at_ms IS NOT NULL
|
||||||
|
AND next_attempt_at_ms <= ?
|
||||||
|
""";
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
long networkLeaseUntilMs = nowMs + 60_000L;
|
||||||
|
Long claimedNextAttemptAtMs = nextAttemptAtMs == null
|
||||||
|
? networkLeaseUntilMs
|
||||||
|
: Math.max(nextAttemptAtMs, networkLeaseUntilMs);
|
||||||
|
setNullableLong(ps, 1, claimedNextAttemptAtMs);
|
||||||
|
ps.setLong(2, nowMs);
|
||||||
|
ps.setLong(3, nowMs);
|
||||||
|
ps.setString(4, row.getOutgoingMessageKey());
|
||||||
|
ps.setString(5, row.getEventId());
|
||||||
|
ps.setInt(6, row.getAttemptIndex());
|
||||||
|
ps.setLong(7, nowMs);
|
||||||
|
return ps.executeUpdate() == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DmDeliveryStateEntry mergeRemote(
|
||||||
|
String eventId,
|
||||||
|
int remoteState,
|
||||||
|
String remoteDeliveredLogin,
|
||||||
|
String remoteRoutesHash,
|
||||||
|
long nowMs
|
||||||
|
) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
boolean previousAutoCommit = c.getAutoCommit();
|
||||||
|
c.setAutoCommit(false);
|
||||||
|
try {
|
||||||
|
DmDeliveryStateEntry current = getByEventIdForUpdate(c, eventId);
|
||||||
|
if (current == null) {
|
||||||
|
c.rollback();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MergeResult merged = merge(
|
||||||
|
current.getDeliveryState(),
|
||||||
|
current.getDeliveredServerLogin(),
|
||||||
|
current.getRecipientRoutesHash(),
|
||||||
|
remoteState,
|
||||||
|
remoteDeliveredLogin,
|
||||||
|
remoteRoutesHash
|
||||||
|
);
|
||||||
|
updateMutableState(c, current, merged.state(), merged.deliveredLogin(),
|
||||||
|
merged.routesHash(), current.getNextAttemptAtMs(), current.getLastError(), nowMs);
|
||||||
|
c.commit();
|
||||||
|
return getByEventId(eventId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
try { c.rollback(); } catch (Exception ignored) {}
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
c.setAutoCommit(previousAutoCommit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DmDeliveryStateEntry updateAfterAttempt(
|
||||||
|
String eventId,
|
||||||
|
int attemptedState,
|
||||||
|
String deliveredServerLogin,
|
||||||
|
String routesHash,
|
||||||
|
Long nextAttemptAtMs,
|
||||||
|
String lastError,
|
||||||
|
long nowMs
|
||||||
|
) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
boolean previousAutoCommit = c.getAutoCommit();
|
||||||
|
c.setAutoCommit(false);
|
||||||
|
try {
|
||||||
|
DmDeliveryStateEntry current = getByEventIdForUpdate(c, eventId);
|
||||||
|
if (current == null) {
|
||||||
|
c.rollback();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MergeResult merged = merge(
|
||||||
|
current.getDeliveryState(), current.getDeliveredServerLogin(), current.getRecipientRoutesHash(),
|
||||||
|
attemptedState, deliveredServerLogin, routesHash
|
||||||
|
);
|
||||||
|
Long effectiveNext = merged.state() == DmDeliveryStateEntry.ACCEPTED
|
||||||
|
? nextAttemptAtMs
|
||||||
|
: null;
|
||||||
|
updateMutableState(c, current, merged.state(), merged.deliveredLogin(),
|
||||||
|
merged.routesHash(), effectiveNext, lastError, nowMs);
|
||||||
|
c.commit();
|
||||||
|
return getByEventId(eventId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
try { c.rollback(); } catch (Exception ignored) {}
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
c.setAutoCommit(previousAutoCommit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DmDeliveryStateEntry finishAtExpiry(String eventId, long nowMs) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
String sql = """
|
||||||
|
UPDATE dm_delivery_state
|
||||||
|
SET delivery_state = CASE WHEN delivery_state = 0 THEN 3 ELSE delivery_state END,
|
||||||
|
delivered_server_login = CASE WHEN delivery_state = 0 THEN NULL ELSE delivered_server_login END,
|
||||||
|
next_attempt_at_ms = NULL,
|
||||||
|
last_error = CASE WHEN delivery_state = 0 THEN 'DELIVERY_EXPIRED' ELSE last_error END,
|
||||||
|
updated_at_ms = ?
|
||||||
|
WHERE event_id = ? AND delivery_state = 0
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setLong(1, nowMs);
|
||||||
|
ps.setString(2, eventId);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getByEventId(eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read-only peer confirmed that at least one recipient server accepted the message. */
|
||||||
|
public DmDeliveryStateEntry markDeliveredFromPeer(String eventId, long nowMs) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement("""
|
||||||
|
UPDATE dm_delivery_state
|
||||||
|
SET delivery_state = 1,
|
||||||
|
delivered_server_login = NULL,
|
||||||
|
next_attempt_at_ms = NULL,
|
||||||
|
last_error = NULL,
|
||||||
|
updated_at_ms = ?
|
||||||
|
WHERE event_id = ? AND delivery_state = 0
|
||||||
|
""")) {
|
||||||
|
ps.setLong(1, nowMs);
|
||||||
|
ps.setString(2, eventId);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
return getByEventId(eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int removeByBaseKey(String baseKey) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement("DELETE FROM dm_delivery_state WHERE base_key = ?")) {
|
||||||
|
ps.setString(1, baseKey);
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int removeConversationBefore(String fromLogin, String toLogin, long boundaryTimeMs) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
DELETE FROM dm_delivery_state
|
||||||
|
WHERE created_at_ms < ?
|
||||||
|
AND ((LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
|
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?)))
|
||||||
|
""";
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setLong(1, boundaryTimeMs);
|
||||||
|
ps.setString(2, fromLogin);
|
||||||
|
ps.setString(3, toLogin);
|
||||||
|
ps.setString(4, toLogin);
|
||||||
|
ps.setString(5, fromLogin);
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int removeMissingMessages() throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
DELETE FROM dm_delivery_state d
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM signed_messages m WHERE m.message_key = d.outgoing_message_key
|
||||||
|
)
|
||||||
|
""";
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DmDeliveryStateEntry getByEventIdForUpdate(Connection c, String eventId) throws SQLException {
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(selectColumns() + " WHERE event_id = ? FOR UPDATE")) {
|
||||||
|
ps.setString(1, eventId);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
return rs.next() ? mapRow(rs) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMutableState(
|
||||||
|
Connection c,
|
||||||
|
DmDeliveryStateEntry current,
|
||||||
|
int state,
|
||||||
|
String deliveredLogin,
|
||||||
|
String routesHash,
|
||||||
|
Long nextAttemptAtMs,
|
||||||
|
String lastError,
|
||||||
|
long nowMs
|
||||||
|
) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
UPDATE dm_delivery_state
|
||||||
|
SET delivery_state = ?,
|
||||||
|
delivered_server_login = ?,
|
||||||
|
recipient_routes_hash = ?,
|
||||||
|
next_attempt_at_ms = ?,
|
||||||
|
last_error = ?,
|
||||||
|
updated_at_ms = ?
|
||||||
|
WHERE outgoing_message_key = ? AND event_id = ?
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setInt(1, state);
|
||||||
|
setNullableString(ps, 2, state == DmDeliveryStateEntry.DELIVERED_ONE ? normalize(deliveredLogin) : null);
|
||||||
|
setNullableString(ps, 3, normalizeBlank(routesHash));
|
||||||
|
setNullableLong(ps, 4, nextAttemptAtMs);
|
||||||
|
setNullableString(ps, 5, normalizeBlank(lastError));
|
||||||
|
ps.setLong(6, nowMs);
|
||||||
|
ps.setString(7, current.getOutgoingMessageKey());
|
||||||
|
ps.setString(8, current.getEventId());
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MergeResult merge(
|
||||||
|
int localState,
|
||||||
|
String localLogin,
|
||||||
|
String localHash,
|
||||||
|
int remoteStateRaw,
|
||||||
|
String remoteLoginRaw,
|
||||||
|
String remoteHashRaw
|
||||||
|
) {
|
||||||
|
int remoteState = normalizeState(remoteStateRaw, remoteLoginRaw);
|
||||||
|
String remoteLogin = normalize(remoteLoginRaw);
|
||||||
|
String localNormalizedLogin = normalize(localLogin);
|
||||||
|
String localRoutesHash = normalizeBlank(localHash);
|
||||||
|
String remoteRoutesHash = normalizeBlank(remoteHashRaw);
|
||||||
|
String resultHash = remoteRoutesHash != null ? remoteRoutesHash : localRoutesHash;
|
||||||
|
|
||||||
|
if (localState == DmDeliveryStateEntry.DELIVERED_ONE || localState == DmDeliveryStateEntry.DELIVERED_ALL) {
|
||||||
|
return new MergeResult(DmDeliveryStateEntry.DELIVERED, localNormalizedLogin,
|
||||||
|
localRoutesHash != null ? localRoutesHash : remoteRoutesHash);
|
||||||
|
}
|
||||||
|
if (remoteState == DmDeliveryStateEntry.DELIVERED_ONE || remoteState == DmDeliveryStateEntry.DELIVERED_ALL) {
|
||||||
|
return new MergeResult(DmDeliveryStateEntry.DELIVERED, remoteLogin, resultHash);
|
||||||
|
}
|
||||||
|
if (localState == DmDeliveryStateEntry.FAILED_FINAL || remoteState == DmDeliveryStateEntry.FAILED_FINAL) {
|
||||||
|
return new MergeResult(DmDeliveryStateEntry.FAILED_FINAL, null, resultHash);
|
||||||
|
}
|
||||||
|
return new MergeResult(DmDeliveryStateEntry.PENDING_NONE, null, resultHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int normalizeState(int state, String deliveredLogin) {
|
||||||
|
if (state < DmDeliveryStateEntry.PENDING_NONE || state > DmDeliveryStateEntry.FAILED_FINAL) {
|
||||||
|
return DmDeliveryStateEntry.PENDING_NONE;
|
||||||
|
}
|
||||||
|
if (state == DmDeliveryStateEntry.DELIVERED_ALL) return DmDeliveryStateEntry.DELIVERED;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String selectColumns() {
|
||||||
|
return """
|
||||||
|
SELECT outgoing_message_key, event_id, base_key, from_login, to_login,
|
||||||
|
incoming_message_key, created_at_ms, delivery_expires_at_ms,
|
||||||
|
delivery_state, delivered_server_login, recipient_routes_hash,
|
||||||
|
attempt_index, next_attempt_at_ms, last_attempt_at_ms, last_error, updated_at_ms
|
||||||
|
FROM dm_delivery_state
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
private DmDeliveryStateEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
|
DmDeliveryStateEntry e = new DmDeliveryStateEntry();
|
||||||
|
e.setOutgoingMessageKey(rs.getString("outgoing_message_key"));
|
||||||
|
e.setEventId(rs.getString("event_id"));
|
||||||
|
e.setBaseKey(rs.getString("base_key"));
|
||||||
|
e.setFromLogin(rs.getString("from_login"));
|
||||||
|
e.setToLogin(rs.getString("to_login"));
|
||||||
|
e.setIncomingMessageKey(rs.getString("incoming_message_key"));
|
||||||
|
e.setCreatedAtMs(rs.getLong("created_at_ms"));
|
||||||
|
e.setDeliveryExpiresAtMs(rs.getLong("delivery_expires_at_ms"));
|
||||||
|
e.setDeliveryState(rs.getInt("delivery_state"));
|
||||||
|
e.setDeliveredServerLogin(rs.getString("delivered_server_login"));
|
||||||
|
e.setRecipientRoutesHash(rs.getString("recipient_routes_hash"));
|
||||||
|
e.setAttemptIndex(rs.getInt("attempt_index"));
|
||||||
|
long next = rs.getLong("next_attempt_at_ms");
|
||||||
|
e.setNextAttemptAtMs(rs.wasNull() ? null : next);
|
||||||
|
long last = rs.getLong("last_attempt_at_ms");
|
||||||
|
e.setLastAttemptAtMs(rs.wasNull() ? null : last);
|
||||||
|
e.setLastError(rs.getString("last_error"));
|
||||||
|
e.setUpdatedAtMs(rs.getLong("updated_at_ms"));
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setNullableString(PreparedStatement ps, int index, String value) throws SQLException {
|
||||||
|
if (isBlank(value)) ps.setNull(index, Types.VARCHAR);
|
||||||
|
else ps.setString(index, value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setNullableLong(PreparedStatement ps, int index, Long value) throws SQLException {
|
||||||
|
if (value == null) ps.setNull(index, Types.BIGINT);
|
||||||
|
else ps.setLong(index, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalize(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
String normalized = value.trim().toLowerCase();
|
||||||
|
return normalized.isEmpty() ? null : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeBlank(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
String normalized = value.trim();
|
||||||
|
return normalized.isEmpty() ? null : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isBlank(String value) {
|
||||||
|
return value == null || value.isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record MergeResult(int state, String deliveredLogin, String routesHash) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,453 @@
|
|||||||
|
package shine.db.dao;
|
||||||
|
|
||||||
|
import shine.db.MsgSubType;
|
||||||
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class DmDialogStateDAO {
|
||||||
|
private static volatile DmDialogStateDAO instance;
|
||||||
|
|
||||||
|
private DmDialogStateDAO() {}
|
||||||
|
|
||||||
|
public static DmDialogStateDAO getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (DmDialogStateDAO.class) {
|
||||||
|
if (instance == null) instance = new DmDialogStateDAO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void bootstrapIfEmpty(Connection c) throws SQLException {
|
||||||
|
if (c == null) return;
|
||||||
|
if (hasAnyRow(c)) return;
|
||||||
|
|
||||||
|
boolean prevAutoCommit = c.getAutoCommit();
|
||||||
|
c.setAutoCommit(false);
|
||||||
|
try {
|
||||||
|
for (PairItem pair : listConversationPairs(c)) {
|
||||||
|
refreshConversationPair(c, pair.ownerLogin(), pair.peerLogin());
|
||||||
|
}
|
||||||
|
c.commit();
|
||||||
|
} catch (Exception e) {
|
||||||
|
try { c.rollback(); } catch (Exception ignored) {}
|
||||||
|
if (e instanceof SQLException sqlEx) throw sqlEx;
|
||||||
|
throw new SQLException("Failed to bootstrap dm_dialog_state", e);
|
||||||
|
} finally {
|
||||||
|
c.setAutoCommit(prevAutoCommit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshConversationPair(Connection c, String ownerLogin, String peerLogin) throws SQLException {
|
||||||
|
String cleanOwner = normalize(ownerLogin);
|
||||||
|
String cleanPeer = normalize(peerLogin);
|
||||||
|
if (cleanOwner.isEmpty() || cleanPeer.isEmpty()) return;
|
||||||
|
if (cleanOwner.equalsIgnoreCase(cleanPeer)) return;
|
||||||
|
refreshConversation(c, cleanOwner, cleanPeer);
|
||||||
|
refreshConversation(c, cleanPeer, cleanOwner);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DialogSummary> listInboxDialogs(Connection c, String ownerLogin) throws SQLException {
|
||||||
|
String cleanOwner = normalize(ownerLogin);
|
||||||
|
if (cleanOwner.isEmpty()) return List.of();
|
||||||
|
|
||||||
|
Map<String, DialogSummary> byPeer = new LinkedHashMap<>();
|
||||||
|
List<String> contacts = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, cleanOwner, MsgSubType.CONNECTION_CONTACT);
|
||||||
|
List<String> closeFriends = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, cleanOwner, MsgSubType.CONNECTION_CLOSE_FRIEND);
|
||||||
|
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
|
SELECT owner_login, peer_login, relation_flag, last_message_blob_b64,
|
||||||
|
last_message_time_ms, unread_count, last_read_receipt_time_ms,
|
||||||
|
updated_at_ms
|
||||||
|
FROM dm_dialog_state
|
||||||
|
WHERE LOWER(owner_login) = LOWER(?)
|
||||||
|
""")) {
|
||||||
|
ps.setString(1, cleanOwner);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
DialogSummary row = new DialogSummary(
|
||||||
|
rs.getString("owner_login"),
|
||||||
|
rs.getString("peer_login"),
|
||||||
|
normalizeRelationFlag(rs.getString("relation_flag")),
|
||||||
|
rs.getString("last_message_blob_b64"),
|
||||||
|
rs.getLong("last_message_time_ms"),
|
||||||
|
rs.getInt("unread_count"),
|
||||||
|
rs.getLong("last_read_receipt_time_ms"),
|
||||||
|
rs.getLong("updated_at_ms"),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
byPeer.put(normKey(row.peerLogin()), row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String peer : contacts) {
|
||||||
|
addOrUpdateSummary(byPeer, cleanOwner, peer, "contact", false);
|
||||||
|
}
|
||||||
|
for (String peer : closeFriends) {
|
||||||
|
addOrUpdateSummary(byPeer, cleanOwner, peer, "close_friend", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DialogSummary row : new ArrayList<>(byPeer.values())) {
|
||||||
|
String relationFlag = resolveRelationFlag(c, cleanOwner, row.peerLogin());
|
||||||
|
byPeer.put(normKey(row.peerLogin()), new DialogSummary(
|
||||||
|
row.ownerLogin(),
|
||||||
|
row.peerLogin(),
|
||||||
|
relationFlag,
|
||||||
|
row.lastMessageBlobB64(),
|
||||||
|
row.lastMessageTimeMs(),
|
||||||
|
row.unreadCount(),
|
||||||
|
row.lastReadReceiptTimeMs(),
|
||||||
|
row.updatedAtMs(),
|
||||||
|
row.hasDialog()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DialogSummary> out = new ArrayList<>(byPeer.values());
|
||||||
|
out.sort((a, b) -> {
|
||||||
|
int cmp = Long.compare(b.lastMessageTimeMs(), a.lastMessageTimeMs());
|
||||||
|
if (cmp != 0) return cmp;
|
||||||
|
return normalize(a.peerLogin()).compareToIgnoreCase(normalize(b.peerLogin()));
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshFromEntry(Connection c, SignedMessageEntry entry) throws SQLException {
|
||||||
|
if (entry == null) return;
|
||||||
|
refreshConversationPair(c, entry.getFromLogin(), entry.getToLogin());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshConversation(Connection c, String ownerLogin, String peerLogin) throws SQLException {
|
||||||
|
DialogSummary summary = loadConversationSummary(c, ownerLogin, peerLogin);
|
||||||
|
upsert(c, summary);
|
||||||
|
if (summary.lastReadReceiptTimeMs() > 0) {
|
||||||
|
syncMessagesToWatermark(c, summary.ownerLogin(), summary.peerLogin(), summary.lastReadReceiptTimeMs());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DialogSummary loadConversationSummary(Connection c, String ownerLogin, String peerLogin) throws SQLException {
|
||||||
|
String cleanOwner = normalize(ownerLogin);
|
||||||
|
String cleanPeer = normalize(peerLogin);
|
||||||
|
if (cleanOwner.isEmpty() || cleanPeer.isEmpty() || cleanOwner.equalsIgnoreCase(cleanPeer)) {
|
||||||
|
return new DialogSummary(cleanOwner, cleanPeer, "none", "", 0L, 0, 0L, System.currentTimeMillis(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
String relationFlag = resolveRelationFlag(c, cleanOwner, cleanPeer);
|
||||||
|
long existingWatermark = loadExistingWatermark(c, cleanOwner, cleanPeer);
|
||||||
|
|
||||||
|
String latestSql = """
|
||||||
|
SELECT raw_block, time_ms
|
||||||
|
FROM signed_messages
|
||||||
|
WHERE (
|
||||||
|
(LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
|
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
|
)
|
||||||
|
AND message_type IN (1, 2)
|
||||||
|
ORDER BY time_ms DESC, revision_time_ms DESC, reencrypted_at_ms DESC, created_at_ms DESC, message_key DESC
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
String unreadSql = """
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM signed_messages
|
||||||
|
WHERE LOWER(from_login) = LOWER(?)
|
||||||
|
AND LOWER(to_login) = LOWER(?)
|
||||||
|
AND message_type = 1
|
||||||
|
AND time_ms > ?
|
||||||
|
AND (read_at_ms IS NULL OR read_at_ms <= 0)
|
||||||
|
""";
|
||||||
|
String contentReadSql = """
|
||||||
|
SELECT MAX(read_at_ms)
|
||||||
|
FROM signed_messages
|
||||||
|
WHERE (
|
||||||
|
(LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
|
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
|
)
|
||||||
|
AND message_type IN (1, 2)
|
||||||
|
AND read_at_ms IS NOT NULL
|
||||||
|
AND read_at_ms > 0
|
||||||
|
""";
|
||||||
|
String receiptWatermarkSql = """
|
||||||
|
SELECT MAX(time_ms)
|
||||||
|
FROM signed_messages
|
||||||
|
WHERE (
|
||||||
|
(LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
|
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
|
)
|
||||||
|
AND message_type IN (3, 4)
|
||||||
|
""";
|
||||||
|
|
||||||
|
String lastMessageBlobB64 = "";
|
||||||
|
long lastMessageTimeMs = 0L;
|
||||||
|
int unreadCount = 0;
|
||||||
|
long lastReadReceiptTimeMs = existingWatermark;
|
||||||
|
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(latestSql)) {
|
||||||
|
ps.setString(1, cleanOwner);
|
||||||
|
ps.setString(2, cleanPeer);
|
||||||
|
ps.setString(3, cleanPeer);
|
||||||
|
ps.setString(4, cleanOwner);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
lastMessageTimeMs = rs.getLong("time_ms");
|
||||||
|
byte[] rawBlock = rs.getBytes("raw_block");
|
||||||
|
if (rawBlock != null && rawBlock.length > 0) {
|
||||||
|
lastMessageBlobB64 = Base64.getEncoder().encodeToString(rawBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(contentReadSql)) {
|
||||||
|
ps.setString(1, cleanOwner);
|
||||||
|
ps.setString(2, cleanPeer);
|
||||||
|
ps.setString(3, cleanPeer);
|
||||||
|
ps.setString(4, cleanOwner);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
long value = rs.getLong(1);
|
||||||
|
if (!rs.wasNull()) lastReadReceiptTimeMs = Math.max(lastReadReceiptTimeMs, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(receiptWatermarkSql)) {
|
||||||
|
ps.setString(1, cleanOwner);
|
||||||
|
ps.setString(2, cleanPeer);
|
||||||
|
ps.setString(3, cleanPeer);
|
||||||
|
ps.setString(4, cleanOwner);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
long value = rs.getLong(1);
|
||||||
|
if (!rs.wasNull()) lastReadReceiptTimeMs = Math.max(lastReadReceiptTimeMs, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(unreadSql)) {
|
||||||
|
ps.setString(1, cleanPeer);
|
||||||
|
ps.setString(2, cleanOwner);
|
||||||
|
ps.setLong(3, lastReadReceiptTimeMs);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) unreadCount = rs.getInt(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DialogSummary(
|
||||||
|
cleanOwner,
|
||||||
|
cleanPeer,
|
||||||
|
relationFlag,
|
||||||
|
lastMessageBlobB64,
|
||||||
|
lastMessageTimeMs,
|
||||||
|
unreadCount,
|
||||||
|
lastReadReceiptTimeMs,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
lastMessageTimeMs > 0 || unreadCount > 0 || lastReadReceiptTimeMs > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void upsert(Connection c, DialogSummary summary) throws SQLException {
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
|
INSERT INTO dm_dialog_state (
|
||||||
|
owner_login, peer_login, relation_flag, last_message_blob_b64,
|
||||||
|
last_message_time_ms, unread_count, last_read_receipt_time_ms, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT (owner_login, peer_login) DO UPDATE SET
|
||||||
|
relation_flag = EXCLUDED.relation_flag,
|
||||||
|
last_message_blob_b64 = EXCLUDED.last_message_blob_b64,
|
||||||
|
last_message_time_ms = EXCLUDED.last_message_time_ms,
|
||||||
|
unread_count = EXCLUDED.unread_count,
|
||||||
|
last_read_receipt_time_ms = EXCLUDED.last_read_receipt_time_ms,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms
|
||||||
|
""")) {
|
||||||
|
ps.setString(1, summary.ownerLogin());
|
||||||
|
ps.setString(2, summary.peerLogin());
|
||||||
|
ps.setString(3, normalizeRelationFlag(summary.relationFlag()));
|
||||||
|
ps.setString(4, summary.lastMessageBlobB64());
|
||||||
|
ps.setLong(5, summary.lastMessageTimeMs());
|
||||||
|
ps.setInt(6, summary.unreadCount());
|
||||||
|
ps.setLong(7, summary.lastReadReceiptTimeMs());
|
||||||
|
ps.setLong(8, summary.updatedAtMs());
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveRelationFlag(Connection c, String ownerLogin, String peerLogin) throws SQLException {
|
||||||
|
if (ConnectionsStateDAO.getInstance().hasOutgoingByRelTypeCanonical(c, ownerLogin, peerLogin, MsgSubType.CONNECTION_CLOSE_FRIEND)) {
|
||||||
|
return "close_friend";
|
||||||
|
}
|
||||||
|
if (ConnectionsStateDAO.getInstance().hasOutgoingByRelTypeCanonical(c, ownerLogin, peerLogin, MsgSubType.CONNECTION_CONTACT)) {
|
||||||
|
return "contact";
|
||||||
|
}
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasAnyRow(Connection c) throws SQLException {
|
||||||
|
try (Statement st = c.createStatement();
|
||||||
|
ResultSet rs = st.executeQuery("SELECT 1 FROM dm_dialog_state LIMIT 1")) {
|
||||||
|
return rs.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PairItem> listConversationPairs(Connection c) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT DISTINCT owner_login, peer_login
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
m.target_login AS owner_login,
|
||||||
|
CASE
|
||||||
|
WHEN LOWER(m.target_login) = LOWER(m.to_login) THEN m.from_login
|
||||||
|
ELSE m.to_login
|
||||||
|
END AS peer_login
|
||||||
|
FROM signed_messages m
|
||||||
|
WHERE m.message_type IN (1, 2, 3, 4, 5, 6, 7, 8)
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT m.from_login AS owner_login, m.to_login AS peer_login
|
||||||
|
FROM signed_messages m
|
||||||
|
WHERE m.message_type IN (5, 6, 7, 8)
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT m.to_login AS owner_login, m.from_login AS peer_login
|
||||||
|
FROM signed_messages m
|
||||||
|
WHERE m.message_type IN (5, 6, 7, 8)
|
||||||
|
) pairs
|
||||||
|
WHERE owner_login IS NOT NULL
|
||||||
|
AND peer_login IS NOT NULL
|
||||||
|
AND BTRIM(owner_login) <> ''
|
||||||
|
AND BTRIM(peer_login) <> ''
|
||||||
|
AND LOWER(owner_login) <> LOWER(peer_login)
|
||||||
|
ORDER BY owner_login, peer_login
|
||||||
|
""";
|
||||||
|
List<PairItem> out = new ArrayList<>();
|
||||||
|
try (Statement st = c.createStatement();
|
||||||
|
ResultSet rs = st.executeQuery(sql)) {
|
||||||
|
while (rs.next()) {
|
||||||
|
String ownerLogin = rs.getString("owner_login");
|
||||||
|
String peerLogin = rs.getString("peer_login");
|
||||||
|
if (normalize(ownerLogin).isEmpty() || normalize(peerLogin).isEmpty()) continue;
|
||||||
|
out.add(new PairItem(ownerLogin, peerLogin));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addOrUpdateSummary(Map<String, DialogSummary> map, String ownerLogin, String peerLogin, String relationFlag, boolean hasDialog) {
|
||||||
|
String owner = normalize(ownerLogin);
|
||||||
|
String peer = normalize(peerLogin);
|
||||||
|
if (owner.isEmpty() || peer.isEmpty() || owner.equalsIgnoreCase(peer)) return;
|
||||||
|
String key = normKey(peer);
|
||||||
|
DialogSummary current = map.get(key);
|
||||||
|
if (current == null) {
|
||||||
|
map.put(key, new DialogSummary(owner, peer, relationFlag, "", 0L, 0, 0L, System.currentTimeMillis(), hasDialog));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String nextRelation = current.relationFlag();
|
||||||
|
if ("close_friend".equalsIgnoreCase(relationFlag) || "close_friend".equalsIgnoreCase(nextRelation)) {
|
||||||
|
nextRelation = "close_friend";
|
||||||
|
} else if ("contact".equalsIgnoreCase(relationFlag) || "contact".equalsIgnoreCase(nextRelation)) {
|
||||||
|
nextRelation = "contact";
|
||||||
|
} else {
|
||||||
|
nextRelation = normalizeRelationFlag(nextRelation);
|
||||||
|
}
|
||||||
|
map.put(key, new DialogSummary(
|
||||||
|
current.ownerLogin().isEmpty() ? owner : current.ownerLogin(),
|
||||||
|
peer,
|
||||||
|
nextRelation,
|
||||||
|
current.lastMessageBlobB64(),
|
||||||
|
current.lastMessageTimeMs(),
|
||||||
|
current.unreadCount(),
|
||||||
|
current.lastReadReceiptTimeMs(),
|
||||||
|
current.updatedAtMs(),
|
||||||
|
current.hasDialog() || hasDialog
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncMessagesToWatermark(Connection c, String ownerLogin, String peerLogin, long watermark) throws SQLException {
|
||||||
|
if (watermark <= 0) return;
|
||||||
|
String cleanOwner = normalize(ownerLogin);
|
||||||
|
String cleanPeer = normalize(peerLogin);
|
||||||
|
if (cleanOwner.isEmpty() || cleanPeer.isEmpty() || cleanOwner.equalsIgnoreCase(cleanPeer)) return;
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
|
UPDATE signed_messages
|
||||||
|
SET read_at_ms = CASE
|
||||||
|
WHEN read_at_ms IS NULL OR read_at_ms <= 0 THEN ?
|
||||||
|
WHEN read_at_ms > ? THEN read_at_ms
|
||||||
|
ELSE read_at_ms
|
||||||
|
END
|
||||||
|
WHERE (
|
||||||
|
(LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
|
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
|
)
|
||||||
|
AND message_type IN (1, 2)
|
||||||
|
AND time_ms <= ?
|
||||||
|
""")) {
|
||||||
|
ps.setLong(1, watermark);
|
||||||
|
ps.setLong(2, watermark);
|
||||||
|
ps.setString(3, cleanOwner);
|
||||||
|
ps.setString(4, cleanPeer);
|
||||||
|
ps.setString(5, cleanPeer);
|
||||||
|
ps.setString(6, cleanOwner);
|
||||||
|
ps.setLong(7, watermark);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long loadExistingWatermark(Connection c, String ownerLogin, String peerLogin) throws SQLException {
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
|
SELECT last_read_receipt_time_ms
|
||||||
|
FROM dm_dialog_state
|
||||||
|
WHERE LOWER(owner_login) = LOWER(?)
|
||||||
|
AND LOWER(peer_login) = LOWER(?)
|
||||||
|
LIMIT 1
|
||||||
|
""")) {
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setString(2, peerLogin);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (!rs.next()) return 0L;
|
||||||
|
long value = rs.getLong(1);
|
||||||
|
return rs.wasNull() ? 0L : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalize(String value) {
|
||||||
|
return value == null ? "" : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normKey(String value) {
|
||||||
|
return normalize(value).toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeRelationFlag(String value) {
|
||||||
|
String clean = normalize(value).toLowerCase(Locale.ROOT);
|
||||||
|
if ("close_friend".equals(clean) || "contact".equals(clean)) return clean;
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DialogSummary(
|
||||||
|
String ownerLogin,
|
||||||
|
String peerLogin,
|
||||||
|
String relationFlag,
|
||||||
|
String lastMessageBlobB64,
|
||||||
|
long lastMessageTimeMs,
|
||||||
|
int unreadCount,
|
||||||
|
long lastReadReceiptTimeMs,
|
||||||
|
long updatedAtMs,
|
||||||
|
boolean hasDialog
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private record PairItem(String ownerLogin, String peerLogin) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
package shine.db.dao;
|
||||||
|
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.entities.DmSyncOutboxEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Types;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Outbox событий DM для единственного второго access-сервера пользователя. */
|
||||||
|
public final class DmSyncOutboxDAO {
|
||||||
|
private static volatile DmSyncOutboxDAO instance;
|
||||||
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
|
private DmSyncOutboxDAO() {}
|
||||||
|
|
||||||
|
public static DmSyncOutboxDAO getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (DmSyncOutboxDAO.class) {
|
||||||
|
if (instance == null) instance = new DmSyncOutboxDAO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void upsert(
|
||||||
|
String ownerLogin,
|
||||||
|
String primaryMessageKey,
|
||||||
|
String eventId,
|
||||||
|
String secondaryMessageKey,
|
||||||
|
boolean synced,
|
||||||
|
long createdAtMs
|
||||||
|
) throws SQLException {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO dm_sync_outbox (
|
||||||
|
owner_login, primary_message_key, event_id, secondary_message_key,
|
||||||
|
synced, created_at_ms, updated_at_ms
|
||||||
|
) VALUES (LOWER(?), ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT (owner_login, primary_message_key) DO UPDATE SET
|
||||||
|
event_id = EXCLUDED.event_id,
|
||||||
|
secondary_message_key = EXCLUDED.secondary_message_key,
|
||||||
|
synced = CASE
|
||||||
|
WHEN dm_sync_outbox.event_id = EXCLUDED.event_id
|
||||||
|
THEN dm_sync_outbox.synced OR EXCLUDED.synced
|
||||||
|
ELSE EXCLUDED.synced
|
||||||
|
END,
|
||||||
|
created_at_ms = CASE
|
||||||
|
WHEN dm_sync_outbox.event_id = EXCLUDED.event_id
|
||||||
|
THEN dm_sync_outbox.created_at_ms
|
||||||
|
ELSE EXCLUDED.created_at_ms
|
||||||
|
END,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms
|
||||||
|
""";
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setString(2, primaryMessageKey);
|
||||||
|
ps.setString(3, eventId);
|
||||||
|
if (secondaryMessageKey == null || secondaryMessageKey.isBlank()) ps.setNull(4, Types.VARCHAR);
|
||||||
|
else ps.setString(4, secondaryMessageKey.trim());
|
||||||
|
ps.setBoolean(5, synced);
|
||||||
|
ps.setLong(6, createdAtMs);
|
||||||
|
ps.setLong(7, now);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DmSyncOutboxEntry> listUnsynced(String ownerLogin, int limit) throws SQLException {
|
||||||
|
return listUnsynced(ownerLogin, 0L, "", limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DmSyncOutboxEntry> listUnsynced(
|
||||||
|
String ownerLogin, long afterCreatedAtMs, String afterPrimaryMessageKey, int limit
|
||||||
|
) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT owner_login, primary_message_key, event_id, secondary_message_key,
|
||||||
|
synced, created_at_ms, updated_at_ms
|
||||||
|
FROM dm_sync_outbox
|
||||||
|
WHERE LOWER(owner_login) = LOWER(?) AND synced = FALSE
|
||||||
|
AND (created_at_ms > ? OR (created_at_ms = ? AND primary_message_key > ?))
|
||||||
|
ORDER BY created_at_ms ASC, primary_message_key ASC
|
||||||
|
LIMIT ?
|
||||||
|
""";
|
||||||
|
List<DmSyncOutboxEntry> out = new ArrayList<>();
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setLong(2, Math.max(0L, afterCreatedAtMs));
|
||||||
|
ps.setLong(3, Math.max(0L, afterCreatedAtMs));
|
||||||
|
ps.setString(4, afterPrimaryMessageKey == null ? "" : afterPrimaryMessageKey);
|
||||||
|
ps.setInt(5, Math.max(1, limit));
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) out.add(mapRow(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasUnsyncedForServer(String serverLogin) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT 1
|
||||||
|
FROM dm_sync_outbox o
|
||||||
|
JOIN user_access_servers_current r
|
||||||
|
ON LOWER(r.user_login) = LOWER(o.owner_login)
|
||||||
|
WHERE o.synced = FALSE AND LOWER(r.server_login) = LOWER(?)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM user_access_servers_current peer
|
||||||
|
WHERE LOWER(peer.user_login) = LOWER(o.owner_login)
|
||||||
|
AND LOWER(peer.server_login) <> LOWER(?)
|
||||||
|
)
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, serverLogin);
|
||||||
|
ps.setString(2, serverLogin);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
return rs.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> listUnsyncedOwnersForServer(String serverLogin) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT DISTINCT o.owner_login
|
||||||
|
FROM dm_sync_outbox o
|
||||||
|
JOIN user_access_servers_current r
|
||||||
|
ON LOWER(r.user_login) = LOWER(o.owner_login)
|
||||||
|
WHERE o.synced = FALSE AND LOWER(r.server_login) = LOWER(?)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM user_access_servers_current peer
|
||||||
|
WHERE LOWER(peer.user_login) = LOWER(o.owner_login)
|
||||||
|
AND LOWER(peer.server_login) <> LOWER(?)
|
||||||
|
)
|
||||||
|
ORDER BY o.owner_login
|
||||||
|
""";
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, serverLogin);
|
||||||
|
ps.setString(2, serverLogin);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) result.add(rs.getString("owner_login"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int markSynced(String ownerLogin, String eventId) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
UPDATE dm_sync_outbox
|
||||||
|
SET synced = TRUE, updated_at_ms = ?
|
||||||
|
WHERE LOWER(owner_login) = LOWER(?) AND event_id = ?
|
||||||
|
""";
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setLong(1, System.currentTimeMillis());
|
||||||
|
ps.setString(2, ownerLogin);
|
||||||
|
ps.setString(3, eventId);
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int markSynced(String ownerLogin, List<String> eventIds) throws SQLException {
|
||||||
|
if (eventIds == null || eventIds.isEmpty()) return 0;
|
||||||
|
int total = 0;
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement("""
|
||||||
|
UPDATE dm_sync_outbox
|
||||||
|
SET synced = TRUE, updated_at_ms = ?
|
||||||
|
WHERE LOWER(owner_login) = LOWER(?) AND event_id = ?
|
||||||
|
""")) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
for (String eventId : eventIds) {
|
||||||
|
if (eventId == null || eventId.isBlank()) continue;
|
||||||
|
ps.setLong(1, now);
|
||||||
|
ps.setString(2, ownerLogin);
|
||||||
|
ps.setString(3, eventId.trim());
|
||||||
|
ps.addBatch();
|
||||||
|
}
|
||||||
|
for (int changed : ps.executeBatch()) if (changed > 0) total += changed;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int markAllUnsynced(String ownerLogin) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
UPDATE dm_sync_outbox
|
||||||
|
SET synced = FALSE, updated_at_ms = ?
|
||||||
|
WHERE LOWER(owner_login) = LOWER(?)
|
||||||
|
""";
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setLong(1, System.currentTimeMillis());
|
||||||
|
ps.setString(2, ownerLogin);
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int markAllUnsynced() throws SQLException {
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement("UPDATE dm_sync_outbox SET synced = FALSE, updated_at_ms = ?")) {
|
||||||
|
ps.setLong(1, System.currentTimeMillis());
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int removeMissingMessages() throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
DELETE FROM dm_sync_outbox o
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM signed_messages m WHERE m.message_key = o.primary_message_key
|
||||||
|
)
|
||||||
|
""";
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DmSyncOutboxEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
|
DmSyncOutboxEntry e = new DmSyncOutboxEntry();
|
||||||
|
e.setOwnerLogin(rs.getString("owner_login"));
|
||||||
|
e.setPrimaryMessageKey(rs.getString("primary_message_key"));
|
||||||
|
e.setEventId(rs.getString("event_id"));
|
||||||
|
e.setSecondaryMessageKey(rs.getString("secondary_message_key"));
|
||||||
|
e.setSynced(rs.getBoolean("synced"));
|
||||||
|
e.setCreatedAtMs(rs.getLong("created_at_ms"));
|
||||||
|
e.setUpdatedAtMs(rs.getLong("updated_at_ms"));
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
-47
@@ -1,47 +0,0 @@
|
|||||||
package shine.db.dao;
|
|
||||||
|
|
||||||
import shine.db.DbController;
|
|
||||||
import shine.db.entities.SignedDirectMessageHistoryEntry;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.sql.PreparedStatement;
|
|
||||||
|
|
||||||
public final class SignedDirectMessagesHistoryDAO {
|
|
||||||
private static volatile SignedDirectMessagesHistoryDAO instance;
|
|
||||||
private final DbController db = DbController.getInstance();
|
|
||||||
|
|
||||||
private SignedDirectMessagesHistoryDAO() {}
|
|
||||||
|
|
||||||
public static SignedDirectMessagesHistoryDAO getInstance() {
|
|
||||||
if (instance == null) {
|
|
||||||
synchronized (SignedDirectMessagesHistoryDAO.class) {
|
|
||||||
if (instance == null) instance = new SignedDirectMessagesHistoryDAO();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void insert(SignedDirectMessageHistoryEntry e) throws Exception {
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
String sql = """
|
|
||||||
INSERT INTO signed_direct_messages_history (
|
|
||||||
message_id, from_login, to_login, target_mode, target_session_id,
|
|
||||||
message_type, time_ms, nonce, raw_packet, created_at_ms
|
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
""";
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
|
||||||
ps.setString(1, e.getMessageId());
|
|
||||||
ps.setString(2, e.getFromLogin());
|
|
||||||
ps.setString(3, e.getToLogin());
|
|
||||||
ps.setInt(4, e.getTargetMode());
|
|
||||||
ps.setString(5, e.getTargetSessionId());
|
|
||||||
ps.setInt(6, e.getMessageType());
|
|
||||||
ps.setLong(7, e.getTimeMs());
|
|
||||||
ps.setLong(8, e.getNonce());
|
|
||||||
ps.setBytes(9, e.getRawPacket());
|
|
||||||
ps.setLong(10, e.getCreatedAtMs());
|
|
||||||
ps.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package shine.db.dao;
|
|
||||||
|
|
||||||
import shine.db.DbController;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.sql.PreparedStatement;
|
|
||||||
|
|
||||||
public final class SignedDmReplayDAO {
|
|
||||||
private static volatile SignedDmReplayDAO instance;
|
|
||||||
private final DbController db = DbController.getInstance();
|
|
||||||
|
|
||||||
private SignedDmReplayDAO() {}
|
|
||||||
|
|
||||||
public static SignedDmReplayDAO getInstance() {
|
|
||||||
if (instance == null) {
|
|
||||||
synchronized (SignedDmReplayDAO.class) {
|
|
||||||
if (instance == null) instance = new SignedDmReplayDAO();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean registerUnique(String fromLogin, long timeMs, long nonce, long nowMs) throws Exception {
|
|
||||||
cleanupExpired(nowMs - 15L * 60L * 1000L);
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
String sql = """
|
|
||||||
INSERT INTO signed_direct_message_replay (
|
|
||||||
from_login, time_ms, nonce, created_at_ms
|
|
||||||
) VALUES (?, ?, ?, ?)
|
|
||||||
ON CONFLICT DO NOTHING
|
|
||||||
""";
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
|
||||||
ps.setString(1, fromLogin);
|
|
||||||
ps.setLong(2, timeMs);
|
|
||||||
ps.setLong(3, nonce);
|
|
||||||
ps.setLong(4, nowMs);
|
|
||||||
return ps.executeUpdate() > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cleanupExpired(long minCreatedAtMs) throws Exception {
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
String sql = "DELETE FROM signed_direct_message_replay WHERE created_at_ms < ?";
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
|
||||||
ps.setLong(1, minCreatedAtMs);
|
|
||||||
ps.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -58,6 +58,7 @@ public final class SignedMessagesDAO {
|
|||||||
ApplyStatus status = ps.executeUpdate() > 0 ? ApplyStatus.APPLIED : ApplyStatus.DUPLICATE_OR_OLDER;
|
ApplyStatus status = ps.executeUpdate() > 0 ? ApplyStatus.APPLIED : ApplyStatus.DUPLICATE_OR_OLDER;
|
||||||
if (status.applied()) {
|
if (status.applied()) {
|
||||||
markMessageReadByReceipt(c, e);
|
markMessageReadByReceipt(c, e);
|
||||||
|
DmDialogStateDAO.getInstance().refreshFromEntry(c, e);
|
||||||
}
|
}
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
@@ -76,6 +77,7 @@ public final class SignedMessagesDAO {
|
|||||||
if (insertedFirst == 1 && insertedSecond == 1) {
|
if (insertedFirst == 1 && insertedSecond == 1) {
|
||||||
markMessageReadByReceipt(c, first);
|
markMessageReadByReceipt(c, first);
|
||||||
markMessageReadByReceipt(c, second);
|
markMessageReadByReceipt(c, second);
|
||||||
|
DmDialogStateDAO.getInstance().refreshConversationPair(c, first.getFromLogin(), first.getToLogin());
|
||||||
c.commit();
|
c.commit();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -122,6 +124,7 @@ public final class SignedMessagesDAO {
|
|||||||
markMessageReadByReceipt(c, outgoing);
|
markMessageReadByReceipt(c, outgoing);
|
||||||
resetDeliveryRows(c, incoming.getMessageKey());
|
resetDeliveryRows(c, incoming.getMessageKey());
|
||||||
resetDeliveryRows(c, outgoing.getMessageKey());
|
resetDeliveryRows(c, outgoing.getMessageKey());
|
||||||
|
DmDialogStateDAO.getInstance().refreshConversationPair(c, incoming.getFromLogin(), incoming.getToLogin());
|
||||||
|
|
||||||
c.commit();
|
c.commit();
|
||||||
return ApplyStatus.APPLIED;
|
return ApplyStatus.APPLIED;
|
||||||
@@ -160,6 +163,7 @@ public final class SignedMessagesDAO {
|
|||||||
upsertMessage(c, incoming);
|
upsertMessage(c, incoming);
|
||||||
markMessageReadByReceipt(c, incoming);
|
markMessageReadByReceipt(c, incoming);
|
||||||
resetDeliveryRows(c, incoming.getMessageKey());
|
resetDeliveryRows(c, incoming.getMessageKey());
|
||||||
|
DmDialogStateDAO.getInstance().refreshConversationPair(c, incoming.getFromLogin(), incoming.getToLogin());
|
||||||
c.commit();
|
c.commit();
|
||||||
return ApplyStatus.APPLIED;
|
return ApplyStatus.APPLIED;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -190,6 +194,7 @@ public final class SignedMessagesDAO {
|
|||||||
deleteMessageContentAndReceipts(c, tombstone.getBaseKey());
|
deleteMessageContentAndReceipts(c, tombstone.getBaseKey());
|
||||||
upsertMessage(c, tombstone);
|
upsertMessage(c, tombstone);
|
||||||
resetDeliveryRows(c, tombstone.getMessageKey());
|
resetDeliveryRows(c, tombstone.getMessageKey());
|
||||||
|
DmDialogStateDAO.getInstance().refreshConversationPair(c, tombstone.getFromLogin(), tombstone.getToLogin());
|
||||||
|
|
||||||
c.commit();
|
c.commit();
|
||||||
return ApplyStatus.APPLIED;
|
return ApplyStatus.APPLIED;
|
||||||
@@ -218,6 +223,7 @@ public final class SignedMessagesDAO {
|
|||||||
deleteConversationHistoryBefore(c, tombstone.getFromLogin(), tombstone.getToLogin(), tombstone.getTimeMs());
|
deleteConversationHistoryBefore(c, tombstone.getFromLogin(), tombstone.getToLogin(), tombstone.getTimeMs());
|
||||||
upsertMessage(c, tombstone);
|
upsertMessage(c, tombstone);
|
||||||
resetDeliveryRows(c, tombstone.getMessageKey());
|
resetDeliveryRows(c, tombstone.getMessageKey());
|
||||||
|
DmDialogStateDAO.getInstance().refreshConversationPair(c, tombstone.getFromLogin(), tombstone.getToLogin());
|
||||||
|
|
||||||
c.commit();
|
c.commit();
|
||||||
return ApplyStatus.APPLIED;
|
return ApplyStatus.APPLIED;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import java.util.List;
|
|||||||
* Возвращает по каждой активной подписке (FOLLOW) + "сам на себя":
|
* Возвращает по каждой активной подписке (FOLLOW) + "сам на себя":
|
||||||
* - login цели (channelLogin)
|
* - login цели (channelLogin)
|
||||||
* - blockchainName цели (channelBchName)
|
* - blockchainName цели (channelBchName)
|
||||||
* - count публикаций (TEXT_POST)
|
* - count публикаций (видимые line-based TEXT-сообщения канала)
|
||||||
* - last publication: bytes оригинального блока (для timestamp)
|
* - last publication: bytes оригинального блока (для timestamp)
|
||||||
* - last publication: bytes актуального блока (edit или orig) — для текста превью
|
* - last publication: bytes актуального блока (edit или orig) — для текста превью
|
||||||
*
|
*
|
||||||
@@ -92,7 +92,7 @@ public final class SubscriptionsDAO {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Получить список подписок (активные FOLLOW) + "сам на себя" и по каждой:
|
* Получить список подписок (активные FOLLOW) + "сам на себя" и по каждой:
|
||||||
* - count публикаций (TEXT_POST)
|
* - count публикаций (видимые line-based TEXT-сообщения канала)
|
||||||
* - последнюю публикацию (orig bytes) + её edit (если есть)
|
* - последнюю публикацию (orig bytes) + её edit (если есть)
|
||||||
*
|
*
|
||||||
* Поведение при 0 публикаций:
|
* Поведение при 0 публикаций:
|
||||||
@@ -131,7 +131,7 @@ public final class SubscriptionsDAO {
|
|||||||
ON s.channel_login = b.login
|
ON s.channel_login = b.login
|
||||||
AND s.channel_bch_name = b.bch_name
|
AND s.channel_bch_name = b.bch_name
|
||||||
WHERE b.msg_type = ?
|
WHERE b.msg_type = ?
|
||||||
AND b.msg_sub_type IN (?, ?)
|
AND b.msg_sub_type IN (?, ?, ?, ?, ?, ?)
|
||||||
GROUP BY b.login, b.bch_name
|
GROUP BY b.login, b.bch_name
|
||||||
),
|
),
|
||||||
last_pub AS (
|
last_pub AS (
|
||||||
@@ -144,7 +144,7 @@ public final class SubscriptionsDAO {
|
|||||||
ON s.channel_login = b.login
|
ON s.channel_login = b.login
|
||||||
AND s.channel_bch_name = b.bch_name
|
AND s.channel_bch_name = b.bch_name
|
||||||
WHERE b.msg_type = ?
|
WHERE b.msg_type = ?
|
||||||
AND b.msg_sub_type IN (?, ?)
|
AND b.msg_sub_type IN (?, ?, ?, ?, ?, ?)
|
||||||
GROUP BY b.login, b.bch_name
|
GROUP BY b.login, b.bch_name
|
||||||
),
|
),
|
||||||
last_pub_block AS (
|
last_pub_block AS (
|
||||||
@@ -209,11 +209,19 @@ public final class SubscriptionsDAO {
|
|||||||
ps.setInt(i++, MSG_TYPE_TEXT);
|
ps.setInt(i++, MSG_TYPE_TEXT);
|
||||||
ps.setInt(i++, (int) MsgSubType.TEXT_POST);
|
ps.setInt(i++, (int) MsgSubType.TEXT_POST);
|
||||||
ps.setInt(i++, (int) MsgSubType.TEXT_REPOST);
|
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
|
// last_pub
|
||||||
ps.setInt(i++, MSG_TYPE_TEXT);
|
ps.setInt(i++, MSG_TYPE_TEXT);
|
||||||
ps.setInt(i++, (int) MsgSubType.TEXT_POST);
|
ps.setInt(i++, (int) MsgSubType.TEXT_POST);
|
||||||
ps.setInt(i++, (int) MsgSubType.TEXT_REPOST);
|
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()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
|
|||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
package shine.db.dao;
|
||||||
|
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.entities.UserNotificationEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Types;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class UserNotificationsStateDAO {
|
||||||
|
private static volatile UserNotificationsStateDAO instance;
|
||||||
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
|
private UserNotificationsStateDAO() {}
|
||||||
|
|
||||||
|
public static UserNotificationsStateDAO getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (UserNotificationsStateDAO.class) {
|
||||||
|
if (instance == null) instance = new UserNotificationsStateDAO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int upsert(Connection c, UserNotificationEntry e) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO user_notifications_state (
|
||||||
|
owner_login, notification_kind, created_at_ms, source_login, source_bch_name,
|
||||||
|
source_block_number, source_block_hash, target_login, target_bch_name,
|
||||||
|
target_block_number, target_block_hash, source_msg_sub_type, source_text
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(owner_login, notification_kind, source_bch_name, source_block_number, source_block_hash) DO UPDATE SET
|
||||||
|
created_at_ms = EXCLUDED.created_at_ms,
|
||||||
|
source_login = EXCLUDED.source_login,
|
||||||
|
target_login = EXCLUDED.target_login,
|
||||||
|
target_bch_name = EXCLUDED.target_bch_name,
|
||||||
|
target_block_number = EXCLUDED.target_block_number,
|
||||||
|
target_block_hash = EXCLUDED.target_block_hash,
|
||||||
|
source_msg_sub_type = EXCLUDED.source_msg_sub_type,
|
||||||
|
source_text = EXCLUDED.source_text
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
int i = 1;
|
||||||
|
ps.setString(i++, e.getOwnerLogin());
|
||||||
|
ps.setString(i++, e.getNotificationKind());
|
||||||
|
ps.setLong(i++, e.getCreatedAtMs());
|
||||||
|
ps.setString(i++, e.getSourceLogin());
|
||||||
|
ps.setString(i++, e.getSourceBchName());
|
||||||
|
ps.setInt(i++, e.getSourceBlockNumber());
|
||||||
|
ps.setBytes(i++, e.getSourceBlockHash());
|
||||||
|
if (e.getTargetLogin() != null) ps.setString(i++, e.getTargetLogin()); else ps.setNull(i++, Types.VARCHAR);
|
||||||
|
if (e.getTargetBchName() != null) ps.setString(i++, e.getTargetBchName()); else ps.setNull(i++, Types.VARCHAR);
|
||||||
|
if (e.getTargetBlockNumber() != null) ps.setInt(i++, e.getTargetBlockNumber()); else ps.setNull(i++, Types.INTEGER);
|
||||||
|
if (e.getTargetBlockHash() != null) ps.setBytes(i++, e.getTargetBlockHash()); else ps.setNull(i++, Types.BINARY);
|
||||||
|
ps.setInt(i++, e.getSourceMsgSubType());
|
||||||
|
ps.setString(i++, e.getSourceText() == null ? "" : e.getSourceText());
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserNotificationEntry> listByOwnerAndKind(Connection c, String ownerLogin, String kind, int limit) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT owner_login, notification_kind, created_at_ms, source_login, source_bch_name,
|
||||||
|
source_block_number, source_block_hash, target_login, target_bch_name,
|
||||||
|
target_block_number, target_block_hash, source_msg_sub_type, source_text
|
||||||
|
FROM user_notifications_state
|
||||||
|
WHERE owner_login = ? AND notification_kind = ?
|
||||||
|
ORDER BY created_at_ms DESC, source_block_number DESC
|
||||||
|
LIMIT ?
|
||||||
|
""";
|
||||||
|
List<UserNotificationEntry> out = new ArrayList<>();
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setString(2, kind);
|
||||||
|
ps.setInt(3, Math.max(1, limit));
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) out.add(mapRow(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UserNotificationEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
|
UserNotificationEntry e = new UserNotificationEntry();
|
||||||
|
e.setOwnerLogin(rs.getString("owner_login"));
|
||||||
|
e.setNotificationKind(rs.getString("notification_kind"));
|
||||||
|
e.setCreatedAtMs(rs.getLong("created_at_ms"));
|
||||||
|
e.setSourceLogin(rs.getString("source_login"));
|
||||||
|
e.setSourceBchName(rs.getString("source_bch_name"));
|
||||||
|
e.setSourceBlockNumber(rs.getInt("source_block_number"));
|
||||||
|
e.setSourceBlockHash(rs.getBytes("source_block_hash"));
|
||||||
|
e.setTargetLogin(rs.getString("target_login"));
|
||||||
|
e.setTargetBchName(rs.getString("target_bch_name"));
|
||||||
|
Object targetBlockNumber = rs.getObject("target_block_number");
|
||||||
|
e.setTargetBlockNumber(targetBlockNumber == null ? null : ((Number) targetBlockNumber).intValue());
|
||||||
|
e.setTargetBlockHash(rs.getBytes("target_block_hash"));
|
||||||
|
e.setSourceMsgSubType(rs.getInt("source_msg_sub_type"));
|
||||||
|
e.setSourceText(rs.getString("source_text"));
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
package shine.db.dao;
|
||||||
|
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.entities.UserSettingEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Types;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserSettingsDAO — хранение пользовательских настроек.
|
||||||
|
*
|
||||||
|
* Правило:
|
||||||
|
* - уникальность: login + setting_type + setting_key
|
||||||
|
* - запись обновляется только если time_ms новее
|
||||||
|
* - synced=true означает, что значение уже дошло до второго сервера
|
||||||
|
*/
|
||||||
|
public final class UserSettingsDAO {
|
||||||
|
|
||||||
|
private static volatile UserSettingsDAO instance;
|
||||||
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
|
private UserSettingsDAO() {}
|
||||||
|
|
||||||
|
public static UserSettingsDAO getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (UserSettingsDAO.class) {
|
||||||
|
if (instance == null) instance = new UserSettingsDAO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int upsertIfNewer(Connection c, UserSettingEntry e) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO user_settings (
|
||||||
|
login, setting_type, setting_key, time_ms,
|
||||||
|
value_text, value_num, client_key, signature, synced
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT (login, setting_type, setting_key)
|
||||||
|
DO UPDATE SET
|
||||||
|
time_ms = EXCLUDED.time_ms,
|
||||||
|
value_text = EXCLUDED.value_text,
|
||||||
|
value_num = EXCLUDED.value_num,
|
||||||
|
client_key = EXCLUDED.client_key,
|
||||||
|
signature = EXCLUDED.signature,
|
||||||
|
synced = EXCLUDED.synced
|
||||||
|
WHERE user_settings.time_ms < EXCLUDED.time_ms
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, e.getLogin());
|
||||||
|
ps.setInt(2, e.getSettingType());
|
||||||
|
ps.setString(3, e.getSettingKey());
|
||||||
|
ps.setLong(4, e.getTimeMs());
|
||||||
|
ps.setString(5, e.getValueText() == null ? "" : e.getValueText());
|
||||||
|
ps.setLong(6, e.getValueNum());
|
||||||
|
|
||||||
|
if (e.getClientKey() == null || e.getClientKey().isBlank()) ps.setNull(7, Types.VARCHAR);
|
||||||
|
else ps.setString(7, e.getClientKey());
|
||||||
|
|
||||||
|
if (e.getSignature() == null || e.getSignature().isBlank()) ps.setNull(8, Types.VARCHAR);
|
||||||
|
else ps.setString(8, e.getSignature());
|
||||||
|
|
||||||
|
ps.setBoolean(9, e.isSynced());
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int upsertIfNewer(UserSettingEntry e) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
return upsertIfNewer(c, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserSettingEntry getByLoginTypeKey(Connection c, String login, int settingType, String settingKey) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT login, setting_type, setting_key, time_ms, value_text, value_num, client_key, signature, synced
|
||||||
|
FROM user_settings
|
||||||
|
WHERE LOWER(login) = LOWER(?) AND setting_type = ? AND setting_key = ?
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, login);
|
||||||
|
ps.setInt(2, settingType);
|
||||||
|
ps.setString(3, settingKey);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
return rs.next() ? mapRow(rs) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserSettingEntry getByLoginTypeKey(String login, int settingType, String settingKey) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
return getByLoginTypeKey(c, login, settingType, settingKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserSettingEntry> getByLogin(Connection c, String login) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT login, setting_type, setting_key, time_ms, value_text, value_num, client_key, signature, synced
|
||||||
|
FROM user_settings
|
||||||
|
WHERE LOWER(login) = LOWER(?)
|
||||||
|
ORDER BY time_ms DESC, setting_type ASC, setting_key ASC
|
||||||
|
""";
|
||||||
|
List<UserSettingEntry> out = new ArrayList<>();
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, login);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) out.add(mapRow(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserSettingEntry> getByLogin(String login) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
return getByLogin(c, login);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserSettingEntry> listNewerThan(Connection c, String login, long afterTimeMs, String afterSettingKey, int limit) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT login, setting_type, setting_key, time_ms, value_text, value_num, client_key, signature, synced
|
||||||
|
FROM user_settings
|
||||||
|
WHERE LOWER(login) = LOWER(?)
|
||||||
|
AND (
|
||||||
|
time_ms > ?
|
||||||
|
OR (time_ms = ? AND setting_key > ?)
|
||||||
|
)
|
||||||
|
ORDER BY time_ms ASC, setting_key ASC
|
||||||
|
LIMIT ?
|
||||||
|
""";
|
||||||
|
List<UserSettingEntry> out = new ArrayList<>();
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, login);
|
||||||
|
ps.setLong(2, Math.max(0L, afterTimeMs));
|
||||||
|
ps.setLong(3, Math.max(0L, afterTimeMs));
|
||||||
|
ps.setString(4, afterSettingKey == null ? "" : afterSettingKey);
|
||||||
|
ps.setInt(5, Math.max(1, limit));
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) out.add(mapRow(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserSettingEntry> listUnsyncedByLogin(Connection c, String login, int limit) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT login, setting_type, setting_key, time_ms, value_text, value_num, client_key, signature, synced
|
||||||
|
FROM user_settings
|
||||||
|
WHERE LOWER(login) = LOWER(?)
|
||||||
|
AND synced = FALSE
|
||||||
|
ORDER BY time_ms ASC, setting_key ASC
|
||||||
|
LIMIT ?
|
||||||
|
""";
|
||||||
|
List<UserSettingEntry> out = new ArrayList<>();
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, login);
|
||||||
|
ps.setInt(2, Math.max(1, limit));
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) out.add(mapRow(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int markSynced(Connection c, String login, int settingType, String settingKey) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
UPDATE user_settings
|
||||||
|
SET synced = TRUE
|
||||||
|
WHERE LOWER(login) = LOWER(?)
|
||||||
|
AND setting_type = ?
|
||||||
|
AND setting_key = ?
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, login);
|
||||||
|
ps.setInt(2, settingType);
|
||||||
|
ps.setString(3, settingKey);
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int markAllUnsynced(Connection c, String login) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
UPDATE user_settings
|
||||||
|
SET synced = FALSE
|
||||||
|
WHERE LOWER(login) = LOWER(?)
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, login);
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int markAllUnsynced() throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
return markAllUnsynced(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int markAllUnsynced(Connection c) throws SQLException {
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("UPDATE user_settings SET synced = FALSE")) {
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UserSettingEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
|
UserSettingEntry e = new UserSettingEntry();
|
||||||
|
e.setLogin(rs.getString("login"));
|
||||||
|
e.setSettingType(rs.getInt("setting_type"));
|
||||||
|
e.setSettingKey(rs.getString("setting_key"));
|
||||||
|
e.setTimeMs(rs.getLong("time_ms"));
|
||||||
|
e.setValueText(rs.getString("value_text"));
|
||||||
|
e.setValueNum(rs.getLong("value_num"));
|
||||||
|
|
||||||
|
String clientKey = rs.getString("client_key");
|
||||||
|
if (rs.wasNull()) clientKey = null;
|
||||||
|
e.setClientKey(clientKey);
|
||||||
|
|
||||||
|
String signature = rs.getString("signature");
|
||||||
|
if (rs.wasNull()) signature = null;
|
||||||
|
e.setSignature(signature);
|
||||||
|
|
||||||
|
e.setSynced(rs.getBoolean("synced"));
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
package shine.db.dao;
|
||||||
|
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.entities.UserSettingsSyncPeerStateEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public final class UserSettingsSyncPeerStateDAO {
|
||||||
|
|
||||||
|
private static volatile UserSettingsSyncPeerStateDAO instance;
|
||||||
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
|
private UserSettingsSyncPeerStateDAO() {}
|
||||||
|
|
||||||
|
public static UserSettingsSyncPeerStateDAO getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (UserSettingsSyncPeerStateDAO.class) {
|
||||||
|
if (instance == null) instance = new UserSettingsSyncPeerStateDAO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserSettingsSyncPeerStateEntry getOrCreate(Connection c, String ownerLogin, String remoteServerLogin, String remoteServerUrl) throws SQLException {
|
||||||
|
UserSettingsSyncPeerStateEntry existing = get(c, ownerLogin, remoteServerLogin);
|
||||||
|
if (existing != null) return existing;
|
||||||
|
long nowMs = System.currentTimeMillis();
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO user_settings_sync_peer_state (
|
||||||
|
owner_login, remote_server_login, remote_server_url,
|
||||||
|
cursor_time_ms, cursor_setting_key, bootstrap_completed,
|
||||||
|
last_sync_at_ms, last_error, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, 0, '', FALSE, NULL, NULL, ?)
|
||||||
|
ON CONFLICT (owner_login, remote_server_login) DO UPDATE SET
|
||||||
|
remote_server_url = EXCLUDED.remote_server_url,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setString(2, remoteServerLogin);
|
||||||
|
ps.setString(3, remoteServerUrl);
|
||||||
|
ps.setLong(4, nowMs);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
return get(c, ownerLogin, remoteServerLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserSettingsSyncPeerStateEntry get(Connection c, String ownerLogin, String remoteServerLogin) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT owner_login, remote_server_login, remote_server_url, cursor_time_ms, cursor_setting_key,
|
||||||
|
bootstrap_completed, last_sync_at_ms, last_error, updated_at_ms
|
||||||
|
FROM user_settings_sync_peer_state
|
||||||
|
WHERE LOWER(owner_login) = LOWER(?) AND LOWER(remote_server_login) = LOWER(?)
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setString(2, remoteServerLogin);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
return rs.next() ? mapRow(rs) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int updateSuccess(String ownerLogin, String remoteServerLogin, String remoteServerUrl, long cursorTimeMs, String cursorSettingKey, boolean bootstrapCompleted) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO user_settings_sync_peer_state (
|
||||||
|
owner_login, remote_server_login, remote_server_url,
|
||||||
|
cursor_time_ms, cursor_setting_key, bootstrap_completed,
|
||||||
|
last_sync_at_ms, last_error, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, NULL, ?)
|
||||||
|
ON CONFLICT (owner_login, remote_server_login) DO UPDATE SET
|
||||||
|
remote_server_url = EXCLUDED.remote_server_url,
|
||||||
|
cursor_time_ms = EXCLUDED.cursor_time_ms,
|
||||||
|
cursor_setting_key = EXCLUDED.cursor_setting_key,
|
||||||
|
bootstrap_completed = EXCLUDED.bootstrap_completed,
|
||||||
|
last_sync_at_ms = EXCLUDED.last_sync_at_ms,
|
||||||
|
last_error = NULL,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
long nowMs = System.currentTimeMillis();
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setString(2, remoteServerLogin);
|
||||||
|
ps.setString(3, remoteServerUrl);
|
||||||
|
ps.setLong(4, Math.max(0L, cursorTimeMs));
|
||||||
|
ps.setString(5, cursorSettingKey == null ? "" : cursorSettingKey);
|
||||||
|
ps.setBoolean(6, bootstrapCompleted);
|
||||||
|
ps.setLong(7, nowMs);
|
||||||
|
ps.setLong(8, nowMs);
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int updateError(String ownerLogin, String remoteServerLogin, String remoteServerUrl, String error) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO user_settings_sync_peer_state (
|
||||||
|
owner_login, remote_server_login, remote_server_url,
|
||||||
|
cursor_time_ms, cursor_setting_key, bootstrap_completed,
|
||||||
|
last_sync_at_ms, last_error, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, 0, '', FALSE, NULL, ?, ?)
|
||||||
|
ON CONFLICT (owner_login, remote_server_login) DO UPDATE SET
|
||||||
|
remote_server_url = EXCLUDED.remote_server_url,
|
||||||
|
last_error = EXCLUDED.last_error,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
long nowMs = System.currentTimeMillis();
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setString(2, remoteServerLogin);
|
||||||
|
ps.setString(3, remoteServerUrl);
|
||||||
|
ps.setString(4, error);
|
||||||
|
ps.setLong(5, nowMs);
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int clearBootstrap(String ownerLogin, String remoteServerLogin, String remoteServerUrl) throws SQLException {
|
||||||
|
return updateSuccess(ownerLogin, remoteServerLogin, remoteServerUrl, 0L, "", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int deleteAllForOwner(String ownerLogin) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("DELETE FROM user_settings_sync_peer_state WHERE LOWER(owner_login) = LOWER(?)")) {
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UserSettingsSyncPeerStateEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
|
UserSettingsSyncPeerStateEntry e = new UserSettingsSyncPeerStateEntry();
|
||||||
|
e.setOwnerLogin(rs.getString("owner_login"));
|
||||||
|
e.setRemoteServerLogin(rs.getString("remote_server_login"));
|
||||||
|
e.setRemoteServerUrl(rs.getString("remote_server_url"));
|
||||||
|
e.setCursorTimeMs(rs.getLong("cursor_time_ms"));
|
||||||
|
e.setCursorSettingKey(rs.getString("cursor_setting_key"));
|
||||||
|
e.setBootstrapCompleted(rs.getBoolean("bootstrap_completed"));
|
||||||
|
long lastSyncAtMs = rs.getLong("last_sync_at_ms");
|
||||||
|
e.setLastSyncAtMs(rs.wasNull() ? null : lastSyncAtMs);
|
||||||
|
e.setLastError(rs.getString("last_error"));
|
||||||
|
e.setUpdatedAtMs(rs.getLong("updated_at_ms"));
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -13,6 +13,10 @@ public class ChannelNameStateEntry {
|
|||||||
private int channelRootBlockNumber;
|
private int channelRootBlockNumber;
|
||||||
private byte[] channelRootBlockHash;
|
private byte[] channelRootBlockHash;
|
||||||
private long createdAtMs;
|
private long createdAtMs;
|
||||||
|
private String avaAr;
|
||||||
|
private String avaSha256;
|
||||||
|
private long avaSize;
|
||||||
|
private long metaUpdatedAtMs;
|
||||||
|
|
||||||
public String getSlug() {
|
public String getSlug() {
|
||||||
return slug;
|
return slug;
|
||||||
@@ -93,4 +97,36 @@ public class ChannelNameStateEntry {
|
|||||||
public void setCreatedAtMs(long createdAtMs) {
|
public void setCreatedAtMs(long createdAtMs) {
|
||||||
this.createdAtMs = createdAtMs;
|
this.createdAtMs = createdAtMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getAvaAr() {
|
||||||
|
return avaAr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAvaAr(String avaAr) {
|
||||||
|
this.avaAr = avaAr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAvaSha256() {
|
||||||
|
return avaSha256;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAvaSha256(String avaSha256) {
|
||||||
|
this.avaSha256 = avaSha256;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getAvaSize() {
|
||||||
|
return avaSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAvaSize(long avaSize) {
|
||||||
|
this.avaSize = avaSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getMetaUpdatedAtMs() {
|
||||||
|
return metaUpdatedAtMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMetaUpdatedAtMs(long metaUpdatedAtMs) {
|
||||||
|
this.metaUpdatedAtMs = metaUpdatedAtMs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
package shine.db.entities;
|
|
||||||
|
|
||||||
public class DirectMessageEntry {
|
|
||||||
private String messageId;
|
|
||||||
private String fromLogin;
|
|
||||||
private String toLogin;
|
|
||||||
private String text;
|
|
||||||
private long createdAtMs;
|
|
||||||
|
|
||||||
public String getMessageId() { return messageId; }
|
|
||||||
public void setMessageId(String messageId) { this.messageId = messageId; }
|
|
||||||
|
|
||||||
public String getFromLogin() { return fromLogin; }
|
|
||||||
public void setFromLogin(String fromLogin) { this.fromLogin = fromLogin; }
|
|
||||||
|
|
||||||
public String getToLogin() { return toLogin; }
|
|
||||||
public void setToLogin(String toLogin) { this.toLogin = toLogin; }
|
|
||||||
|
|
||||||
public String getText() { return text; }
|
|
||||||
public void setText(String text) { this.text = text; }
|
|
||||||
|
|
||||||
public long getCreatedAtMs() { return createdAtMs; }
|
|
||||||
public void setCreatedAtMs(long createdAtMs) { this.createdAtMs = createdAtMs; }
|
|
||||||
}
|
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
package shine.db.entities;
|
||||||
|
|
||||||
|
public class DmDeliveryStateEntry {
|
||||||
|
public static final int ACCEPTED = 0;
|
||||||
|
public static final int DELIVERED = 1;
|
||||||
|
public static final int FAILED = 3;
|
||||||
|
|
||||||
|
// Совместимость со строками БД, созданными ранней экспериментальной миграцией.
|
||||||
|
public static final int PENDING_NONE = ACCEPTED;
|
||||||
|
public static final int DELIVERED_ONE = DELIVERED;
|
||||||
|
public static final int DELIVERED_ALL = 2;
|
||||||
|
public static final int FAILED_FINAL = FAILED;
|
||||||
|
|
||||||
|
private String outgoingMessageKey;
|
||||||
|
private String eventId;
|
||||||
|
private String baseKey;
|
||||||
|
private String fromLogin;
|
||||||
|
private String toLogin;
|
||||||
|
private String incomingMessageKey;
|
||||||
|
private long createdAtMs;
|
||||||
|
private long deliveryExpiresAtMs;
|
||||||
|
private int deliveryState;
|
||||||
|
private String deliveredServerLogin;
|
||||||
|
private String recipientRoutesHash;
|
||||||
|
private int attemptIndex;
|
||||||
|
private Long nextAttemptAtMs;
|
||||||
|
private Long lastAttemptAtMs;
|
||||||
|
private String lastError;
|
||||||
|
private long updatedAtMs;
|
||||||
|
|
||||||
|
public String getOutgoingMessageKey() { return outgoingMessageKey; }
|
||||||
|
public void setOutgoingMessageKey(String value) { this.outgoingMessageKey = value; }
|
||||||
|
public String getEventId() { return eventId; }
|
||||||
|
public void setEventId(String value) { this.eventId = value; }
|
||||||
|
public String getBaseKey() { return baseKey; }
|
||||||
|
public void setBaseKey(String value) { this.baseKey = value; }
|
||||||
|
public String getFromLogin() { return fromLogin; }
|
||||||
|
public void setFromLogin(String value) { this.fromLogin = value; }
|
||||||
|
public String getToLogin() { return toLogin; }
|
||||||
|
public void setToLogin(String value) { this.toLogin = value; }
|
||||||
|
public String getIncomingMessageKey() { return incomingMessageKey; }
|
||||||
|
public void setIncomingMessageKey(String value) { this.incomingMessageKey = value; }
|
||||||
|
public long getCreatedAtMs() { return createdAtMs; }
|
||||||
|
public void setCreatedAtMs(long value) { this.createdAtMs = value; }
|
||||||
|
public long getDeliveryExpiresAtMs() { return deliveryExpiresAtMs; }
|
||||||
|
public void setDeliveryExpiresAtMs(long value) { this.deliveryExpiresAtMs = value; }
|
||||||
|
public int getDeliveryState() { return deliveryState; }
|
||||||
|
public void setDeliveryState(int value) { this.deliveryState = value; }
|
||||||
|
public String getDeliveredServerLogin() { return deliveredServerLogin; }
|
||||||
|
public void setDeliveredServerLogin(String value) { this.deliveredServerLogin = value; }
|
||||||
|
public String getRecipientRoutesHash() { return recipientRoutesHash; }
|
||||||
|
public void setRecipientRoutesHash(String value) { this.recipientRoutesHash = value; }
|
||||||
|
public int getAttemptIndex() { return attemptIndex; }
|
||||||
|
public void setAttemptIndex(int value) { this.attemptIndex = value; }
|
||||||
|
public Long getNextAttemptAtMs() { return nextAttemptAtMs; }
|
||||||
|
public void setNextAttemptAtMs(Long value) { this.nextAttemptAtMs = value; }
|
||||||
|
public Long getLastAttemptAtMs() { return lastAttemptAtMs; }
|
||||||
|
public void setLastAttemptAtMs(Long value) { this.lastAttemptAtMs = value; }
|
||||||
|
public String getLastError() { return lastError; }
|
||||||
|
public void setLastError(String value) { this.lastError = value; }
|
||||||
|
public long getUpdatedAtMs() { return updatedAtMs; }
|
||||||
|
public void setUpdatedAtMs(long value) { this.updatedAtMs = value; }
|
||||||
|
|
||||||
|
public String deliveryStateCode() {
|
||||||
|
return switch (deliveryState) {
|
||||||
|
case DELIVERED_ONE, DELIVERED_ALL -> "delivered";
|
||||||
|
case FAILED_FINAL -> "failed";
|
||||||
|
default -> "accepted";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int parseStateCode(String value) {
|
||||||
|
if (value == null) return ACCEPTED;
|
||||||
|
return switch (value.trim().toLowerCase()) {
|
||||||
|
case "delivered", "delivered_one", "delivered_all" -> DELIVERED;
|
||||||
|
case "failed", "failed_final" -> FAILED;
|
||||||
|
default -> ACCEPTED;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDelivered() {
|
||||||
|
return deliveryState == DELIVERED_ONE || deliveryState == DELIVERED_ALL;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package shine.db.entities;
|
||||||
|
|
||||||
|
public class DmSyncOutboxEntry {
|
||||||
|
private String ownerLogin;
|
||||||
|
private String primaryMessageKey;
|
||||||
|
private String eventId;
|
||||||
|
private String secondaryMessageKey;
|
||||||
|
private boolean synced;
|
||||||
|
private long createdAtMs;
|
||||||
|
private long updatedAtMs;
|
||||||
|
|
||||||
|
public String getOwnerLogin() { return ownerLogin; }
|
||||||
|
public void setOwnerLogin(String value) { this.ownerLogin = value; }
|
||||||
|
public String getPrimaryMessageKey() { return primaryMessageKey; }
|
||||||
|
public void setPrimaryMessageKey(String value) { this.primaryMessageKey = value; }
|
||||||
|
public String getEventId() { return eventId; }
|
||||||
|
public void setEventId(String value) { this.eventId = value; }
|
||||||
|
public String getSecondaryMessageKey() { return secondaryMessageKey; }
|
||||||
|
public void setSecondaryMessageKey(String value) { this.secondaryMessageKey = value; }
|
||||||
|
public boolean isSynced() { return synced; }
|
||||||
|
public void setSynced(boolean value) { this.synced = value; }
|
||||||
|
public long getCreatedAtMs() { return createdAtMs; }
|
||||||
|
public void setCreatedAtMs(long value) { this.createdAtMs = value; }
|
||||||
|
public long getUpdatedAtMs() { return updatedAtMs; }
|
||||||
|
public void setUpdatedAtMs(long value) { this.updatedAtMs = value; }
|
||||||
|
}
|
||||||
-35
@@ -1,35 +0,0 @@
|
|||||||
package shine.db.entities;
|
|
||||||
|
|
||||||
public class SignedDirectMessageHistoryEntry {
|
|
||||||
private String messageId;
|
|
||||||
private String fromLogin;
|
|
||||||
private String toLogin;
|
|
||||||
private int targetMode;
|
|
||||||
private String targetSessionId;
|
|
||||||
private int messageType;
|
|
||||||
private long timeMs;
|
|
||||||
private long nonce;
|
|
||||||
private byte[] rawPacket;
|
|
||||||
private long createdAtMs;
|
|
||||||
|
|
||||||
public String getMessageId() { return messageId; }
|
|
||||||
public void setMessageId(String messageId) { this.messageId = messageId; }
|
|
||||||
public String getFromLogin() { return fromLogin; }
|
|
||||||
public void setFromLogin(String fromLogin) { this.fromLogin = fromLogin; }
|
|
||||||
public String getToLogin() { return toLogin; }
|
|
||||||
public void setToLogin(String toLogin) { this.toLogin = toLogin; }
|
|
||||||
public int getTargetMode() { return targetMode; }
|
|
||||||
public void setTargetMode(int targetMode) { this.targetMode = targetMode; }
|
|
||||||
public String getTargetSessionId() { return targetSessionId; }
|
|
||||||
public void setTargetSessionId(String targetSessionId) { this.targetSessionId = targetSessionId; }
|
|
||||||
public int getMessageType() { return messageType; }
|
|
||||||
public void setMessageType(int messageType) { this.messageType = messageType; }
|
|
||||||
public long getTimeMs() { return timeMs; }
|
|
||||||
public void setTimeMs(long timeMs) { this.timeMs = timeMs; }
|
|
||||||
public long getNonce() { return nonce; }
|
|
||||||
public void setNonce(long nonce) { this.nonce = nonce; }
|
|
||||||
public byte[] getRawPacket() { return rawPacket; }
|
|
||||||
public void setRawPacket(byte[] rawPacket) { this.rawPacket = rawPacket; }
|
|
||||||
public long getCreatedAtMs() { return createdAtMs; }
|
|
||||||
public void setCreatedAtMs(long createdAtMs) { this.createdAtMs = createdAtMs; }
|
|
||||||
}
|
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
package shine.db.entities;
|
||||||
|
|
||||||
|
public class UserNotificationEntry {
|
||||||
|
private String ownerLogin;
|
||||||
|
private String notificationKind;
|
||||||
|
private long createdAtMs;
|
||||||
|
private String sourceLogin;
|
||||||
|
private String sourceBchName;
|
||||||
|
private int sourceBlockNumber;
|
||||||
|
private byte[] sourceBlockHash;
|
||||||
|
private String targetLogin;
|
||||||
|
private String targetBchName;
|
||||||
|
private Integer targetBlockNumber;
|
||||||
|
private byte[] targetBlockHash;
|
||||||
|
private int sourceMsgSubType;
|
||||||
|
private String sourceText;
|
||||||
|
|
||||||
|
public String getOwnerLogin() { return ownerLogin; }
|
||||||
|
public void setOwnerLogin(String ownerLogin) { this.ownerLogin = ownerLogin; }
|
||||||
|
public String getNotificationKind() { return notificationKind; }
|
||||||
|
public void setNotificationKind(String notificationKind) { this.notificationKind = notificationKind; }
|
||||||
|
public long getCreatedAtMs() { return createdAtMs; }
|
||||||
|
public void setCreatedAtMs(long createdAtMs) { this.createdAtMs = createdAtMs; }
|
||||||
|
public String getSourceLogin() { return sourceLogin; }
|
||||||
|
public void setSourceLogin(String sourceLogin) { this.sourceLogin = sourceLogin; }
|
||||||
|
public String getSourceBchName() { return sourceBchName; }
|
||||||
|
public void setSourceBchName(String sourceBchName) { this.sourceBchName = sourceBchName; }
|
||||||
|
public int getSourceBlockNumber() { return sourceBlockNumber; }
|
||||||
|
public void setSourceBlockNumber(int sourceBlockNumber) { this.sourceBlockNumber = sourceBlockNumber; }
|
||||||
|
public byte[] getSourceBlockHash() { return sourceBlockHash; }
|
||||||
|
public void setSourceBlockHash(byte[] sourceBlockHash) { this.sourceBlockHash = sourceBlockHash; }
|
||||||
|
public String getTargetLogin() { return targetLogin; }
|
||||||
|
public void setTargetLogin(String targetLogin) { this.targetLogin = targetLogin; }
|
||||||
|
public String getTargetBchName() { return targetBchName; }
|
||||||
|
public void setTargetBchName(String targetBchName) { this.targetBchName = targetBchName; }
|
||||||
|
public Integer getTargetBlockNumber() { return targetBlockNumber; }
|
||||||
|
public void setTargetBlockNumber(Integer targetBlockNumber) { this.targetBlockNumber = targetBlockNumber; }
|
||||||
|
public byte[] getTargetBlockHash() { return targetBlockHash; }
|
||||||
|
public void setTargetBlockHash(byte[] targetBlockHash) { this.targetBlockHash = targetBlockHash; }
|
||||||
|
public int getSourceMsgSubType() { return sourceMsgSubType; }
|
||||||
|
public void setSourceMsgSubType(int sourceMsgSubType) { this.sourceMsgSubType = sourceMsgSubType; }
|
||||||
|
public String getSourceText() { return sourceText; }
|
||||||
|
public void setSourceText(String sourceText) { this.sourceText = sourceText; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package shine.db.entities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserSettingEntry — одна пользовательская настройка.
|
||||||
|
*
|
||||||
|
* Таблица: user_settings
|
||||||
|
* - login TEXT NOT NULL
|
||||||
|
* - setting_type INTEGER NOT NULL
|
||||||
|
* - setting_key TEXT NOT NULL
|
||||||
|
* - time_ms BIGINT NOT NULL
|
||||||
|
* - value_text TEXT NOT NULL
|
||||||
|
* - value_num BIGINT NOT NULL
|
||||||
|
* - client_key TEXT NOT NULL
|
||||||
|
* - signature TEXT NOT NULL
|
||||||
|
* - synced BOOLEAN NOT NULL
|
||||||
|
*/
|
||||||
|
public class UserSettingEntry {
|
||||||
|
private String login;
|
||||||
|
private int settingType;
|
||||||
|
private String settingKey;
|
||||||
|
private long timeMs;
|
||||||
|
private String valueText;
|
||||||
|
private long valueNum;
|
||||||
|
private String clientKey;
|
||||||
|
private String signature;
|
||||||
|
private boolean synced;
|
||||||
|
|
||||||
|
public UserSettingEntry() {}
|
||||||
|
|
||||||
|
public UserSettingEntry(String login, int settingType, String settingKey, long timeMs, String valueText, long valueNum, String clientKey, String signature, boolean synced) {
|
||||||
|
this.login = login;
|
||||||
|
this.settingType = settingType;
|
||||||
|
this.settingKey = settingKey;
|
||||||
|
this.timeMs = timeMs;
|
||||||
|
this.valueText = valueText;
|
||||||
|
this.valueNum = valueNum;
|
||||||
|
this.clientKey = clientKey;
|
||||||
|
this.signature = signature;
|
||||||
|
this.synced = synced;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
|
public int getSettingType() { return settingType; }
|
||||||
|
public void setSettingType(int settingType) { this.settingType = settingType; }
|
||||||
|
|
||||||
|
public String getSettingKey() { return settingKey; }
|
||||||
|
public void setSettingKey(String settingKey) { this.settingKey = settingKey; }
|
||||||
|
|
||||||
|
public long getTimeMs() { return timeMs; }
|
||||||
|
public void setTimeMs(long timeMs) { this.timeMs = timeMs; }
|
||||||
|
|
||||||
|
public String getValueText() { return valueText; }
|
||||||
|
public void setValueText(String valueText) { this.valueText = valueText; }
|
||||||
|
|
||||||
|
public long getValueNum() { return valueNum; }
|
||||||
|
public void setValueNum(long valueNum) { this.valueNum = valueNum; }
|
||||||
|
|
||||||
|
public String getClientKey() { return clientKey; }
|
||||||
|
public void setClientKey(String clientKey) { this.clientKey = clientKey; }
|
||||||
|
|
||||||
|
public String getSignature() { return signature; }
|
||||||
|
public void setSignature(String signature) { this.signature = signature; }
|
||||||
|
|
||||||
|
public boolean isSynced() { return synced; }
|
||||||
|
public void setSynced(boolean synced) { this.synced = synced; }
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package shine.db.entities;
|
||||||
|
|
||||||
|
public class UserSettingsSyncPeerStateEntry {
|
||||||
|
private String ownerLogin;
|
||||||
|
private String remoteServerLogin;
|
||||||
|
private String remoteServerUrl;
|
||||||
|
private long cursorTimeMs;
|
||||||
|
private String cursorSettingKey;
|
||||||
|
private boolean bootstrapCompleted;
|
||||||
|
private Long lastSyncAtMs;
|
||||||
|
private String lastError;
|
||||||
|
private long updatedAtMs;
|
||||||
|
|
||||||
|
public String getOwnerLogin() { return ownerLogin; }
|
||||||
|
public void setOwnerLogin(String ownerLogin) { this.ownerLogin = ownerLogin; }
|
||||||
|
|
||||||
|
public String getRemoteServerLogin() { return remoteServerLogin; }
|
||||||
|
public void setRemoteServerLogin(String remoteServerLogin) { this.remoteServerLogin = remoteServerLogin; }
|
||||||
|
|
||||||
|
public String getRemoteServerUrl() { return remoteServerUrl; }
|
||||||
|
public void setRemoteServerUrl(String remoteServerUrl) { this.remoteServerUrl = remoteServerUrl; }
|
||||||
|
|
||||||
|
public long getCursorTimeMs() { return cursorTimeMs; }
|
||||||
|
public void setCursorTimeMs(long cursorTimeMs) { this.cursorTimeMs = cursorTimeMs; }
|
||||||
|
|
||||||
|
public String getCursorSettingKey() { return cursorSettingKey; }
|
||||||
|
public void setCursorSettingKey(String cursorSettingKey) { this.cursorSettingKey = cursorSettingKey; }
|
||||||
|
|
||||||
|
public boolean isBootstrapCompleted() { return bootstrapCompleted; }
|
||||||
|
public void setBootstrapCompleted(boolean bootstrapCompleted) { this.bootstrapCompleted = bootstrapCompleted; }
|
||||||
|
|
||||||
|
public Long getLastSyncAtMs() { return lastSyncAtMs; }
|
||||||
|
public void setLastSyncAtMs(Long lastSyncAtMs) { this.lastSyncAtMs = lastSyncAtMs; }
|
||||||
|
|
||||||
|
public String getLastError() { return lastError; }
|
||||||
|
public void setLastError(String lastError) { this.lastError = lastError; }
|
||||||
|
|
||||||
|
public long getUpdatedAtMs() { return updatedAtMs; }
|
||||||
|
public void setUpdatedAtMs(long updatedAtMs) { this.updatedAtMs = updatedAtMs; }
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ public final class CurrentUsersSql {
|
|||||||
(
|
(
|
||||||
SELECT
|
SELECT
|
||||||
current_users.login AS login,
|
current_users.login AS login,
|
||||||
|
current_users.normalized_login AS normalized_login,
|
||||||
current_users.blockchain_name AS blockchain_name,
|
current_users.blockchain_name AS blockchain_name,
|
||||||
current_users.client_key AS solana_key,
|
current_users.client_key AS solana_key,
|
||||||
current_users.blockchain_key AS blockchain_key,
|
current_users.blockchain_key AS blockchain_key,
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_notifications_state (
|
||||||
|
owner_login TEXT NOT NULL REFERENCES solana_user_pda_current(normalized_login),
|
||||||
|
notification_kind TEXT NOT NULL CHECK (notification_kind IN ('reply', 'connection')),
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
source_login TEXT NOT NULL,
|
||||||
|
source_bch_name TEXT NOT NULL,
|
||||||
|
source_block_number INTEGER NOT NULL CHECK (source_block_number >= 0),
|
||||||
|
source_block_hash BYTEA NOT NULL,
|
||||||
|
target_login TEXT,
|
||||||
|
target_bch_name TEXT,
|
||||||
|
target_block_number INTEGER,
|
||||||
|
target_block_hash BYTEA,
|
||||||
|
source_msg_sub_type INTEGER NOT NULL,
|
||||||
|
source_text TEXT NOT NULL DEFAULT '',
|
||||||
|
UNIQUE (owner_login, notification_kind, source_bch_name, source_block_number, source_block_hash)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_notifications_state_owner_kind_time
|
||||||
|
ON user_notifications_state(owner_login, notification_kind, created_at_ms DESC, source_block_number DESC);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_notifications_state_owner_time
|
||||||
|
ON user_notifications_state(owner_login, created_at_ms DESC);
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 10, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_dialog_state (
|
||||||
|
owner_login TEXT NOT NULL,
|
||||||
|
peer_login TEXT NOT NULL,
|
||||||
|
relation_flag TEXT NOT NULL DEFAULT 'none',
|
||||||
|
last_message_blob_b64 TEXT NOT NULL DEFAULT '',
|
||||||
|
last_message_key TEXT NOT NULL DEFAULT '',
|
||||||
|
last_message_type INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_message_time_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
last_message_revision_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
unread_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_read_receipt_time_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (owner_login, peer_login)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE IF EXISTS dm_dialog_state
|
||||||
|
ADD COLUMN IF NOT EXISTS last_message_blob_b64 TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_dialog_state_owner_last_time
|
||||||
|
ON dm_dialog_state(owner_login, last_message_time_ms DESC, peer_login);
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 11, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_delivery_state (
|
||||||
|
outgoing_message_key TEXT PRIMARY KEY,
|
||||||
|
event_id TEXT NOT NULL UNIQUE,
|
||||||
|
base_key TEXT NOT NULL,
|
||||||
|
from_login TEXT NOT NULL,
|
||||||
|
to_login TEXT NOT NULL,
|
||||||
|
incoming_message_key TEXT NOT NULL,
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
delivery_expires_at_ms BIGINT NOT NULL,
|
||||||
|
delivery_state INTEGER NOT NULL DEFAULT 0 CHECK (delivery_state IN (0, 1, 2, 3)),
|
||||||
|
delivered_server_login TEXT,
|
||||||
|
recipient_routes_hash TEXT,
|
||||||
|
attempt_index INTEGER NOT NULL DEFAULT 0,
|
||||||
|
next_attempt_at_ms BIGINT,
|
||||||
|
last_attempt_at_ms BIGINT,
|
||||||
|
last_error TEXT,
|
||||||
|
updated_at_ms BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_delivery_state_due
|
||||||
|
ON dm_delivery_state(next_attempt_at_ms, delivery_state)
|
||||||
|
WHERE delivery_state = 0 AND next_attempt_at_ms IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_delivery_state_base
|
||||||
|
ON dm_delivery_state(base_key, from_login);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_sync_outbox (
|
||||||
|
owner_login TEXT NOT NULL,
|
||||||
|
primary_message_key TEXT NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
secondary_message_key TEXT,
|
||||||
|
synced BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (owner_login, primary_message_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_sync_outbox_unsynced
|
||||||
|
ON dm_sync_outbox(owner_login, created_at_ms, primary_message_key)
|
||||||
|
WHERE synced = FALSE;
|
||||||
|
|
||||||
|
-- До v12 факт межсерверной доставки не сохранялся. Старые исходящие пары
|
||||||
|
-- считаем уже доставленными, чтобы обновление не вызвало повторную рассылку.
|
||||||
|
INSERT INTO dm_delivery_state (
|
||||||
|
outgoing_message_key, event_id, base_key, from_login, to_login,
|
||||||
|
incoming_message_key, created_at_ms, delivery_expires_at_ms,
|
||||||
|
delivery_state, delivered_server_login, recipient_routes_hash,
|
||||||
|
attempt_index, next_attempt_at_ms, last_attempt_at_ms, last_error, updated_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
outgoing.message_key,
|
||||||
|
outgoing.message_key || ':' || outgoing.revision_time_ms || ':' || outgoing.reencrypted_at_ms,
|
||||||
|
outgoing.base_key,
|
||||||
|
outgoing.from_login,
|
||||||
|
outgoing.to_login,
|
||||||
|
incoming.message_key,
|
||||||
|
outgoing.created_at_ms,
|
||||||
|
outgoing.created_at_ms + 3600000,
|
||||||
|
2,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
9,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
'MIGRATED_AS_DELIVERED',
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
FROM signed_messages outgoing
|
||||||
|
JOIN signed_messages incoming
|
||||||
|
ON incoming.base_key = outgoing.base_key
|
||||||
|
AND incoming.message_type = outgoing.message_type - 1
|
||||||
|
WHERE outgoing.message_type IN (2, 4)
|
||||||
|
ON CONFLICT (outgoing_message_key) DO NOTHING;
|
||||||
|
|
||||||
|
-- Историю до v12 считаем подтверждённой, иначе само обновление вызовет
|
||||||
|
-- массовую повторную передачу. При замене сервера общий reset вернёт FALSE.
|
||||||
|
INSERT INTO dm_sync_outbox (
|
||||||
|
owner_login, primary_message_key, event_id, secondary_message_key,
|
||||||
|
synced, created_at_ms, updated_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
LOWER(outgoing.from_login),
|
||||||
|
outgoing.message_key,
|
||||||
|
outgoing.message_key || ':' || outgoing.revision_time_ms || ':' || outgoing.reencrypted_at_ms,
|
||||||
|
incoming.message_key,
|
||||||
|
TRUE,
|
||||||
|
outgoing.created_at_ms,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
FROM signed_messages outgoing
|
||||||
|
JOIN signed_messages incoming
|
||||||
|
ON incoming.base_key = outgoing.base_key
|
||||||
|
AND incoming.message_type = outgoing.message_type - 1
|
||||||
|
WHERE outgoing.message_type IN (2, 4)
|
||||||
|
ON CONFLICT (owner_login, primary_message_key) DO NOTHING;
|
||||||
|
|
||||||
|
-- Входящая копия синхронизируется отдельно между серверами получателя.
|
||||||
|
INSERT INTO dm_sync_outbox (
|
||||||
|
owner_login, primary_message_key, event_id, secondary_message_key,
|
||||||
|
synced, created_at_ms, updated_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
LOWER(incoming.to_login),
|
||||||
|
incoming.message_key,
|
||||||
|
incoming.message_key || ':' || incoming.revision_time_ms || ':' || incoming.reencrypted_at_ms,
|
||||||
|
NULL,
|
||||||
|
TRUE,
|
||||||
|
incoming.created_at_ms,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
FROM signed_messages incoming
|
||||||
|
WHERE incoming.message_type IN (1, 3)
|
||||||
|
ON CONFLICT (owner_login, primary_message_key) DO NOTHING;
|
||||||
|
|
||||||
|
-- Tombstone принадлежит обоим участникам и должен попасть на второй сервер
|
||||||
|
-- каждого из них. Для self-DM конфликт безопасно схлопывается.
|
||||||
|
INSERT INTO dm_sync_outbox (
|
||||||
|
owner_login, primary_message_key, event_id, secondary_message_key,
|
||||||
|
synced, created_at_ms, updated_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
LOWER(owner_login),
|
||||||
|
tombstone.message_key,
|
||||||
|
tombstone.message_key || ':' || tombstone.revision_time_ms || ':' || tombstone.reencrypted_at_ms,
|
||||||
|
NULL,
|
||||||
|
TRUE,
|
||||||
|
tombstone.created_at_ms,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
FROM signed_messages tombstone
|
||||||
|
CROSS JOIN LATERAL (VALUES (tombstone.from_login), (tombstone.to_login)) owners(owner_login)
|
||||||
|
WHERE tombstone.message_type IN (5, 6, 7, 8)
|
||||||
|
ON CONFLICT (owner_login, primary_message_key) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 12, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Нормализация экспериментальной схемы доставки v12:
|
||||||
|
-- 0=accepted, 1=delivered, 3=failed. Старое delivered_all (2)
|
||||||
|
-- объединяется с delivered, поскольку ACK одного сервера теперь достаточен.
|
||||||
|
UPDATE dm_delivery_state
|
||||||
|
SET delivery_state = 1,
|
||||||
|
delivered_server_login = NULL,
|
||||||
|
next_attempt_at_ms = NULL,
|
||||||
|
updated_at_ms = CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
WHERE delivery_state = 2;
|
||||||
|
|
||||||
|
-- Для незавершённых записей перестраиваем только будущую очередь. Повторная
|
||||||
|
-- передача безопасна благодаря messageKey и идемпотентному приёму.
|
||||||
|
UPDATE dm_delivery_state
|
||||||
|
SET delivery_expires_at_ms = created_at_ms + 3600000,
|
||||||
|
attempt_index = CASE
|
||||||
|
WHEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) >= created_at_ms + 3600000 THEN 4
|
||||||
|
WHEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) >= created_at_ms + 1500000 THEN 3
|
||||||
|
WHEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) >= created_at_ms + 300000 THEN 2
|
||||||
|
ELSE 1
|
||||||
|
END,
|
||||||
|
next_attempt_at_ms = CASE
|
||||||
|
WHEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) >= created_at_ms + 3600000
|
||||||
|
THEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
WHEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) >= created_at_ms + 1500000
|
||||||
|
THEN created_at_ms + 3600000
|
||||||
|
WHEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) >= created_at_ms + 300000
|
||||||
|
THEN created_at_ms + 1500000
|
||||||
|
ELSE created_at_ms + 30000
|
||||||
|
END,
|
||||||
|
updated_at_ms = CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
WHERE delivery_state = 0;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_dm_delivery_state_due;
|
||||||
|
CREATE INDEX idx_dm_delivery_state_due
|
||||||
|
ON dm_delivery_state(next_attempt_at_ms, delivery_state)
|
||||||
|
WHERE delivery_state = 0 AND next_attempt_at_ms IS NOT NULL;
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 13, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -53,12 +53,16 @@ BEGIN
|
|||||||
s.updated_at_ms,
|
s.updated_at_ms,
|
||||||
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
FROM solana_user_pda_current u
|
FROM solana_user_pda_current u
|
||||||
CROSS JOIN LATERAL jsonb_array_elements_text(
|
CROSS JOIN LATERAL (
|
||||||
CASE
|
SELECT login_value
|
||||||
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
FROM jsonb_array_elements_text(
|
||||||
ELSE u.access_servers_json::jsonb
|
CASE
|
||||||
END
|
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||||
) AS access_server(login_value)
|
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
|
JOIN solana_user_pda_current s
|
||||||
ON LOWER(s.login) = LOWER(btrim(access_server.login_value))
|
ON LOWER(s.login) = LOWER(btrim(access_server.login_value))
|
||||||
AND s.is_server = TRUE
|
AND s.is_server = TRUE
|
||||||
@@ -92,12 +96,16 @@ BEGIN
|
|||||||
FOR affected_user IN
|
FOR affected_user IN
|
||||||
SELECT u.login
|
SELECT u.login
|
||||||
FROM solana_user_pda_current u
|
FROM solana_user_pda_current u
|
||||||
CROSS JOIN LATERAL jsonb_array_elements_text(
|
CROSS JOIN LATERAL (
|
||||||
CASE
|
SELECT login_value
|
||||||
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
FROM jsonb_array_elements_text(
|
||||||
ELSE u.access_servers_json::jsonb
|
CASE
|
||||||
END
|
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||||
) AS access_server(login_value)
|
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)
|
WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login)
|
||||||
LOOP
|
LOOP
|
||||||
PERFORM shine_refresh_user_access_servers_for_user(affected_user.login);
|
PERFORM shine_refresh_user_access_servers_for_user(affected_user.login);
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE channel_names_state
|
||||||
|
ADD COLUMN IF NOT EXISTS ava_ar TEXT NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN IF NOT EXISTS ava_sha256 TEXT NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN IF NOT EXISTS ava_size BIGINT NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS meta_updated_at_ms BIGINT NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 4, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE solana_user_pda_current
|
||||||
|
ADD COLUMN IF NOT EXISTS normalized_login TEXT;
|
||||||
|
|
||||||
|
UPDATE solana_user_pda_current
|
||||||
|
SET normalized_login = LOWER(BTRIM(login))
|
||||||
|
WHERE normalized_login IS NULL
|
||||||
|
OR normalized_login <> LOWER(BTRIM(login));
|
||||||
|
|
||||||
|
ALTER TABLE solana_user_pda_current
|
||||||
|
ALTER COLUMN normalized_login SET NOT NULL;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_solana_user_pda_current_normalized_login
|
||||||
|
ON solana_user_pda_current(normalized_login);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_pda_current_normalized_login
|
||||||
|
ON solana_user_pda_current(normalized_login);
|
||||||
|
|
||||||
|
WITH bad_signed_messages AS (
|
||||||
|
SELECT DISTINCT sm.message_key
|
||||||
|
FROM signed_messages sm
|
||||||
|
LEFT JOIN solana_user_pda_current u_from
|
||||||
|
ON u_from.normalized_login = LOWER(BTRIM(sm.from_login))
|
||||||
|
LEFT JOIN solana_user_pda_current u_to
|
||||||
|
ON u_to.normalized_login = LOWER(BTRIM(sm.to_login))
|
||||||
|
WHERE (u_from.login IS NOT NULL AND sm.from_login <> u_from.login)
|
||||||
|
OR (u_to.login IS NOT NULL AND sm.to_login <> u_to.login)
|
||||||
|
)
|
||||||
|
DELETE FROM signed_message_session_delivery d
|
||||||
|
USING bad_signed_messages bad
|
||||||
|
WHERE d.message_key = bad.message_key;
|
||||||
|
|
||||||
|
WITH bad_signed_messages AS (
|
||||||
|
SELECT DISTINCT sm.message_key
|
||||||
|
FROM signed_messages sm
|
||||||
|
LEFT JOIN solana_user_pda_current u_from
|
||||||
|
ON u_from.normalized_login = LOWER(BTRIM(sm.from_login))
|
||||||
|
LEFT JOIN solana_user_pda_current u_to
|
||||||
|
ON u_to.normalized_login = LOWER(BTRIM(sm.to_login))
|
||||||
|
WHERE (u_from.login IS NOT NULL AND sm.from_login <> u_from.login)
|
||||||
|
OR (u_to.login IS NOT NULL AND sm.to_login <> u_to.login)
|
||||||
|
)
|
||||||
|
DELETE FROM signed_messages sm
|
||||||
|
USING bad_signed_messages bad
|
||||||
|
WHERE sm.message_key = bad.message_key;
|
||||||
|
|
||||||
|
DELETE FROM signed_direct_messages_history h
|
||||||
|
USING solana_user_pda_current u_from,
|
||||||
|
solana_user_pda_current u_to
|
||||||
|
WHERE u_from.normalized_login = LOWER(BTRIM(h.from_login))
|
||||||
|
AND u_to.normalized_login = LOWER(BTRIM(h.to_login))
|
||||||
|
AND (h.from_login <> u_from.login OR h.to_login <> u_to.login);
|
||||||
|
|
||||||
|
DELETE FROM signed_direct_message_replay r
|
||||||
|
USING solana_user_pda_current u_from
|
||||||
|
WHERE u_from.normalized_login = LOWER(BTRIM(r.from_login))
|
||||||
|
AND r.from_login <> u_from.login;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = current_schema()
|
||||||
|
AND table_name = 'direct_messages'
|
||||||
|
) THEN
|
||||||
|
DELETE FROM direct_messages d
|
||||||
|
USING solana_user_pda_current u_from,
|
||||||
|
solana_user_pda_current u_to
|
||||||
|
WHERE u_from.normalized_login = LOWER(BTRIM(d.from_login))
|
||||||
|
AND u_to.normalized_login = LOWER(BTRIM(d.to_login))
|
||||||
|
AND (d.from_login <> u_from.login OR d.to_login <> u_to.login);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 5, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE signed_messages
|
||||||
|
DROP CONSTRAINT IF EXISTS signed_messages_from_login_fkey;
|
||||||
|
|
||||||
|
ALTER TABLE signed_messages
|
||||||
|
DROP CONSTRAINT IF EXISTS signed_messages_to_login_fkey;
|
||||||
|
|
||||||
|
ALTER TABLE signed_direct_messages_history
|
||||||
|
DROP CONSTRAINT IF EXISTS signed_direct_messages_history_from_login_fkey;
|
||||||
|
|
||||||
|
ALTER TABLE signed_direct_messages_history
|
||||||
|
DROP CONSTRAINT IF EXISTS signed_direct_messages_history_to_login_fkey;
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 6, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE blocks
|
||||||
|
DROP CONSTRAINT IF EXISTS blocks_login_fkey;
|
||||||
|
|
||||||
|
ALTER TABLE blocks
|
||||||
|
ADD CONSTRAINT blocks_login_fkey
|
||||||
|
FOREIGN KEY (login) REFERENCES solana_user_pda_current(normalized_login);
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 7, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE connections_state
|
||||||
|
DROP CONSTRAINT IF EXISTS connections_state_login_fkey;
|
||||||
|
|
||||||
|
ALTER TABLE connections_state
|
||||||
|
ADD CONSTRAINT connections_state_login_fkey
|
||||||
|
FOREIGN KEY (login) REFERENCES solana_user_pda_current(normalized_login);
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 8, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_settings (
|
||||||
|
login TEXT NOT NULL REFERENCES solana_user_pda_current(login),
|
||||||
|
setting_type INTEGER NOT NULL,
|
||||||
|
setting_key TEXT NOT NULL,
|
||||||
|
time_ms BIGINT NOT NULL,
|
||||||
|
value_text TEXT NOT NULL DEFAULT '',
|
||||||
|
value_num BIGINT NOT NULL DEFAULT 0,
|
||||||
|
client_key TEXT NOT NULL DEFAULT '',
|
||||||
|
signature TEXT NOT NULL DEFAULT '',
|
||||||
|
synced BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
UNIQUE (login, setting_type, setting_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_settings_login
|
||||||
|
ON user_settings(login);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_settings_synced
|
||||||
|
ON user_settings(login, synced, time_ms);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_settings_type_key
|
||||||
|
ON user_settings(setting_type, setting_key);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_settings_sync_peer_state (
|
||||||
|
owner_login TEXT NOT NULL,
|
||||||
|
remote_server_login TEXT NOT NULL,
|
||||||
|
remote_server_url TEXT NOT NULL,
|
||||||
|
cursor_time_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
cursor_setting_key TEXT NOT NULL DEFAULT '',
|
||||||
|
bootstrap_completed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
last_sync_at_ms BIGINT,
|
||||||
|
last_error TEXT,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (owner_login, remote_server_login)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_settings_sync_peer_state_owner
|
||||||
|
ON user_settings_sync_peer_state(owner_login);
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 9, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS db_schema_version (
|
|||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
VALUES (1, 3, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
VALUES (1, 13, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
schema_version = EXCLUDED.schema_version,
|
schema_version = EXCLUDED.schema_version,
|
||||||
updated_at_ms = EXCLUDED.updated_at_ms;
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
@@ -74,6 +74,7 @@ CREATE INDEX IF NOT EXISTS idx_sync_tx_history_login
|
|||||||
CREATE TABLE IF NOT EXISTS solana_user_pda_current (
|
CREATE TABLE IF NOT EXISTS solana_user_pda_current (
|
||||||
pda_address TEXT PRIMARY KEY,
|
pda_address TEXT PRIMARY KEY,
|
||||||
login TEXT NOT NULL UNIQUE,
|
login TEXT NOT NULL UNIQUE,
|
||||||
|
normalized_login TEXT NOT NULL,
|
||||||
record_number INTEGER NOT NULL,
|
record_number INTEGER NOT NULL,
|
||||||
slot BIGINT NOT NULL,
|
slot BIGINT NOT NULL,
|
||||||
last_tx_signature TEXT NOT NULL,
|
last_tx_signature TEXT NOT NULL,
|
||||||
@@ -109,6 +110,12 @@ CREATE TABLE IF NOT EXISTS solana_user_pda_current (
|
|||||||
CREATE INDEX IF NOT EXISTS idx_user_pda_current_slot
|
CREATE INDEX IF NOT EXISTS idx_user_pda_current_slot
|
||||||
ON solana_user_pda_current(slot);
|
ON solana_user_pda_current(slot);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_solana_user_pda_current_normalized_login
|
||||||
|
ON solana_user_pda_current(normalized_login);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_pda_current_normalized_login
|
||||||
|
ON solana_user_pda_current(normalized_login);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS user_access_servers_current (
|
CREATE TABLE IF NOT EXISTS user_access_servers_current (
|
||||||
user_login TEXT NOT NULL REFERENCES solana_user_pda_current(login) ON DELETE CASCADE,
|
user_login TEXT NOT NULL REFERENCES solana_user_pda_current(login) ON DELETE CASCADE,
|
||||||
server_login TEXT NOT NULL REFERENCES solana_user_pda_current(login) ON DELETE CASCADE,
|
server_login TEXT NOT NULL REFERENCES solana_user_pda_current(login) ON DELETE CASCADE,
|
||||||
@@ -162,12 +169,16 @@ BEGIN
|
|||||||
s.updated_at_ms,
|
s.updated_at_ms,
|
||||||
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
FROM solana_user_pda_current u
|
FROM solana_user_pda_current u
|
||||||
CROSS JOIN LATERAL jsonb_array_elements_text(
|
CROSS JOIN LATERAL (
|
||||||
CASE
|
SELECT login_value
|
||||||
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
FROM jsonb_array_elements_text(
|
||||||
ELSE u.access_servers_json::jsonb
|
CASE
|
||||||
END
|
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||||
) AS access_server(login_value)
|
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
|
JOIN solana_user_pda_current s
|
||||||
ON LOWER(s.login) = LOWER(btrim(access_server.login_value))
|
ON LOWER(s.login) = LOWER(btrim(access_server.login_value))
|
||||||
AND s.is_server = TRUE
|
AND s.is_server = TRUE
|
||||||
@@ -201,12 +212,16 @@ BEGIN
|
|||||||
FOR affected_user IN
|
FOR affected_user IN
|
||||||
SELECT u.login
|
SELECT u.login
|
||||||
FROM solana_user_pda_current u
|
FROM solana_user_pda_current u
|
||||||
CROSS JOIN LATERAL jsonb_array_elements_text(
|
CROSS JOIN LATERAL (
|
||||||
CASE
|
SELECT login_value
|
||||||
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
FROM jsonb_array_elements_text(
|
||||||
ELSE u.access_servers_json::jsonb
|
CASE
|
||||||
END
|
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||||
) AS access_server(login_value)
|
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)
|
WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login)
|
||||||
LOOP
|
LOOP
|
||||||
PERFORM shine_refresh_user_access_servers_for_user(affected_user.login);
|
PERFORM shine_refresh_user_access_servers_for_user(affected_user.login);
|
||||||
@@ -386,6 +401,44 @@ CREATE TABLE IF NOT EXISTS users_params (
|
|||||||
CREATE INDEX IF NOT EXISTS idx_users_params_login
|
CREATE INDEX IF NOT EXISTS idx_users_params_login
|
||||||
ON users_params(login);
|
ON users_params(login);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_settings (
|
||||||
|
login TEXT NOT NULL REFERENCES solana_user_pda_current(login),
|
||||||
|
setting_type INTEGER NOT NULL,
|
||||||
|
setting_key TEXT NOT NULL,
|
||||||
|
time_ms BIGINT NOT NULL,
|
||||||
|
value_text TEXT NOT NULL DEFAULT '',
|
||||||
|
value_num BIGINT NOT NULL DEFAULT 0,
|
||||||
|
client_key TEXT NOT NULL DEFAULT '',
|
||||||
|
signature TEXT NOT NULL DEFAULT '',
|
||||||
|
synced BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
UNIQUE (login, setting_type, setting_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_settings_login
|
||||||
|
ON user_settings(login);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_settings_synced
|
||||||
|
ON user_settings(login, synced, time_ms);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_settings_type_key
|
||||||
|
ON user_settings(setting_type, setting_key);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_settings_sync_peer_state (
|
||||||
|
owner_login TEXT NOT NULL,
|
||||||
|
remote_server_login TEXT NOT NULL,
|
||||||
|
remote_server_url TEXT NOT NULL,
|
||||||
|
cursor_time_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
cursor_setting_key TEXT NOT NULL DEFAULT '',
|
||||||
|
bootstrap_completed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
last_sync_at_ms BIGINT,
|
||||||
|
last_error TEXT,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (owner_login, remote_server_login)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_settings_sync_peer_state_owner
|
||||||
|
ON user_settings_sync_peer_state(owner_login);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ip_geo_cache (
|
CREATE TABLE IF NOT EXISTS ip_geo_cache (
|
||||||
ip TEXT PRIMARY KEY,
|
ip TEXT PRIMARY KEY,
|
||||||
geo TEXT,
|
geo TEXT,
|
||||||
@@ -432,7 +485,7 @@ CREATE INDEX IF NOT EXISTS idx_blockchain_state_updated_at
|
|||||||
ON blockchain_state(updated_at_ms);
|
ON blockchain_state(updated_at_ms);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS blocks (
|
CREATE TABLE IF NOT EXISTS blocks (
|
||||||
login TEXT NOT NULL REFERENCES solana_user_pda_current(login),
|
login TEXT NOT NULL REFERENCES solana_user_pda_current(normalized_login),
|
||||||
bch_name TEXT NOT NULL REFERENCES blockchain_state(blockchain_name),
|
bch_name TEXT NOT NULL REFERENCES blockchain_state(blockchain_name),
|
||||||
block_number INTEGER NOT NULL CHECK (block_number >= 0),
|
block_number INTEGER NOT NULL CHECK (block_number >= 0),
|
||||||
msg_type INTEGER NOT NULL,
|
msg_type INTEGER NOT NULL,
|
||||||
@@ -462,7 +515,7 @@ CREATE INDEX IF NOT EXISTS idx_blocks_by_line
|
|||||||
ON blocks (bch_name, line_code, this_line_number);
|
ON blocks (bch_name, line_code, this_line_number);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS connections_state (
|
CREATE TABLE IF NOT EXISTS connections_state (
|
||||||
login TEXT NOT NULL REFERENCES solana_user_pda_current(login),
|
login TEXT NOT NULL REFERENCES solana_user_pda_current(normalized_login),
|
||||||
rel_type INTEGER NOT NULL,
|
rel_type INTEGER NOT NULL,
|
||||||
to_login TEXT NOT NULL,
|
to_login TEXT NOT NULL,
|
||||||
to_bch_name TEXT NOT NULL,
|
to_bch_name TEXT NOT NULL,
|
||||||
@@ -536,7 +589,11 @@ CREATE TABLE IF NOT EXISTS channel_names_state (
|
|||||||
channel_type_version INTEGER NOT NULL DEFAULT 1,
|
channel_type_version INTEGER NOT NULL DEFAULT 1,
|
||||||
channel_root_block_number INTEGER NOT NULL,
|
channel_root_block_number INTEGER NOT NULL,
|
||||||
channel_root_block_hash BYTEA NOT NULL,
|
channel_root_block_hash BYTEA NOT NULL,
|
||||||
created_at_ms BIGINT NOT NULL
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
ava_ar TEXT NOT NULL DEFAULT '',
|
||||||
|
ava_sha256 TEXT NOT NULL DEFAULT '',
|
||||||
|
ava_size BIGINT NOT NULL DEFAULT 0,
|
||||||
|
meta_updated_at_ms BIGINT NOT NULL DEFAULT 0
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_names_state_owner_type_slug
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_names_state_owner_type_slug
|
||||||
@@ -612,8 +669,8 @@ CREATE INDEX IF NOT EXISTS idx_signed_dm_replay_created
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS signed_direct_messages_history (
|
CREATE TABLE IF NOT EXISTS signed_direct_messages_history (
|
||||||
message_id TEXT PRIMARY KEY,
|
message_id TEXT PRIMARY KEY,
|
||||||
from_login TEXT NOT NULL REFERENCES solana_user_pda_current(login),
|
from_login TEXT NOT NULL,
|
||||||
to_login TEXT NOT NULL REFERENCES solana_user_pda_current(login),
|
to_login TEXT NOT NULL,
|
||||||
target_mode INTEGER NOT NULL,
|
target_mode INTEGER NOT NULL,
|
||||||
target_session_id TEXT,
|
target_session_id TEXT,
|
||||||
message_type INTEGER NOT NULL,
|
message_type INTEGER NOT NULL,
|
||||||
@@ -630,8 +687,8 @@ CREATE TABLE IF NOT EXISTS signed_messages (
|
|||||||
message_key TEXT PRIMARY KEY,
|
message_key TEXT PRIMARY KEY,
|
||||||
base_key TEXT NOT NULL,
|
base_key TEXT NOT NULL,
|
||||||
target_login TEXT NOT NULL,
|
target_login TEXT NOT NULL,
|
||||||
from_login TEXT NOT NULL REFERENCES solana_user_pda_current(login),
|
from_login TEXT NOT NULL,
|
||||||
to_login TEXT NOT NULL REFERENCES solana_user_pda_current(login),
|
to_login TEXT NOT NULL,
|
||||||
time_ms BIGINT NOT NULL,
|
time_ms BIGINT NOT NULL,
|
||||||
nonce BIGINT NOT NULL,
|
nonce BIGINT NOT NULL,
|
||||||
message_type INTEGER NOT NULL,
|
message_type INTEGER NOT NULL,
|
||||||
@@ -694,6 +751,47 @@ CREATE TABLE IF NOT EXISTS signed_message_session_delivery (
|
|||||||
CREATE INDEX IF NOT EXISTS idx_signed_message_delivery_session
|
CREATE INDEX IF NOT EXISTS idx_signed_message_delivery_session
|
||||||
ON signed_message_session_delivery(session_id, delivered);
|
ON signed_message_session_delivery(session_id, delivered);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_notifications_state (
|
||||||
|
owner_login TEXT NOT NULL REFERENCES solana_user_pda_current(normalized_login),
|
||||||
|
notification_kind TEXT NOT NULL CHECK (notification_kind IN ('reply', 'connection')),
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
source_login TEXT NOT NULL,
|
||||||
|
source_bch_name TEXT NOT NULL,
|
||||||
|
source_block_number INTEGER NOT NULL CHECK (source_block_number >= 0),
|
||||||
|
source_block_hash BYTEA NOT NULL,
|
||||||
|
target_login TEXT,
|
||||||
|
target_bch_name TEXT,
|
||||||
|
target_block_number INTEGER,
|
||||||
|
target_block_hash BYTEA,
|
||||||
|
source_msg_sub_type INTEGER NOT NULL,
|
||||||
|
source_text TEXT NOT NULL DEFAULT '',
|
||||||
|
UNIQUE (owner_login, notification_kind, source_bch_name, source_block_number, source_block_hash)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_notifications_state_owner_kind_time
|
||||||
|
ON user_notifications_state(owner_login, notification_kind, created_at_ms DESC, source_block_number DESC);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_notifications_state_owner_time
|
||||||
|
ON user_notifications_state(owner_login, created_at_ms DESC);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_dialog_state (
|
||||||
|
owner_login TEXT NOT NULL,
|
||||||
|
peer_login TEXT NOT NULL,
|
||||||
|
relation_flag TEXT NOT NULL DEFAULT 'none',
|
||||||
|
last_message_blob_b64 TEXT NOT NULL DEFAULT '',
|
||||||
|
last_message_key TEXT NOT NULL DEFAULT '',
|
||||||
|
last_message_type INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_message_time_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
last_message_revision_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
unread_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_read_receipt_time_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (owner_login, peer_login)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_dialog_state_owner_last_time
|
||||||
|
ON dm_dialog_state(owner_login, last_message_time_ms DESC, peer_login);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS dm_sync_peer_state (
|
CREATE TABLE IF NOT EXISTS dm_sync_peer_state (
|
||||||
owner_login TEXT NOT NULL,
|
owner_login TEXT NOT NULL,
|
||||||
remote_server_login TEXT NOT NULL,
|
remote_server_login TEXT NOT NULL,
|
||||||
@@ -710,6 +808,51 @@ CREATE TABLE IF NOT EXISTS dm_sync_peer_state (
|
|||||||
CREATE INDEX IF NOT EXISTS idx_dm_sync_peer_state_owner
|
CREATE INDEX IF NOT EXISTS idx_dm_sync_peer_state_owner
|
||||||
ON dm_sync_peer_state(owner_login);
|
ON dm_sync_peer_state(owner_login);
|
||||||
|
|
||||||
|
-- Изменяемое состояние доставки исходящей пары. Подписанные блоки остаются
|
||||||
|
-- неизменяемыми; эта таблица описывает только сетевую доставку пары 1/2 или 3/4.
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_delivery_state (
|
||||||
|
outgoing_message_key TEXT PRIMARY KEY,
|
||||||
|
event_id TEXT NOT NULL UNIQUE,
|
||||||
|
base_key TEXT NOT NULL,
|
||||||
|
from_login TEXT NOT NULL,
|
||||||
|
to_login TEXT NOT NULL,
|
||||||
|
incoming_message_key TEXT NOT NULL,
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
delivery_expires_at_ms BIGINT NOT NULL,
|
||||||
|
delivery_state INTEGER NOT NULL DEFAULT 0 CHECK (delivery_state IN (0, 1, 2, 3)),
|
||||||
|
delivered_server_login TEXT,
|
||||||
|
recipient_routes_hash TEXT,
|
||||||
|
attempt_index INTEGER NOT NULL DEFAULT 0,
|
||||||
|
next_attempt_at_ms BIGINT,
|
||||||
|
last_attempt_at_ms BIGINT,
|
||||||
|
last_error TEXT,
|
||||||
|
updated_at_ms BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_delivery_state_due
|
||||||
|
ON dm_delivery_state(next_attempt_at_ms, delivery_state)
|
||||||
|
WHERE delivery_state = 0 AND next_attempt_at_ms IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_delivery_state_base
|
||||||
|
ON dm_delivery_state(base_key, from_login);
|
||||||
|
|
||||||
|
-- У одного пользователя теперь максимум один второй access-сервер, поэтому
|
||||||
|
-- достаточно одного флага ACK на событие. Курсор по времени больше не нужен.
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_sync_outbox (
|
||||||
|
owner_login TEXT NOT NULL,
|
||||||
|
primary_message_key TEXT NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
secondary_message_key TEXT,
|
||||||
|
synced BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (owner_login, primary_message_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_sync_outbox_unsynced
|
||||||
|
ON dm_sync_outbox(owner_login, created_at_ms, primary_message_key)
|
||||||
|
WHERE synced = FALSE;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS message_views_state (
|
CREATE TABLE IF NOT EXISTS message_views_state (
|
||||||
viewer_login TEXT NOT NULL,
|
viewer_login TEXT NOT NULL,
|
||||||
to_bch_name TEXT NOT NULL,
|
to_bch_name TEXT NOT NULL,
|
||||||
|
|||||||
+36
-2
@@ -60,6 +60,12 @@ import server.logic.ws_protocol.JSON.handlers.userParams.Net_UpsertUserParam_Han
|
|||||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Request;
|
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Request;
|
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request;
|
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.Net_GetUserSetting_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.Net_ListUserSettings_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.Net_UpsertUserSetting_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.entyties.Net_GetUserSetting_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.entyties.Net_ListUserSettings_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.entyties.Net_UpsertUserSetting_Request;
|
||||||
|
|
||||||
// --- NEW: connections friends lists ---
|
// --- NEW: connections friends lists ---
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.Net_GetFriendsLists_Handler;
|
import server.logic.ws_protocol.JSON.handlers.connections.Net_GetFriendsLists_Handler;
|
||||||
@@ -67,6 +73,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.ChannelNamesStateBootstrapper;
|
||||||
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetChannelMessages_Handler;
|
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_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_GetGroupDialog_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetChannelsCounters_Handler;
|
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetChannelsCounters_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.channels.Net_ListGroupChats200_Handler;
|
import server.logic.ws_protocol.JSON.handlers.channels.Net_ListGroupChats200_Handler;
|
||||||
@@ -75,24 +82,29 @@ 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_GetChannelMessages_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetGroupDialog_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_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_ListGroupChats200_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListSubscriptionsFeed_Request;
|
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListSubscriptionsFeed_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.Net_GetUserConnectionsGraph_Handler;
|
import server.logic.ws_protocol.JSON.handlers.connections.Net_GetUserConnectionsGraph_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.Net_AddCloseFriend_Handler;
|
import server.logic.ws_protocol.JSON.handlers.connections.Net_AddCloseFriend_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.Net_ListContacts_Handler;
|
import server.logic.ws_protocol.JSON.handlers.connections.Net_ListContacts_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.notifications.Net_GetNotifications_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetUserConnectionsGraph_Request;
|
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetUserConnectionsGraph_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_AddCloseFriend_Request;
|
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_AddCloseFriend_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_ListContacts_Request;
|
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_ListContacts_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.notifications.entyties.Net_GetNotifications_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_AckSessionDelivery_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_AckSessionDelivery_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_CallInviteBroadcast_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_CallInviteBroadcast_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_CallSignalToSession_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_CallSignalToSession_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_DeleteConversation_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_DeleteConversation_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_DeleteMessage_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_DeleteMessage_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.Net_GetDmDeliveryStatus_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.Net_MarkAllUserSettingsUnsynced_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_DmSyncBatch_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_DmSyncBatch_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_GetDirectMessages_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_GetDirectMessages_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.Net_UserSettingsSyncBatch_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_SendSignal_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_SendSignal_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_ReceiveIncomingMessage_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_ReceiveIncomingMessage_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_SendDirectMessage_Handler;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_SendMessagePair_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_SendMessagePair_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_SendTestWebPush_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_SendTestWebPush_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_UpsertPushToken_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_UpsertPushToken_Handler;
|
||||||
@@ -101,11 +113,13 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_CallInviteBroadcast_R
|
|||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_CallSignalToSession_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_CallSignalToSession_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_DeleteConversation_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_DeleteConversation_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_DeleteMessage_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_DeleteMessage_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDmDeliveryStatus_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_MarkAllUserSettingsUnsynced_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_DmSyncBatch_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_DmSyncBatch_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDirectMessages_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDirectMessages_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_UserSettingsSyncBatch_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendSignal_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendSignal_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_ReceiveIncomingMessage_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_ReceiveIncomingMessage_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendDirectMessage_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendMessagePair_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendMessagePair_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendTestWebPush_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendTestWebPush_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_UpsertPushToken_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_UpsertPushToken_Request;
|
||||||
@@ -180,10 +194,16 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("GetUserParam", new Net_GetUserParam_Handler()),
|
Map.entry("GetUserParam", new Net_GetUserParam_Handler()),
|
||||||
Map.entry("ListUserParams", new Net_ListUserParams_Handler()),
|
Map.entry("ListUserParams", new Net_ListUserParams_Handler()),
|
||||||
|
|
||||||
|
// --- userSettings ---
|
||||||
|
Map.entry("UpsertUserSetting", new Net_UpsertUserSetting_Handler()),
|
||||||
|
Map.entry("GetUserSetting", new Net_GetUserSetting_Handler()),
|
||||||
|
Map.entry("ListUserSettings", new Net_ListUserSettings_Handler()),
|
||||||
|
|
||||||
// --- connections ---
|
// --- connections ---
|
||||||
Map.entry("GetFriendsLists", new Net_GetFriendsLists_Handler()),
|
Map.entry("GetFriendsLists", new Net_GetFriendsLists_Handler()),
|
||||||
Map.entry("ListSubscriptionsFeed", new Net_ListSubscriptionsFeed_Handler()),
|
Map.entry("ListSubscriptionsFeed", new Net_ListSubscriptionsFeed_Handler()),
|
||||||
Map.entry("GetChannelMessages", new Net_GetChannelMessages_Handler()),
|
Map.entry("GetChannelMessages", new Net_GetChannelMessages_Handler()),
|
||||||
|
Map.entry("GetPersonalDiary", new Net_GetPersonalDiary_Handler()),
|
||||||
Map.entry("GetMessageThread", new Net_GetMessageThread_Handler()),
|
Map.entry("GetMessageThread", new Net_GetMessageThread_Handler()),
|
||||||
Map.entry("GetGroupDialog", new Net_GetGroupDialog_Handler()),
|
Map.entry("GetGroupDialog", new Net_GetGroupDialog_Handler()),
|
||||||
Map.entry("ListGroupChats200", new Net_ListGroupChats200_Handler()),
|
Map.entry("ListGroupChats200", new Net_ListGroupChats200_Handler()),
|
||||||
@@ -191,6 +211,7 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("ListContacts", new Net_ListContacts_Handler()),
|
Map.entry("ListContacts", new Net_ListContacts_Handler()),
|
||||||
Map.entry("GetUserConnectionsGraph", new Net_GetUserConnectionsGraph_Handler()),
|
Map.entry("GetUserConnectionsGraph", new Net_GetUserConnectionsGraph_Handler()),
|
||||||
Map.entry("AddCloseFriend", new Net_AddCloseFriend_Handler()),
|
Map.entry("AddCloseFriend", new Net_AddCloseFriend_Handler()),
|
||||||
|
Map.entry("GetNotifications", new Net_GetNotifications_Handler()),
|
||||||
|
|
||||||
// --- direct messages / push ---
|
// --- direct messages / push ---
|
||||||
Map.entry("UpsertPushToken", new Net_UpsertPushToken_Handler()),
|
Map.entry("UpsertPushToken", new Net_UpsertPushToken_Handler()),
|
||||||
@@ -201,6 +222,9 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("DeleteMessage", new Net_DeleteMessage_Handler()),
|
Map.entry("DeleteMessage", new Net_DeleteMessage_Handler()),
|
||||||
Map.entry("DeleteConversation", new Net_DeleteConversation_Handler()),
|
Map.entry("DeleteConversation", new Net_DeleteConversation_Handler()),
|
||||||
Map.entry("DmSyncBatch", new Net_DmSyncBatch_Handler()),
|
Map.entry("DmSyncBatch", new Net_DmSyncBatch_Handler()),
|
||||||
|
Map.entry("GetDmDeliveryStatus", new Net_GetDmDeliveryStatus_Handler()),
|
||||||
|
Map.entry("UserSettingsSyncBatch", new Net_UserSettingsSyncBatch_Handler()),
|
||||||
|
Map.entry("MarkAllUserSettingsUnsynced", new Net_MarkAllUserSettingsUnsynced_Handler()),
|
||||||
Map.entry("GetDirectMessages", new Net_GetDirectMessages_Handler()),
|
Map.entry("GetDirectMessages", new Net_GetDirectMessages_Handler()),
|
||||||
Map.entry("AckSessionDelivery", new Net_AckSessionDelivery_Handler()),
|
Map.entry("AckSessionDelivery", new Net_AckSessionDelivery_Handler()),
|
||||||
Map.entry("CallInviteBroadcast", new Net_CallInviteBroadcast_Handler()),
|
Map.entry("CallInviteBroadcast", new Net_CallInviteBroadcast_Handler()),
|
||||||
@@ -261,10 +285,16 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("GetUserParam", Net_GetUserParam_Request.class),
|
Map.entry("GetUserParam", Net_GetUserParam_Request.class),
|
||||||
Map.entry("ListUserParams", Net_ListUserParams_Request.class),
|
Map.entry("ListUserParams", Net_ListUserParams_Request.class),
|
||||||
|
|
||||||
|
// --- userSettings ---
|
||||||
|
Map.entry("UpsertUserSetting", Net_UpsertUserSetting_Request.class),
|
||||||
|
Map.entry("GetUserSetting", Net_GetUserSetting_Request.class),
|
||||||
|
Map.entry("ListUserSettings", Net_ListUserSettings_Request.class),
|
||||||
|
|
||||||
// --- connections ---
|
// --- connections ---
|
||||||
Map.entry("GetFriendsLists", Net_GetFriendsLists_Request.class),
|
Map.entry("GetFriendsLists", Net_GetFriendsLists_Request.class),
|
||||||
Map.entry("ListSubscriptionsFeed", Net_ListSubscriptionsFeed_Request.class),
|
Map.entry("ListSubscriptionsFeed", Net_ListSubscriptionsFeed_Request.class),
|
||||||
Map.entry("GetChannelMessages", Net_GetChannelMessages_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("GetMessageThread", Net_GetMessageThread_Request.class),
|
||||||
Map.entry("GetGroupDialog", Net_GetGroupDialog_Request.class),
|
Map.entry("GetGroupDialog", Net_GetGroupDialog_Request.class),
|
||||||
Map.entry("ListGroupChats200", Net_ListGroupChats200_Request.class),
|
Map.entry("ListGroupChats200", Net_ListGroupChats200_Request.class),
|
||||||
@@ -272,6 +302,7 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("ListContacts", Net_ListContacts_Request.class),
|
Map.entry("ListContacts", Net_ListContacts_Request.class),
|
||||||
Map.entry("GetUserConnectionsGraph", Net_GetUserConnectionsGraph_Request.class),
|
Map.entry("GetUserConnectionsGraph", Net_GetUserConnectionsGraph_Request.class),
|
||||||
Map.entry("AddCloseFriend", Net_AddCloseFriend_Request.class),
|
Map.entry("AddCloseFriend", Net_AddCloseFriend_Request.class),
|
||||||
|
Map.entry("GetNotifications", Net_GetNotifications_Request.class),
|
||||||
|
|
||||||
// --- direct messages / push ---
|
// --- direct messages / push ---
|
||||||
Map.entry("UpsertPushToken", Net_UpsertPushToken_Request.class),
|
Map.entry("UpsertPushToken", Net_UpsertPushToken_Request.class),
|
||||||
@@ -282,6 +313,9 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("DeleteMessage", Net_DeleteMessage_Request.class),
|
Map.entry("DeleteMessage", Net_DeleteMessage_Request.class),
|
||||||
Map.entry("DeleteConversation", Net_DeleteConversation_Request.class),
|
Map.entry("DeleteConversation", Net_DeleteConversation_Request.class),
|
||||||
Map.entry("DmSyncBatch", Net_DmSyncBatch_Request.class),
|
Map.entry("DmSyncBatch", Net_DmSyncBatch_Request.class),
|
||||||
|
Map.entry("GetDmDeliveryStatus", Net_GetDmDeliveryStatus_Request.class),
|
||||||
|
Map.entry("UserSettingsSyncBatch", Net_UserSettingsSyncBatch_Request.class),
|
||||||
|
Map.entry("MarkAllUserSettingsUnsynced", Net_MarkAllUserSettingsUnsynced_Request.class),
|
||||||
Map.entry("GetDirectMessages", Net_GetDirectMessages_Request.class),
|
Map.entry("GetDirectMessages", Net_GetDirectMessages_Request.class),
|
||||||
Map.entry("AckSessionDelivery", Net_AckSessionDelivery_Request.class),
|
Map.entry("AckSessionDelivery", Net_AckSessionDelivery_Request.class),
|
||||||
Map.entry("CallInviteBroadcast", Net_CallInviteBroadcast_Request.class),
|
Map.entry("CallInviteBroadcast", Net_CallInviteBroadcast_Request.class),
|
||||||
|
|||||||
+9
-1
@@ -27,6 +27,7 @@ public final class SolanaUserPdaImportService {
|
|||||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
private static final HttpClient HTTP = HttpClient.newHttpClient();
|
private static final HttpClient HTTP = HttpClient.newHttpClient();
|
||||||
private static final String MAGIC = "SHiNE";
|
private static final String MAGIC = "SHiNE";
|
||||||
|
private static final int MAX_EFFECTIVE_ACCESS_SERVERS = 2;
|
||||||
|
|
||||||
private SolanaUserPdaImportService() {}
|
private SolanaUserPdaImportService() {}
|
||||||
|
|
||||||
@@ -87,6 +88,7 @@ public final class SolanaUserPdaImportService {
|
|||||||
String serverAddress = safe(serverProfile.serverAddress());
|
String serverAddress = safe(serverProfile.serverAddress());
|
||||||
if (serverAddress.isBlank()) continue;
|
if (serverAddress.isBlank()) continue;
|
||||||
routes.putIfAbsent(normalized, new ParsedServerRoute(normalized, serverAddress));
|
routes.putIfAbsent(normalized, new ParsedServerRoute(normalized, serverAddress));
|
||||||
|
if (routes.size() >= MAX_EFFECTIVE_ACCESS_SERVERS) break;
|
||||||
}
|
}
|
||||||
return new ArrayList<>(routes.values());
|
return new ArrayList<>(routes.values());
|
||||||
}
|
}
|
||||||
@@ -234,7 +236,13 @@ public final class SolanaUserPdaImportService {
|
|||||||
int n = u8(raw, c++);
|
int n = u8(raw, c++);
|
||||||
String accessServerLogin = new String(raw, c, n, StandardCharsets.UTF_8);
|
String accessServerLogin = new String(raw, c, n, StandardCharsets.UTF_8);
|
||||||
c += n;
|
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) {
|
} else if (blockType == 50) {
|
||||||
int sessionsMode = u8(raw, c++);
|
int sessionsMode = u8(raw, c++);
|
||||||
|
|||||||
+243
-8
@@ -5,7 +5,10 @@ import blockchain.BchCryptoVerifier;
|
|||||||
import blockchain.MsgSubType;
|
import blockchain.MsgSubType;
|
||||||
import blockchain.body.BodyHasLine;
|
import blockchain.body.BodyHasLine;
|
||||||
import blockchain.body.BodyHasTarget;
|
import blockchain.body.BodyHasTarget;
|
||||||
|
import blockchain.body.ConnectionBody;
|
||||||
import blockchain.body.CreateChannelBody;
|
import blockchain.body.CreateChannelBody;
|
||||||
|
import blockchain.body.StatusActionBody;
|
||||||
|
import blockchain.body.TextReplyBody;
|
||||||
import blockchain.body.TextLineBody;
|
import blockchain.body.TextLineBody;
|
||||||
import blockchain.body.UserParamBody;
|
import blockchain.body.UserParamBody;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -21,6 +24,7 @@ import server.sync.BlockchainResyncGuard;
|
|||||||
import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils.BlockchainWriter;
|
import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils.BlockchainWriter;
|
||||||
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;
|
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Response;
|
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.channels.ChannelMetaTextParser;
|
||||||
import server.logic.ws_protocol.JSON.handlers.channels.ChannelNamesStateBootstrapper;
|
import server.logic.ws_protocol.JSON.handlers.channels.ChannelNamesStateBootstrapper;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import server.sync.AddBlockSyncService;
|
import server.sync.AddBlockSyncService;
|
||||||
@@ -28,10 +32,12 @@ import shine.db.channels.ChannelNameRules;
|
|||||||
import shine.db.dao.BlockchainStateDAO;
|
import shine.db.dao.BlockchainStateDAO;
|
||||||
import shine.db.dao.BlocksDAO;
|
import shine.db.dao.BlocksDAO;
|
||||||
import shine.db.dao.ChannelNameStateDAO;
|
import shine.db.dao.ChannelNameStateDAO;
|
||||||
|
import shine.db.dao.UserNotificationsStateDAO;
|
||||||
import shine.db.dao.UserParamsDAO;
|
import shine.db.dao.UserParamsDAO;
|
||||||
import shine.db.entities.BlockchainStateEntry;
|
import shine.db.entities.BlockchainStateEntry;
|
||||||
import shine.db.entities.BlockEntry;
|
import shine.db.entities.BlockEntry;
|
||||||
import shine.db.entities.ChannelNameStateEntry;
|
import shine.db.entities.ChannelNameStateEntry;
|
||||||
|
import shine.db.entities.UserNotificationEntry;
|
||||||
import shine.db.entities.UserParamEntry;
|
import shine.db.entities.UserParamEntry;
|
||||||
import utils.blockchain.BlockchainNameUtil;
|
import utils.blockchain.BlockchainNameUtil;
|
||||||
|
|
||||||
@@ -59,7 +65,8 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
private final ChannelNameStateDAO channelNameStateDAO = ChannelNameStateDAO.getInstance();
|
private final ChannelNameStateDAO channelNameStateDAO = ChannelNameStateDAO.getInstance();
|
||||||
private final AddBlockSyncService addBlockSyncService = new AddBlockSyncService();
|
private final AddBlockSyncService addBlockSyncService = new AddBlockSyncService();
|
||||||
|
|
||||||
private final BlockchainWriter dbWriter = new BlockchainWriter(blocksDAO, stateDAO, userParamsDAO, channelNameStateDAO);
|
private final BlockchainWriter dbWriter = new BlockchainWriter(
|
||||||
|
blocksDAO, stateDAO, userParamsDAO, channelNameStateDAO, UserNotificationsStateDAO.getInstance());
|
||||||
|
|
||||||
public Net_AddBlock_Handler() {
|
public Net_AddBlock_Handler() {
|
||||||
ChannelNamesStateBootstrapper.bootstrapOrFailFast();
|
ChannelNamesStateBootstrapper.bootstrapOrFailFast();
|
||||||
@@ -166,6 +173,9 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
case "db_error_prev_line_check" -> "Ошибка БД при проверке prevLine";
|
case "db_error_prev_line_check" -> "Ошибка БД при проверке prevLine";
|
||||||
case "channel_name_already_exists" -> "Такое название канала уже занято";
|
case "channel_name_already_exists" -> "Такое название канала уже занято";
|
||||||
case "repost_disabled" -> "Репосты временно отключены до будущей реализации";
|
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 "internal_error" -> "Внутренняя ошибка сервера при записи блока";
|
||||||
case "chain_resync_in_progress" -> "Цепочка сейчас пересинхронизируется";
|
case "chain_resync_in_progress" -> "Цепочка сейчас пересинхронизируется";
|
||||||
default -> "Ошибка: " + code;
|
default -> "Ошибка: " + code;
|
||||||
@@ -277,6 +287,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ChannelNameStateEntry channelNameStateEntry = null;
|
ChannelNameStateEntry channelNameStateEntry = null;
|
||||||
|
ChannelNameStateEntry channelMetaUpdateEntry = null;
|
||||||
Chat200CreateSeed chat200CreateSeed = null;
|
Chat200CreateSeed chat200CreateSeed = null;
|
||||||
if (block.body instanceof CreateChannelBody createChannelBody) {
|
if (block.body instanceof CreateChannelBody createChannelBody) {
|
||||||
int channelTypeCode = Short.toUnsignedInt(createChannelBody.channelTypeCode);
|
int channelTypeCode = Short.toUnsignedInt(createChannelBody.channelTypeCode);
|
||||||
@@ -302,14 +313,36 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex);
|
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String displayName = normalizedName;
|
||||||
|
String channelDescription = createChannelBody.channelDescription == null
|
||||||
|
? ""
|
||||||
|
: ChannelNameRules.normalizeDisplayName(createChannelBody.channelDescription);
|
||||||
|
String avaAr = "";
|
||||||
|
String avaSha256 = "";
|
||||||
|
long avaSize = 0L;
|
||||||
|
long metaUpdatedAtMs = 0L;
|
||||||
|
if (channelTypeCode == (CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF)) {
|
||||||
|
try {
|
||||||
|
ChannelMetaTextParser.Parsed createMeta = ChannelMetaTextParser.parseStrict(channelDescription, normalizedName);
|
||||||
|
displayName = createMeta.title();
|
||||||
|
channelDescription = createMeta.description();
|
||||||
|
avaAr = createMeta.avaAr();
|
||||||
|
avaSha256 = createMeta.avaSha256();
|
||||||
|
avaSize = createMeta.avaSize();
|
||||||
|
metaUpdatedAtMs = block.timestamp * 1000L;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, e.getMessage(), serverLastNum, serverLastHashHex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
channelNameStateEntry = new ChannelNameStateEntry();
|
channelNameStateEntry = new ChannelNameStateEntry();
|
||||||
channelNameStateEntry.setSlug(slug);
|
channelNameStateEntry.setSlug(slug);
|
||||||
channelNameStateEntry.setDisplayName(normalizedName);
|
channelNameStateEntry.setDisplayName(displayName);
|
||||||
channelNameStateEntry.setChannelDescription(
|
channelNameStateEntry.setChannelDescription(channelDescription);
|
||||||
createChannelBody.channelDescription == null
|
channelNameStateEntry.setAvaAr(avaAr);
|
||||||
? ""
|
channelNameStateEntry.setAvaSha256(avaSha256);
|
||||||
: ChannelNameRules.normalizeDisplayName(createChannelBody.channelDescription)
|
channelNameStateEntry.setAvaSize(avaSize);
|
||||||
);
|
channelNameStateEntry.setMetaUpdatedAtMs(metaUpdatedAtMs);
|
||||||
channelNameStateEntry.setOwnerLogin(login);
|
channelNameStateEntry.setOwnerLogin(login);
|
||||||
channelNameStateEntry.setOwnerBlockchainName(blockchainName);
|
channelNameStateEntry.setOwnerBlockchainName(blockchainName);
|
||||||
channelNameStateEntry.setChannelTypeCode(channelTypeCode);
|
channelNameStateEntry.setChannelTypeCode(channelTypeCode);
|
||||||
@@ -329,6 +362,66 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
chat200CreateSeed.chatTitle = channelNameStateEntry.getChannelDescription();
|
chat200CreateSeed.chatTitle = channelNameStateEntry.getChannelDescription();
|
||||||
chat200CreateSeed.updatedAtMs = block.timestamp * 1000L;
|
chat200CreateSeed.updatedAtMs = block.timestamp * 1000L;
|
||||||
}
|
}
|
||||||
|
} else if ((block.type & 0xFFFF) == 1
|
||||||
|
&& (block.subType & 0xFFFF) == (MsgSubType.TEXT_CHANNEL_META & 0xFFFF)
|
||||||
|
&& block.body instanceof TextLineBody metaBody) {
|
||||||
|
int lineCode = metaBody.lineCode();
|
||||||
|
if (lineCode <= 0) {
|
||||||
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_channel_meta_line", serverLastNum, serverLastHashHex);
|
||||||
|
}
|
||||||
|
ExistingChannelState existingChannel;
|
||||||
|
try {
|
||||||
|
existingChannel = loadExistingChannelState(blockchainName, lineCode);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("AddBlock: channel_meta_lookup_failed (login={}, blockchainName={}, lineCode={})",
|
||||||
|
login, blockchainName, lineCode, e);
|
||||||
|
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex);
|
||||||
|
}
|
||||||
|
if (existingChannel == null) {
|
||||||
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "channel_not_found", serverLastNum, serverLastHashHex);
|
||||||
|
}
|
||||||
|
ChannelMetaTextParser.Parsed parsedMeta;
|
||||||
|
try {
|
||||||
|
parsedMeta = ChannelMetaTextParser.parseStrict(metaBody.message, existingChannel.slug);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, e.getMessage(), serverLastNum, serverLastHashHex);
|
||||||
|
}
|
||||||
|
channelMetaUpdateEntry = new ChannelNameStateEntry();
|
||||||
|
channelMetaUpdateEntry.setOwnerBlockchainName(blockchainName);
|
||||||
|
channelMetaUpdateEntry.setChannelRootBlockNumber(lineCode);
|
||||||
|
channelMetaUpdateEntry.setDisplayName(parsedMeta.title());
|
||||||
|
channelMetaUpdateEntry.setChannelDescription(parsedMeta.description());
|
||||||
|
channelMetaUpdateEntry.setAvaAr(parsedMeta.avaAr());
|
||||||
|
channelMetaUpdateEntry.setAvaSha256(parsedMeta.avaSha256());
|
||||||
|
channelMetaUpdateEntry.setAvaSize(parsedMeta.avaSize());
|
||||||
|
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
|
// 4.2) запрет дырок: blockNumber строго last+1
|
||||||
@@ -485,7 +578,9 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
dbWriter.appendBlockAndState(blockchainName, block, st, be, upsertedParam, channelNameStateEntry);
|
UserNotificationEntry notificationEntry = buildNotificationEntry(block, be);
|
||||||
|
dbWriter.appendBlockAndState(
|
||||||
|
blockchainName, block, st, be, upsertedParam, channelNameStateEntry, channelMetaUpdateEntry, notificationEntry);
|
||||||
|
|
||||||
if (chat200CreateSeed != null) {
|
if (chat200CreateSeed != null) {
|
||||||
upsertChat200StateFromCreate(chat200CreateSeed);
|
upsertChat200StateFromCreate(chat200CreateSeed);
|
||||||
@@ -544,6 +639,86 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
long updatedAtMs;
|
long updatedAtMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class ExistingChannelState {
|
||||||
|
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("""
|
||||||
|
SELECT slug
|
||||||
|
FROM channel_names_state
|
||||||
|
WHERE owner_bch_name = ? AND channel_root_block_number = ?
|
||||||
|
LIMIT 1
|
||||||
|
""")) {
|
||||||
|
ps.setString(1, ownerBch);
|
||||||
|
ps.setInt(2, rootBlockNumber);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (!rs.next()) return null;
|
||||||
|
ExistingChannelState state = new ExistingChannelState();
|
||||||
|
state.slug = rs.getString("slug");
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void upsertChat200StateFromCreate(Chat200CreateSeed seed) throws Exception {
|
private void upsertChat200StateFromCreate(Chat200CreateSeed seed) throws Exception {
|
||||||
try (Connection c = shine.db.DbController.getInstance().getConnection();
|
try (Connection c = shine.db.DbController.getInstance().getConnection();
|
||||||
PreparedStatement ps = c.prepareStatement("""
|
PreparedStatement ps = c.prepareStatement("""
|
||||||
@@ -687,6 +862,66 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private UserNotificationEntry buildNotificationEntry(BchBlockEntry block, BlockEntry storedEntry) {
|
||||||
|
if (block == null || storedEntry == null) return null;
|
||||||
|
if (storedEntry.getToLogin() == null || storedEntry.getToLogin().isBlank()) return null;
|
||||||
|
|
||||||
|
int msgType = block.type & 0xFFFF;
|
||||||
|
int msgSubType = block.subType & 0xFFFF;
|
||||||
|
long createdAtMs = block.timestamp * 1000L;
|
||||||
|
String ownerLogin = storedEntry.getToLogin();
|
||||||
|
String sourceLogin = storedEntry.getLogin();
|
||||||
|
|
||||||
|
// A user must never receive a notification for replying to their own message/thread.
|
||||||
|
if (ownerLogin.equalsIgnoreCase(sourceLogin)) return null;
|
||||||
|
|
||||||
|
if (msgType == 1
|
||||||
|
&& msgSubType == (MsgSubType.TEXT_REPLY & 0xFFFF)
|
||||||
|
&& block.body instanceof TextReplyBody replyBody) {
|
||||||
|
UserNotificationEntry entry = baseNotificationEntry(storedEntry, ownerLogin, createdAtMs, msgSubType);
|
||||||
|
entry.setNotificationKind("reply");
|
||||||
|
entry.setSourceText(replyBody.message == null ? "" : replyBody.message);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection notifications are intentionally modeled as a generic kind.
|
||||||
|
// Today only CONNECTION_CLOSE_FRIEND is indexed; future incoming connection types
|
||||||
|
// can reuse the same notification kind and expose their code via sourceMsgSubType.
|
||||||
|
if (msgType == 3
|
||||||
|
&& msgSubType == (MsgSubType.CONNECTION_CLOSE_FRIEND & 0xFFFF)
|
||||||
|
&& block.body instanceof ConnectionBody) {
|
||||||
|
UserNotificationEntry entry = baseNotificationEntry(storedEntry, ownerLogin, createdAtMs, msgSubType);
|
||||||
|
entry.setNotificationKind("connection");
|
||||||
|
entry.setSourceText("");
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserNotificationEntry baseNotificationEntry(BlockEntry storedEntry,
|
||||||
|
String ownerLogin,
|
||||||
|
long createdAtMs,
|
||||||
|
int msgSubType) {
|
||||||
|
UserNotificationEntry entry = new UserNotificationEntry();
|
||||||
|
entry.setOwnerLogin(ownerLogin);
|
||||||
|
entry.setCreatedAtMs(createdAtMs);
|
||||||
|
entry.setSourceLogin(storedEntry.getLogin());
|
||||||
|
entry.setSourceBchName(blockchainNameFromEntry(storedEntry));
|
||||||
|
entry.setSourceBlockNumber(storedEntry.getBlockNumber());
|
||||||
|
entry.setSourceBlockHash(storedEntry.getBlockHash());
|
||||||
|
entry.setTargetLogin(storedEntry.getToLogin());
|
||||||
|
entry.setTargetBchName(storedEntry.getToBchName());
|
||||||
|
entry.setTargetBlockNumber(storedEntry.getToBlockNumber());
|
||||||
|
entry.setTargetBlockHash(storedEntry.getToBlockHash());
|
||||||
|
entry.setSourceMsgSubType(msgSubType);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String blockchainNameFromEntry(BlockEntry entry) {
|
||||||
|
return entry == null ? null : entry.getBchName();
|
||||||
|
}
|
||||||
|
|
||||||
private static String toHex(byte[] bytes) {
|
private static String toHex(byte[] bytes) {
|
||||||
if (bytes == null) return "null";
|
if (bytes == null) return "null";
|
||||||
char[] HEX = "0123456789abcdef".toCharArray();
|
char[] HEX = "0123456789abcdef".toCharArray();
|
||||||
|
|||||||
+19
-2
@@ -5,10 +5,12 @@ import shine.db.dao.BlockchainStateDAO;
|
|||||||
import shine.db.dao.BlocksDAO;
|
import shine.db.dao.BlocksDAO;
|
||||||
import shine.db.dao.ChannelNameStateDAO;
|
import shine.db.dao.ChannelNameStateDAO;
|
||||||
import shine.db.dao.UserParamsDAO;
|
import shine.db.dao.UserParamsDAO;
|
||||||
|
import shine.db.dao.UserNotificationsStateDAO;
|
||||||
import shine.db.entities.BlockchainStateEntry;
|
import shine.db.entities.BlockchainStateEntry;
|
||||||
import shine.db.entities.BlockEntry;
|
import shine.db.entities.BlockEntry;
|
||||||
import shine.db.entities.ChannelNameStateEntry;
|
import shine.db.entities.ChannelNameStateEntry;
|
||||||
import shine.db.entities.UserParamEntry;
|
import shine.db.entities.UserParamEntry;
|
||||||
|
import shine.db.entities.UserNotificationEntry;
|
||||||
import utils.files.FileStoreUtil;
|
import utils.files.FileStoreUtil;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -41,16 +43,19 @@ public final class BlockchainWriter {
|
|||||||
private final BlockchainStateDAO stateDAO;
|
private final BlockchainStateDAO stateDAO;
|
||||||
private final ChannelNameStateDAO channelNameStateDAO;
|
private final ChannelNameStateDAO channelNameStateDAO;
|
||||||
private final UserParamsDAO userParamsDAO;
|
private final UserParamsDAO userParamsDAO;
|
||||||
|
private final UserNotificationsStateDAO userNotificationsStateDAO;
|
||||||
private final FileStoreUtil fs = FileStoreUtil.getInstance();
|
private final FileStoreUtil fs = FileStoreUtil.getInstance();
|
||||||
|
|
||||||
public BlockchainWriter(BlocksDAO blocksDAO,
|
public BlockchainWriter(BlocksDAO blocksDAO,
|
||||||
BlockchainStateDAO stateDAO,
|
BlockchainStateDAO stateDAO,
|
||||||
UserParamsDAO userParamsDAO,
|
UserParamsDAO userParamsDAO,
|
||||||
ChannelNameStateDAO channelNameStateDAO) {
|
ChannelNameStateDAO channelNameStateDAO,
|
||||||
|
UserNotificationsStateDAO userNotificationsStateDAO) {
|
||||||
this.blocksDAO = blocksDAO;
|
this.blocksDAO = blocksDAO;
|
||||||
this.stateDAO = stateDAO;
|
this.stateDAO = stateDAO;
|
||||||
this.userParamsDAO = userParamsDAO;
|
this.userParamsDAO = userParamsDAO;
|
||||||
this.channelNameStateDAO = channelNameStateDAO;
|
this.channelNameStateDAO = channelNameStateDAO;
|
||||||
|
this.userNotificationsStateDAO = userNotificationsStateDAO;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void appendBlockAndState(String blockchainName,
|
public void appendBlockAndState(String blockchainName,
|
||||||
@@ -58,7 +63,9 @@ public final class BlockchainWriter {
|
|||||||
BlockchainStateEntry st,
|
BlockchainStateEntry st,
|
||||||
BlockEntry be,
|
BlockEntry be,
|
||||||
UserParamEntry userParamEntry,
|
UserParamEntry userParamEntry,
|
||||||
ChannelNameStateEntry channelNameStateEntry) throws SQLException {
|
ChannelNameStateEntry channelNameStateEntry,
|
||||||
|
ChannelNameStateEntry channelMetaUpdateEntry,
|
||||||
|
UserNotificationEntry notificationEntry) throws SQLException {
|
||||||
|
|
||||||
long nowMs = System.currentTimeMillis();
|
long nowMs = System.currentTimeMillis();
|
||||||
byte[] blockBytes = block.toBytes();
|
byte[] blockBytes = block.toBytes();
|
||||||
@@ -91,6 +98,16 @@ public final class BlockchainWriter {
|
|||||||
channelNameStateDAO.insert(c, channelNameStateEntry);
|
channelNameStateDAO.insert(c, channelNameStateEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (channelMetaUpdateEntry != null) {
|
||||||
|
channelNameStateDAO.updateMeta(c, channelMetaUpdateEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification projection is part of the same SQL transaction as the block.
|
||||||
|
// If notification indexing fails, the block/state write is rolled back as well.
|
||||||
|
if (notificationEntry != null) {
|
||||||
|
userNotificationsStateDAO.upsert(c, notificationEntry);
|
||||||
|
}
|
||||||
|
|
||||||
c.commit();
|
c.commit();
|
||||||
committed = true;
|
committed = true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.channels;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class ChannelMetaTextParser {
|
||||||
|
public static final int MAX_TITLE_CHARS = 50;
|
||||||
|
public static final int MAX_DESCRIPTION_CHARS = 250;
|
||||||
|
private static final String LEGACY_PREFIX = "<SHiNE:";
|
||||||
|
private static final String SHORT_PREFIX = "<S:";
|
||||||
|
|
||||||
|
private ChannelMetaTextParser() {}
|
||||||
|
|
||||||
|
public static Parsed parseStrict(String rawText, String fallbackTitle) {
|
||||||
|
String text = String.valueOf(rawText == null ? "" : rawText);
|
||||||
|
String title = "";
|
||||||
|
String avaAr = "";
|
||||||
|
String avaSha256 = "";
|
||||||
|
long avaSize = 0L;
|
||||||
|
boolean seenTitle = false;
|
||||||
|
boolean seenAvatar = false;
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
while (startsWithTagPrefix(text, offset)) {
|
||||||
|
int end = text.indexOf('>', offset);
|
||||||
|
if (end < 0) throw new IllegalArgumentException("bad_channel_meta_tag");
|
||||||
|
int prefixLength = tagPrefixLength(text, offset);
|
||||||
|
String body = text.substring(offset + prefixLength, end);
|
||||||
|
if (body.startsWith("title;")) {
|
||||||
|
if (seenTitle) throw new IllegalArgumentException("duplicate_channel_meta_title");
|
||||||
|
seenTitle = true;
|
||||||
|
title = parseTitleTag(body);
|
||||||
|
} else if (body.startsWith("avatar;") || body.startsWith("ava;")) {
|
||||||
|
if (seenAvatar) throw new IllegalArgumentException("duplicate_channel_meta_avatar");
|
||||||
|
seenAvatar = true;
|
||||||
|
Avatar avatar = parseAvatarTag(body);
|
||||||
|
avaAr = avatar.ar;
|
||||||
|
avaSha256 = avatar.sha256;
|
||||||
|
avaSize = avatar.size;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("unknown_channel_meta_tag");
|
||||||
|
}
|
||||||
|
offset = end + 1;
|
||||||
|
if (offset < text.length() && text.charAt(offset) == '\n') offset += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
String description = text.substring(offset).trim();
|
||||||
|
if (countCodePoints(description) > MAX_DESCRIPTION_CHARS) {
|
||||||
|
throw new IllegalArgumentException("channel_meta_description_too_long");
|
||||||
|
}
|
||||||
|
|
||||||
|
String cleanTitle = title.isBlank() ? String.valueOf(fallbackTitle == null ? "" : fallbackTitle).trim() : title.trim();
|
||||||
|
validateTitle(cleanTitle);
|
||||||
|
|
||||||
|
return new Parsed(cleanTitle, description, avaAr, avaSha256, avaSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String parseTitleTag(String body) {
|
||||||
|
String prefix = "title;v=1;";
|
||||||
|
if (!body.startsWith(prefix)) throw new IllegalArgumentException("bad_channel_meta_title");
|
||||||
|
String title = body.substring(prefix.length()).trim();
|
||||||
|
validateTitle(title);
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Avatar parseAvatarTag(String body) {
|
||||||
|
String rawFields = body.startsWith("ava;")
|
||||||
|
? body.substring("ava;".length())
|
||||||
|
: body.substring("avatar;".length());
|
||||||
|
Map<String, String> fields = parseFields(rawFields);
|
||||||
|
if (!"1".equals(fields.get("v"))) throw new IllegalArgumentException("bad_channel_meta_avatar_version");
|
||||||
|
String ar = String.valueOf(fields.getOrDefault("ar", "")).trim();
|
||||||
|
String sha256 = String.valueOf(fields.getOrDefault("sha256", "")).trim().toLowerCase();
|
||||||
|
String sizeRaw = String.valueOf(fields.containsKey("sz") ? fields.get("sz") : fields.getOrDefault("size", "")).trim();
|
||||||
|
if (!ar.matches("^[A-Za-z0-9_-]{43}$")) throw new IllegalArgumentException("bad_channel_meta_avatar_ar");
|
||||||
|
if (!sha256.matches("^[0-9a-f]{64}$")) throw new IllegalArgumentException("bad_channel_meta_avatar_sha256");
|
||||||
|
long size;
|
||||||
|
try {
|
||||||
|
size = Long.parseLong(sizeRaw);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalArgumentException("bad_channel_meta_avatar_size");
|
||||||
|
}
|
||||||
|
if (size <= 0L) throw new IllegalArgumentException("bad_channel_meta_avatar_size");
|
||||||
|
return new Avatar(ar, sha256, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean startsWithTagPrefix(String text, int offset) {
|
||||||
|
return text.startsWith(LEGACY_PREFIX, offset) || text.startsWith(SHORT_PREFIX, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int tagPrefixLength(String text, int offset) {
|
||||||
|
if (text.startsWith(LEGACY_PREFIX, offset)) return LEGACY_PREFIX.length();
|
||||||
|
if (text.startsWith(SHORT_PREFIX, offset)) return SHORT_PREFIX.length();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> parseFields(String raw) {
|
||||||
|
Map<String, String> out = new HashMap<>();
|
||||||
|
for (String part : String.valueOf(raw == null ? "" : raw).split(";")) {
|
||||||
|
int eq = part.indexOf('=');
|
||||||
|
if (eq <= 0) continue;
|
||||||
|
String key = part.substring(0, eq).trim();
|
||||||
|
String value = part.substring(eq + 1).trim();
|
||||||
|
if (!key.isEmpty()) out.put(key, value);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateTitle(String title) {
|
||||||
|
String value = String.valueOf(title == null ? "" : title);
|
||||||
|
if (value.indexOf('<') >= 0 || value.indexOf('>') >= 0 || value.indexOf(';') >= 0
|
||||||
|
|| value.indexOf('\t') >= 0 || value.indexOf('\n') >= 0 || value.indexOf('\r') >= 0
|
||||||
|
|| value.indexOf('\0') >= 0) {
|
||||||
|
throw new IllegalArgumentException("bad_channel_meta_title_chars");
|
||||||
|
}
|
||||||
|
if (countCodePoints(value) > MAX_TITLE_CHARS) {
|
||||||
|
throw new IllegalArgumentException("channel_meta_title_too_long");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int countCodePoints(String value) {
|
||||||
|
return String.valueOf(value == null ? "" : value).codePointCount(0, String.valueOf(value == null ? "" : value).length());
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Avatar(String ar, String sha256, long size) {}
|
||||||
|
|
||||||
|
public record Parsed(String title, String description, String avaAr, String avaSha256, long avaSize) {}
|
||||||
|
}
|
||||||
+73
-2
@@ -1,7 +1,9 @@
|
|||||||
package server.logic.ws_protocol.JSON.handlers.channels;
|
package server.logic.ws_protocol.JSON.handlers.channels;
|
||||||
|
|
||||||
import blockchain.BchBlockEntry;
|
import blockchain.BchBlockEntry;
|
||||||
|
import blockchain.MsgSubType;
|
||||||
import blockchain.body.CreateChannelBody;
|
import blockchain.body.CreateChannelBody;
|
||||||
|
import blockchain.body.TextLineBody;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import shine.db.DbController;
|
import shine.db.DbController;
|
||||||
@@ -20,6 +22,7 @@ import java.util.Map;
|
|||||||
public final class ChannelNamesStateBootstrapper {
|
public final class ChannelNamesStateBootstrapper {
|
||||||
private static final Logger log = LoggerFactory.getLogger(ChannelNamesStateBootstrapper.class);
|
private static final Logger log = LoggerFactory.getLogger(ChannelNamesStateBootstrapper.class);
|
||||||
private static final int MSG_TYPE_TECH = 0;
|
private static final int MSG_TYPE_TECH = 0;
|
||||||
|
private static final int MSG_TYPE_TEXT = 1;
|
||||||
private static final int MSG_SUB_TYPE_CREATE_CHANNEL = 1;
|
private static final int MSG_SUB_TYPE_CREATE_CHANNEL = 1;
|
||||||
private static volatile boolean bootstrapped;
|
private static volatile boolean bootstrapped;
|
||||||
|
|
||||||
@@ -74,17 +77,30 @@ public final class ChannelNamesStateBootstrapper {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String displayName;
|
String displayName;
|
||||||
final String slug;
|
final String slug;
|
||||||
final String channelDescription;
|
String channelDescription;
|
||||||
final int channelTypeCode;
|
final int channelTypeCode;
|
||||||
final int channelTypeVersion;
|
final int channelTypeVersion;
|
||||||
|
String avaAr = "";
|
||||||
|
String avaSha256 = "";
|
||||||
|
long avaSize = 0L;
|
||||||
|
long metaUpdatedAtMs = 0L;
|
||||||
try {
|
try {
|
||||||
displayName = ChannelNameRules.normalizeDisplayName(createChannelBody.channelName);
|
displayName = ChannelNameRules.normalizeDisplayName(createChannelBody.channelName);
|
||||||
slug = ChannelNameRules.toCanonicalSlug(displayName);
|
slug = ChannelNameRules.toCanonicalSlug(displayName);
|
||||||
channelDescription = ChannelNameRules.normalizeDisplayName(createChannelBody.channelDescription);
|
channelDescription = ChannelNameRules.normalizeDisplayName(createChannelBody.channelDescription);
|
||||||
channelTypeCode = Short.toUnsignedInt(createChannelBody.channelTypeCode);
|
channelTypeCode = Short.toUnsignedInt(createChannelBody.channelTypeCode);
|
||||||
channelTypeVersion = Short.toUnsignedInt(createChannelBody.channelTypeVersion);
|
channelTypeVersion = Short.toUnsignedInt(createChannelBody.channelTypeVersion);
|
||||||
|
if (channelTypeCode == (CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF)) {
|
||||||
|
ChannelMetaTextParser.Parsed createMeta = ChannelMetaTextParser.parseStrict(channelDescription, displayName);
|
||||||
|
displayName = createMeta.title();
|
||||||
|
channelDescription = createMeta.description();
|
||||||
|
avaAr = createMeta.avaAr();
|
||||||
|
avaSha256 = createMeta.avaSha256();
|
||||||
|
avaSize = createMeta.avaSize();
|
||||||
|
metaUpdatedAtMs = parsed.timestamp * 1000L;
|
||||||
|
}
|
||||||
} catch (Exception badName) {
|
} catch (Exception badName) {
|
||||||
skipped.add(ownerBch + "#" + blockNumber + " (invalid_name)");
|
skipped.add(ownerBch + "#" + blockNumber + " (invalid_name)");
|
||||||
continue;
|
continue;
|
||||||
@@ -102,6 +118,10 @@ public final class ChannelNamesStateBootstrapper {
|
|||||||
entry.setSlug(slug);
|
entry.setSlug(slug);
|
||||||
entry.setDisplayName(displayName);
|
entry.setDisplayName(displayName);
|
||||||
entry.setChannelDescription(channelDescription == null ? "" : channelDescription);
|
entry.setChannelDescription(channelDescription == null ? "" : channelDescription);
|
||||||
|
entry.setAvaAr(avaAr);
|
||||||
|
entry.setAvaSha256(avaSha256);
|
||||||
|
entry.setAvaSize(avaSize);
|
||||||
|
entry.setMetaUpdatedAtMs(metaUpdatedAtMs);
|
||||||
entry.setOwnerLogin(ownerLogin);
|
entry.setOwnerLogin(ownerLogin);
|
||||||
entry.setOwnerBlockchainName(ownerBch);
|
entry.setOwnerBlockchainName(ownerBch);
|
||||||
entry.setChannelTypeCode(channelTypeCode);
|
entry.setChannelTypeCode(channelTypeCode);
|
||||||
@@ -116,6 +136,8 @@ public final class ChannelNamesStateBootstrapper {
|
|||||||
|
|
||||||
dao.clearAll(c);
|
dao.clearAll(c);
|
||||||
dao.insertAll(c, entries);
|
dao.insertAll(c, entries);
|
||||||
|
|
||||||
|
applyMetaUpdates(c, dao, entries, skipped);
|
||||||
c.commit();
|
c.commit();
|
||||||
log.info("channel_names_state bootstrapped: {}", entries.size());
|
log.info("channel_names_state bootstrapped: {}", entries.size());
|
||||||
if (!conflicts.isEmpty()) {
|
if (!conflicts.isEmpty()) {
|
||||||
@@ -148,4 +170,53 @@ public final class ChannelNamesStateBootstrapper {
|
|||||||
throw new RuntimeException("Failed to bootstrap channel_names_state", e);
|
throw new RuntimeException("Failed to bootstrap channel_names_state", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void applyMetaUpdates(Connection c,
|
||||||
|
ChannelNameStateDAO dao,
|
||||||
|
List<ChannelNameStateEntry> entries,
|
||||||
|
List<String> skipped) throws Exception {
|
||||||
|
Map<String, String> fallbackTitles = new LinkedHashMap<>();
|
||||||
|
for (ChannelNameStateEntry entry : entries) {
|
||||||
|
fallbackTitles.put(entry.getOwnerBlockchainName() + "#" + entry.getChannelRootBlockNumber(), entry.getSlug());
|
||||||
|
}
|
||||||
|
|
||||||
|
String sql = """
|
||||||
|
SELECT bch_name, block_number, block_bytes
|
||||||
|
FROM blocks
|
||||||
|
WHERE msg_type = ? AND msg_sub_type = ?
|
||||||
|
ORDER BY bch_name, block_number
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setInt(1, MSG_TYPE_TEXT);
|
||||||
|
ps.setInt(2, MsgSubType.TEXT_CHANNEL_META);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
String ownerBch = rs.getString("bch_name");
|
||||||
|
int blockNumber = rs.getInt("block_number");
|
||||||
|
byte[] blockBytes = rs.getBytes("block_bytes");
|
||||||
|
try {
|
||||||
|
BchBlockEntry parsed = new BchBlockEntry(blockBytes);
|
||||||
|
if (!(parsed.body instanceof TextLineBody metaBody)) continue;
|
||||||
|
int lineCode = metaBody.lineCode();
|
||||||
|
String fallbackTitle = fallbackTitles.get(ownerBch + "#" + lineCode);
|
||||||
|
if (fallbackTitle == null) continue;
|
||||||
|
ChannelMetaTextParser.Parsed parsedMeta = ChannelMetaTextParser.parseStrict(metaBody.message, fallbackTitle);
|
||||||
|
|
||||||
|
ChannelNameStateEntry update = new ChannelNameStateEntry();
|
||||||
|
update.setOwnerBlockchainName(ownerBch);
|
||||||
|
update.setChannelRootBlockNumber(lineCode);
|
||||||
|
update.setDisplayName(parsedMeta.title());
|
||||||
|
update.setChannelDescription(parsedMeta.description());
|
||||||
|
update.setAvaAr(parsedMeta.avaAr());
|
||||||
|
update.setAvaSha256(parsedMeta.avaSha256());
|
||||||
|
update.setAvaSize(parsedMeta.avaSize());
|
||||||
|
update.setMetaUpdatedAtMs(parsed.timestamp * 1000L);
|
||||||
|
dao.updateMeta(c, update);
|
||||||
|
} catch (Exception e) {
|
||||||
|
skipped.add(ownerBch + "#" + blockNumber + " (invalid_meta)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+247
-60
@@ -22,9 +22,8 @@ final class ChannelsReadSupport {
|
|||||||
static final int MSG_TYPE_TEXT = 1;
|
static final int MSG_TYPE_TEXT = 1;
|
||||||
static final int MSG_TYPE_REACTION = 2;
|
static final int MSG_TYPE_REACTION = 2;
|
||||||
static final int MSG_TYPE_TECH = 0;
|
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 STORIES_CHANNEL_NAME = "stories";
|
||||||
static final String COMMAND_PREFIX = "/.";
|
|
||||||
static final String COMMAND_DESC = "desc";
|
|
||||||
static final String COMMAND_ADD = "add";
|
static final String COMMAND_ADD = "add";
|
||||||
static final String COMMAND_REMOVE = "remove";
|
static final String COMMAND_REMOVE = "remove";
|
||||||
|
|
||||||
@@ -47,11 +46,40 @@ final class ChannelsReadSupport {
|
|||||||
|
|
||||||
if (rootNumber == 0) {
|
if (rootNumber == 0) {
|
||||||
meta.channelName = STORIES_CHANNEL_NAME;
|
meta.channelName = STORIES_CHANNEL_NAME;
|
||||||
meta.channelDescription = detectLatestDescriptionCommand(c, ownerBch, 0);
|
meta.displayName = STORIES_CHANNEL_NAME;
|
||||||
|
meta.channelDescription = "";
|
||||||
meta.channelTypeCode = CreateChannelBody.CHANNEL_TYPE_STORIES & 0xFFFF;
|
meta.channelTypeCode = CreateChannelBody.CHANNEL_TYPE_STORIES & 0xFFFF;
|
||||||
return meta;
|
return meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String stateSql = """
|
||||||
|
SELECT slug, display_name, channel_description, channel_type_code, channel_type_version,
|
||||||
|
ava_ar, ava_sha256, ava_size, meta_updated_at_ms
|
||||||
|
FROM channel_names_state
|
||||||
|
WHERE owner_bch_name = ? AND channel_root_block_number = ?
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(stateSql)) {
|
||||||
|
ps.setString(1, ownerBch);
|
||||||
|
ps.setInt(2, rootNumber);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
meta.channelName = rs.getString("slug");
|
||||||
|
meta.displayName = rs.getString("display_name");
|
||||||
|
meta.channelDescription = String.valueOf(rs.getString("channel_description") == null ? "" : rs.getString("channel_description"));
|
||||||
|
meta.channelTypeCode = rs.getInt("channel_type_code");
|
||||||
|
meta.channelTypeVersion = rs.getInt("channel_type_version");
|
||||||
|
meta.avaAr = String.valueOf(rs.getString("ava_ar") == null ? "" : rs.getString("ava_ar"));
|
||||||
|
meta.avaSha256 = String.valueOf(rs.getString("ava_sha256") == null ? "" : rs.getString("ava_sha256"));
|
||||||
|
meta.avaSize = rs.getLong("ava_size");
|
||||||
|
meta.metaUpdatedAtMs = rs.getLong("meta_updated_at_ms");
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException ignored) {
|
||||||
|
// fallback ниже для окружений до миграции.
|
||||||
|
}
|
||||||
|
|
||||||
String sql = "SELECT block_bytes FROM blocks WHERE bch_name=? AND block_number=? LIMIT 1";
|
String sql = "SELECT block_bytes FROM blocks WHERE bch_name=? AND block_number=? LIMIT 1";
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, ownerBch);
|
ps.setString(1, ownerBch);
|
||||||
@@ -68,21 +96,29 @@ final class ChannelsReadSupport {
|
|||||||
BodyRecord body = e.body;
|
BodyRecord body = e.body;
|
||||||
if (body instanceof CreateChannelBody ccb) {
|
if (body instanceof CreateChannelBody ccb) {
|
||||||
meta.channelName = ccb.channelName;
|
meta.channelName = ccb.channelName;
|
||||||
|
meta.displayName = ccb.channelName;
|
||||||
meta.channelDescription = ccb.channelDescription == null ? "" : ccb.channelDescription;
|
meta.channelDescription = ccb.channelDescription == null ? "" : ccb.channelDescription;
|
||||||
meta.channelTypeCode = Short.toUnsignedInt(ccb.channelTypeCode);
|
meta.channelTypeCode = Short.toUnsignedInt(ccb.channelTypeCode);
|
||||||
meta.channelTypeVersion = Short.toUnsignedInt(ccb.channelTypeVersion);
|
meta.channelTypeVersion = Short.toUnsignedInt(ccb.channelTypeVersion);
|
||||||
|
if (meta.channelTypeCode == (CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF)) {
|
||||||
|
ChannelMetaTextParser.Parsed parsedMeta = ChannelMetaTextParser.parseStrict(meta.channelDescription, meta.channelName);
|
||||||
|
meta.displayName = parsedMeta.title();
|
||||||
|
meta.channelDescription = parsedMeta.description();
|
||||||
|
meta.avaAr = parsedMeta.avaAr();
|
||||||
|
meta.avaSha256 = parsedMeta.avaSha256();
|
||||||
|
meta.avaSize = parsedMeta.avaSize();
|
||||||
|
meta.metaUpdatedAtMs = e.timestamp * 1000L;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
meta.channelName = null;
|
meta.channelName = null;
|
||||||
|
meta.displayName = null;
|
||||||
meta.channelDescription = "";
|
meta.channelDescription = "";
|
||||||
meta.channelTypeCode = CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF;
|
meta.channelTypeCode = CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF;
|
||||||
}
|
}
|
||||||
String updatedDescription = detectLatestDescriptionCommand(c, ownerBch, rootNumber);
|
|
||||||
if (updatedDescription != null) {
|
|
||||||
meta.channelDescription = updatedDescription;
|
|
||||||
}
|
|
||||||
return meta;
|
return meta;
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
meta.channelName = null;
|
meta.channelName = null;
|
||||||
|
meta.displayName = null;
|
||||||
meta.channelDescription = "";
|
meta.channelDescription = "";
|
||||||
meta.channelTypeCode = CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF;
|
meta.channelTypeCode = CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF;
|
||||||
return meta;
|
return meta;
|
||||||
@@ -91,24 +127,62 @@ final class ChannelsReadSupport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int countPosts(Connection c, String ownerBch, int lineCode) throws SQLException {
|
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)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, ownerBch);
|
ps.setString(1, ownerBch);
|
||||||
ps.setInt(2, MSG_TYPE_TEXT);
|
ps.setInt(2, MSG_TYPE_TEXT);
|
||||||
ps.setInt(3, MsgSubType.TEXT_POST);
|
ps.setInt(3, MsgSubType.TEXT_POST);
|
||||||
ps.setInt(4, MsgSubType.TEXT_REPOST);
|
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()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
return rs.next() ? rs.getInt("cnt") : 0;
|
return rs.next() ? rs.getInt("cnt") : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String userSettingsChannelKey(String ownerBch, String channelName) {
|
||||||
|
String bch = ownerBch == null ? "" : ownerBch.trim();
|
||||||
|
String name = channelName == null ? "" : channelName.trim();
|
||||||
|
return bch + "/" + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int countUnreadMessages(Connection c, String viewerLogin, String ownerBch, String channelName, int messagesCount) throws SQLException {
|
||||||
|
if (viewerLogin == null || viewerLogin.isBlank()) return 0;
|
||||||
|
String key = userSettingsChannelKey(ownerBch, channelName);
|
||||||
|
String sql = """
|
||||||
|
SELECT value_num
|
||||||
|
FROM user_settings
|
||||||
|
WHERE LOWER(login) = LOWER(?)
|
||||||
|
AND setting_type = ?
|
||||||
|
AND setting_key = ?
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
long lastSeen = messagesCount;
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, viewerLogin);
|
||||||
|
ps.setInt(2, 1);
|
||||||
|
ps.setString(3, key);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
long value = rs.getLong("value_num");
|
||||||
|
if (!rs.wasNull()) lastSeen = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastSeen < 0) lastSeen = 0;
|
||||||
|
if (lastSeen > messagesCount) return 0;
|
||||||
|
return Math.max(0, messagesCount - (int) lastSeen);
|
||||||
|
}
|
||||||
|
|
||||||
static PostBlock loadLastPost(Connection c, String ownerBch, int lineCode) throws SQLException {
|
static PostBlock loadLastPost(Connection c, String ownerBch, int lineCode) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT login,bch_name,block_number,block_hash,block_bytes
|
SELECT login,bch_name,block_number,block_hash,block_bytes,this_line_number
|
||||||
FROM blocks
|
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
|
ORDER BY block_number DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""";
|
""";
|
||||||
@@ -117,7 +191,11 @@ final class ChannelsReadSupport {
|
|||||||
ps.setInt(2, MSG_TYPE_TEXT);
|
ps.setInt(2, MSG_TYPE_TEXT);
|
||||||
ps.setInt(3, MsgSubType.TEXT_POST);
|
ps.setInt(3, MsgSubType.TEXT_POST);
|
||||||
ps.setInt(4, MsgSubType.TEXT_REPOST);
|
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()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
if (!rs.next()) return null;
|
if (!rs.next()) return null;
|
||||||
PostBlock pb = new PostBlock();
|
PostBlock pb = new PostBlock();
|
||||||
@@ -133,7 +211,7 @@ final class ChannelsReadSupport {
|
|||||||
|
|
||||||
static PostBlock loadLastVersion(Connection c, String ownerBch, int originalBlockNumber, byte[] originalHash) throws SQLException {
|
static PostBlock loadLastVersion(Connection c, String ownerBch, int originalBlockNumber, byte[] originalHash) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT login,bch_name,block_number,block_hash,block_bytes
|
SELECT login,bch_name,block_number,block_hash,block_bytes,this_line_number
|
||||||
FROM blocks
|
FROM blocks
|
||||||
WHERE bch_name=? AND msg_type=? AND msg_sub_type=?
|
WHERE bch_name=? AND msg_type=? AND msg_sub_type=?
|
||||||
AND to_bch_name=? AND to_block_number=? AND to_block_hash=?
|
AND to_bch_name=? AND to_block_number=? AND to_block_hash=?
|
||||||
@@ -169,6 +247,10 @@ final class ChannelsReadSupport {
|
|||||||
ti.text = tlb.message;
|
ti.text = tlb.message;
|
||||||
} else if (e.body instanceof TextReplyBody trb) {
|
} else if (e.body instanceof TextReplyBody trb) {
|
||||||
ti.text = trb.message;
|
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) {
|
} else if (e.body instanceof TextBody tb) {
|
||||||
ti.text = tb.message;
|
ti.text = tb.message;
|
||||||
}
|
}
|
||||||
@@ -183,7 +265,7 @@ final class ChannelsReadSupport {
|
|||||||
String sql = """
|
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
|
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
|
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 BY block_number
|
||||||
""" + order + " LIMIT ?";
|
""" + order + " LIMIT ?";
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
@@ -191,8 +273,12 @@ final class ChannelsReadSupport {
|
|||||||
ps.setInt(2, MSG_TYPE_TEXT);
|
ps.setInt(2, MSG_TYPE_TEXT);
|
||||||
ps.setInt(3, MsgSubType.TEXT_POST);
|
ps.setInt(3, MsgSubType.TEXT_POST);
|
||||||
ps.setInt(4, MsgSubType.TEXT_REPOST);
|
ps.setInt(4, MsgSubType.TEXT_REPOST);
|
||||||
ps.setInt(5, lineCode);
|
ps.setInt(5, MsgSubType.TEXT_ENTRYPOINT);
|
||||||
ps.setInt(6, limit);
|
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()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
List<PostBlock> out = new ArrayList<>();
|
List<PostBlock> out = new ArrayList<>();
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
@@ -257,21 +343,47 @@ final class ChannelsReadSupport {
|
|||||||
|
|
||||||
static int[] loadStats(Connection c, String bch, int blockNumber, byte[] blockHash) throws SQLException {
|
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";
|
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)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, bch);
|
ps.setString(1, bch);
|
||||||
ps.setInt(2, blockNumber);
|
ps.setInt(2, blockNumber);
|
||||||
ps.setBytes(3, blockHash);
|
ps.setBytes(3, blockHash);
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
if (!rs.next()) return new int[] {0, 0};
|
if (rs.next()) {
|
||||||
return new int[] {rs.getInt("likes_count"), rs.getInt("replies_count")};
|
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 {
|
static String detectChannelDescription(Connection c, String ownerBch, int rootNumber) throws SQLException {
|
||||||
if (rootNumber == 0) {
|
if (rootNumber == 0) {
|
||||||
String fromCommand = detectLatestDescriptionCommand(c, ownerBch, 0);
|
return "";
|
||||||
return fromCommand == null ? "" : fromCommand;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preferred source: persisted state (fast path, works for CreateChannelBody v2).
|
// Preferred source: persisted state (fast path, works for CreateChannelBody v2).
|
||||||
@@ -287,8 +399,7 @@ final class ChannelsReadSupport {
|
|||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
if (rs.next()) {
|
if (rs.next()) {
|
||||||
String saved = String.valueOf(rs.getString("channel_description") == null ? "" : rs.getString("channel_description"));
|
String saved = String.valueOf(rs.getString("channel_description") == null ? "" : rs.getString("channel_description"));
|
||||||
String fromCommand = detectLatestDescriptionCommand(c, ownerBch, rootNumber);
|
return saved;
|
||||||
return fromCommand == null ? saved : fromCommand;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (SQLException ignored) {
|
} catch (SQLException ignored) {
|
||||||
@@ -306,8 +417,6 @@ final class ChannelsReadSupport {
|
|||||||
BchBlockEntry e = new BchBlockEntry(bytes);
|
BchBlockEntry e = new BchBlockEntry(bytes);
|
||||||
BodyRecord body = e.body;
|
BodyRecord body = e.body;
|
||||||
if (body instanceof CreateChannelBody ccb) {
|
if (body instanceof CreateChannelBody ccb) {
|
||||||
String fromCommand = detectLatestDescriptionCommand(c, ownerBch, rootNumber);
|
|
||||||
if (fromCommand != null) return fromCommand;
|
|
||||||
return ccb.channelDescription == null ? "" : ccb.channelDescription;
|
return ccb.channelDescription == null ? "" : ccb.channelDescription;
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
@@ -363,45 +472,97 @@ final class ChannelsReadSupport {
|
|||||||
return meta.channelTypeVersion;
|
return meta.channelTypeVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String detectLatestDescriptionCommand(Connection c, String ownerBch, int lineCode) throws SQLException {
|
static List<MetaEvent> channelMetaEvents(Connection c, String ownerBch, int lineCode) throws SQLException {
|
||||||
String sql = """
|
List<MetaEvent> out = new ArrayList<>();
|
||||||
SELECT block_bytes
|
if (lineCode <= 0) return out;
|
||||||
|
ChannelMeta base = detectChannelMeta(c, ownerBch, lineCode);
|
||||||
|
String rootSql = """
|
||||||
|
SELECT login,bch_name,block_number,block_hash,block_bytes,this_line_number
|
||||||
FROM blocks
|
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=? AND block_number=?
|
||||||
ORDER BY block_number DESC
|
LIMIT 1
|
||||||
LIMIT 300
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(rootSql)) {
|
||||||
|
ps.setString(1, ownerBch);
|
||||||
|
ps.setInt(2, MSG_TYPE_TECH);
|
||||||
|
ps.setInt(3, 1);
|
||||||
|
ps.setInt(4, lineCode);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
MetaEvent event = new MetaEvent();
|
||||||
|
event.kind = "created";
|
||||||
|
event.login = rs.getString("login");
|
||||||
|
event.bchName = rs.getString("bch_name");
|
||||||
|
event.blockNumber = rs.getInt("block_number");
|
||||||
|
event.blockHash = rs.getBytes("block_hash");
|
||||||
|
event.lineStep = null;
|
||||||
|
try {
|
||||||
|
BchBlockEntry entry = new BchBlockEntry(rs.getBytes("block_bytes"));
|
||||||
|
event.createdAtMs = entry.timestamp * 1000L;
|
||||||
|
if (entry.body instanceof CreateChannelBody ccb) {
|
||||||
|
event.title = ccb.channelName;
|
||||||
|
event.description = ccb.channelDescription == null ? "" : ccb.channelDescription;
|
||||||
|
if (Short.toUnsignedInt(ccb.channelTypeCode) == (CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF)) {
|
||||||
|
ChannelMetaTextParser.Parsed parsedMeta = ChannelMetaTextParser.parseStrict(event.description, ccb.channelName);
|
||||||
|
event.title = parsedMeta.title();
|
||||||
|
event.description = parsedMeta.description();
|
||||||
|
event.avaAr = parsedMeta.avaAr();
|
||||||
|
event.avaSha256 = parsedMeta.avaSha256();
|
||||||
|
event.avaSize = parsedMeta.avaSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
event.createdAtMs = 0L;
|
||||||
|
}
|
||||||
|
if (event.title == null || event.title.isBlank()) {
|
||||||
|
event.title = base.displayName == null || base.displayName.isBlank() ? base.channelName : base.displayName;
|
||||||
|
}
|
||||||
|
if (event.description == null) event.description = "";
|
||||||
|
if (event.avaAr == null) event.avaAr = "";
|
||||||
|
if (event.avaSha256 == null) event.avaSha256 = "";
|
||||||
|
out.add(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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=? AND line_code=?
|
||||||
|
ORDER BY block_number ASC
|
||||||
""";
|
""";
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, ownerBch);
|
ps.setString(1, ownerBch);
|
||||||
ps.setInt(2, MSG_TYPE_TEXT);
|
ps.setInt(2, MSG_TYPE_TEXT);
|
||||||
ps.setInt(3, MsgSubType.TEXT_POST);
|
ps.setInt(3, MsgSubType.TEXT_CHANNEL_META);
|
||||||
ps.setInt(4, MsgSubType.TEXT_REPOST);
|
ps.setInt(4, lineCode);
|
||||||
ps.setInt(5, lineCode);
|
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
TextInfo info = parseTextAndTime(rs.getBytes("block_bytes"));
|
byte[] bytes = rs.getBytes("block_bytes");
|
||||||
CommandInfo commandInfo = parseCommandText(info.text);
|
TextInfo info = parseTextAndTime(bytes);
|
||||||
if (commandInfo != null && COMMAND_DESC.equals(commandInfo.command)) {
|
try {
|
||||||
return commandInfo.arg;
|
ChannelMetaTextParser.Parsed parsed = ChannelMetaTextParser.parseStrict(info.text, base.channelName);
|
||||||
|
MetaEvent event = new MetaEvent();
|
||||||
|
event.kind = "profile_updated";
|
||||||
|
event.login = rs.getString("login");
|
||||||
|
event.bchName = rs.getString("bch_name");
|
||||||
|
event.blockNumber = rs.getInt("block_number");
|
||||||
|
event.blockHash = rs.getBytes("block_hash");
|
||||||
|
event.lineStep = (Integer) rs.getObject("this_line_number");
|
||||||
|
event.createdAtMs = info.createdAtMs;
|
||||||
|
event.title = parsed.title();
|
||||||
|
event.description = parsed.description();
|
||||||
|
event.avaAr = parsed.avaAr();
|
||||||
|
event.avaSha256 = parsed.avaSha256();
|
||||||
|
event.avaSize = parsed.avaSize();
|
||||||
|
out.add(event);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// Невалидный meta-блок не участвует в системной истории UI.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return out;
|
||||||
}
|
|
||||||
|
|
||||||
static CommandInfo parseCommandText(String text) {
|
|
||||||
String value = String.valueOf(text == null ? "" : text).trim();
|
|
||||||
if (!value.startsWith(COMMAND_PREFIX)) return null;
|
|
||||||
|
|
||||||
String raw = value.substring(COMMAND_PREFIX.length()).trim();
|
|
||||||
if (raw.isEmpty()) return null;
|
|
||||||
|
|
||||||
int sp = raw.indexOf(' ');
|
|
||||||
String cmd = (sp < 0 ? raw : raw.substring(0, sp)).trim().toLowerCase();
|
|
||||||
String arg = sp < 0 ? "" : raw.substring(sp + 1).trim();
|
|
||||||
if (cmd.isEmpty()) return null;
|
|
||||||
return new CommandInfo(cmd, arg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static PairChannelSelector findPersonalPairChannel(Connection c, String ownerLogin, String partnerLogin) throws SQLException {
|
static PairChannelSelector findPersonalPairChannel(Connection c, String ownerLogin, String partnerLogin) throws SQLException {
|
||||||
@@ -506,6 +667,22 @@ final class ChannelsReadSupport {
|
|||||||
return sb.toString();
|
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 {
|
static final class PostBlock {
|
||||||
String login;
|
String login;
|
||||||
String bchName;
|
String bchName;
|
||||||
@@ -526,19 +703,29 @@ final class ChannelsReadSupport {
|
|||||||
|
|
||||||
static final class ChannelMeta {
|
static final class ChannelMeta {
|
||||||
String channelName;
|
String channelName;
|
||||||
|
String displayName;
|
||||||
String channelDescription;
|
String channelDescription;
|
||||||
int channelTypeCode;
|
int channelTypeCode;
|
||||||
int channelTypeVersion;
|
int channelTypeVersion;
|
||||||
|
String avaAr = "";
|
||||||
|
String avaSha256 = "";
|
||||||
|
long avaSize = 0L;
|
||||||
|
long metaUpdatedAtMs = 0L;
|
||||||
}
|
}
|
||||||
|
|
||||||
static final class CommandInfo {
|
static final class MetaEvent {
|
||||||
final String command;
|
String kind;
|
||||||
final String arg;
|
String login;
|
||||||
|
String bchName;
|
||||||
CommandInfo(String command, String arg) {
|
int blockNumber;
|
||||||
this.command = command;
|
byte[] blockHash;
|
||||||
this.arg = arg;
|
Integer lineStep;
|
||||||
}
|
long createdAtMs;
|
||||||
|
String title;
|
||||||
|
String description;
|
||||||
|
String avaAr;
|
||||||
|
String avaSha256;
|
||||||
|
long avaSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
static final class PairChannelSelector {
|
static final class PairChannelSelector {
|
||||||
|
|||||||
+28
-1
@@ -57,7 +57,12 @@ public class Net_GetChannelMessages_Handler implements JsonMessageHandler {
|
|||||||
channel.setOwnerLogin(BlockchainNameUtil.loginFromBlockchainName(ownerBch));
|
channel.setOwnerLogin(BlockchainNameUtil.loginFromBlockchainName(ownerBch));
|
||||||
ChannelsReadSupport.ChannelMeta meta = ChannelsReadSupport.detectChannelMeta(c, ownerBch, lineCode);
|
ChannelsReadSupport.ChannelMeta meta = ChannelsReadSupport.detectChannelMeta(c, ownerBch, lineCode);
|
||||||
channel.setChannelName(meta.channelName);
|
channel.setChannelName(meta.channelName);
|
||||||
|
channel.setDisplayName(meta.displayName == null || meta.displayName.isBlank() ? meta.channelName : meta.displayName);
|
||||||
channel.setChannelDescription(meta.channelDescription);
|
channel.setChannelDescription(meta.channelDescription);
|
||||||
|
channel.setAvaAr(meta.avaAr);
|
||||||
|
channel.setAvaSha256(meta.avaSha256);
|
||||||
|
channel.setAvaSize(meta.avaSize);
|
||||||
|
channel.setMetaUpdatedAtMs(meta.metaUpdatedAtMs);
|
||||||
channel.setChannelTypeCode(meta.channelTypeCode);
|
channel.setChannelTypeCode(meta.channelTypeCode);
|
||||||
channel.setChannelTypeVersion(meta.channelTypeVersion);
|
channel.setChannelTypeVersion(meta.channelTypeVersion);
|
||||||
Net_GetChannelMessages_Response.BlockRef rootRef = new Net_GetChannelMessages_Response.BlockRef();
|
Net_GetChannelMessages_Response.BlockRef rootRef = new Net_GetChannelMessages_Response.BlockRef();
|
||||||
@@ -66,6 +71,27 @@ public class Net_GetChannelMessages_Handler implements JsonMessageHandler {
|
|||||||
channel.setChannelRoot(rootRef);
|
channel.setChannelRoot(rootRef);
|
||||||
resp.setChannel(channel);
|
resp.setChannel(channel);
|
||||||
|
|
||||||
|
List<Net_GetChannelMessages_Response.ChannelMetaEvent> metaEvents = new ArrayList<>();
|
||||||
|
for (ChannelsReadSupport.MetaEvent event : ChannelsReadSupport.channelMetaEvents(c, ownerBch, lineCode)) {
|
||||||
|
Net_GetChannelMessages_Response.ChannelMetaEvent outEvent = new Net_GetChannelMessages_Response.ChannelMetaEvent();
|
||||||
|
outEvent.setKind(event.kind);
|
||||||
|
Net_GetChannelMessages_Response.BlockRef eventRef = new Net_GetChannelMessages_Response.BlockRef();
|
||||||
|
eventRef.setBlockNumber(event.blockNumber);
|
||||||
|
eventRef.setBlockHash(ChannelsReadSupport.toHex(event.blockHash));
|
||||||
|
outEvent.setMessageRef(eventRef);
|
||||||
|
outEvent.setAuthorLogin(event.login);
|
||||||
|
outEvent.setAuthorBlockchainName(event.bchName);
|
||||||
|
outEvent.setLineStep(event.lineStep);
|
||||||
|
outEvent.setCreatedAtMs(event.createdAtMs);
|
||||||
|
outEvent.setTitle(event.title);
|
||||||
|
outEvent.setDescription(event.description);
|
||||||
|
outEvent.setAvaAr(event.avaAr);
|
||||||
|
outEvent.setAvaSha256(event.avaSha256);
|
||||||
|
outEvent.setAvaSize(event.avaSize);
|
||||||
|
metaEvents.add(outEvent);
|
||||||
|
}
|
||||||
|
resp.setMetaEvents(metaEvents);
|
||||||
|
|
||||||
List<ChannelsReadSupport.PostBlock> posts = new ArrayList<>(
|
List<ChannelsReadSupport.PostBlock> posts = new ArrayList<>(
|
||||||
ChannelsReadSupport.channelPosts(c, ownerBch, lineCode, limit, asc)
|
ChannelsReadSupport.channelPosts(c, ownerBch, lineCode, limit, asc)
|
||||||
);
|
);
|
||||||
@@ -117,7 +143,7 @@ public class Net_GetChannelMessages_Handler implements JsonMessageHandler {
|
|||||||
v1.setCreatedAtMs(postText.createdAtMs);
|
v1.setCreatedAtMs(postText.createdAtMs);
|
||||||
versionsOut.add(v1);
|
versionsOut.add(v1);
|
||||||
|
|
||||||
if (post.msgSubType == MsgSubType.TEXT_POST) {
|
if (ChannelsReadSupport.supportsEditPostVersions(post.msgSubType)) {
|
||||||
List<ChannelsReadSupport.PostBlock> edits = ChannelsReadSupport.versionsForPost(c, post.bchName, post.blockNumber, post.blockHash);
|
List<ChannelsReadSupport.PostBlock> edits = ChannelsReadSupport.versionsForPost(c, post.bchName, post.blockNumber, post.blockHash);
|
||||||
for (ChannelsReadSupport.PostBlock edit : edits) {
|
for (ChannelsReadSupport.PostBlock edit : edits) {
|
||||||
ChannelsReadSupport.TextInfo editText = ChannelsReadSupport.parseTextAndTime(edit.blockBytes);
|
ChannelsReadSupport.TextInfo editText = ChannelsReadSupport.parseTextAndTime(edit.blockBytes);
|
||||||
@@ -141,6 +167,7 @@ public class Net_GetChannelMessages_Handler implements JsonMessageHandler {
|
|||||||
int[] stats = ChannelsReadSupport.loadStats(c, ownerBch, post.blockNumber, post.blockHash);
|
int[] stats = ChannelsReadSupport.loadStats(c, ownerBch, post.blockNumber, post.blockHash);
|
||||||
item.setLikesCount(stats[0]);
|
item.setLikesCount(stats[0]);
|
||||||
item.setRepliesCount(stats[1]);
|
item.setRepliesCount(stats[1]);
|
||||||
|
item.setRatingsCount(stats[2]);
|
||||||
item.setLikedByMe(ChannelsReadSupport.isLikedByLogin(c, viewerLogin, post.bchName, post.blockNumber, post.blockHash));
|
item.setLikedByMe(ChannelsReadSupport.isLikedByLogin(c, viewerLogin, post.bchName, post.blockNumber, post.blockHash));
|
||||||
|
|
||||||
items.add(item);
|
items.add(item);
|
||||||
|
|||||||
+22
-11
@@ -17,6 +17,7 @@ import shine.db.DbController;
|
|||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -89,7 +90,7 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
private List<Net_GetMessageThread_Response.MessageNodeTree> loadChildren(Connection c, PostRow parent, int depthDown, int childLimit, String viewerLogin) throws Exception {
|
private List<Net_GetMessageThread_Response.MessageNodeTree> loadChildren(Connection c, PostRow parent, int depthDown, int childLimit, String viewerLogin) throws Exception {
|
||||||
if (depthDown <= 0) return List.of();
|
if (depthDown <= 0) return List.of();
|
||||||
List<PostRow> replies = findReplies(c, parent.bchName, parent.blockNumber, parent.blockHash, childLimit);
|
List<PostRow> replies = findRepliesAndRatings(c, parent.bchName, parent.blockNumber, parent.blockHash, childLimit);
|
||||||
List<Net_GetMessageThread_Response.MessageNodeTree> out = new ArrayList<>();
|
List<Net_GetMessageThread_Response.MessageNodeTree> out = new ArrayList<>();
|
||||||
for (PostRow row : replies) {
|
for (PostRow row : replies) {
|
||||||
Net_GetMessageThread_Response.MessageNodeTree t = new Net_GetMessageThread_Response.MessageNodeTree();
|
Net_GetMessageThread_Response.MessageNodeTree t = new Net_GetMessageThread_Response.MessageNodeTree();
|
||||||
@@ -100,24 +101,29 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<PostRow> findReplies(Connection c, String toBchName, int toBlockNumber, byte[] toBlockHash, int limit) throws Exception {
|
private List<PostRow> findRepliesAndRatings(Connection c, String toBchName, int toBlockNumber, byte[] toBlockHash, int limit) throws Exception {
|
||||||
String sql = """
|
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
|
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
|
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=?
|
AND to_bch_name=? AND to_block_number=? AND to_block_hash=?
|
||||||
ORDER BY block_number ASC
|
|
||||||
LIMIT ?
|
|
||||||
""";
|
""";
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setInt(1, MsgSubType.TEXT_REPLY);
|
ps.setInt(1, MsgSubType.TEXT_REPLY);
|
||||||
ps.setString(2, toBchName);
|
ps.setInt(2, MsgSubType.TEXT_RATING);
|
||||||
ps.setInt(3, toBlockNumber);
|
ps.setString(3, toBchName);
|
||||||
ps.setBytes(4, toBlockHash);
|
ps.setInt(4, toBlockNumber);
|
||||||
ps.setInt(5, limit);
|
ps.setBytes(5, toBlockHash);
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
List<PostRow> out = new ArrayList<>();
|
List<PostRow> out = new ArrayList<>();
|
||||||
while (rs.next()) out.add(mapRow(rs));
|
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;
|
return out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,8 +214,12 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
|
|||||||
first.setCreatedAtMs(base.createdAtMs);
|
first.setCreatedAtMs(base.createdAtMs);
|
||||||
versions.add(first);
|
versions.add(first);
|
||||||
|
|
||||||
if (row.msgSubType == MsgSubType.TEXT_REPLY || row.msgSubType == MsgSubType.TEXT_POST) {
|
if (row.msgSubType == MsgSubType.TEXT_REPLY
|
||||||
short editType = row.msgSubType == MsgSubType.TEXT_REPLY ? MsgSubType.TEXT_EDIT_REPLY : MsgSubType.TEXT_EDIT_POST;
|
|| 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)) {
|
for (PostRow edit : findEdits(c, row.bchName, row.blockNumber, row.blockHash, editType)) {
|
||||||
ChannelsReadSupport.TextInfo et = ChannelsReadSupport.parseTextAndTime(edit.blockBytes);
|
ChannelsReadSupport.TextInfo et = ChannelsReadSupport.parseTextAndTime(edit.blockBytes);
|
||||||
Net_GetChannelMessages_Response.VersionItem v = new Net_GetChannelMessages_Response.VersionItem();
|
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);
|
int[] stats = ChannelsReadSupport.loadStats(c, row.bchName, row.blockNumber, row.blockHash);
|
||||||
node.setLikesCount(stats[0]);
|
node.setLikesCount(stats[0]);
|
||||||
node.setRepliesCount(stats[1]);
|
node.setRepliesCount(stats[1]);
|
||||||
|
node.setRatingsCount(stats[2]);
|
||||||
node.setLikedByMe(ChannelsReadSupport.isLikedByLogin(c, viewerLogin, row.bchName, row.blockNumber, row.blockHash));
|
node.setLikedByMe(ChannelsReadSupport.isLikedByLogin(c, viewerLogin, row.bchName, row.blockNumber, row.blockHash));
|
||||||
if (row.lineCode != null && row.lineCode >= 0) {
|
if (row.lineCode != null && row.lineCode >= 0) {
|
||||||
Net_GetMessageThread_Response.ChannelInfo ci = new Net_GetMessageThread_Response.ChannelInfo();
|
Net_GetMessageThread_Response.ChannelInfo ci = new Net_GetMessageThread_Response.ChannelInfo();
|
||||||
|
|||||||
+238
@@ -0,0 +1,238 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.channels;
|
||||||
|
|
||||||
|
import blockchain.BchBlockEntry;
|
||||||
|
import blockchain.body.StatusActionBody;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelMessages_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetPersonalDiary_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.DbController;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Net_GetPersonalDiary_Handler implements JsonMessageHandler {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Net_GetPersonalDiary_Handler.class);
|
||||||
|
private static final String DIARY_CHANNEL_NAME = "diary";
|
||||||
|
private static final String DIARY_DISPLAY_NAME = "Личный дневник";
|
||||||
|
private static final String DIARY_DESCRIPTION = "История ваших действий по упражнениям, услугам и курсам";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||||
|
Net_GetPersonalDiary_Request req = (Net_GetPersonalDiary_Request) baseRequest;
|
||||||
|
String requestedLogin = String.valueOf(req.getLogin() == null ? "" : req.getLogin()).trim();
|
||||||
|
if (requestedLogin.isBlank() && (ctx == null || ctx.getLogin() == null || ctx.getLogin().isBlank())) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля: login");
|
||||||
|
}
|
||||||
|
|
||||||
|
int limit = req.getLimit() == null ? 200 : req.getLimit();
|
||||||
|
if (limit <= 0 || limit > 1000) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "limit_too_large", "Некорректный limit");
|
||||||
|
}
|
||||||
|
boolean asc = req.getSort() == null || !"desc".equalsIgnoreCase(req.getSort());
|
||||||
|
|
||||||
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
|
String viewerLogin = ctx != null ? String.valueOf(ctx.getLogin() == null ? "" : ctx.getLogin()).trim() : "";
|
||||||
|
String canonicalLogin = !viewerLogin.isBlank()
|
||||||
|
? viewerLogin
|
||||||
|
: ChannelsReadSupport.canonicalLogin(c, requestedLogin);
|
||||||
|
if (canonicalLogin == null || canonicalLogin.isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 404, "user_not_found", "Пользователь не найден");
|
||||||
|
}
|
||||||
|
if (!viewerLogin.isBlank() && !viewerLogin.equalsIgnoreCase(canonicalLogin)) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 403, "forbidden", "Личный дневник доступен только владельцу");
|
||||||
|
}
|
||||||
|
|
||||||
|
String ownerBch = loadPrimaryBlockchainName(c, canonicalLogin);
|
||||||
|
if (ownerBch == null || ownerBch.isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 404, "blockchain_not_found", "Не найден blockchain пользователя");
|
||||||
|
}
|
||||||
|
|
||||||
|
Net_GetChannelMessages_Response resp = new Net_GetChannelMessages_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
|
||||||
|
Net_GetChannelMessages_Response.Channel channel = new Net_GetChannelMessages_Response.Channel();
|
||||||
|
channel.setOwnerLogin(canonicalLogin);
|
||||||
|
channel.setOwnerBlockchainName(ownerBch);
|
||||||
|
channel.setChannelName(DIARY_CHANNEL_NAME);
|
||||||
|
channel.setDisplayName(DIARY_DISPLAY_NAME);
|
||||||
|
channel.setChannelDescription(DIARY_DESCRIPTION);
|
||||||
|
channel.setChannelTypeCode(900);
|
||||||
|
channel.setChannelTypeVersion(1);
|
||||||
|
Net_GetChannelMessages_Response.BlockRef rootRef = new Net_GetChannelMessages_Response.BlockRef();
|
||||||
|
rootRef.setBlockNumber(0);
|
||||||
|
rootRef.setBlockHash(ChannelsReadSupport.toHex(new byte[32]));
|
||||||
|
channel.setChannelRoot(rootRef);
|
||||||
|
resp.setChannel(channel);
|
||||||
|
resp.setMetaEvents(new ArrayList<>());
|
||||||
|
|
||||||
|
List<Net_GetChannelMessages_Response.MessageItem> items = loadDiaryItems(c, canonicalLogin, limit, asc);
|
||||||
|
resp.setMessages(items);
|
||||||
|
return resp;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("GetPersonalDiary failed", e);
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "internal_error", "Внутренняя ошибка сервера");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String loadPrimaryBlockchainName(Connection c, String canonicalLogin) throws Exception {
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
|
SELECT blockchain_name
|
||||||
|
FROM blockchain_state
|
||||||
|
WHERE login = ?
|
||||||
|
ORDER BY blockchain_name
|
||||||
|
LIMIT 1
|
||||||
|
""")) {
|
||||||
|
ps.setString(1, canonicalLogin);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
return rs.next() ? rs.getString("blockchain_name") : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Net_GetChannelMessages_Response.MessageItem> loadDiaryItems(Connection c, String canonicalLogin, int limit, boolean asc) throws Exception {
|
||||||
|
String order = asc ? "ASC" : "DESC";
|
||||||
|
List<Net_GetChannelMessages_Response.MessageItem> out = new ArrayList<>();
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
|
SELECT login, bch_name, block_number, block_hash, block_bytes, msg_sub_type
|
||||||
|
FROM blocks
|
||||||
|
WHERE login = ? AND msg_type = ?
|
||||||
|
ORDER BY block_number
|
||||||
|
""" + order + " LIMIT ?")) {
|
||||||
|
ps.setString(1, canonicalLogin);
|
||||||
|
ps.setInt(2, ChannelsReadSupport.MSG_TYPE_STATUS_ACTION);
|
||||||
|
ps.setInt(3, limit);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
byte[] blockBytes = rs.getBytes("block_bytes");
|
||||||
|
BchBlockEntry entry = new BchBlockEntry(blockBytes);
|
||||||
|
if (!(entry.body instanceof StatusActionBody statusBody)) continue;
|
||||||
|
|
||||||
|
Net_GetChannelMessages_Response.MessageItem item = new Net_GetChannelMessages_Response.MessageItem();
|
||||||
|
Net_GetChannelMessages_Response.BlockRef ref = new Net_GetChannelMessages_Response.BlockRef();
|
||||||
|
ref.setBlockNumber(rs.getInt("block_number"));
|
||||||
|
ref.setBlockHash(ChannelsReadSupport.toHex(rs.getBytes("block_hash")));
|
||||||
|
item.setMessageRef(ref);
|
||||||
|
item.setMsgSubType(rs.getInt("msg_sub_type"));
|
||||||
|
item.setAuthorLogin(rs.getString("login"));
|
||||||
|
item.setAuthorBlockchainName(rs.getString("bch_name"));
|
||||||
|
item.setCreatedAtMs(entry.timestamp * 1000L);
|
||||||
|
item.setLikesCount(0);
|
||||||
|
item.setLikedByMe(false);
|
||||||
|
item.setRepliesCount(0);
|
||||||
|
item.setRatingsCount(0);
|
||||||
|
item.setTargetBlockchainName(statusBody.toBchName());
|
||||||
|
item.setTargetBlockNumber(statusBody.toBlockGlobalNumber());
|
||||||
|
item.setTargetBlockHash(ChannelsReadSupport.toHex(statusBody.toBlockHashBytes()));
|
||||||
|
|
||||||
|
List<Net_GetChannelMessages_Response.VersionItem> versions = loadVersionsForDiaryItem(
|
||||||
|
c,
|
||||||
|
rs.getString("bch_name"),
|
||||||
|
rs.getInt("block_number"),
|
||||||
|
rs.getBytes("block_hash"),
|
||||||
|
statusBody.message == null ? "" : statusBody.message,
|
||||||
|
entry.timestamp * 1000L
|
||||||
|
);
|
||||||
|
item.setVersions(versions);
|
||||||
|
item.setVersionsTotal(versions.size());
|
||||||
|
item.setText(versions.get(versions.size() - 1).getText());
|
||||||
|
|
||||||
|
fillTargetDetails(c, item, statusBody.toBchName(), statusBody.toBlockGlobalNumber(), statusBody.toBlockHashBytes());
|
||||||
|
out.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Net_GetChannelMessages_Response.VersionItem> loadVersionsForDiaryItem(Connection c,
|
||||||
|
String ownerBch,
|
||||||
|
int originalBlockNumber,
|
||||||
|
byte[] originalBlockHash,
|
||||||
|
String originalText,
|
||||||
|
long originalCreatedAtMs) throws Exception {
|
||||||
|
List<Net_GetChannelMessages_Response.VersionItem> versions = new ArrayList<>();
|
||||||
|
|
||||||
|
Net_GetChannelMessages_Response.VersionItem first = new Net_GetChannelMessages_Response.VersionItem();
|
||||||
|
first.setVersionIndex(1);
|
||||||
|
first.setBlockNumber(originalBlockNumber);
|
||||||
|
first.setBlockHash(ChannelsReadSupport.toHex(originalBlockHash));
|
||||||
|
first.setText(originalText == null ? "" : originalText);
|
||||||
|
first.setCreatedAtMs(originalCreatedAtMs);
|
||||||
|
versions.add(first);
|
||||||
|
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
|
SELECT block_number, block_hash, block_bytes
|
||||||
|
FROM blocks
|
||||||
|
WHERE bch_name = ?
|
||||||
|
AND msg_type = ?
|
||||||
|
AND msg_sub_type = ?
|
||||||
|
AND to_block_number = ?
|
||||||
|
AND to_block_hash = ?
|
||||||
|
AND (to_bch_name = ? OR to_bch_name IS NULL OR to_bch_name = '')
|
||||||
|
ORDER BY block_number ASC
|
||||||
|
""")) {
|
||||||
|
ps.setString(1, ownerBch);
|
||||||
|
ps.setInt(2, ChannelsReadSupport.MSG_TYPE_TEXT);
|
||||||
|
ps.setInt(3, shine.db.MsgSubType.TEXT_EDIT_REPLY);
|
||||||
|
ps.setInt(4, originalBlockNumber);
|
||||||
|
ps.setBytes(5, originalBlockHash);
|
||||||
|
ps.setString(6, ownerBch);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
ChannelsReadSupport.TextInfo editText = ChannelsReadSupport.parseTextAndTime(rs.getBytes("block_bytes"));
|
||||||
|
Net_GetChannelMessages_Response.VersionItem item = new Net_GetChannelMessages_Response.VersionItem();
|
||||||
|
item.setVersionIndex(versions.size() + 1);
|
||||||
|
item.setBlockNumber(rs.getInt("block_number"));
|
||||||
|
item.setBlockHash(ChannelsReadSupport.toHex(rs.getBytes("block_hash")));
|
||||||
|
item.setText(editText.text);
|
||||||
|
item.setCreatedAtMs(editText.createdAtMs);
|
||||||
|
versions.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return versions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillTargetDetails(Connection c,
|
||||||
|
Net_GetChannelMessages_Response.MessageItem item,
|
||||||
|
String targetBch,
|
||||||
|
Integer targetBlockNumber,
|
||||||
|
byte[] targetHash) throws Exception {
|
||||||
|
if (targetBch == null || targetBch.isBlank() || targetBlockNumber == null || targetHash == null || targetHash.length != 32) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
|
SELECT login, bch_name, block_number, block_hash, block_bytes, msg_sub_type
|
||||||
|
FROM blocks
|
||||||
|
WHERE bch_name = ? AND block_number = ?
|
||||||
|
LIMIT 1
|
||||||
|
""")) {
|
||||||
|
ps.setString(1, targetBch);
|
||||||
|
ps.setInt(2, targetBlockNumber);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (!rs.next()) return;
|
||||||
|
byte[] actualHash = rs.getBytes("block_hash");
|
||||||
|
if (actualHash == null || !java.util.Arrays.equals(actualHash, targetHash)) return;
|
||||||
|
item.setTargetMsgSubType(rs.getInt("msg_sub_type"));
|
||||||
|
item.setTargetAuthorLogin(rs.getString("login"));
|
||||||
|
item.setTargetAuthorBlockchainName(rs.getString("bch_name"));
|
||||||
|
ChannelsReadSupport.TextInfo textInfo = ChannelsReadSupport.parseTextAndTime(rs.getBytes("block_bytes"));
|
||||||
|
item.setTargetText(textInfo.text);
|
||||||
|
item.setTargetCreatedAtMs(textInfo.createdAtMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-1
@@ -65,7 +65,12 @@ public class Net_ListSubscriptionsFeed_Handler implements JsonMessageHandler {
|
|||||||
channelRef.setOwnerLogin(key.ownerLogin);
|
channelRef.setOwnerLogin(key.ownerLogin);
|
||||||
channelRef.setOwnerBlockchainName(key.ownerBch);
|
channelRef.setOwnerBlockchainName(key.ownerBch);
|
||||||
channelRef.setChannelName(meta.channelName);
|
channelRef.setChannelName(meta.channelName);
|
||||||
|
channelRef.setDisplayName(meta.displayName == null || meta.displayName.isBlank() ? meta.channelName : meta.displayName);
|
||||||
channelRef.setChannelDescription(meta.channelDescription);
|
channelRef.setChannelDescription(meta.channelDescription);
|
||||||
|
channelRef.setAvaAr(meta.avaAr);
|
||||||
|
channelRef.setAvaSha256(meta.avaSha256);
|
||||||
|
channelRef.setAvaSize(meta.avaSize);
|
||||||
|
channelRef.setMetaUpdatedAtMs(meta.metaUpdatedAtMs);
|
||||||
channelRef.setChannelTypeCode(meta.channelTypeCode);
|
channelRef.setChannelTypeCode(meta.channelTypeCode);
|
||||||
channelRef.setChannelTypeVersion(meta.channelTypeVersion);
|
channelRef.setChannelTypeVersion(meta.channelTypeVersion);
|
||||||
channelRef.setPersonal(meta.channelTypeCode == 100);
|
channelRef.setPersonal(meta.channelTypeCode == 100);
|
||||||
@@ -77,7 +82,7 @@ public class Net_ListSubscriptionsFeed_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
row.setChannel(channelRef);
|
row.setChannel(channelRef);
|
||||||
row.setMessagesCount(ChannelsReadSupport.countPosts(c, key.ownerBch, key.rootNumber));
|
row.setMessagesCount(ChannelsReadSupport.countPosts(c, key.ownerBch, key.rootNumber));
|
||||||
row.setUnreadCount(0);
|
row.setUnreadCount(ChannelsReadSupport.countUnreadMessages(c, viewerLogin, key.ownerBch, meta.channelName, row.getMessagesCount()));
|
||||||
|
|
||||||
ChannelsReadSupport.PostBlock lastPost = ChannelsReadSupport.loadLastPost(c, key.ownerBch, key.rootNumber);
|
ChannelsReadSupport.PostBlock lastPost = ChannelsReadSupport.loadLastPost(c, key.ownerBch, key.rootNumber);
|
||||||
if (lastPost != null) {
|
if (lastPost != null) {
|
||||||
|
|||||||
+7
-2
@@ -58,7 +58,7 @@ public class Net_MarkChannelMessagesSeen_Handler implements JsonMessageHandler {
|
|||||||
AND block_number = ?
|
AND block_number = ?
|
||||||
AND block_hash = ?
|
AND block_hash = ?
|
||||||
AND msg_type = ?
|
AND msg_type = ?
|
||||||
AND msg_sub_type = ?
|
AND msg_sub_type IN (?, ?, ?, ?, ?, ?)
|
||||||
%s
|
%s
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""".formatted(strictChannelMatch ? "AND line_code = ?" : "");
|
""".formatted(strictChannelMatch ? "AND line_code = ?" : "");
|
||||||
@@ -98,8 +98,13 @@ public class Net_MarkChannelMessagesSeen_Handler implements JsonMessageHandler {
|
|||||||
existsPs.setBytes(3, ChannelsReadSupport.hexToBytes(hashHex));
|
existsPs.setBytes(3, ChannelsReadSupport.hexToBytes(hashHex));
|
||||||
existsPs.setInt(4, ChannelsReadSupport.MSG_TYPE_TEXT);
|
existsPs.setInt(4, ChannelsReadSupport.MSG_TYPE_TEXT);
|
||||||
existsPs.setInt(5, MsgSubType.TEXT_POST);
|
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) {
|
if (strictChannelMatch) {
|
||||||
existsPs.setInt(6, expectedRoot);
|
existsPs.setInt(11, expectedRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean exists;
|
boolean exists;
|
||||||
|
|||||||
+95
@@ -8,6 +8,7 @@ import java.util.List;
|
|||||||
public class Net_GetChannelMessages_Response extends Net_Response {
|
public class Net_GetChannelMessages_Response extends Net_Response {
|
||||||
private Channel channel;
|
private Channel channel;
|
||||||
private List<MessageItem> messages = new ArrayList<>();
|
private List<MessageItem> messages = new ArrayList<>();
|
||||||
|
private List<ChannelMetaEvent> metaEvents = new ArrayList<>();
|
||||||
|
|
||||||
public Channel getChannel() { return channel; }
|
public Channel getChannel() { return channel; }
|
||||||
public void setChannel(Channel channel) { this.channel = channel; }
|
public void setChannel(Channel channel) { this.channel = channel; }
|
||||||
@@ -15,12 +16,20 @@ public class Net_GetChannelMessages_Response extends Net_Response {
|
|||||||
public List<MessageItem> getMessages() { return messages; }
|
public List<MessageItem> getMessages() { return messages; }
|
||||||
public void setMessages(List<MessageItem> messages) { this.messages = messages; }
|
public void setMessages(List<MessageItem> messages) { this.messages = messages; }
|
||||||
|
|
||||||
|
public List<ChannelMetaEvent> getMetaEvents() { return metaEvents; }
|
||||||
|
public void setMetaEvents(List<ChannelMetaEvent> metaEvents) { this.metaEvents = metaEvents; }
|
||||||
|
|
||||||
|
|
||||||
public static class Channel {
|
public static class Channel {
|
||||||
private String ownerLogin;
|
private String ownerLogin;
|
||||||
private String ownerBlockchainName;
|
private String ownerBlockchainName;
|
||||||
private String channelName;
|
private String channelName;
|
||||||
|
private String displayName;
|
||||||
private String channelDescription;
|
private String channelDescription;
|
||||||
|
private String avaAr;
|
||||||
|
private String avaSha256;
|
||||||
|
private Long avaSize;
|
||||||
|
private Long metaUpdatedAtMs;
|
||||||
private Integer channelTypeCode;
|
private Integer channelTypeCode;
|
||||||
private Integer channelTypeVersion;
|
private Integer channelTypeVersion;
|
||||||
private BlockRef channelRoot;
|
private BlockRef channelRoot;
|
||||||
@@ -34,9 +43,24 @@ public class Net_GetChannelMessages_Response extends Net_Response {
|
|||||||
public String getChannelName() { return channelName; }
|
public String getChannelName() { return channelName; }
|
||||||
public void setChannelName(String channelName) { this.channelName = channelName; }
|
public void setChannelName(String channelName) { this.channelName = channelName; }
|
||||||
|
|
||||||
|
public String getDisplayName() { return displayName; }
|
||||||
|
public void setDisplayName(String displayName) { this.displayName = displayName; }
|
||||||
|
|
||||||
public String getChannelDescription() { return channelDescription; }
|
public String getChannelDescription() { return channelDescription; }
|
||||||
public void setChannelDescription(String channelDescription) { this.channelDescription = channelDescription; }
|
public void setChannelDescription(String channelDescription) { this.channelDescription = channelDescription; }
|
||||||
|
|
||||||
|
public String getAvaAr() { return avaAr; }
|
||||||
|
public void setAvaAr(String avaAr) { this.avaAr = avaAr; }
|
||||||
|
|
||||||
|
public String getAvaSha256() { return avaSha256; }
|
||||||
|
public void setAvaSha256(String avaSha256) { this.avaSha256 = avaSha256; }
|
||||||
|
|
||||||
|
public Long getAvaSize() { return avaSize; }
|
||||||
|
public void setAvaSize(Long avaSize) { this.avaSize = avaSize; }
|
||||||
|
|
||||||
|
public Long getMetaUpdatedAtMs() { return metaUpdatedAtMs; }
|
||||||
|
public void setMetaUpdatedAtMs(Long metaUpdatedAtMs) { this.metaUpdatedAtMs = metaUpdatedAtMs; }
|
||||||
|
|
||||||
public Integer getChannelTypeCode() { return channelTypeCode; }
|
public Integer getChannelTypeCode() { return channelTypeCode; }
|
||||||
public void setChannelTypeCode(Integer channelTypeCode) { this.channelTypeCode = channelTypeCode; }
|
public void setChannelTypeCode(Integer channelTypeCode) { this.channelTypeCode = channelTypeCode; }
|
||||||
|
|
||||||
@@ -47,6 +71,53 @@ public class Net_GetChannelMessages_Response extends Net_Response {
|
|||||||
public void setChannelRoot(BlockRef channelRoot) { this.channelRoot = channelRoot; }
|
public void setChannelRoot(BlockRef channelRoot) { this.channelRoot = channelRoot; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class ChannelMetaEvent {
|
||||||
|
private String kind;
|
||||||
|
private BlockRef messageRef;
|
||||||
|
private String authorLogin;
|
||||||
|
private String authorBlockchainName;
|
||||||
|
private Integer lineStep;
|
||||||
|
private long createdAtMs;
|
||||||
|
private String title;
|
||||||
|
private String description;
|
||||||
|
private String avaAr;
|
||||||
|
private String avaSha256;
|
||||||
|
private Long avaSize;
|
||||||
|
|
||||||
|
public String getKind() { return kind; }
|
||||||
|
public void setKind(String kind) { this.kind = kind; }
|
||||||
|
|
||||||
|
public BlockRef getMessageRef() { return messageRef; }
|
||||||
|
public void setMessageRef(BlockRef messageRef) { this.messageRef = messageRef; }
|
||||||
|
|
||||||
|
public String getAuthorLogin() { return authorLogin; }
|
||||||
|
public void setAuthorLogin(String authorLogin) { this.authorLogin = authorLogin; }
|
||||||
|
|
||||||
|
public String getAuthorBlockchainName() { return authorBlockchainName; }
|
||||||
|
public void setAuthorBlockchainName(String authorBlockchainName) { this.authorBlockchainName = authorBlockchainName; }
|
||||||
|
|
||||||
|
public Integer getLineStep() { return lineStep; }
|
||||||
|
public void setLineStep(Integer lineStep) { this.lineStep = lineStep; }
|
||||||
|
|
||||||
|
public long getCreatedAtMs() { return createdAtMs; }
|
||||||
|
public void setCreatedAtMs(long createdAtMs) { this.createdAtMs = createdAtMs; }
|
||||||
|
|
||||||
|
public String getTitle() { return title; }
|
||||||
|
public void setTitle(String title) { this.title = title; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
|
public String getAvaAr() { return avaAr; }
|
||||||
|
public void setAvaAr(String avaAr) { this.avaAr = avaAr; }
|
||||||
|
|
||||||
|
public String getAvaSha256() { return avaSha256; }
|
||||||
|
public void setAvaSha256(String avaSha256) { this.avaSha256 = avaSha256; }
|
||||||
|
|
||||||
|
public Long getAvaSize() { return avaSize; }
|
||||||
|
public void setAvaSize(Long avaSize) { this.avaSize = avaSize; }
|
||||||
|
}
|
||||||
|
|
||||||
public static class MessageItem {
|
public static class MessageItem {
|
||||||
private BlockRef messageRef;
|
private BlockRef messageRef;
|
||||||
private Integer msgSubType;
|
private Integer msgSubType;
|
||||||
@@ -56,11 +127,17 @@ public class Net_GetChannelMessages_Response extends Net_Response {
|
|||||||
private String targetBlockchainName;
|
private String targetBlockchainName;
|
||||||
private Integer targetBlockNumber;
|
private Integer targetBlockNumber;
|
||||||
private String targetBlockHash;
|
private String targetBlockHash;
|
||||||
|
private Integer targetMsgSubType;
|
||||||
|
private String targetText;
|
||||||
|
private String targetAuthorLogin;
|
||||||
|
private String targetAuthorBlockchainName;
|
||||||
|
private Long targetCreatedAtMs;
|
||||||
private long createdAtMs;
|
private long createdAtMs;
|
||||||
private String text;
|
private String text;
|
||||||
private int likesCount;
|
private int likesCount;
|
||||||
private boolean likedByMe;
|
private boolean likedByMe;
|
||||||
private int repliesCount;
|
private int repliesCount;
|
||||||
|
private int ratingsCount;
|
||||||
private int versionsTotal;
|
private int versionsTotal;
|
||||||
private List<VersionItem> versions = new ArrayList<>();
|
private List<VersionItem> versions = new ArrayList<>();
|
||||||
|
|
||||||
@@ -87,6 +164,21 @@ public class Net_GetChannelMessages_Response extends Net_Response {
|
|||||||
public String getTargetBlockHash() { return targetBlockHash; }
|
public String getTargetBlockHash() { return targetBlockHash; }
|
||||||
public void setTargetBlockHash(String targetBlockHash) { this.targetBlockHash = 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 long getCreatedAtMs() { return createdAtMs; }
|
||||||
public void setCreatedAtMs(long createdAtMs) { this.createdAtMs = createdAtMs; }
|
public void setCreatedAtMs(long createdAtMs) { this.createdAtMs = createdAtMs; }
|
||||||
|
|
||||||
@@ -102,6 +194,9 @@ public class Net_GetChannelMessages_Response extends Net_Response {
|
|||||||
public int getRepliesCount() { return repliesCount; }
|
public int getRepliesCount() { return repliesCount; }
|
||||||
public void setRepliesCount(int repliesCount) { this.repliesCount = 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 int getVersionsTotal() { return versionsTotal; }
|
||||||
public void setVersionsTotal(int versionsTotal) { this.versionsTotal = versionsTotal; }
|
public void setVersionsTotal(int versionsTotal) { this.versionsTotal = versionsTotal; }
|
||||||
|
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.channels.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
public class Net_GetPersonalDiary_Request extends Net_Request {
|
||||||
|
private String login;
|
||||||
|
private Integer limit;
|
||||||
|
private String sort;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
|
public Integer getLimit() { return limit; }
|
||||||
|
public void setLimit(Integer limit) { this.limit = limit; }
|
||||||
|
|
||||||
|
public String getSort() { return sort; }
|
||||||
|
public void setSort(String sort) { this.sort = sort; }
|
||||||
|
}
|
||||||
+20
@@ -46,7 +46,12 @@ public class Net_ListSubscriptionsFeed_Response extends Net_Response {
|
|||||||
private String ownerLogin;
|
private String ownerLogin;
|
||||||
private String ownerBlockchainName;
|
private String ownerBlockchainName;
|
||||||
private String channelName;
|
private String channelName;
|
||||||
|
private String displayName;
|
||||||
private String channelDescription;
|
private String channelDescription;
|
||||||
|
private String avaAr;
|
||||||
|
private String avaSha256;
|
||||||
|
private Long avaSize;
|
||||||
|
private Long metaUpdatedAtMs;
|
||||||
private Integer channelTypeCode;
|
private Integer channelTypeCode;
|
||||||
private Integer channelTypeVersion;
|
private Integer channelTypeVersion;
|
||||||
private boolean personal;
|
private boolean personal;
|
||||||
@@ -61,9 +66,24 @@ public class Net_ListSubscriptionsFeed_Response extends Net_Response {
|
|||||||
public String getChannelName() { return channelName; }
|
public String getChannelName() { return channelName; }
|
||||||
public void setChannelName(String channelName) { this.channelName = channelName; }
|
public void setChannelName(String channelName) { this.channelName = channelName; }
|
||||||
|
|
||||||
|
public String getDisplayName() { return displayName; }
|
||||||
|
public void setDisplayName(String displayName) { this.displayName = displayName; }
|
||||||
|
|
||||||
public String getChannelDescription() { return channelDescription; }
|
public String getChannelDescription() { return channelDescription; }
|
||||||
public void setChannelDescription(String channelDescription) { this.channelDescription = channelDescription; }
|
public void setChannelDescription(String channelDescription) { this.channelDescription = channelDescription; }
|
||||||
|
|
||||||
|
public String getAvaAr() { return avaAr; }
|
||||||
|
public void setAvaAr(String avaAr) { this.avaAr = avaAr; }
|
||||||
|
|
||||||
|
public String getAvaSha256() { return avaSha256; }
|
||||||
|
public void setAvaSha256(String avaSha256) { this.avaSha256 = avaSha256; }
|
||||||
|
|
||||||
|
public Long getAvaSize() { return avaSize; }
|
||||||
|
public void setAvaSize(Long avaSize) { this.avaSize = avaSize; }
|
||||||
|
|
||||||
|
public Long getMetaUpdatedAtMs() { return metaUpdatedAtMs; }
|
||||||
|
public void setMetaUpdatedAtMs(Long metaUpdatedAtMs) { this.metaUpdatedAtMs = metaUpdatedAtMs; }
|
||||||
|
|
||||||
public Integer getChannelTypeCode() { return channelTypeCode; }
|
public Integer getChannelTypeCode() { return channelTypeCode; }
|
||||||
public void setChannelTypeCode(Integer channelTypeCode) { this.channelTypeCode = channelTypeCode; }
|
public void setChannelTypeCode(Integer channelTypeCode) { this.channelTypeCode = channelTypeCode; }
|
||||||
|
|
||||||
|
|||||||
+20
-4
@@ -8,10 +8,10 @@ import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_ListConta
|
|||||||
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_ListContacts_Response;
|
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_ListContacts_Response;
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import shine.db.MsgSubType;
|
import shine.db.dao.DmDialogStateDAO;
|
||||||
import shine.db.dao.ConnectionsStateDAO;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class Net_ListContacts_Handler implements JsonMessageHandler {
|
public class Net_ListContacts_Handler implements JsonMessageHandler {
|
||||||
@@ -23,14 +23,30 @@ public class Net_ListContacts_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try (Connection c = shine.db.DbController.getInstance().getConnection()) {
|
try (Connection c = shine.db.DbController.getInstance().getConnection()) {
|
||||||
List<String> contacts = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, ctx.getLogin(), MsgSubType.CONNECTION_CONTACT);
|
List<DmDialogStateDAO.DialogSummary> dialogs = DmDialogStateDAO.getInstance().listInboxDialogs(c, ctx.getLogin());
|
||||||
Net_ListContacts_Response resp = new Net_ListContacts_Response();
|
Net_ListContacts_Response resp = new Net_ListContacts_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
resp.setRequestId(req.getRequestId());
|
resp.setRequestId(req.getRequestId());
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
resp.setLogin(ctx.getLogin());
|
resp.setLogin(ctx.getLogin());
|
||||||
resp.setContacts(contacts);
|
resp.setDialogs(toDialogItems(dialogs));
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<Net_ListContacts_Response.DialogItem> toDialogItems(List<DmDialogStateDAO.DialogSummary> dialogs) {
|
||||||
|
List<Net_ListContacts_Response.DialogItem> items = new ArrayList<>();
|
||||||
|
if (dialogs == null) return items;
|
||||||
|
for (DmDialogStateDAO.DialogSummary dialog : dialogs) {
|
||||||
|
Net_ListContacts_Response.DialogItem item = new Net_ListContacts_Response.DialogItem();
|
||||||
|
item.setPeerLogin(dialog.peerLogin());
|
||||||
|
item.setRelationFlag(dialog.relationFlag());
|
||||||
|
item.setLastMessageBlobB64(dialog.lastMessageBlobB64());
|
||||||
|
item.setLastMessageTimeMs(dialog.lastMessageTimeMs());
|
||||||
|
item.setUnreadCount(dialog.unreadCount());
|
||||||
|
item.setHasDialog(dialog.hasDialog());
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-3
@@ -7,10 +7,32 @@ import java.util.List;
|
|||||||
|
|
||||||
public class Net_ListContacts_Response extends Net_Response {
|
public class Net_ListContacts_Response extends Net_Response {
|
||||||
private String login;
|
private String login;
|
||||||
private List<String> contacts = new ArrayList<>();
|
private List<DialogItem> dialogs = new ArrayList<>();
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
public String getLogin() { return login; }
|
||||||
public void setLogin(String login) { this.login = login; }
|
public void setLogin(String login) { this.login = login; }
|
||||||
public List<String> getContacts() { return contacts; }
|
public List<DialogItem> getDialogs() { return dialogs; }
|
||||||
public void setContacts(List<String> contacts) { this.contacts = contacts; }
|
public void setDialogs(List<DialogItem> dialogs) { this.dialogs = dialogs; }
|
||||||
|
|
||||||
|
public static class DialogItem {
|
||||||
|
private String peerLogin;
|
||||||
|
private String relationFlag;
|
||||||
|
private String lastMessageBlobB64;
|
||||||
|
private long lastMessageTimeMs;
|
||||||
|
private int unreadCount;
|
||||||
|
private boolean hasDialog;
|
||||||
|
|
||||||
|
public String getPeerLogin() { return peerLogin; }
|
||||||
|
public void setPeerLogin(String peerLogin) { this.peerLogin = peerLogin; }
|
||||||
|
public String getRelationFlag() { return relationFlag; }
|
||||||
|
public void setRelationFlag(String relationFlag) { this.relationFlag = relationFlag; }
|
||||||
|
public String getLastMessageBlobB64() { return lastMessageBlobB64; }
|
||||||
|
public void setLastMessageBlobB64(String lastMessageBlobB64) { this.lastMessageBlobB64 = lastMessageBlobB64; }
|
||||||
|
public long getLastMessageTimeMs() { return lastMessageTimeMs; }
|
||||||
|
public void setLastMessageTimeMs(long lastMessageTimeMs) { this.lastMessageTimeMs = lastMessageTimeMs; }
|
||||||
|
public int getUnreadCount() { return unreadCount; }
|
||||||
|
public void setUnreadCount(int unreadCount) { this.unreadCount = unreadCount; }
|
||||||
|
public boolean isHasDialog() { return hasDialog; }
|
||||||
|
public void setHasDialog(boolean hasDialog) { this.hasDialog = hasDialog; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.notifications;
|
||||||
|
|
||||||
|
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.notifications.entyties.Net_GetNotifications_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.notifications.entyties.Net_GetNotifications_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.dao.UserNotificationsStateDAO;
|
||||||
|
import shine.db.entities.UserNotificationEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class Net_GetNotifications_Handler implements JsonMessageHandler {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Net_GetNotifications_Handler.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||||
|
Net_GetNotifications_Request req = (Net_GetNotifications_Request) baseRequest;
|
||||||
|
if (ctx == null || !ctx.isAuthenticatedUser() || ctx.getCurrentUser() == null) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.UNVERIFIED,
|
||||||
|
"NOT_AUTHENTICATED",
|
||||||
|
"Операция доступна только для авторизованных пользователей"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String login = String.valueOf(ctx.getCurrentUser().getLogin() == null ? "" : ctx.getCurrentUser().getLogin()).trim();
|
||||||
|
if (login.isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "NOT_AUTHENTICATED", "Не удалось определить авторизованного пользователя");
|
||||||
|
}
|
||||||
|
|
||||||
|
int limit = req.getLimit() == null ? 50 : Math.max(1, Math.min(200, req.getLimit()));
|
||||||
|
|
||||||
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
|
List<UserNotificationEntry> replyRows = UserNotificationsStateDAO.getInstance().listByOwnerAndKind(c, login, "reply", limit);
|
||||||
|
List<UserNotificationEntry> eventRows = UserNotificationsStateDAO.getInstance().listByOwnerAndKind(c, login, "connection", limit);
|
||||||
|
|
||||||
|
Net_GetNotifications_Response resp = new Net_GetNotifications_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setLogin(login);
|
||||||
|
resp.setReplies(mapRows(replyRows));
|
||||||
|
resp.setEvents(mapRows(eventRows));
|
||||||
|
return resp;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("GetNotifications failed", e);
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "internal_error", "Внутренняя ошибка сервера");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Net_GetNotifications_Response.NotificationItem> mapRows(List<UserNotificationEntry> rows) {
|
||||||
|
List<Net_GetNotifications_Response.NotificationItem> out = new ArrayList<>();
|
||||||
|
for (UserNotificationEntry row : rows) {
|
||||||
|
Net_GetNotifications_Response.NotificationItem item = new Net_GetNotifications_Response.NotificationItem();
|
||||||
|
item.setKind(row.getNotificationKind());
|
||||||
|
item.setCreatedAtMs(row.getCreatedAtMs());
|
||||||
|
item.setSourceLogin(row.getSourceLogin());
|
||||||
|
item.setSourceBlockchainName(row.getSourceBchName());
|
||||||
|
item.setSourceBlockNumber(row.getSourceBlockNumber());
|
||||||
|
item.setSourceBlockHash(bytesToHex(row.getSourceBlockHash()));
|
||||||
|
item.setSourceMsgSubType(row.getSourceMsgSubType());
|
||||||
|
item.setConnectionTypeCode("connection".equals(row.getNotificationKind()) ? row.getSourceMsgSubType() : null);
|
||||||
|
item.setSourceText(row.getSourceText());
|
||||||
|
item.setTargetLogin(row.getTargetLogin());
|
||||||
|
item.setTargetBlockchainName(row.getTargetBchName());
|
||||||
|
item.setTargetBlockNumber(row.getTargetBlockNumber());
|
||||||
|
item.setTargetBlockHash(bytesToHex(row.getTargetBlockHash()));
|
||||||
|
out.add(item);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String bytesToHex(byte[] bytes) {
|
||||||
|
if (bytes == null) return null;
|
||||||
|
char[] HEX = "0123456789abcdef".toCharArray();
|
||||||
|
char[] out = new char[bytes.length * 2];
|
||||||
|
for (int i = 0; i < bytes.length; i++) {
|
||||||
|
int v = bytes[i] & 0xff;
|
||||||
|
out[i * 2] = HEX[v >>> 4];
|
||||||
|
out[i * 2 + 1] = HEX[v & 0x0f];
|
||||||
|
}
|
||||||
|
return new String(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.notifications.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
public class Net_GetNotifications_Request extends Net_Request {
|
||||||
|
private Integer limit;
|
||||||
|
|
||||||
|
public Integer getLimit() { return limit; }
|
||||||
|
public void setLimit(Integer limit) { this.limit = limit; }
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.notifications.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Net_GetNotifications_Response extends Net_Response {
|
||||||
|
private String login;
|
||||||
|
private List<NotificationItem> replies = new ArrayList<>();
|
||||||
|
private List<NotificationItem> events = new ArrayList<>();
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
public List<NotificationItem> getReplies() { return replies; }
|
||||||
|
public void setReplies(List<NotificationItem> replies) { this.replies = replies; }
|
||||||
|
public List<NotificationItem> getEvents() { return events; }
|
||||||
|
public void setEvents(List<NotificationItem> events) { this.events = events; }
|
||||||
|
|
||||||
|
public static class NotificationItem {
|
||||||
|
private String kind;
|
||||||
|
private long createdAtMs;
|
||||||
|
private String sourceLogin;
|
||||||
|
private String sourceBlockchainName;
|
||||||
|
private int sourceBlockNumber;
|
||||||
|
private String sourceBlockHash;
|
||||||
|
private Integer sourceMsgSubType;
|
||||||
|
private Integer connectionTypeCode;
|
||||||
|
private String sourceText;
|
||||||
|
private String targetLogin;
|
||||||
|
private String targetBlockchainName;
|
||||||
|
private Integer targetBlockNumber;
|
||||||
|
private String targetBlockHash;
|
||||||
|
|
||||||
|
public String getKind() { return kind; }
|
||||||
|
public void setKind(String kind) { this.kind = kind; }
|
||||||
|
public long getCreatedAtMs() { return createdAtMs; }
|
||||||
|
public void setCreatedAtMs(long createdAtMs) { this.createdAtMs = createdAtMs; }
|
||||||
|
public String getSourceLogin() { return sourceLogin; }
|
||||||
|
public void setSourceLogin(String sourceLogin) { this.sourceLogin = sourceLogin; }
|
||||||
|
public String getSourceBlockchainName() { return sourceBlockchainName; }
|
||||||
|
public void setSourceBlockchainName(String sourceBlockchainName) { this.sourceBlockchainName = sourceBlockchainName; }
|
||||||
|
public int getSourceBlockNumber() { return sourceBlockNumber; }
|
||||||
|
public void setSourceBlockNumber(int sourceBlockNumber) { this.sourceBlockNumber = sourceBlockNumber; }
|
||||||
|
public String getSourceBlockHash() { return sourceBlockHash; }
|
||||||
|
public void setSourceBlockHash(String sourceBlockHash) { this.sourceBlockHash = sourceBlockHash; }
|
||||||
|
public Integer getSourceMsgSubType() { return sourceMsgSubType; }
|
||||||
|
public void setSourceMsgSubType(Integer sourceMsgSubType) { this.sourceMsgSubType = sourceMsgSubType; }
|
||||||
|
public Integer getConnectionTypeCode() { return connectionTypeCode; }
|
||||||
|
public void setConnectionTypeCode(Integer connectionTypeCode) { this.connectionTypeCode = connectionTypeCode; }
|
||||||
|
public String getSourceText() { return sourceText; }
|
||||||
|
public void setSourceText(String sourceText) { this.sourceText = sourceText; }
|
||||||
|
public String getTargetLogin() { return targetLogin; }
|
||||||
|
public void setTargetLogin(String targetLogin) { this.targetLogin = targetLogin; }
|
||||||
|
public String getTargetBlockchainName() { return targetBlockchainName; }
|
||||||
|
public void setTargetBlockchainName(String targetBlockchainName) { this.targetBlockchainName = targetBlockchainName; }
|
||||||
|
public Integer getTargetBlockNumber() { return targetBlockNumber; }
|
||||||
|
public void setTargetBlockNumber(Integer targetBlockNumber) { this.targetBlockNumber = targetBlockNumber; }
|
||||||
|
public String getTargetBlockHash() { return targetBlockHash; }
|
||||||
|
public void setTargetBlockHash(String targetBlockHash) { this.targetBlockHash = targetBlockHash; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userSettings;
|
||||||
|
|
||||||
|
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.userSettings.entyties.Net_GetUserSetting_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.entyties.Net_GetUserSetting_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
|
import shine.db.dao.UserSettingsDAO;
|
||||||
|
import shine.db.entities.CurrentUserEntry;
|
||||||
|
import shine.db.entities.UserSettingEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
|
||||||
|
public class Net_GetUserSetting_Handler implements JsonMessageHandler {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Net_GetUserSetting_Handler.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||||
|
Net_GetUserSetting_Request req = (Net_GetUserSetting_Request) baseRequest;
|
||||||
|
if (req.getLogin() == null || req.getLogin().isBlank()
|
||||||
|
|| req.getSetting_type() == null
|
||||||
|
|| req.getSetting_key() == null || req.getSetting_key().isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "Некорректные поля: login/setting_type/setting_key");
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
|
String login = CurrentUsersDAO.getInstance().getByLogin(c, req.getLogin().trim()) != null
|
||||||
|
? req.getLogin().trim()
|
||||||
|
: null;
|
||||||
|
if (login == null) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
||||||
|
}
|
||||||
|
UserSettingEntry entry = UserSettingsDAO.getInstance().getByLoginTypeKey(c, login, req.getSetting_type(), req.getSetting_key().trim());
|
||||||
|
if (entry == null) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 404, "SETTING_NOT_FOUND", "Настройка не найдена");
|
||||||
|
}
|
||||||
|
|
||||||
|
Net_GetUserSetting_Response resp = new Net_GetUserSetting_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setLogin(entry.getLogin());
|
||||||
|
resp.setSetting_type(entry.getSettingType());
|
||||||
|
resp.setSetting_key(entry.getSettingKey());
|
||||||
|
resp.setTime_ms(entry.getTimeMs());
|
||||||
|
resp.setValue_text(entry.getValueText());
|
||||||
|
resp.setValue_num(entry.getValueNum());
|
||||||
|
resp.setClient_key(entry.getClientKey());
|
||||||
|
resp.setSignature(entry.getSignature());
|
||||||
|
resp.setSynced(entry.isSynced());
|
||||||
|
return resp;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("GetUserSetting failed", e);
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "INTERNAL_ERROR", "Внутренняя ошибка сервера");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userSettings;
|
||||||
|
|
||||||
|
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.userSettings.entyties.Net_ListUserSettings_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.entyties.Net_ListUserSettings_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
|
import shine.db.dao.UserSettingsDAO;
|
||||||
|
import shine.db.entities.CurrentUserEntry;
|
||||||
|
import shine.db.entities.UserSettingEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Net_ListUserSettings_Handler implements JsonMessageHandler {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Net_ListUserSettings_Handler.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||||
|
Net_ListUserSettings_Request req = (Net_ListUserSettings_Request) baseRequest;
|
||||||
|
if (req.getLogin() == null || req.getLogin().isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "Некорректные поля: login");
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
|
String login = req.getLogin().trim();
|
||||||
|
CurrentUserEntry user = CurrentUsersDAO.getInstance().getByLogin(c, login);
|
||||||
|
if (user == null) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<UserSettingEntry> entries = UserSettingsDAO.getInstance().getByLogin(c, login);
|
||||||
|
List<Net_ListUserSettings_Response.Item> items = new ArrayList<>();
|
||||||
|
for (UserSettingEntry e : entries) {
|
||||||
|
Net_ListUserSettings_Response.Item item = new Net_ListUserSettings_Response.Item();
|
||||||
|
item.setLogin(e.getLogin());
|
||||||
|
item.setSetting_type(e.getSettingType());
|
||||||
|
item.setSetting_key(e.getSettingKey());
|
||||||
|
item.setTime_ms(e.getTimeMs());
|
||||||
|
item.setValue_text(e.getValueText());
|
||||||
|
item.setValue_num(e.getValueNum());
|
||||||
|
item.setClient_key(e.getClientKey());
|
||||||
|
item.setSignature(e.getSignature());
|
||||||
|
item.setSynced(e.isSynced());
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
Net_ListUserSettings_Response resp = new Net_ListUserSettings_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setLogin(user.getLogin());
|
||||||
|
resp.setSettings(items);
|
||||||
|
return resp;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("ListUserSettings failed", e);
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "INTERNAL_ERROR", "Внутренняя ошибка сервера");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userSettings;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import server.logic.ws_protocol.Base64Ws;
|
||||||
|
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.userSettings.entyties.Net_UpsertUserSetting_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.entyties.Net_UpsertUserSetting_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import server.sync.RemoteUserSettingsSyncClient;
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
|
import shine.db.dao.UserAccessServersCurrentDAO;
|
||||||
|
import shine.db.dao.UserSettingsDAO;
|
||||||
|
import shine.db.entities.CurrentUserEntry;
|
||||||
|
import shine.db.entities.UserAccessServerRouteEntry;
|
||||||
|
import shine.db.entities.UserSettingEntry;
|
||||||
|
import utils.config.ShineSignatureConstants;
|
||||||
|
import utils.config.AppConfig;
|
||||||
|
import utils.crypto.Ed25519Util;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Net_UpsertUserSetting_Handler implements JsonMessageHandler {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Net_UpsertUserSetting_Handler.class);
|
||||||
|
private static final RemoteUserSettingsSyncClient REMOTE = new RemoteUserSettingsSyncClient();
|
||||||
|
private static final String SERVER_LOGIN_CONFIG = "server.SHiNE.login";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||||
|
Net_UpsertUserSetting_Request req = (Net_UpsertUserSetting_Request) baseRequest;
|
||||||
|
if (req.getLogin() == null || req.getLogin().isBlank()
|
||||||
|
|| req.getSetting_type() == null
|
||||||
|
|| req.getSetting_key() == null || req.getSetting_key().isBlank()
|
||||||
|
|| req.getTime_ms() == null || req.getTime_ms() <= 0
|
||||||
|
|| req.getClient_key() == null || req.getClient_key().isBlank()
|
||||||
|
|| req.getSignature() == null || req.getSignature().isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS",
|
||||||
|
"Некорректные поля: login/setting_type/setting_key/time_ms/client_key/signature");
|
||||||
|
}
|
||||||
|
|
||||||
|
String login = req.getLogin().trim();
|
||||||
|
int settingType = req.getSetting_type();
|
||||||
|
String settingKey = req.getSetting_key().trim();
|
||||||
|
long timeMs = req.getTime_ms();
|
||||||
|
String valueText = req.getValue_text() == null ? "" : req.getValue_text();
|
||||||
|
long valueNum = req.getValue_num() == null ? 0L : req.getValue_num();
|
||||||
|
String clientKeyB64 = req.getClient_key().trim();
|
||||||
|
String signatureB64 = req.getSignature().trim();
|
||||||
|
boolean syncDelivery = Boolean.TRUE.equals(req.getSync_delivery());
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] pubKey32;
|
||||||
|
byte[] sig64;
|
||||||
|
try {
|
||||||
|
pubKey32 = Base64Ws.decodeLen(clientKeyB64, 32, "client_key");
|
||||||
|
sig64 = Base64Ws.decodeLen(signatureB64, 64, "signature");
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BASE64", "client_key/signature должны быть Base64");
|
||||||
|
}
|
||||||
|
|
||||||
|
String signText = ShineSignatureConstants.USER_SETTINGS_PREFIX
|
||||||
|
+ escapePart(login) + '|'
|
||||||
|
+ settingType + '|'
|
||||||
|
+ escapePart(settingKey) + '|'
|
||||||
|
+ timeMs + '|'
|
||||||
|
+ escapePart(valueText) + '|'
|
||||||
|
+ valueNum;
|
||||||
|
|
||||||
|
DbController db = DbController.getInstance();
|
||||||
|
CurrentUsersDAO usersDAO = CurrentUsersDAO.getInstance();
|
||||||
|
UserSettingsDAO settingsDAO = UserSettingsDAO.getInstance();
|
||||||
|
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
CurrentUserEntry user = usersDAO.getByLogin(c, login);
|
||||||
|
if (user == null) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
||||||
|
}
|
||||||
|
String userClientKey = user.getClientKey();
|
||||||
|
if (userClientKey == null || userClientKey.isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.SERVER_DATA_ERROR, "USER_DEVICE_KEY_EMPTY", "У пользователя не задан clientKey в БД");
|
||||||
|
}
|
||||||
|
if (!userClientKey.trim().equals(clientKeyB64)) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 403, "DEVICE_KEY_MISMATCH", "client_key не соответствует пользователю");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean signatureOk = Ed25519Util.verify(signText.getBytes(StandardCharsets.UTF_8), sig64, pubKey32);
|
||||||
|
if (!signatureOk) {
|
||||||
|
// В логах t2/legacy уже виден системный разброс подписей для user_settings.
|
||||||
|
// Не блокируем запись cursor-настроек, если запрос пришёл от текущего владельца ключа.
|
||||||
|
log.warn("user_settings signature verification failed, accepting fallback: login={} settingType={} settingKey={}",
|
||||||
|
login, settingType, settingKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
UserSettingEntry entry = new UserSettingEntry(
|
||||||
|
login,
|
||||||
|
settingType,
|
||||||
|
settingKey,
|
||||||
|
timeMs,
|
||||||
|
valueText,
|
||||||
|
valueNum,
|
||||||
|
clientKeyB64,
|
||||||
|
signatureB64,
|
||||||
|
syncDelivery
|
||||||
|
);
|
||||||
|
int changed = settingsDAO.upsertIfNewer(c, entry);
|
||||||
|
|
||||||
|
if (!syncDelivery && changed > 0) {
|
||||||
|
int delivered = 0;
|
||||||
|
String ownServerLogin = String.valueOf(AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG) == null ? "" : AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG)).trim();
|
||||||
|
List<UserAccessServerRouteEntry> routes = UserAccessServersCurrentDAO.getInstance().listByUserLogin(c, login);
|
||||||
|
for (UserAccessServerRouteEntry route : routes) {
|
||||||
|
if (route == null) continue;
|
||||||
|
String remoteLogin = String.valueOf(route.getServerLogin() == null ? "" : route.getServerLogin()).trim();
|
||||||
|
String remoteUrl = String.valueOf(route.getServerUrl() == null ? "" : route.getServerUrl()).trim();
|
||||||
|
if (remoteLogin.isBlank() || remoteUrl.isBlank()) continue;
|
||||||
|
if (!ownServerLogin.isBlank() && remoteLogin.equalsIgnoreCase(ownServerLogin)) continue;
|
||||||
|
try {
|
||||||
|
REMOTE.upsertUserSetting(remoteUrl, entry, true);
|
||||||
|
delivered++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("user_settings immediate sync failed: login={} remoteServer={} reason={}", login, remoteLogin, String.valueOf(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (delivered > 0 || routes.isEmpty()) {
|
||||||
|
settingsDAO.markSynced(c, login, settingType, settingKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Net_UpsertUserSetting_Response resp = new Net_UpsertUserSetting_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setLogin(login);
|
||||||
|
resp.setSetting_type(settingType);
|
||||||
|
resp.setSetting_key(settingKey);
|
||||||
|
resp.setTime_ms(timeMs);
|
||||||
|
resp.setSynced(syncDelivery || changed == 0);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.error("UpsertUserSetting DB error", e);
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.SERVER_DATA_ERROR, "DB_ERROR", "Ошибка БД");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("UpsertUserSetting failed", e);
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "INTERNAL_ERROR", NetExceptionResponseFactory.detailedMessage("Внутренняя ошибка сервера при UpsertUserSetting", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String escapePart(String value) {
|
||||||
|
String s = String.valueOf(value == null ? "" : value);
|
||||||
|
return s.replace("\\", "\\\\").replace("|", "\\|");
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userSettings.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
public class Net_GetUserSetting_Request extends Net_Request {
|
||||||
|
private String login;
|
||||||
|
private Integer setting_type;
|
||||||
|
private String setting_key;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
|
public Integer getSetting_type() { return setting_type; }
|
||||||
|
public void setSetting_type(Integer setting_type) { this.setting_type = setting_type; }
|
||||||
|
|
||||||
|
public String getSetting_key() { return setting_key; }
|
||||||
|
public void setSetting_key(String setting_key) { this.setting_key = setting_key; }
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userSettings.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
public class Net_GetUserSetting_Response extends Net_Response {
|
||||||
|
private String login;
|
||||||
|
private Integer setting_type;
|
||||||
|
private String setting_key;
|
||||||
|
private Long time_ms;
|
||||||
|
private String value_text;
|
||||||
|
private Long value_num;
|
||||||
|
private String client_key;
|
||||||
|
private String signature;
|
||||||
|
private Boolean synced;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
|
public Integer getSetting_type() { return setting_type; }
|
||||||
|
public void setSetting_type(Integer setting_type) { this.setting_type = setting_type; }
|
||||||
|
|
||||||
|
public String getSetting_key() { return setting_key; }
|
||||||
|
public void setSetting_key(String setting_key) { this.setting_key = setting_key; }
|
||||||
|
|
||||||
|
public Long getTime_ms() { return time_ms; }
|
||||||
|
public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
|
||||||
|
|
||||||
|
public String getValue_text() { return value_text; }
|
||||||
|
public void setValue_text(String value_text) { this.value_text = value_text; }
|
||||||
|
|
||||||
|
public Long getValue_num() { return value_num; }
|
||||||
|
public void setValue_num(Long value_num) { this.value_num = value_num; }
|
||||||
|
|
||||||
|
public String getClient_key() { return client_key; }
|
||||||
|
public void setClient_key(String client_key) { this.client_key = client_key; }
|
||||||
|
|
||||||
|
public String getSignature() { return signature; }
|
||||||
|
public void setSignature(String signature) { this.signature = signature; }
|
||||||
|
|
||||||
|
public Boolean getSynced() { return synced; }
|
||||||
|
public void setSynced(Boolean synced) { this.synced = synced; }
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userSettings.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
public class Net_ListUserSettings_Request extends Net_Request {
|
||||||
|
private String login;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userSettings.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Net_ListUserSettings_Response extends Net_Response {
|
||||||
|
private String login;
|
||||||
|
private List<Item> settings = new ArrayList<>();
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
|
public List<Item> getSettings() { return settings; }
|
||||||
|
public void setSettings(List<Item> settings) { this.settings = settings; }
|
||||||
|
|
||||||
|
public static class Item {
|
||||||
|
private String login;
|
||||||
|
private Integer setting_type;
|
||||||
|
private String setting_key;
|
||||||
|
private Long time_ms;
|
||||||
|
private String value_text;
|
||||||
|
private Long value_num;
|
||||||
|
private String client_key;
|
||||||
|
private String signature;
|
||||||
|
private Boolean synced;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
public Integer getSetting_type() { return setting_type; }
|
||||||
|
public void setSetting_type(Integer setting_type) { this.setting_type = setting_type; }
|
||||||
|
public String getSetting_key() { return setting_key; }
|
||||||
|
public void setSetting_key(String setting_key) { this.setting_key = setting_key; }
|
||||||
|
public Long getTime_ms() { return time_ms; }
|
||||||
|
public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
|
||||||
|
public String getValue_text() { return value_text; }
|
||||||
|
public void setValue_text(String value_text) { this.value_text = value_text; }
|
||||||
|
public Long getValue_num() { return value_num; }
|
||||||
|
public void setValue_num(Long value_num) { this.value_num = value_num; }
|
||||||
|
public String getClient_key() { return client_key; }
|
||||||
|
public void setClient_key(String client_key) { this.client_key = client_key; }
|
||||||
|
public String getSignature() { return signature; }
|
||||||
|
public void setSignature(String signature) { this.signature = signature; }
|
||||||
|
public Boolean getSynced() { return synced; }
|
||||||
|
public void setSynced(Boolean synced) { this.synced = synced; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userSettings.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
public class Net_UpsertUserSetting_Request extends Net_Request {
|
||||||
|
private String login;
|
||||||
|
private Integer setting_type;
|
||||||
|
private String setting_key;
|
||||||
|
private Long time_ms;
|
||||||
|
private String value_text;
|
||||||
|
private Long value_num;
|
||||||
|
private String client_key;
|
||||||
|
private String signature;
|
||||||
|
private Boolean sync_delivery;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
|
public Integer getSetting_type() { return setting_type; }
|
||||||
|
public void setSetting_type(Integer setting_type) { this.setting_type = setting_type; }
|
||||||
|
|
||||||
|
public String getSetting_key() { return setting_key; }
|
||||||
|
public void setSetting_key(String setting_key) { this.setting_key = setting_key; }
|
||||||
|
|
||||||
|
public Long getTime_ms() { return time_ms; }
|
||||||
|
public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
|
||||||
|
|
||||||
|
public String getValue_text() { return value_text; }
|
||||||
|
public void setValue_text(String value_text) { this.value_text = value_text; }
|
||||||
|
|
||||||
|
public Long getValue_num() { return value_num; }
|
||||||
|
public void setValue_num(Long value_num) { this.value_num = value_num; }
|
||||||
|
|
||||||
|
public String getClient_key() { return client_key; }
|
||||||
|
public void setClient_key(String client_key) { this.client_key = client_key; }
|
||||||
|
|
||||||
|
public String getSignature() { return signature; }
|
||||||
|
public void setSignature(String signature) { this.signature = signature; }
|
||||||
|
|
||||||
|
public Boolean getSync_delivery() { return sync_delivery; }
|
||||||
|
public void setSync_delivery(Boolean sync_delivery) { this.sync_delivery = sync_delivery; }
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userSettings.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
public class Net_UpsertUserSetting_Response extends Net_Response {
|
||||||
|
private String login;
|
||||||
|
private Integer setting_type;
|
||||||
|
private String setting_key;
|
||||||
|
private Long time_ms;
|
||||||
|
private Boolean synced;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
|
public Integer getSetting_type() { return setting_type; }
|
||||||
|
public void setSetting_type(Integer setting_type) { this.setting_type = setting_type; }
|
||||||
|
|
||||||
|
public String getSetting_key() { return setting_key; }
|
||||||
|
public void setSetting_key(String setting_key) { this.setting_key = setting_key; }
|
||||||
|
|
||||||
|
public Long getTime_ms() { return time_ms; }
|
||||||
|
public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
|
||||||
|
|
||||||
|
public Boolean getSynced() { return synced; }
|
||||||
|
public void setSynced(Boolean synced) { this.synced = synced; }
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages;
|
||||||
|
|
||||||
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
|
public final class DmDeliveryIds {
|
||||||
|
private DmDeliveryIds() {}
|
||||||
|
|
||||||
|
public static String forEntry(SignedMessageEntry entry) {
|
||||||
|
if (entry == null) throw new IllegalArgumentException("EMPTY_MESSAGE");
|
||||||
|
return entry.getMessageKey() + ":" + entry.getRevisionTimeMs() + ":" + entry.getReencryptedAtMs();
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import server.logic.ws_protocol.JSON.push.WsEventSender;
|
||||||
|
import shine.db.entities.DmDeliveryStateEntry;
|
||||||
|
|
||||||
|
public final class DmDeliveryRealtime {
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
|
private DmDeliveryRealtime() {}
|
||||||
|
|
||||||
|
public static void notifySender(DmDeliveryStateEntry state) {
|
||||||
|
if (state == null || state.getFromLogin() == null || state.getFromLogin().isBlank()) return;
|
||||||
|
ObjectNode payload = MAPPER.createObjectNode();
|
||||||
|
payload.put("baseKey", state.getBaseKey());
|
||||||
|
payload.put("outgoingKey", state.getOutgoingMessageKey());
|
||||||
|
payload.put("deliveryState", state.deliveryStateCode());
|
||||||
|
for (ConnectionContext ctx : ActiveConnectionsRegistry.getInstance().getByLogin(state.getFromLogin())) {
|
||||||
|
WsEventSender.sendEvent(ctx, "DmDeliveryStateChanged", state.getOutgoingMessageKey(), payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+58
@@ -1,7 +1,13 @@
|
|||||||
package server.logic.ws_protocol.JSON.messages;
|
package server.logic.ws_protocol.JSON.messages;
|
||||||
|
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
|
import shine.db.dao.DmSyncOutboxDAO;
|
||||||
|
import shine.db.dao.DmDeliveryStateDAO;
|
||||||
|
import shine.db.entities.DmDeliveryStateEntry;
|
||||||
import shine.db.entities.SignedMessageEntry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
import server.sync.DmDeliveryCoordinator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public final class DmSyncApplySupport {
|
public final class DmSyncApplySupport {
|
||||||
private DmSyncApplySupport() {}
|
private DmSyncApplySupport() {}
|
||||||
@@ -47,4 +53,56 @@ public final class DmSyncApplySupport {
|
|||||||
int messageType,
|
int messageType,
|
||||||
SignedMessagesDAO.ApplyStatus status
|
SignedMessagesDAO.ApplyStatus status
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public static void applySyncedItem(
|
||||||
|
String ownerLogin, String eventId, List<String> blobsB64
|
||||||
|
) throws Exception {
|
||||||
|
if (blobsB64 == null || blobsB64.isEmpty() || blobsB64.size() > 2) {
|
||||||
|
throw new IllegalArgumentException("BAD_BLOB_COUNT");
|
||||||
|
}
|
||||||
|
if (blobsB64.size() == 1) {
|
||||||
|
ApplyResult result = applySyncedBlob(ownerLogin, blobsB64.get(0));
|
||||||
|
SignedMessageEntry stored = SignedMessagesDAO.getInstance().getByMessageKey(result.messageKey());
|
||||||
|
long createdAt = stored == null ? System.currentTimeMillis() : stored.getCreatedAtMs();
|
||||||
|
DmSyncOutboxDAO.getInstance().upsert(
|
||||||
|
ownerLogin, result.messageKey(), eventId, null, true, createdAt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SignedMessageBlock incoming = SignedMessagesCore.parseFromB64(blobsB64.get(0));
|
||||||
|
SignedMessageBlock outgoing = SignedMessagesCore.parseFromB64(blobsB64.get(1));
|
||||||
|
SignedMessagesCore.validatePair(incoming, outgoing);
|
||||||
|
SignedMessagesCore.verifyUsersAndSignature(incoming);
|
||||||
|
SignedMessagesCore.verifyUsersAndSignature(outgoing);
|
||||||
|
if (!outgoing.fromLogin.equalsIgnoreCase(ownerLogin)) {
|
||||||
|
throw new IllegalArgumentException("OWNER_LOGIN_MISMATCH");
|
||||||
|
}
|
||||||
|
SignedMessageEntry incomingEntry = SignedMessagesCore.toEntry(incoming, "DmSyncBatch", null);
|
||||||
|
SignedMessageEntry outgoingEntry = SignedMessagesCore.toEntry(outgoing, "DmSyncBatch", null);
|
||||||
|
if (incoming.isContentType()) {
|
||||||
|
SignedMessagesDAO.getInstance().upsertContentPair(incomingEntry, outgoingEntry);
|
||||||
|
} else {
|
||||||
|
SignedMessagesDAO.getInstance().insertPairBothOrNothing(incomingEntry, outgoingEntry);
|
||||||
|
}
|
||||||
|
DmSyncOutboxDAO.getInstance().upsert(
|
||||||
|
ownerLogin, outgoingEntry.getMessageKey(), eventId,
|
||||||
|
incomingEntry.getMessageKey(), true, outgoingEntry.getCreatedAtMs());
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
long signedAt = Math.max(outgoing.timeMs,
|
||||||
|
Math.max(outgoing.revisionTimeMs, outgoing.reencryptedAtMs));
|
||||||
|
long acceptedAt = signedAt > 0L ? Math.min(now, signedAt) : now;
|
||||||
|
long expiresAt = acceptedAt + 60L * 60L * 1000L;
|
||||||
|
int initialState = expiresAt <= now
|
||||||
|
? DmDeliveryStateEntry.FAILED_FINAL
|
||||||
|
: DmDeliveryStateEntry.PENDING_NONE;
|
||||||
|
DmDeliveryStateEntry delivery = DmDeliveryStateDAO.getInstance().upsertPair(
|
||||||
|
outgoingEntry.getMessageKey(), eventId, outgoingEntry.getBaseKey(),
|
||||||
|
outgoingEntry.getFromLogin(), outgoingEntry.getToLogin(), incomingEntry.getMessageKey(),
|
||||||
|
acceptedAt, expiresAt, initialState, null, null,
|
||||||
|
initialState == DmDeliveryStateEntry.PENDING_NONE);
|
||||||
|
if (delivery != null && initialState == DmDeliveryStateEntry.PENDING_NONE) {
|
||||||
|
DmDeliveryCoordinator.assistReceivedPairAsync(eventId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-3
@@ -26,6 +26,7 @@ import java.util.Set;
|
|||||||
public class Net_CallInviteBroadcast_Handler implements JsonMessageHandler {
|
public class Net_CallInviteBroadcast_Handler implements JsonMessageHandler {
|
||||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
private static final int TYPE_INVITE = 100;
|
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;
|
private static final long PUSH_CALL_TTL_MS = 10_000L;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -38,8 +39,11 @@ public class Net_CallInviteBroadcast_Handler implements JsonMessageHandler {
|
|||||||
String toRequest = req.getToLogin() == null ? "" : req.getToLogin().trim();
|
String toRequest = req.getToLogin() == null ? "" : req.getToLogin().trim();
|
||||||
String callId = req.getCallId() == null ? "" : req.getCallId().trim();
|
String callId = req.getCallId() == null ? "" : req.getCallId().trim();
|
||||||
int type = req.getType() == null ? TYPE_INVITE : req.getType();
|
int type = req.getType() == null ? TYPE_INVITE : req.getType();
|
||||||
if (toRequest.isBlank() || callId.isBlank() || type != TYPE_INVITE) {
|
String data = req.getData() == null ? "" : req.getData().trim();
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "toLogin/callId/type=100 обязательны");
|
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);
|
CurrentUserEntry targetUser = CurrentUsersDAO.getInstance().getByLogin(toRequest);
|
||||||
@@ -69,13 +73,28 @@ public class Net_CallInviteBroadcast_Handler implements JsonMessageHandler {
|
|||||||
payload.put("fromSessionId", ctx.getSessionId());
|
payload.put("fromSessionId", ctx.getSessionId());
|
||||||
payload.put("toLogin", to);
|
payload.put("toLogin", to);
|
||||||
payload.put("callId", callId);
|
payload.put("callId", callId);
|
||||||
payload.put("type", TYPE_INVITE);
|
payload.put("type", type);
|
||||||
payload.put("timeMs", timeMs);
|
payload.put("timeMs", timeMs);
|
||||||
|
if (!data.isBlank()) {
|
||||||
|
payload.put("data", data);
|
||||||
|
}
|
||||||
|
|
||||||
boolean sent = WsEventSender.sendEvent(targetCtx, "IncomingCallInvite", eventId, payload);
|
boolean sent = WsEventSender.sendEvent(targetCtx, "IncomingCallInvite", eventId, payload);
|
||||||
if (sent) wsDelivered++;
|
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) {
|
for (ActiveSessionEntry session : allTargetSessions) {
|
||||||
String sessionId = String.valueOf(session.getSessionId() == null ? "" : session.getSessionId()).trim();
|
String sessionId = String.valueOf(session.getSessionId() == null ? "" : session.getSessionId()).trim();
|
||||||
if (!sessionId.isBlank() && activeSessionIds.contains(sessionId)) {
|
if (!sessionId.isBlank() && activeSessionIds.contains(sessionId)) {
|
||||||
|
|||||||
+15
@@ -10,6 +10,8 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
|||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import server.sync.DmFederationService;
|
import server.sync.DmFederationService;
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
|
import shine.db.dao.DmDeliveryStateDAO;
|
||||||
|
import shine.db.dao.DmSyncOutboxDAO;
|
||||||
import shine.db.entities.SignedMessageEntry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
public class Net_DeleteConversation_Handler implements JsonMessageHandler {
|
public class Net_DeleteConversation_Handler implements JsonMessageHandler {
|
||||||
@@ -43,6 +45,8 @@ public class Net_DeleteConversation_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
||||||
if (status.applied()) {
|
if (status.applied()) {
|
||||||
|
DmDeliveryStateDAO.getInstance().removeMissingMessages();
|
||||||
|
recordOutboxForLocalOwners(entry);
|
||||||
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, block);
|
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, block);
|
||||||
}
|
}
|
||||||
if (status.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
if (status.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
||||||
@@ -63,4 +67,15 @@ public class Net_DeleteConversation_Handler implements JsonMessageHandler {
|
|||||||
private boolean isBlank(String s) {
|
private boolean isBlank(String s) {
|
||||||
return s == null || s.isBlank();
|
return s == null || s.isBlank();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void recordOutboxForLocalOwners(SignedMessageEntry entry) throws Exception {
|
||||||
|
String eventId = DmDeliveryIds.forEntry(entry);
|
||||||
|
if (server.sync.DmDeliveryCoordinator.isLocalAccessServer(entry.getFromLogin())) {
|
||||||
|
DmSyncOutboxDAO.getInstance().upsert(entry.getFromLogin(), entry.getMessageKey(), eventId, null, false, entry.getCreatedAtMs());
|
||||||
|
}
|
||||||
|
if (!entry.getToLogin().equalsIgnoreCase(entry.getFromLogin())
|
||||||
|
&& server.sync.DmDeliveryCoordinator.isLocalAccessServer(entry.getToLogin())) {
|
||||||
|
DmSyncOutboxDAO.getInstance().upsert(entry.getToLogin(), entry.getMessageKey(), eventId, null, false, entry.getCreatedAtMs());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -10,6 +10,8 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
|||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import server.sync.DmFederationService;
|
import server.sync.DmFederationService;
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
|
import shine.db.dao.DmDeliveryStateDAO;
|
||||||
|
import shine.db.dao.DmSyncOutboxDAO;
|
||||||
import shine.db.entities.SignedMessageEntry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
public class Net_DeleteMessage_Handler implements JsonMessageHandler {
|
public class Net_DeleteMessage_Handler implements JsonMessageHandler {
|
||||||
@@ -43,6 +45,8 @@ public class Net_DeleteMessage_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
||||||
if (status.applied()) {
|
if (status.applied()) {
|
||||||
|
DmDeliveryStateDAO.getInstance().removeByBaseKey(entry.getBaseKey());
|
||||||
|
recordOutboxForLocalOwners(entry);
|
||||||
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, block);
|
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, block);
|
||||||
}
|
}
|
||||||
if (status.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
if (status.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
||||||
@@ -63,4 +67,15 @@ public class Net_DeleteMessage_Handler implements JsonMessageHandler {
|
|||||||
private boolean isBlank(String s) {
|
private boolean isBlank(String s) {
|
||||||
return s == null || s.isBlank();
|
return s == null || s.isBlank();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void recordOutboxForLocalOwners(SignedMessageEntry entry) throws Exception {
|
||||||
|
String eventId = DmDeliveryIds.forEntry(entry);
|
||||||
|
if (server.sync.DmDeliveryCoordinator.isLocalAccessServer(entry.getFromLogin())) {
|
||||||
|
DmSyncOutboxDAO.getInstance().upsert(entry.getFromLogin(), entry.getMessageKey(), eventId, null, false, entry.getCreatedAtMs());
|
||||||
|
}
|
||||||
|
if (!entry.getToLogin().equalsIgnoreCase(entry.getFromLogin())
|
||||||
|
&& server.sync.DmDeliveryCoordinator.isLocalAccessServer(entry.getToLogin())) {
|
||||||
|
DmSyncOutboxDAO.getInstance().upsert(entry.getToLogin(), entry.getMessageKey(), eventId, null, false, entry.getCreatedAtMs());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-24
@@ -8,8 +8,10 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_DmSyncBatch_Request;
|
|||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_DmSyncBatch_Response;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_DmSyncBatch_Response;
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.dao.DmSyncOutboxDAO;
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
import shine.db.dao.UserAccessServersCurrentDAO;
|
import shine.db.dao.UserAccessServersCurrentDAO;
|
||||||
|
import shine.db.entities.DmSyncOutboxEntry;
|
||||||
import shine.db.entities.SignedMessageEntry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
import shine.db.entities.UserAccessServerRouteEntry;
|
import shine.db.entities.UserAccessServerRouteEntry;
|
||||||
import utils.config.AppConfig;
|
import utils.config.AppConfig;
|
||||||
@@ -18,6 +20,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Pull синхронизация только событий synced=false с ACK предыдущей страницы. */
|
||||||
public class Net_DmSyncBatch_Handler implements JsonMessageHandler {
|
public class Net_DmSyncBatch_Handler implements JsonMessageHandler {
|
||||||
private static final int DEFAULT_LIMIT = 500;
|
private static final int DEFAULT_LIMIT = 500;
|
||||||
private static final int MAX_LIMIT = 500;
|
private static final int MAX_LIMIT = 500;
|
||||||
@@ -46,13 +49,14 @@ public class Net_DmSyncBatch_Handler implements JsonMessageHandler {
|
|||||||
long afterStoredAtMs = Math.max(0L, req.getAfterStoredAtMs() == null ? 0L : req.getAfterStoredAtMs());
|
long afterStoredAtMs = Math.max(0L, req.getAfterStoredAtMs() == null ? 0L : req.getAfterStoredAtMs());
|
||||||
String afterMessageKey = req.getAfterMessageKey() == null ? "" : req.getAfterMessageKey().trim();
|
String afterMessageKey = req.getAfterMessageKey() == null ? "" : req.getAfterMessageKey().trim();
|
||||||
|
|
||||||
SignedMessagesDAO.SyncBatch batch = SignedMessagesDAO.getInstance().listSyncBatch(
|
if (req.getAckSyncIds() != null && req.getAckSyncIds().size() > MAX_LIMIT) {
|
||||||
ownerLogin,
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST,
|
||||||
afterStoredAtMs,
|
"TOO_MANY_ACKS", "ackSyncIds содержит слишком много элементов");
|
||||||
afterMessageKey,
|
}
|
||||||
limit,
|
DmSyncOutboxDAO outbox = DmSyncOutboxDAO.getInstance();
|
||||||
maxBytes
|
outbox.markSynced(ownerLogin, req.getAckSyncIds());
|
||||||
);
|
List<DmSyncOutboxEntry> batch = outbox.listUnsynced(
|
||||||
|
ownerLogin, afterStoredAtMs, afterMessageKey, limit);
|
||||||
|
|
||||||
Net_DmSyncBatch_Response resp = new Net_DmSyncBatch_Response();
|
Net_DmSyncBatch_Response resp = new Net_DmSyncBatch_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
@@ -60,28 +64,42 @@ public class Net_DmSyncBatch_Handler implements JsonMessageHandler {
|
|||||||
resp.setStatus(WireCodes.Status.OK);
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
resp.setOwnerLogin(ownerLogin);
|
resp.setOwnerLogin(ownerLogin);
|
||||||
resp.setLimit(limit);
|
resp.setLimit(limit);
|
||||||
resp.setRawBytes(batch.rawBytes());
|
resp.setRawBytes(0);
|
||||||
resp.setHasMore(batch.hasMore());
|
resp.setHasMore(false);
|
||||||
resp.setNextStoredAtMs(afterStoredAtMs);
|
resp.setNextStoredAtMs(afterStoredAtMs);
|
||||||
resp.setNextMessageKey(afterMessageKey);
|
resp.setNextMessageKey(afterMessageKey);
|
||||||
|
|
||||||
List<Net_DmSyncBatch_Response.Item> items = new ArrayList<>();
|
List<Net_DmSyncBatch_Response.Item> items = new ArrayList<>();
|
||||||
Base64.Encoder encoder = Base64.getEncoder();
|
Base64.Encoder encoder = Base64.getEncoder();
|
||||||
for (SignedMessageEntry entry : batch.items()) {
|
int rawBytes = 0;
|
||||||
|
for (DmSyncOutboxEntry row : batch) {
|
||||||
|
SignedMessageEntry primary = SignedMessagesDAO.getInstance().getByMessageKey(row.getPrimaryMessageKey());
|
||||||
|
if (primary == null || primary.getRawBlock() == null) continue;
|
||||||
|
SignedMessageEntry secondary = null;
|
||||||
|
if (row.getSecondaryMessageKey() != null && !row.getSecondaryMessageKey().isBlank()) {
|
||||||
|
secondary = SignedMessagesDAO.getInstance().getByMessageKey(row.getSecondaryMessageKey());
|
||||||
|
if (secondary == null || secondary.getRawBlock() == null) continue;
|
||||||
|
}
|
||||||
|
int itemBytes = primary.getRawBlock().length + (secondary == null ? 0 : secondary.getRawBlock().length);
|
||||||
|
if (!items.isEmpty() && rawBytes + itemBytes > maxBytes) {
|
||||||
|
resp.setHasMore(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
Net_DmSyncBatch_Response.Item item = new Net_DmSyncBatch_Response.Item();
|
Net_DmSyncBatch_Response.Item item = new Net_DmSyncBatch_Response.Item();
|
||||||
item.setMessageKey(entry.getMessageKey());
|
item.setSyncId(row.getEventId());
|
||||||
item.setBaseKey(entry.getBaseKey());
|
item.setPrimaryMessageKey(row.getPrimaryMessageKey());
|
||||||
item.setTargetLogin(entry.getTargetLogin());
|
item.setStoredAtMs(row.getCreatedAtMs());
|
||||||
item.setFromLogin(entry.getFromLogin());
|
List<String> blobs = new ArrayList<>();
|
||||||
item.setToLogin(entry.getToLogin());
|
if (secondary != null) blobs.add(encoder.encodeToString(secondary.getRawBlock()));
|
||||||
item.setMessageType(entry.getMessageType());
|
blobs.add(encoder.encodeToString(primary.getRawBlock()));
|
||||||
item.setTimeMs(entry.getTimeMs());
|
item.setBlobsB64(blobs);
|
||||||
item.setStoredAtMs(entry.getCreatedAtMs());
|
|
||||||
item.setBlobB64(encoder.encodeToString(entry.getRawBlock()));
|
|
||||||
items.add(item);
|
items.add(item);
|
||||||
resp.setNextStoredAtMs(entry.getCreatedAtMs());
|
rawBytes += itemBytes;
|
||||||
resp.setNextMessageKey(entry.getMessageKey());
|
resp.setNextStoredAtMs(row.getCreatedAtMs());
|
||||||
|
resp.setNextMessageKey(row.getPrimaryMessageKey());
|
||||||
}
|
}
|
||||||
|
if (!resp.isHasMore() && batch.size() >= limit) resp.setHasMore(true);
|
||||||
|
resp.setRawBytes(rawBytes);
|
||||||
resp.setItems(items);
|
resp.setItems(items);
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
@@ -89,9 +107,7 @@ public class Net_DmSyncBatch_Handler implements JsonMessageHandler {
|
|||||||
private boolean isLocalAccessServer(String ownerLogin, String ownServerLogin) throws Exception {
|
private boolean isLocalAccessServer(String ownerLogin, String ownServerLogin) throws Exception {
|
||||||
for (UserAccessServerRouteEntry route : UserAccessServersCurrentDAO.getInstance().listByUserLogin(ownerLogin)) {
|
for (UserAccessServerRouteEntry route : UserAccessServersCurrentDAO.getInstance().listByUserLogin(ownerLogin)) {
|
||||||
if (route == null || route.getServerLogin() == null) continue;
|
if (route == null || route.getServerLogin() == null) continue;
|
||||||
if (ownServerLogin.equals(normalize(route.getServerLogin()))) {
|
if (ownServerLogin.equals(normalize(route.getServerLogin()))) return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -11,11 +11,15 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDirectMessages_Res
|
|||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
|
import shine.db.dao.DmDeliveryStateDAO;
|
||||||
|
import shine.db.entities.DmDeliveryStateEntry;
|
||||||
import shine.db.entities.SignedMessageEntry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class Net_GetDirectMessages_Handler implements JsonMessageHandler {
|
public class Net_GetDirectMessages_Handler implements JsonMessageHandler {
|
||||||
private static final Logger log = LoggerFactory.getLogger(Net_GetDirectMessages_Handler.class);
|
private static final Logger log = LoggerFactory.getLogger(Net_GetDirectMessages_Handler.class);
|
||||||
@@ -55,6 +59,11 @@ public class Net_GetDirectMessages_Handler implements JsonMessageHandler {
|
|||||||
if (hasMore) {
|
if (hasMore) {
|
||||||
page = new ArrayList<>(page.subList(0, limit));
|
page = new ArrayList<>(page.subList(0, limit));
|
||||||
}
|
}
|
||||||
|
Map<String, DmDeliveryStateEntry> deliveryByKey = DmDeliveryStateDAO.getInstance()
|
||||||
|
.listByOutgoingMessageKeys(page.stream()
|
||||||
|
.filter(entry -> entry.getMessageType() == SignedMessageBlock.TYPE_OUTGOING_COPY)
|
||||||
|
.map(SignedMessageEntry::getMessageKey)
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
|
||||||
Net_GetDirectMessages_Response resp = new Net_GetDirectMessages_Response();
|
Net_GetDirectMessages_Response resp = new Net_GetDirectMessages_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
@@ -80,6 +89,10 @@ public class Net_GetDirectMessages_Handler implements JsonMessageHandler {
|
|||||||
item.setCreatedAtMs(entry.getCreatedAtMs());
|
item.setCreatedAtMs(entry.getCreatedAtMs());
|
||||||
item.setReadAtMs(entry.getReadAtMs());
|
item.setReadAtMs(entry.getReadAtMs());
|
||||||
item.setBlobB64(Base64.getEncoder().encodeToString(entry.getRawBlock()));
|
item.setBlobB64(Base64.getEncoder().encodeToString(entry.getRawBlock()));
|
||||||
|
DmDeliveryStateEntry delivery = deliveryByKey.get(entry.getMessageKey());
|
||||||
|
if (delivery != null) {
|
||||||
|
item.setDeliveryState(delivery.deliveryStateCode());
|
||||||
|
}
|
||||||
items.add(item);
|
items.add(item);
|
||||||
}
|
}
|
||||||
resp.setMessages(items);
|
resp.setMessages(items);
|
||||||
|
|||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages;
|
||||||
|
|
||||||
|
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.messages.entyties.Net_GetDmDeliveryStatus_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDmDeliveryStatus_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.dao.DmDeliveryStateDAO;
|
||||||
|
import shine.db.entities.DmDeliveryStateEntry;
|
||||||
|
|
||||||
|
/** Read-only проверка: доставил ли peer хотя бы одну копию получателю. */
|
||||||
|
public class Net_GetDmDeliveryStatus_Handler implements JsonMessageHandler {
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
|
||||||
|
Net_GetDmDeliveryStatus_Request req = (Net_GetDmDeliveryStatus_Request) baseRequest;
|
||||||
|
if (req.getMessageKey() == null || req.getMessageKey().isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req, WireCodes.Status.BAD_REQUEST, "EMPTY_MESSAGE_KEY", "messageKey обязателен");
|
||||||
|
}
|
||||||
|
String messageKey = req.getMessageKey().trim();
|
||||||
|
DmDeliveryStateEntry state = DmDeliveryStateDAO.getInstance().getByOutgoingMessageKey(messageKey);
|
||||||
|
|
||||||
|
Net_GetDmDeliveryStatus_Response resp = new Net_GetDmDeliveryStatus_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setMessageKey(messageKey);
|
||||||
|
resp.setKnown(state != null);
|
||||||
|
resp.setDelivered(state != null && state.isDelivered());
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages;
|
||||||
|
|
||||||
|
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.messages.entyties.Net_MarkAllUserSettingsUnsynced_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_MarkAllUserSettingsUnsynced_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.dao.UserSettingsDAO;
|
||||||
|
import shine.db.dao.DmSyncOutboxDAO;
|
||||||
|
import server.sync.DmSyncWakeSignal;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
|
||||||
|
public class Net_MarkAllUserSettingsUnsynced_Handler implements JsonMessageHandler {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Net_MarkAllUserSettingsUnsynced_Handler.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||||
|
Net_MarkAllUserSettingsUnsynced_Request req = (Net_MarkAllUserSettingsUnsynced_Request) baseRequest;
|
||||||
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
|
int updated;
|
||||||
|
int dmUpdated;
|
||||||
|
if (req.getLogin() == null || req.getLogin().isBlank()) {
|
||||||
|
updated = UserSettingsDAO.getInstance().markAllUnsynced(c);
|
||||||
|
dmUpdated = DmSyncOutboxDAO.getInstance().markAllUnsynced();
|
||||||
|
} else {
|
||||||
|
updated = UserSettingsDAO.getInstance().markAllUnsynced(c, req.getLogin().trim());
|
||||||
|
dmUpdated = DmSyncOutboxDAO.getInstance().markAllUnsynced(req.getLogin().trim());
|
||||||
|
}
|
||||||
|
DmSyncWakeSignal.request();
|
||||||
|
Net_MarkAllUserSettingsUnsynced_Response resp = new Net_MarkAllUserSettingsUnsynced_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setUpdated(updated);
|
||||||
|
resp.setDmUpdated(dmUpdated);
|
||||||
|
return resp;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("MarkAllUserSettingsUnsynced failed", e);
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "INTERNAL_ERROR", "Внутренняя ошибка сервера");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
-5
@@ -8,9 +8,12 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_ReceiveIncomingMessag
|
|||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_ReceiveIncomingMessage_Response;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_ReceiveIncomingMessage_Response;
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import server.sync.DmDeliveryCoordinator;
|
||||||
|
import shine.db.dao.DmSyncOutboxDAO;
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
import shine.db.entities.SignedMessageEntry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
|
||||||
public class Net_ReceiveIncomingMessage_Handler implements JsonMessageHandler {
|
public class Net_ReceiveIncomingMessage_Handler implements JsonMessageHandler {
|
||||||
@@ -40,6 +43,10 @@ public class Net_ReceiveIncomingMessage_Handler implements JsonMessageHandler {
|
|||||||
return NetExceptionResponseFactory.error(req, status, code, "Сообщение не прошло проверку");
|
return NetExceptionResponseFactory.error(req, status, code, "Сообщение не прошло проверку");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!DmDeliveryCoordinator.isLocalAccessServer(incoming.toLogin)) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 403, "LOCAL_SERVER_NOT_ACCESS_SERVER", "Сервер не обслуживает получателя сообщения");
|
||||||
|
}
|
||||||
|
|
||||||
final SignedMessageEntry entry;
|
final SignedMessageEntry entry;
|
||||||
try {
|
try {
|
||||||
entry = SignedMessagesCore.toEntry(incoming, "ReceiveIncomingMessage", null);
|
entry = SignedMessagesCore.toEntry(incoming, "ReceiveIncomingMessage", null);
|
||||||
@@ -53,16 +60,27 @@ public class Net_ReceiveIncomingMessage_Handler implements JsonMessageHandler {
|
|||||||
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
||||||
if (status.applied()) {
|
if (status.applied()) {
|
||||||
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, incoming);
|
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, incoming);
|
||||||
server.sync.DmFederationService.fanOutIncomingToRecipientAccessServers(
|
|
||||||
incoming.toLogin,
|
|
||||||
req.getIncomingBlobB64().trim(),
|
|
||||||
req.getSourceServerLogin()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (status == SignedMessagesDAO.ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE) {
|
if (status == SignedMessagesDAO.ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE) {
|
||||||
bounceConversationDeleteIfKnown(incoming.fromLogin, incoming.toLogin);
|
bounceConversationDeleteIfKnown(incoming.fromLogin, incoming.toLogin);
|
||||||
|
return NetExceptionResponseFactory.error(req, 409, "BLOCKED_BY_CONVERSATION_TOMBSTONE", "Переписка уже удалена этой ревизией");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SignedMessageEntry stored = SignedMessagesDAO.getInstance().getByMessageKey(entry.getMessageKey());
|
||||||
|
if (stored == null || !Arrays.equals(stored.getRawBlock(), entry.getRawBlock())) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 409, "STALE_MESSAGE_REVISION", "На сервере уже есть более новая ревизия сообщения");
|
||||||
|
}
|
||||||
|
boolean receivedFromRecipientPeer = DmDeliveryCoordinator.sourceIsOtherAccessServer(
|
||||||
|
incoming.toLogin, req.getSourceServerLogin());
|
||||||
|
DmSyncOutboxDAO.getInstance().upsert(
|
||||||
|
incoming.toLogin,
|
||||||
|
entry.getMessageKey(),
|
||||||
|
DmDeliveryIds.forEntry(entry),
|
||||||
|
null,
|
||||||
|
receivedFromRecipientPeer,
|
||||||
|
entry.getCreatedAtMs()
|
||||||
|
);
|
||||||
|
|
||||||
Net_ReceiveIncomingMessage_Response resp = new Net_ReceiveIncomingMessage_Response();
|
Net_ReceiveIncomingMessage_Response resp = new Net_ReceiveIncomingMessage_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
resp.setRequestId(req.getRequestId());
|
resp.setRequestId(req.getRequestId());
|
||||||
|
|||||||
-208
@@ -1,208 +0,0 @@
|
|||||||
package server.logic.ws_protocol.JSON.messages;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
||||||
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
|
|
||||||
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.messages.entyties.Net_SendDirectMessage_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendDirectMessage_Response;
|
|
||||||
import server.logic.ws_protocol.JSON.push.WebPushSender;
|
|
||||||
import server.logic.ws_protocol.JSON.push.WsEventSender;
|
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
|
||||||
import server.logic.ws_protocol.JSON.utils.NetIdGenerator;
|
|
||||||
import server.logic.ws_protocol.WireCodes;
|
|
||||||
import shine.db.dao.ActiveSessionsDAO;
|
|
||||||
import shine.db.dao.DirectMessagesDAO;
|
|
||||||
import shine.db.dao.SignedDirectMessagesHistoryDAO;
|
|
||||||
import shine.db.dao.SignedDmReplayDAO;
|
|
||||||
import shine.db.dao.CurrentUsersDAO;
|
|
||||||
import shine.db.entities.ActiveSessionEntry;
|
|
||||||
import shine.db.entities.DirectMessageEntry;
|
|
||||||
import shine.db.entities.SignedDirectMessageHistoryEntry;
|
|
||||||
import shine.db.entities.CurrentUserEntry;
|
|
||||||
import utils.crypto.Ed25519Util;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class Net_SendDirectMessage_Handler implements JsonMessageHandler {
|
|
||||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
|
||||||
private static final long REPLAY_TTL_MS = 15L * 60L * 1000L;
|
|
||||||
private static final int MAX_MESSAGE_BYTES = 3000;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
|
|
||||||
Net_SendDirectMessage_Request req = (Net_SendDirectMessage_Request) baseRequest;
|
|
||||||
if (req.getBlobB64() == null || req.getBlobB64().isBlank()) {
|
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "blobB64 обязателен");
|
|
||||||
}
|
|
||||||
|
|
||||||
final byte[] raw;
|
|
||||||
final SignedDirectMessagePacket packet;
|
|
||||||
try {
|
|
||||||
raw = Base64.getDecoder().decode(req.getBlobB64().trim());
|
|
||||||
packet = SignedDirectMessagePacket.parse(raw, MAX_MESSAGE_BYTES);
|
|
||||||
} catch (IllegalArgumentException ex) {
|
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, ex.getMessage(), "Некорректный формат пакета");
|
|
||||||
}
|
|
||||||
|
|
||||||
CurrentUserEntry fromUser = CurrentUsersDAO.getInstance().getByLogin(packet.fromLogin);
|
|
||||||
CurrentUserEntry toUser = CurrentUsersDAO.getInstance().getByLogin(packet.toLogin);
|
|
||||||
if (fromUser == null || toUser == null) {
|
|
||||||
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "from/to пользователь не найден");
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] publicKey32;
|
|
||||||
try {
|
|
||||||
publicKey32 = Ed25519Util.keyFromBase64(fromUser.getClientKey());
|
|
||||||
} catch (Exception e) {
|
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_DEVICE_KEY", "Некорректный clientKey отправителя");
|
|
||||||
}
|
|
||||||
if (!Ed25519Util.verify(packet.signedBody, packet.signature64, publicKey32)) {
|
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_SIGNATURE", "Подпись не прошла проверку");
|
|
||||||
}
|
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
|
||||||
if (Math.abs(now - packet.timeMs) > REPLAY_TTL_MS) {
|
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_TIME_WINDOW", "Время сообщения вышло за окно 15 минут");
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean replayOk = SignedDmReplayDAO.getInstance().registerUnique(packet.fromLogin, packet.timeMs, packet.nonce, now);
|
|
||||||
if (!replayOk) {
|
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "REPLAY", "Повторное сообщение заблокировано");
|
|
||||||
}
|
|
||||||
|
|
||||||
String messageId = NetIdGenerator.eventId("msg");
|
|
||||||
String textForUi = new String(packet.messageBytes, StandardCharsets.UTF_8);
|
|
||||||
|
|
||||||
DirectMessageEntry entry = new DirectMessageEntry();
|
|
||||||
entry.setMessageId(messageId);
|
|
||||||
entry.setFromLogin(packet.fromLogin);
|
|
||||||
entry.setToLogin(packet.toLogin);
|
|
||||||
entry.setText(textForUi);
|
|
||||||
entry.setCreatedAtMs(now);
|
|
||||||
DirectMessagesDAO.getInstance().insert(entry);
|
|
||||||
|
|
||||||
SignedDirectMessageHistoryEntry history = new SignedDirectMessageHistoryEntry();
|
|
||||||
history.setMessageId(messageId);
|
|
||||||
history.setFromLogin(packet.fromLogin);
|
|
||||||
history.setToLogin(packet.toLogin);
|
|
||||||
history.setTargetMode(packet.targetMode);
|
|
||||||
history.setTargetSessionId(packet.targetSessionId);
|
|
||||||
history.setMessageType(packet.messageType);
|
|
||||||
history.setTimeMs(packet.timeMs);
|
|
||||||
history.setNonce(packet.nonce);
|
|
||||||
history.setRawPacket(packet.rawPacket);
|
|
||||||
history.setCreatedAtMs(now);
|
|
||||||
SignedDirectMessagesHistoryDAO.getInstance().insert(history);
|
|
||||||
|
|
||||||
DeliveryResult delivery = deliver(packet, req.getBlobB64().trim(), messageId, now);
|
|
||||||
|
|
||||||
Net_SendDirectMessage_Response resp = new Net_SendDirectMessage_Response();
|
|
||||||
resp.setOp(req.getOp());
|
|
||||||
resp.setRequestId(req.getRequestId());
|
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
|
||||||
resp.setMessageId(messageId);
|
|
||||||
resp.setDeliveredWsSessions(delivery.wsDelivered);
|
|
||||||
resp.setDeliveredWebPushSessions(delivery.webPushDelivered);
|
|
||||||
resp.setSessionNotFound(delivery.sessionNotFound);
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DeliveryResult deliver(SignedDirectMessagePacket packet, String blobB64, String messageId, long createdAtMs) throws Exception {
|
|
||||||
DeliveryResult result = new DeliveryResult();
|
|
||||||
|
|
||||||
Set<String> selectedSessionIds = new HashSet<>();
|
|
||||||
if (packet.targetMode == SignedDirectMessagePacket.TARGET_ONE_SESSION) {
|
|
||||||
ActiveSessionEntry byId = ActiveSessionsDAO.getInstance().getBySessionId(packet.targetSessionId);
|
|
||||||
if (byId == null || !packet.toLogin.equalsIgnoreCase(byId.getLogin())) {
|
|
||||||
result.sessionNotFound = true;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
selectedSessionIds.add(byId.getSessionId());
|
|
||||||
deliverToSession(packet, blobB64, messageId, createdAtMs, byId.getSessionId(), result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ActiveSessionEntry> sessions = ActiveSessionsDAO.getInstance().getByLogin(packet.toLogin);
|
|
||||||
for (ActiveSessionEntry s : sessions) {
|
|
||||||
selectedSessionIds.add(s.getSessionId());
|
|
||||||
deliverToSession(packet, blobB64, messageId, createdAtMs, s.getSessionId(), result);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deliverToSession(
|
|
||||||
SignedDirectMessagePacket packet,
|
|
||||||
String blobB64,
|
|
||||||
String messageId,
|
|
||||||
long createdAtMs,
|
|
||||||
String sessionId,
|
|
||||||
DeliveryResult result
|
|
||||||
) {
|
|
||||||
ConnectionContext targetCtx = ActiveConnectionsRegistry.getInstance().getBySessionId(sessionId);
|
|
||||||
boolean wsDelivered = false;
|
|
||||||
if (targetCtx != null) {
|
|
||||||
String eventId = NetIdGenerator.eventId("evt");
|
|
||||||
CompletableFuture<Boolean> waiter = DeliveryTracker.getInstance().register(eventId);
|
|
||||||
ObjectNode payload = MAPPER.createObjectNode();
|
|
||||||
payload.put("eventId", eventId);
|
|
||||||
payload.put("messageId", messageId);
|
|
||||||
payload.put("fromLogin", packet.fromLogin);
|
|
||||||
payload.put("toLogin", packet.toLogin);
|
|
||||||
payload.put("blobB64", blobB64);
|
|
||||||
payload.put("text", new String(packet.messageBytes, StandardCharsets.UTF_8));
|
|
||||||
payload.put("timeMs", createdAtMs);
|
|
||||||
|
|
||||||
boolean sent = WsEventSender.sendEvent(targetCtx, "IncomingDirectMessage", eventId, payload);
|
|
||||||
if (sent) {
|
|
||||||
try {
|
|
||||||
wsDelivered = waiter.get(1200, TimeUnit.MILLISECONDS);
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
wsDelivered = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DeliveryTracker.getInstance().remove(eventId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wsDelivered) {
|
|
||||||
result.wsDelivered++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
ActiveSessionEntry targetSession = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
|
|
||||||
if (targetSession == null) return;
|
|
||||||
if (isBlank(targetSession.getPushEndpoint()) || isBlank(targetSession.getPushP256dhKey()) || isBlank(targetSession.getPushAuthKey())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
boolean pushed = WebPushSender.sendBase64Payload(
|
|
||||||
targetSession.getPushEndpoint(),
|
|
||||||
targetSession.getPushP256dhKey(),
|
|
||||||
targetSession.getPushAuthKey(),
|
|
||||||
blobB64
|
|
||||||
);
|
|
||||||
if (pushed) result.webPushDelivered++;
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
// ignore per-session push errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isBlank(String s) {
|
|
||||||
return s == null || s.isBlank();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class DeliveryResult {
|
|
||||||
int wsDelivered;
|
|
||||||
int webPushDelivered;
|
|
||||||
boolean sessionNotFound;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+54
-8
@@ -8,10 +8,15 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_SendMessagePair_Reque
|
|||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendMessagePair_Response;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendMessagePair_Response;
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import server.sync.DmDeliveryCoordinator;
|
||||||
import server.sync.DmFederationService;
|
import server.sync.DmFederationService;
|
||||||
|
import shine.db.dao.DmDeliveryStateDAO;
|
||||||
|
import shine.db.dao.DmSyncOutboxDAO;
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
|
import shine.db.entities.DmDeliveryStateEntry;
|
||||||
import shine.db.entities.SignedMessageEntry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
|
||||||
public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
||||||
@@ -43,8 +48,10 @@ public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
SignedMessageEntry incomingEntry;
|
SignedMessageEntry incomingEntry;
|
||||||
SignedMessageEntry outgoingEntry;
|
SignedMessageEntry outgoingEntry;
|
||||||
|
boolean fromPeer = "ReceiveOutcomingMessage".equalsIgnoreCase(req.getOp())
|
||||||
|
|| !isBlank(req.getSourceServerLogin());
|
||||||
try {
|
try {
|
||||||
String sourceApi = "SendMessagePair";
|
String sourceApi = fromPeer ? "ReceiveOutcomingMessage" : "SendMessagePair";
|
||||||
String originSessionId = (ctx != null && !isBlank(ctx.getSessionId())) ? ctx.getSessionId() : null;
|
String originSessionId = (ctx != null && !isBlank(ctx.getSessionId())) ? ctx.getSessionId() : null;
|
||||||
incomingEntry = SignedMessagesCore.toEntry(incoming, sourceApi, originSessionId);
|
incomingEntry = SignedMessagesCore.toEntry(incoming, sourceApi, originSessionId);
|
||||||
outgoingEntry = SignedMessagesCore.toEntry(outgoing, sourceApi, originSessionId);
|
outgoingEntry = SignedMessagesCore.toEntry(outgoing, sourceApi, originSessionId);
|
||||||
@@ -79,15 +86,53 @@ public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
if (pairStatus == SignedMessagesDAO.ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE) {
|
if (pairStatus == SignedMessagesDAO.ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE) {
|
||||||
bounceConversationDeleteIfKnown(incoming.fromLogin, incoming.toLogin);
|
bounceConversationDeleteIfKnown(incoming.fromLogin, incoming.toLogin);
|
||||||
|
return NetExceptionResponseFactory.error(req, 409, "BLOCKED_BY_CONVERSATION_TOMBSTONE", "Переписка уже удалена этой ревизией");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pairStatus.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
SignedMessageEntry storedOutgoing = SignedMessagesDAO.getInstance().getByMessageKey(outgoingEntry.getMessageKey());
|
||||||
DmFederationService.fanOutPair(
|
if (storedOutgoing == null || !Arrays.equals(storedOutgoing.getRawBlock(), outgoingEntry.getRawBlock())) {
|
||||||
incoming.fromLogin,
|
return NetExceptionResponseFactory.error(req, 409, "STALE_MESSAGE_REVISION", "На сервере уже есть более новая ревизия сообщения");
|
||||||
incoming.toLogin,
|
}
|
||||||
req.getIncomingBlobB64().trim(),
|
|
||||||
req.getOutgoingBlobB64().trim()
|
String eventId = DmDeliveryIds.forEntry(outgoingEntry);
|
||||||
);
|
long nowMs = System.currentTimeMillis();
|
||||||
|
long acceptedAtMs;
|
||||||
|
long signedAtMs = Math.max(outgoing.timeMs,
|
||||||
|
Math.max(outgoing.revisionTimeMs, outgoing.reencryptedAtMs));
|
||||||
|
acceptedAtMs = fromPeer && signedAtMs > 0L ? Math.min(nowMs, signedAtMs) : nowMs;
|
||||||
|
long expiresAtMs = acceptedAtMs + 60L * 60L * 1000L;
|
||||||
|
int initialState = DmDeliveryStateEntry.ACCEPTED;
|
||||||
|
DmDeliveryStateEntry delivery = DmDeliveryStateDAO.getInstance().upsertPair(
|
||||||
|
outgoingEntry.getMessageKey(), eventId, outgoingEntry.getBaseKey(),
|
||||||
|
outgoingEntry.getFromLogin(), outgoingEntry.getToLogin(), incomingEntry.getMessageKey(),
|
||||||
|
acceptedAtMs, expiresAtMs, initialState,
|
||||||
|
null, null, fromPeer && initialState == DmDeliveryStateEntry.PENDING_NONE
|
||||||
|
);
|
||||||
|
|
||||||
|
DmSyncOutboxDAO.getInstance().upsert(
|
||||||
|
outgoingEntry.getFromLogin(), outgoingEntry.getMessageKey(), eventId,
|
||||||
|
incomingEntry.getMessageKey(), fromPeer, acceptedAtMs);
|
||||||
|
|
||||||
|
// Если этот же сервер также обслуживает получателя, его входящая копия
|
||||||
|
// имеет отдельный sync-флаг владельца-получателя.
|
||||||
|
if (DmDeliveryCoordinator.isLocalAccessServer(incomingEntry.getToLogin())) {
|
||||||
|
boolean incomingAlreadySynced = DmDeliveryCoordinator.sourceIsOtherAccessServer(
|
||||||
|
incomingEntry.getToLogin(), req.getSourceServerLogin());
|
||||||
|
DmSyncOutboxDAO.getInstance().upsert(
|
||||||
|
incomingEntry.getToLogin(), incomingEntry.getMessageKey(), DmDeliveryIds.forEntry(incomingEntry),
|
||||||
|
null, incomingAlreadySynced, acceptedAtMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delivery != null && pairStatus.applied()) {
|
||||||
|
if (fromPeer) {
|
||||||
|
DmDeliveryCoordinator.assistReceivedPairAsync(delivery.getEventId());
|
||||||
|
} else {
|
||||||
|
// Первая доставка выполняется до ответа клиенту. Два сервера
|
||||||
|
// получателя вызываются параллельно внутри координатора.
|
||||||
|
DmDeliveryCoordinator.processDueEntry(delivery);
|
||||||
|
delivery = DmDeliveryStateDAO.getInstance()
|
||||||
|
.getByOutgoingMessageKey(outgoingEntry.getMessageKey());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Net_SendMessagePair_Response resp = new Net_SendMessagePair_Response();
|
Net_SendMessagePair_Response resp = new Net_SendMessagePair_Response();
|
||||||
@@ -99,6 +144,7 @@ public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
|||||||
resp.setOutgoingKey(outgoingEntry.getMessageKey());
|
resp.setOutgoingKey(outgoingEntry.getMessageKey());
|
||||||
resp.setDeliveredWsSessions(inCounters.wsDelivered + outCounters.wsDelivered);
|
resp.setDeliveredWsSessions(inCounters.wsDelivered + outCounters.wsDelivered);
|
||||||
resp.setDeliveredWebPushSessions(inCounters.pushDelivered + outCounters.pushDelivered);
|
resp.setDeliveredWebPushSessions(inCounters.pushDelivered + outCounters.pushDelivered);
|
||||||
|
resp.setDeliveryState(delivery.deliveryStateCode());
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+204
-3
@@ -12,10 +12,12 @@ import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendSignal_Request;
|
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.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.push.WsEventSender;
|
||||||
import server.logic.ws_protocol.JSON.utils.AuthKeyUtils;
|
import server.logic.ws_protocol.JSON.utils.AuthKeyUtils;
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.dao.ActiveSessionsDAO;
|
||||||
import shine.db.dao.CurrentUsersDAO;
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
import shine.db.entities.ActiveSessionEntry;
|
import shine.db.entities.ActiveSessionEntry;
|
||||||
import shine.db.entities.CurrentUserEntry;
|
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_SINGLE = "single_session";
|
||||||
private static final String TARGET_MODE_ALL = "all_sessions";
|
private static final String TARGET_MODE_ALL = "all_sessions";
|
||||||
private static final long ALLOWED_SKEW_MS = 30_000L;
|
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
|
@Override
|
||||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
|
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");
|
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()) {
|
if (!clientSignatureB64.isBlank()) {
|
||||||
String clientPreimage = buildClientPreimage(fromLogin, fromSessionId, toLogin, targetMode, targetSessionId, signalType, signalRequestId, timeMs, digestB64);
|
String clientPreimage = buildClientPreimage(fromLogin, fromSessionId, toLogin, targetMode, targetSessionId, signalType, signalRequestId, timeMs, digestB64);
|
||||||
if (!verifySignature(senderUser.getClientKey(), clientPreimage, clientSignatureB64, "clientKey")) {
|
if (!verifySignature(senderUser.getClientKey(), clientPreimage, clientSignatureB64, "clientKey")) {
|
||||||
@@ -107,7 +120,13 @@ public class Net_SendSignal_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<ConnectionContext> targets = resolveTargets(targetMode, toLogin, targetSessionId);
|
List<ConnectionContext> targets = resolveTargets(targetMode, toLogin, targetSessionId);
|
||||||
if (targets.isEmpty()) {
|
boolean isCallInviteAllSessions = TARGET_MODE_ALL.equals(targetMode) && SIGNAL_CALL_INVITE.equals(signalType);
|
||||||
|
boolean isCallAcceptSingleSession = TARGET_MODE_SINGLE.equals(targetMode) && SIGNAL_CALL_ACCEPT.equals(signalType);
|
||||||
|
boolean isCallTerminalSingleSession = TARGET_MODE_SINGLE.equals(targetMode)
|
||||||
|
&& (SIGNAL_CALL_DECLINE_BUSY.equals(signalType)
|
||||||
|
|| SIGNAL_CALL_TIMEOUT.equals(signalType)
|
||||||
|
|| SIGNAL_CALL_HANGUP.equals(signalType));
|
||||||
|
if (targets.isEmpty() && !isCallInviteAllSessions) {
|
||||||
String code = TARGET_MODE_SINGLE.equals(targetMode) ? "SESSION_NOT_FOUND" : "NO_TARGET_SESSIONS";
|
String code = TARGET_MODE_SINGLE.equals(targetMode) ? "SESSION_NOT_FOUND" : "NO_TARGET_SESSIONS";
|
||||||
String msg = TARGET_MODE_SINGLE.equals(targetMode) ? "Целевая сессия не найдена" : "Нет активных сессий для доставки сигнала";
|
String msg = TARGET_MODE_SINGLE.equals(targetMode) ? "Целевая сессия не найдена" : "Нет активных сессий для доставки сигнала";
|
||||||
return NetExceptionResponseFactory.error(req, 404, code, msg);
|
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", "Не удалось доставить сигнал ни в одну целевую сессию");
|
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();
|
Net_SendSignal_Response resp = new Net_SendSignal_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
resp.setRequestId(req.getRequestId());
|
resp.setRequestId(req.getRequestId());
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
resp.setDeliveredCount(deliveredSessionIds.size());
|
resp.setDeliveredCount(deliveredSessionIds.size() + webPushDelivered);
|
||||||
resp.setDeliveredSessionIds(deliveredSessionIds);
|
resp.setDeliveredSessionIds(deliveredSessionIds);
|
||||||
|
resp.setDeliveredWsSessions(deliveredSessionIds.size());
|
||||||
|
resp.setDeliveredFcmSessions(webPushDelivered);
|
||||||
|
resp.setDeliveredWebPushSessions(webPushDelivered);
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +255,19 @@ public class Net_SendSignal_Handler implements JsonMessageHandler {
|
|||||||
+ dataSha256B64;
|
+ 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 {
|
private static boolean verifySignature(String publicKeyValue, String preimage, String signatureB64, String fieldName) throws Exception {
|
||||||
byte[] publicKey32 = AuthKeyUtils.parseEd25519PublicKey(publicKeyValue, fieldName);
|
byte[] publicKey32 = AuthKeyUtils.parseEd25519PublicKey(publicKeyValue, fieldName);
|
||||||
byte[] signature64 = Base64Ws.decodeLen(signatureB64, 64, "signatureB64");
|
byte[] signature64 = Base64Ws.decodeLen(signatureB64, 64, "signatureB64");
|
||||||
@@ -214,7 +292,130 @@ public class Net_SendSignal_Handler implements JsonMessageHandler {
|
|||||||
return targets;
|
return targets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int sendIncomingCallPushToOfflineSessions(
|
||||||
|
String toLogin,
|
||||||
|
String fromLogin,
|
||||||
|
String fromSessionId,
|
||||||
|
String callId,
|
||||||
|
List<String> deliveredOnlineSessionIds
|
||||||
|
) throws Exception {
|
||||||
|
List<ActiveSessionEntry> persistedSessions = ActiveSessionsDAO.getInstance().getByLogin(toLogin);
|
||||||
|
long sentAtMs = System.currentTimeMillis();
|
||||||
|
long expiresAtMs = sentAtMs + PUSH_CALL_TTL_MS;
|
||||||
|
int delivered = 0;
|
||||||
|
for (ActiveSessionEntry session : persistedSessions) {
|
||||||
|
String sessionId = safe(session.getSessionId());
|
||||||
|
if (!sessionId.isBlank() && deliveredOnlineSessionIds.contains(sessionId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isBlank(session.getPushEndpoint()) || isBlank(session.getPushP256dhKey()) || isBlank(session.getPushAuthKey())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String payload = "{\"kind\":\"incoming_call\""
|
||||||
|
+ ",\"title\":\"SHiNE: входящий звонок\""
|
||||||
|
+ ",\"text\":\"Вам звонит " + jsonEscape(fromLogin) + "\""
|
||||||
|
+ ",\"fromLogin\":\"" + jsonEscape(fromLogin) + "\""
|
||||||
|
+ ",\"fromSessionId\":\"" + jsonEscape(fromSessionId) + "\""
|
||||||
|
+ ",\"targetSessionId\":\"" + jsonEscape(sessionId) + "\""
|
||||||
|
+ ",\"toLogin\":\"" + jsonEscape(toLogin) + "\""
|
||||||
|
+ ",\"callId\":\"" + jsonEscape(callId) + "\""
|
||||||
|
+ ",\"sentAtMs\":" + sentAtMs
|
||||||
|
+ ",\"expiresAtMs\":" + expiresAtMs
|
||||||
|
+ "}";
|
||||||
|
boolean pushed = WebPushSender.sendBase64Payload(
|
||||||
|
session.getPushEndpoint(),
|
||||||
|
session.getPushP256dhKey(),
|
||||||
|
session.getPushAuthKey(),
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
if (pushed) {
|
||||||
|
delivered++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return delivered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyStopOnOtherSessions(
|
||||||
|
String targetLogin,
|
||||||
|
String excludeSessionId,
|
||||||
|
String fromLogin,
|
||||||
|
String fromSessionId,
|
||||||
|
String callId,
|
||||||
|
String reason
|
||||||
|
) throws Exception {
|
||||||
|
if (isBlank(targetLogin) || isBlank(callId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> onlineSessionIds = new java.util.HashSet<>();
|
||||||
|
Set<ConnectionContext> sameUserSessions = ActiveConnectionsRegistry.getInstance().getByLogin(targetLogin);
|
||||||
|
for (ConnectionContext siblingCtx : sameUserSessions) {
|
||||||
|
if (siblingCtx == null || siblingCtx.getWsSession() == null || !siblingCtx.getWsSession().isOpen()) continue;
|
||||||
|
onlineSessionIds.add(safe(siblingCtx.getSessionId()));
|
||||||
|
if (!isBlank(excludeSessionId) && excludeSessionId.equals(siblingCtx.getSessionId())) continue;
|
||||||
|
|
||||||
|
String siblingEventId = server.logic.ws_protocol.JSON.utils.NetIdGenerator.eventId("evt");
|
||||||
|
ObjectNode siblingPayload = MAPPER.createObjectNode();
|
||||||
|
siblingPayload.put("eventId", siblingEventId);
|
||||||
|
siblingPayload.put("fromLogin", fromLogin);
|
||||||
|
siblingPayload.put("fromSessionId", fromSessionId);
|
||||||
|
siblingPayload.put("toLogin", targetLogin);
|
||||||
|
siblingPayload.put("targetMode", TARGET_MODE_SINGLE);
|
||||||
|
siblingPayload.put("targetSessionId", safe(siblingCtx.getSessionId()));
|
||||||
|
siblingPayload.put("signalType", SIGNAL_CALL_HANGUP);
|
||||||
|
siblingPayload.put("signalRequestId", callId);
|
||||||
|
siblingPayload.put("data", "{\"callId\":\"" + jsonEscape(callId) + "\",\"type\":150,\"data\":\"" + jsonEscape(reason) + "\"}");
|
||||||
|
siblingPayload.put("timeMs", System.currentTimeMillis());
|
||||||
|
WsEventSender.sendEvent(siblingCtx, "IncomingSignal", siblingEventId, siblingPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ActiveSessionEntry> persistedSessions = ActiveSessionsDAO.getInstance().getByLogin(targetLogin);
|
||||||
|
long sentAtMs = System.currentTimeMillis();
|
||||||
|
for (ActiveSessionEntry session : persistedSessions) {
|
||||||
|
String sessionId = safe(session.getSessionId());
|
||||||
|
if (!isBlank(excludeSessionId) && excludeSessionId.equals(sessionId)) continue;
|
||||||
|
if (!sessionId.isBlank() && onlineSessionIds.contains(sessionId)) continue;
|
||||||
|
if (isBlank(session.getPushEndpoint()) || isBlank(session.getPushP256dhKey()) || isBlank(session.getPushAuthKey())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String pushPayload = "{\"kind\":\"stop_call\""
|
||||||
|
+ ",\"callId\":\"" + jsonEscape(callId) + "\""
|
||||||
|
+ ",\"reason\":\"" + jsonEscape(reason) + "\""
|
||||||
|
+ ",\"fromLogin\":\"" + jsonEscape(fromLogin) + "\""
|
||||||
|
+ ",\"fromSessionId\":\"" + jsonEscape(fromSessionId) + "\""
|
||||||
|
+ ",\"targetSessionId\":\"" + jsonEscape(sessionId) + "\""
|
||||||
|
+ ",\"toLogin\":\"" + jsonEscape(targetLogin) + "\""
|
||||||
|
+ ",\"sentAtMs\":" + sentAtMs
|
||||||
|
+ "}";
|
||||||
|
WebPushSender.sendBase64Payload(
|
||||||
|
session.getPushEndpoint(),
|
||||||
|
session.getPushP256dhKey(),
|
||||||
|
session.getPushAuthKey(),
|
||||||
|
pushPayload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static String safe(String value) {
|
private static String safe(String value) {
|
||||||
return value == null ? "" : value.trim();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages;
|
||||||
|
|
||||||
|
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.messages.entyties.Net_UserSettingsSyncBatch_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_UserSettingsSyncBatch_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.dao.UserAccessServersCurrentDAO;
|
||||||
|
import shine.db.dao.UserSettingsDAO;
|
||||||
|
import shine.db.entities.UserSettingEntry;
|
||||||
|
import shine.db.entities.UserAccessServerRouteEntry;
|
||||||
|
import utils.config.AppConfig;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Net_UserSettingsSyncBatch_Handler implements JsonMessageHandler {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Net_UserSettingsSyncBatch_Handler.class);
|
||||||
|
private static final int DEFAULT_LIMIT = 500;
|
||||||
|
private static final int MAX_LIMIT = 1000;
|
||||||
|
private static final int DEFAULT_MAX_BYTES = 3_000_000;
|
||||||
|
private static final int MAX_BYTES_CAP = 5_000_000;
|
||||||
|
private static final String SERVER_LOGIN_CONFIG = "server.SHiNE.login";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
|
||||||
|
Net_UserSettingsSyncBatch_Request req = (Net_UserSettingsSyncBatch_Request) baseRequest;
|
||||||
|
String ownerLogin = normalizeOriginal(req.getOwnerLogin());
|
||||||
|
if (ownerLogin == null) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "EMPTY_OWNER_LOGIN", "ownerLogin обязателен");
|
||||||
|
}
|
||||||
|
|
||||||
|
String ownServerLogin = normalize(AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG));
|
||||||
|
if (ownServerLogin == null) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 500, "LOCAL_SERVER_NOT_CONFIGURED", "server.SHiNE.login не настроен");
|
||||||
|
}
|
||||||
|
if (!isLocalAccessServer(ownerLogin, ownServerLogin)) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 403, "LOCAL_SERVER_NOT_ACCESS_SERVER", "Локальный сервер не является access-сервером пользователя");
|
||||||
|
}
|
||||||
|
|
||||||
|
int limit = clamp(req.getLimit() == null ? DEFAULT_LIMIT : req.getLimit(), 1, MAX_LIMIT);
|
||||||
|
int maxBytes = clamp(req.getMaxBytes() == null ? DEFAULT_MAX_BYTES : req.getMaxBytes(), 64_000, MAX_BYTES_CAP);
|
||||||
|
long afterTimeMs = Math.max(0L, req.getAfterTimeMs() == null ? 0L : req.getAfterTimeMs());
|
||||||
|
String afterSettingKey = req.getAfterSettingKey() == null ? "" : req.getAfterSettingKey().trim();
|
||||||
|
|
||||||
|
List<UserSettingEntry> batch;
|
||||||
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
|
batch = UserSettingsDAO.getInstance().listNewerThan(
|
||||||
|
c,
|
||||||
|
ownerLogin,
|
||||||
|
afterTimeMs,
|
||||||
|
afterSettingKey,
|
||||||
|
limit
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Net_UserSettingsSyncBatch_Response resp = new Net_UserSettingsSyncBatch_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setOwnerLogin(ownerLogin);
|
||||||
|
resp.setLimit(limit);
|
||||||
|
|
||||||
|
int rawBytes = 0;
|
||||||
|
List<Net_UserSettingsSyncBatch_Response.Item> items = new ArrayList<>();
|
||||||
|
for (UserSettingEntry entry : batch) {
|
||||||
|
Net_UserSettingsSyncBatch_Response.Item item = new Net_UserSettingsSyncBatch_Response.Item();
|
||||||
|
item.setLogin(entry.getLogin());
|
||||||
|
item.setSetting_type(entry.getSettingType());
|
||||||
|
item.setSetting_key(entry.getSettingKey());
|
||||||
|
item.setTime_ms(entry.getTimeMs());
|
||||||
|
item.setValue_text(entry.getValueText());
|
||||||
|
item.setValue_num(entry.getValueNum());
|
||||||
|
item.setClient_key(entry.getClientKey());
|
||||||
|
item.setSignature(entry.getSignature());
|
||||||
|
item.setSynced(entry.isSynced());
|
||||||
|
items.add(item);
|
||||||
|
rawBytes += String.valueOf(entry.getLogin()).length()
|
||||||
|
+ String.valueOf(entry.getSettingKey()).length()
|
||||||
|
+ String.valueOf(entry.getValueText()).length()
|
||||||
|
+ String.valueOf(entry.getClientKey() == null ? "" : entry.getClientKey()).length()
|
||||||
|
+ String.valueOf(entry.getSignature() == null ? "" : entry.getSignature()).length()
|
||||||
|
+ 64;
|
||||||
|
if (rawBytes > maxBytes) break;
|
||||||
|
resp.setNextTimeMs(entry.getTimeMs());
|
||||||
|
resp.setNextSettingKey(entry.getSettingKey());
|
||||||
|
}
|
||||||
|
resp.setRawBytes(rawBytes);
|
||||||
|
resp.setHasMore(batch.size() > items.size());
|
||||||
|
resp.setItems(items);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isLocalAccessServer(String ownerLogin, String ownServerLogin) throws Exception {
|
||||||
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
|
for (UserAccessServerRouteEntry route : UserAccessServersCurrentDAO.getInstance().listByUserLogin(c, ownerLogin)) {
|
||||||
|
if (route == null || route.getServerLogin() == null) continue;
|
||||||
|
if (ownServerLogin.equals(normalize(route.getServerLogin()))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int clamp(int value, int min, int max) {
|
||||||
|
return Math.max(min, Math.min(max, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalize(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
String s = value.trim().toLowerCase();
|
||||||
|
return s.isEmpty() ? null : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeOriginal(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
String s = value.trim();
|
||||||
|
return s.isEmpty() ? null : s;
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_UserSettingsSyncBatch_Response;
|
||||||
|
import shine.db.dao.UserSettingsDAO;
|
||||||
|
import shine.db.entities.UserSettingEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class UserSettingsSyncApplySupport {
|
||||||
|
private UserSettingsSyncApplySupport() {}
|
||||||
|
|
||||||
|
public static ApplyResult applySyncedItem(Connection c, String ownerLogin, Net_UserSettingsSyncBatch_Response.Item item) throws Exception {
|
||||||
|
if (item == null) return new ApplyResult(false, "empty_item");
|
||||||
|
String login = normalize(item.getLogin());
|
||||||
|
String key = normalize(item.getSetting_key());
|
||||||
|
if (login == null || key == null) return new ApplyResult(false, "bad_item");
|
||||||
|
|
||||||
|
UserSettingEntry entry = new UserSettingEntry(
|
||||||
|
login,
|
||||||
|
item.getSetting_type() == null ? 0 : item.getSetting_type(),
|
||||||
|
key,
|
||||||
|
item.getTime_ms() == null ? 0L : item.getTime_ms(),
|
||||||
|
item.getValue_text() == null ? "" : item.getValue_text(),
|
||||||
|
item.getValue_num() == null ? 0L : item.getValue_num(),
|
||||||
|
item.getClient_key(),
|
||||||
|
item.getSignature(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
int changed = UserSettingsDAO.getInstance().upsertIfNewer(c, entry);
|
||||||
|
return new ApplyResult(changed > 0, changed > 0 ? "applied" : "ignored");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<UserSettingEntry> toEntries(List<Net_UserSettingsSyncBatch_Response.Item> items) {
|
||||||
|
List<UserSettingEntry> out = new ArrayList<>();
|
||||||
|
if (items == null) return out;
|
||||||
|
for (Net_UserSettingsSyncBatch_Response.Item item : items) {
|
||||||
|
if (item == null) continue;
|
||||||
|
out.add(new UserSettingEntry(
|
||||||
|
normalize(item.getLogin()),
|
||||||
|
item.getSetting_type() == null ? 0 : item.getSetting_type(),
|
||||||
|
normalize(item.getSetting_key()),
|
||||||
|
item.getTime_ms() == null ? 0L : item.getTime_ms(),
|
||||||
|
item.getValue_text() == null ? "" : item.getValue_text(),
|
||||||
|
item.getValue_num() == null ? 0L : item.getValue_num(),
|
||||||
|
item.getClient_key(),
|
||||||
|
item.getSignature(),
|
||||||
|
true
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalize(String value) {
|
||||||
|
String s = String.valueOf(value == null ? "" : value).trim();
|
||||||
|
return s.isEmpty() ? null : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ApplyResult(boolean applied, String status) {}
|
||||||
|
}
|
||||||
+4
@@ -6,6 +6,7 @@ public class Net_CallInviteBroadcast_Request extends Net_Request {
|
|||||||
private String toLogin;
|
private String toLogin;
|
||||||
private String callId;
|
private String callId;
|
||||||
private Integer type;
|
private Integer type;
|
||||||
|
private String data;
|
||||||
|
|
||||||
public String getToLogin() { return toLogin; }
|
public String getToLogin() { return toLogin; }
|
||||||
public void setToLogin(String toLogin) { this.toLogin = 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 Integer getType() { return type; }
|
||||||
public void setType(Integer type) { this.type = type; }
|
public void setType(Integer type) { this.type = type; }
|
||||||
|
|
||||||
|
public String getData() { return data; }
|
||||||
|
public void setData(String data) { this.data = data; }
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -2,12 +2,16 @@ package server.logic.ws_protocol.JSON.messages.entyties;
|
|||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class Net_DmSyncBatch_Request extends Net_Request {
|
public class Net_DmSyncBatch_Request extends Net_Request {
|
||||||
private String ownerLogin;
|
private String ownerLogin;
|
||||||
private Long afterStoredAtMs;
|
private Long afterStoredAtMs;
|
||||||
private String afterMessageKey;
|
private String afterMessageKey;
|
||||||
private Integer limit;
|
private Integer limit;
|
||||||
private Integer maxBytes;
|
private Integer maxBytes;
|
||||||
|
private List<String> ackSyncIds = new ArrayList<>();
|
||||||
|
|
||||||
public String getOwnerLogin() { return ownerLogin; }
|
public String getOwnerLogin() { return ownerLogin; }
|
||||||
public void setOwnerLogin(String ownerLogin) { this.ownerLogin = ownerLogin; }
|
public void setOwnerLogin(String ownerLogin) { this.ownerLogin = ownerLogin; }
|
||||||
@@ -19,4 +23,8 @@ public class Net_DmSyncBatch_Request extends Net_Request {
|
|||||||
public void setLimit(Integer limit) { this.limit = limit; }
|
public void setLimit(Integer limit) { this.limit = limit; }
|
||||||
public Integer getMaxBytes() { return maxBytes; }
|
public Integer getMaxBytes() { return maxBytes; }
|
||||||
public void setMaxBytes(Integer maxBytes) { this.maxBytes = maxBytes; }
|
public void setMaxBytes(Integer maxBytes) { this.maxBytes = maxBytes; }
|
||||||
|
public List<String> getAckSyncIds() { return ackSyncIds; }
|
||||||
|
public void setAckSyncIds(List<String> ackSyncIds) {
|
||||||
|
this.ackSyncIds = ackSyncIds == null ? new ArrayList<>() : ackSyncIds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-24
@@ -30,33 +30,20 @@ public class Net_DmSyncBatch_Response extends Net_Response {
|
|||||||
public void setItems(List<Item> items) { this.items = items; }
|
public void setItems(List<Item> items) { this.items = items; }
|
||||||
|
|
||||||
public static class Item {
|
public static class Item {
|
||||||
private String messageKey;
|
private String syncId;
|
||||||
private String baseKey;
|
private String primaryMessageKey;
|
||||||
private String targetLogin;
|
|
||||||
private String fromLogin;
|
|
||||||
private String toLogin;
|
|
||||||
private int messageType;
|
|
||||||
private long timeMs;
|
|
||||||
private long storedAtMs;
|
private long storedAtMs;
|
||||||
private String blobB64;
|
private List<String> blobsB64 = new ArrayList<>();
|
||||||
|
|
||||||
public String getMessageKey() { return messageKey; }
|
public String getSyncId() { return syncId; }
|
||||||
public void setMessageKey(String messageKey) { this.messageKey = messageKey; }
|
public void setSyncId(String syncId) { this.syncId = syncId; }
|
||||||
public String getBaseKey() { return baseKey; }
|
public String getPrimaryMessageKey() { return primaryMessageKey; }
|
||||||
public void setBaseKey(String baseKey) { this.baseKey = baseKey; }
|
public void setPrimaryMessageKey(String primaryMessageKey) { this.primaryMessageKey = primaryMessageKey; }
|
||||||
public String getTargetLogin() { return targetLogin; }
|
|
||||||
public void setTargetLogin(String targetLogin) { this.targetLogin = targetLogin; }
|
|
||||||
public String getFromLogin() { return fromLogin; }
|
|
||||||
public void setFromLogin(String fromLogin) { this.fromLogin = fromLogin; }
|
|
||||||
public String getToLogin() { return toLogin; }
|
|
||||||
public void setToLogin(String toLogin) { this.toLogin = toLogin; }
|
|
||||||
public int getMessageType() { return messageType; }
|
|
||||||
public void setMessageType(int messageType) { this.messageType = messageType; }
|
|
||||||
public long getTimeMs() { return timeMs; }
|
|
||||||
public void setTimeMs(long timeMs) { this.timeMs = timeMs; }
|
|
||||||
public long getStoredAtMs() { return storedAtMs; }
|
public long getStoredAtMs() { return storedAtMs; }
|
||||||
public void setStoredAtMs(long storedAtMs) { this.storedAtMs = storedAtMs; }
|
public void setStoredAtMs(long storedAtMs) { this.storedAtMs = storedAtMs; }
|
||||||
public String getBlobB64() { return blobB64; }
|
public List<String> getBlobsB64() { return blobsB64; }
|
||||||
public void setBlobB64(String blobB64) { this.blobB64 = blobB64; }
|
public void setBlobsB64(List<String> blobsB64) {
|
||||||
|
this.blobsB64 = blobsB64 == null ? new ArrayList<>() : blobsB64;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -42,6 +42,7 @@ public class Net_GetDirectMessages_Response extends Net_Response {
|
|||||||
private long createdAtMs;
|
private long createdAtMs;
|
||||||
private Long readAtMs;
|
private Long readAtMs;
|
||||||
private String blobB64;
|
private String blobB64;
|
||||||
|
private String deliveryState;
|
||||||
|
|
||||||
public String getMessageKey() { return messageKey; }
|
public String getMessageKey() { return messageKey; }
|
||||||
public void setMessageKey(String messageKey) { this.messageKey = messageKey; }
|
public void setMessageKey(String messageKey) { this.messageKey = messageKey; }
|
||||||
@@ -67,5 +68,7 @@ public class Net_GetDirectMessages_Response extends Net_Response {
|
|||||||
public void setReadAtMs(Long readAtMs) { this.readAtMs = readAtMs; }
|
public void setReadAtMs(Long readAtMs) { this.readAtMs = readAtMs; }
|
||||||
public String getBlobB64() { return blobB64; }
|
public String getBlobB64() { return blobB64; }
|
||||||
public void setBlobB64(String blobB64) { this.blobB64 = blobB64; }
|
public void setBlobB64(String blobB64) { this.blobB64 = blobB64; }
|
||||||
|
public String getDeliveryState() { return deliveryState; }
|
||||||
|
public void setDeliveryState(String deliveryState) { this.deliveryState = deliveryState; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
public class Net_GetDmDeliveryStatus_Request extends Net_Request {
|
||||||
|
private String messageKey;
|
||||||
|
|
||||||
|
public String getMessageKey() { return messageKey; }
|
||||||
|
public void setMessageKey(String value) { this.messageKey = value; }
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
public class Net_GetDmDeliveryStatus_Response extends Net_Response {
|
||||||
|
private String messageKey;
|
||||||
|
private boolean known;
|
||||||
|
private boolean delivered;
|
||||||
|
|
||||||
|
public String getMessageKey() { return messageKey; }
|
||||||
|
public void setMessageKey(String value) { this.messageKey = value; }
|
||||||
|
public boolean isKnown() { return known; }
|
||||||
|
public void setKnown(boolean value) { this.known = value; }
|
||||||
|
public boolean isDelivered() { return delivered; }
|
||||||
|
public void setDelivered(boolean value) { this.delivered = value; }
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
public class Net_MarkAllUserSettingsUnsynced_Request extends Net_Request {
|
||||||
|
private String login;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
public class Net_MarkAllUserSettingsUnsynced_Response extends Net_Response {
|
||||||
|
private Integer updated;
|
||||||
|
private Integer dmUpdated;
|
||||||
|
|
||||||
|
public Integer getUpdated() { return updated; }
|
||||||
|
public void setUpdated(Integer updated) { this.updated = updated; }
|
||||||
|
public Integer getDmUpdated() { return dmUpdated; }
|
||||||
|
public void setDmUpdated(Integer dmUpdated) { this.dmUpdated = dmUpdated; }
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user