SHA256
Compare commits
105
Commits
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
7d475ab403 | ||
|
|
143adcbbe4 | ||
|
|
ca8b6a33ba | ||
|
|
2f73cdb0f9 | ||
|
|
d1e06d9ef1 | ||
|
|
0ed78e2202 | ||
|
|
64818c586b | ||
|
|
578f5cad4a | ||
|
|
d2ff277b16 | ||
|
|
df4f9b3f8e | ||
|
|
be7f53a9ae | ||
|
|
6ed91a105e | ||
|
|
0db3c3af5a | ||
|
|
23748504e6 | ||
|
|
f74fecddd8 | ||
|
|
618a30c2ab | ||
|
|
b5116474c7 | ||
|
|
91e7239866 | ||
|
|
1dddb5fb3c | ||
|
|
8ddf592ffc | ||
|
|
59a1117ed3 | ||
|
|
3bb6fa1e59 | ||
|
|
5af98ac911 | ||
|
|
f197196c21 | ||
|
|
ce5595bc16 | ||
|
|
1406111f22 | ||
|
|
bfdada6792 | ||
|
|
5326ad85d8 | ||
|
|
f9dd245481 | ||
|
|
20677a9090 | ||
|
|
d54cb7507a | ||
|
|
d6b313b582 | ||
|
|
f31cdb413b | ||
|
|
69faf55b2b | ||
|
|
4d9e42205f | ||
|
|
5df69d73c7 | ||
|
|
4704f8485b |
@@ -45,6 +45,11 @@
|
|||||||
- `docs/Solana_Architecture/README.md`
|
- `docs/Solana_Architecture/README.md`
|
||||||
- Документ формата пользовательской PDA-записи `shine_users` находится в:
|
- Документ формата пользовательской PDA-записи `shine_users` находится в:
|
||||||
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`
|
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`
|
||||||
|
- Актуальная документация по серверному модулю синхронизации Solana users находится в:
|
||||||
|
- `docs/Solana/SOLANA_USERS_SYNC_MODULE_DESIGN.md`
|
||||||
|
- При любом изменении логики серверной синхронизации `shine_users`, её таблиц PostgreSQL, checkpoint-механизма, startup/lifecycle или deploy-настроек обязательно обновлять:
|
||||||
|
- `docs/Solana/SOLANA_USERS_SYNC_MODULE_DESIGN.md`
|
||||||
|
- `deploy/SOLANA_USERS_SYNC_SERVER_SETUP.md`
|
||||||
|
|
||||||
## Документация блокчейна
|
## Документация блокчейна
|
||||||
- Актуальная документация по форматам блокчейна находится в `docs/Blockchain/README.md`.
|
- Актуальная документация по форматам блокчейна находится в `docs/Blockchain/README.md`.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ SHiNE-server — серверная часть мессенджера SHiNE: Web
|
|||||||
|
|
||||||
- `shine-server-net-server/` — точка входа, запуск HTTP/WS сервера
|
- `shine-server-net-server/` — точка входа, запуск HTTP/WS сервера
|
||||||
- `shine-server-net-protocol/` — обработчики операций (RPC и события WS)
|
- `shine-server-net-protocol/` — обработчики операций (RPC и события WS)
|
||||||
- `shine-server-db/` — DAO, SQL-схема, SQLite
|
- `shine-server-db/` — DAO, SQL-схема, PostgreSQL runtime
|
||||||
- `shine-server-blockchain/` — логика хранения и проверки блоков блокчейна
|
- `shine-server-blockchain/` — логика хранения и проверки блоков блокчейна
|
||||||
- `shine-server-crypto/` — криптографические утилиты
|
- `shine-server-crypto/` — криптографические утилиты
|
||||||
- `shine-server-config/` — конфигурация сервера
|
- `shine-server-config/` — конфигурация сервера
|
||||||
|
|||||||
+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) ===================== */
|
||||||
|
|
||||||
@@ -138,4 +156,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;
|
||||||
}
|
}
|
||||||
|
|||||||
+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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,6 +59,8 @@ public final class AppConfig {
|
|||||||
public String getParam(String name) {
|
public String getParam(String name) {
|
||||||
String fromSystem = System.getProperty(name);
|
String fromSystem = System.getProperty(name);
|
||||||
if (fromSystem != null) return fromSystem;
|
if (fromSystem != null) return fromSystem;
|
||||||
|
String fromEnv = System.getenv(toEnvName(name));
|
||||||
|
if (fromEnv != null && !fromEnv.isBlank()) return fromEnv.trim();
|
||||||
return properties.getProperty(name);
|
return properties.getProperty(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,4 +80,11 @@ public final class AppConfig {
|
|||||||
String v = properties.getProperty(name);
|
String v = properties.getProperty(name);
|
||||||
return v == null ? defaultValue : Boolean.parseBoolean(v);
|
return v == null ? defaultValue : Boolean.parseBoolean(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String toEnvName(String name) {
|
||||||
|
return name
|
||||||
|
.replace('.', '_')
|
||||||
|
.replace('-', '_')
|
||||||
|
.toUpperCase();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.xerial:sqlite-jdbc:3.47.0.0' // sqlite
|
implementation 'org.postgresql:postgresql:42.7.7'
|
||||||
|
|
||||||
implementation "org.slf4j:slf4j-api:2.0.16" // вызов логгера
|
implementation "org.slf4j:slf4j-api:2.0.16" // вызов логгера
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +1,48 @@
|
|||||||
package shine.db;
|
package shine.db;
|
||||||
|
|
||||||
import utils.config.AppConfig;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStream;
|
||||||
import java.nio.file.*;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.DriverManager;
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.sql.Statement;
|
import java.sql.Statement;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DatabaseInitializer — создание новой SQLite-БД по схеме SHiNE.
|
* PostgreSQL runtime schema bootstrapper for SHiNE server.
|
||||||
*
|
|
||||||
* В этой версии:
|
|
||||||
* - создаём ТОЛЬКО таблицы/индексы
|
|
||||||
* - в конце вызываем DatabaseTriggersInstaller.createAllTriggers(st)
|
|
||||||
*
|
|
||||||
* v2 (sessions):
|
|
||||||
* - active_sessions.session_pwd удалён
|
|
||||||
* - active_sessions.session_key хранит публичный ключ сессии целиком одной строкой
|
|
||||||
*/
|
*/
|
||||||
public final class DatabaseInitializer {
|
public final class DatabaseInitializer {
|
||||||
|
|
||||||
public static final String DB_SCHEMA_VERSION_TABLE = "db_schema_version";
|
public static final String DB_SCHEMA_VERSION_TABLE = "db_schema_version";
|
||||||
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_3 = 3;
|
||||||
|
public static final int SCHEMA_VERSION_4 = 4;
|
||||||
|
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_V3_RESOURCE = "postgres/migration_v3.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V4_RESOURCE = "postgres/migration_v4.sql";
|
||||||
|
|
||||||
private DatabaseInitializer() {}
|
private DatabaseInitializer() {}
|
||||||
|
|
||||||
/* ===================== TEXT (msg_type=1) ===================== */
|
|
||||||
|
|
||||||
public static final short TEXT_POST = 10;
|
public static final short TEXT_POST = 10;
|
||||||
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;
|
||||||
/* ===================== REACTION (msg_type=2) ===================== */
|
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;
|
||||||
|
|
||||||
/* ===================== CONNECTION (msg_type=3) ===================== */
|
|
||||||
// Близкий друг (close friend). Исторически в коде использовалось имя FRIEND.
|
|
||||||
public static final short CONNECTION_FRIEND = 10;
|
public static final short CONNECTION_FRIEND = 10;
|
||||||
public static final short CONNECTION_UNFRIEND = 11;
|
public static final short CONNECTION_UNFRIEND = 11;
|
||||||
public static final short CONNECTION_CLOSE_FRIEND = CONNECTION_FRIEND;
|
public static final short CONNECTION_CLOSE_FRIEND = CONNECTION_FRIEND;
|
||||||
@@ -76,636 +75,225 @@ public final class DatabaseInitializer {
|
|||||||
public static final short CONNECTION_SHINE_SEEN = 74;
|
public static final short CONNECTION_SHINE_SEEN = 74;
|
||||||
public static final short CONNECTION_SHINE_UNSEEN = 75;
|
public static final short CONNECTION_SHINE_UNSEEN = 75;
|
||||||
|
|
||||||
public static void createNewDB(String[] args) {
|
public static void ensurePostgresSchemaInitialized(String jdbcUrl,
|
||||||
AppConfig config = AppConfig.getInstance();
|
String user,
|
||||||
String dbPath = config.getParam("db.path");
|
String password) throws SQLException {
|
||||||
|
|
||||||
if (dbPath == null || dbPath.isBlank()) {
|
|
||||||
System.err.println("Параметр db.path не задан в application.properties");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Path dbFile = Paths.get(dbPath);
|
|
||||||
try {
|
try {
|
||||||
Path parent = dbFile.getParent();
|
Class.forName("org.postgresql.Driver");
|
||||||
if (parent != null && !Files.exists(parent)) {
|
|
||||||
Files.createDirectories(parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Files.exists(dbFile)) {
|
|
||||||
System.out.println("Файл базы данных уже существует: " + dbFile.toAbsolutePath());
|
|
||||||
System.out.print("Пересоздать БД (СТАРАЯ БУДЕТ УДАЛЕНА)? [y/N]: ");
|
|
||||||
|
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
|
|
||||||
String answer = reader.readLine();
|
|
||||||
if (!"y".equalsIgnoreCase(answer) && !"yes".equalsIgnoreCase(answer)) {
|
|
||||||
System.out.println("Операция отменена. БД не изменена.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Files.delete(dbFile);
|
|
||||||
System.out.println("Старый файл БД удалён.");
|
|
||||||
}
|
|
||||||
|
|
||||||
createSchema("jdbc:sqlite:" + dbPath);
|
|
||||||
System.out.println("Новая БД успешно создана по пути: " + dbFile.toAbsolutePath());
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
System.err.println("Ошибка работы с файлом БД: " + e.getMessage());
|
|
||||||
} catch (SQLException e) {
|
|
||||||
System.err.println("Ошибка создания схемы БД: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ensureSchemaV1Structure(String jdbcUrl) throws SQLException {
|
|
||||||
createSchema(jdbcUrl, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void createSchema(String jdbcUrl) throws SQLException {
|
|
||||||
createSchema(jdbcUrl, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void createSchema(String jdbcUrl, boolean initializeVersionRow) throws SQLException {
|
|
||||||
try {
|
|
||||||
Class.forName("org.sqlite.JDBC");
|
|
||||||
} catch (ClassNotFoundException e) {
|
} catch (ClassNotFoundException e) {
|
||||||
throw new RuntimeException("SQLite JDBC driver not found", e);
|
throw new RuntimeException("PostgreSQL JDBC driver not found", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try (Connection conn = DriverManager.getConnection(jdbcUrl);
|
try (Connection conn = openConnection(jdbcUrl, user, password)) {
|
||||||
Statement st = conn.createStatement()) {
|
if (!postgresSchemaVersionTableExists(conn)) {
|
||||||
|
runSqlScript(conn, POSTGRES_SCHEMA_RESOURCE);
|
||||||
st.execute("PRAGMA foreign_keys = ON");
|
return;
|
||||||
|
}
|
||||||
// 1. solana_users
|
int currentVersion = readCurrentSchemaVersion(conn);
|
||||||
// ВАЖНО:
|
if (currentVersion < SCHEMA_VERSION_2) {
|
||||||
// - Все требуемые поля теперь лежат в solana_users:
|
runSqlScript(conn, POSTGRES_MIGRATION_V2_RESOURCE);
|
||||||
// login, blockchain_name, solana_key, blockchain_key, client_key
|
currentVersion = SCHEMA_VERSION_2;
|
||||||
// - Поиск по login в DAO сделан case-insensitive.
|
}
|
||||||
// - Для защиты от дублей "Anya" и "anya" добавляем COLLATE NOCASE на PRIMARY KEY.
|
if (currentVersion < SCHEMA_VERSION_3) {
|
||||||
st.executeUpdate("""
|
runSqlScript(conn, POSTGRES_MIGRATION_V3_RESOURCE);
|
||||||
CREATE TABLE IF NOT EXISTS solana_users (
|
currentVersion = SCHEMA_VERSION_3;
|
||||||
login TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,
|
}
|
||||||
blockchain_name TEXT NOT NULL,
|
if (currentVersion < SCHEMA_VERSION_4) {
|
||||||
solana_key TEXT NOT NULL,
|
runSqlScript(conn, POSTGRES_MIGRATION_V4_RESOURCE);
|
||||||
blockchain_key TEXT NOT NULL,
|
}
|
||||||
client_key TEXT NOT NULL
|
}
|
||||||
);
|
}
|
||||||
""");
|
|
||||||
|
private static Connection openConnection(String jdbcUrl, String user, String password) throws SQLException {
|
||||||
st.executeUpdate("""
|
if (user == null || user.isBlank()) {
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_solana_users_blockchain_name
|
return DriverManager.getConnection(jdbcUrl);
|
||||||
ON solana_users (blockchain_name);
|
}
|
||||||
""");
|
return DriverManager.getConnection(jdbcUrl, user, password == null ? "" : password);
|
||||||
|
}
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_solana_users_login
|
private static boolean postgresSchemaVersionTableExists(Connection conn) throws SQLException {
|
||||||
ON solana_users (login);
|
String sql = """
|
||||||
""");
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
// 2. active_sessions (v2)
|
FROM information_schema.tables
|
||||||
st.executeUpdate("""
|
WHERE table_schema = current_schema()
|
||||||
CREATE TABLE IF NOT EXISTS active_sessions (
|
AND table_name = ?
|
||||||
session_id TEXT NOT NULL PRIMARY KEY,
|
)
|
||||||
login TEXT NOT NULL,
|
""";
|
||||||
session_key TEXT NOT NULL,
|
try (var ps = conn.prepareStatement(sql)) {
|
||||||
storage_pwd TEXT NOT NULL,
|
ps.setString(1, DB_SCHEMA_VERSION_TABLE);
|
||||||
session_created_at_ms INTEGER NOT NULL,
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
last_authirificated_at_ms INTEGER NOT NULL,
|
return rs.next() && rs.getBoolean(1);
|
||||||
push_endpoint TEXT,
|
}
|
||||||
push_p256dh_key TEXT,
|
}
|
||||||
push_auth_key TEXT,
|
}
|
||||||
client_ip TEXT,
|
|
||||||
client_info_from_client TEXT,
|
private static int readCurrentSchemaVersion(Connection conn) throws SQLException {
|
||||||
client_info_from_request TEXT,
|
String sql = """
|
||||||
session_type INTEGER NOT NULL DEFAULT 1,
|
SELECT schema_version
|
||||||
client_platform TEXT NOT NULL DEFAULT '',
|
FROM db_schema_version
|
||||||
user_language TEXT,
|
WHERE id = 1
|
||||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
""";
|
||||||
);
|
try (Statement st = conn.createStatement();
|
||||||
""");
|
ResultSet rs = st.executeQuery(sql)) {
|
||||||
|
if (!rs.next()) {
|
||||||
st.executeUpdate("""
|
return SCHEMA_VERSION_1;
|
||||||
CREATE INDEX IF NOT EXISTS idx_active_sessions_login
|
}
|
||||||
ON active_sessions (login);
|
return rs.getInt(1);
|
||||||
""");
|
}
|
||||||
|
}
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS esp_pairing_settings (
|
private static void runSqlScript(Connection conn, String resourcePath) throws SQLException {
|
||||||
login TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,
|
String sqlScript = loadClasspathResource(resourcePath);
|
||||||
enabled INTEGER NOT NULL DEFAULT 0,
|
List<String> statements = splitSqlStatements(sqlScript);
|
||||||
password_hash TEXT NOT NULL DEFAULT '',
|
|
||||||
ttl_seconds INTEGER NOT NULL DEFAULT 300,
|
boolean previousAutoCommit = conn.getAutoCommit();
|
||||||
failed_attempts INTEGER NOT NULL DEFAULT 0,
|
conn.setAutoCommit(true);
|
||||||
first_failed_at_ms INTEGER NOT NULL DEFAULT 0,
|
try (Statement st = conn.createStatement()) {
|
||||||
blocked_until_ms INTEGER NOT NULL DEFAULT 0,
|
for (String statement : statements) {
|
||||||
updated_at_ms INTEGER NOT NULL,
|
String trimmed = statement.trim();
|
||||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
if (!trimmed.isEmpty()) {
|
||||||
);
|
st.execute(trimmed);
|
||||||
""");
|
}
|
||||||
|
}
|
||||||
st.executeUpdate("""
|
} finally {
|
||||||
CREATE TABLE IF NOT EXISTS esp_pairing_requests (
|
conn.setAutoCommit(previousAutoCommit);
|
||||||
pairing_id TEXT NOT NULL PRIMARY KEY,
|
}
|
||||||
login TEXT NOT NULL,
|
}
|
||||||
requester_session_key TEXT NOT NULL,
|
|
||||||
requester_session_type INTEGER NOT NULL DEFAULT 1,
|
private static String loadClasspathResource(String resourcePath) {
|
||||||
requester_client_platform TEXT NOT NULL DEFAULT '',
|
try (InputStream in = DatabaseInitializer.class.getClassLoader().getResourceAsStream(resourcePath)) {
|
||||||
payload_type INTEGER NOT NULL,
|
if (in == null) {
|
||||||
status TEXT NOT NULL,
|
throw new RuntimeException("Resource not found: " + resourcePath);
|
||||||
short_code TEXT NOT NULL,
|
}
|
||||||
fingerprint_b58 TEXT NOT NULL,
|
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
encrypted_payload TEXT,
|
} catch (IOException e) {
|
||||||
reject_reason TEXT,
|
throw new RuntimeException("Failed to read resource: " + resourcePath, e);
|
||||||
approved_by_session_id TEXT,
|
}
|
||||||
created_at_ms INTEGER NOT NULL,
|
}
|
||||||
expires_at_ms INTEGER NOT NULL,
|
|
||||||
updated_at_ms INTEGER NOT NULL,
|
static List<String> splitSqlStatements(String sqlScript) {
|
||||||
delivered_to_homeserver INTEGER NOT NULL DEFAULT 0,
|
List<String> statements = new ArrayList<>();
|
||||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
StringBuilder current = new StringBuilder();
|
||||||
);
|
boolean inSingleQuote = false;
|
||||||
""");
|
boolean inLineComment = false;
|
||||||
|
boolean inBlockComment = false;
|
||||||
st.executeUpdate("""
|
String dollarQuoteTag = null;
|
||||||
CREATE INDEX IF NOT EXISTS idx_esp_pairing_requests_login_status
|
|
||||||
ON esp_pairing_requests (login, status, expires_at_ms);
|
for (int i = 0; i < sqlScript.length(); i++) {
|
||||||
""");
|
char ch = sqlScript.charAt(i);
|
||||||
|
|
||||||
// 3. users_params
|
if (inLineComment) {
|
||||||
st.executeUpdate("""
|
current.append(ch);
|
||||||
CREATE TABLE IF NOT EXISTS users_params (
|
if (ch == '\n') {
|
||||||
login TEXT NOT NULL,
|
inLineComment = false;
|
||||||
param TEXT NOT NULL,
|
}
|
||||||
time_ms INTEGER NOT NULL,
|
continue;
|
||||||
value TEXT NOT NULL,
|
|
||||||
client_key TEXT,
|
|
||||||
signature TEXT,
|
|
||||||
FOREIGN KEY (login) REFERENCES solana_users(login),
|
|
||||||
UNIQUE (login, param)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_params_login
|
|
||||||
ON users_params (login);
|
|
||||||
""");
|
|
||||||
|
|
||||||
// 4. ip_geo_cache
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS ip_geo_cache (
|
|
||||||
ip TEXT NOT NULL PRIMARY KEY,
|
|
||||||
geo TEXT,
|
|
||||||
updated_at_ms INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_ip_geo_cache_updated_at
|
|
||||||
ON ip_geo_cache (updated_at_ms);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS test_free_avatar_uploads (
|
|
||||||
login TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,
|
|
||||||
used_count INTEGER NOT NULL DEFAULT 0,
|
|
||||||
updated_at_ms INTEGER NOT NULL,
|
|
||||||
last_tx_id TEXT NOT NULL DEFAULT '',
|
|
||||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_test_free_avatar_uploads_updated
|
|
||||||
ON test_free_avatar_uploads (updated_at_ms);
|
|
||||||
""");
|
|
||||||
|
|
||||||
// 5. blockchain_state
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS blockchain_state (
|
|
||||||
blockchain_name TEXT NOT NULL PRIMARY KEY,
|
|
||||||
login TEXT NOT NULL,
|
|
||||||
blockchain_key TEXT NOT NULL,
|
|
||||||
|
|
||||||
size_limit INTEGER NOT NULL,
|
|
||||||
file_size_bytes INTEGER NOT NULL,
|
|
||||||
|
|
||||||
last_block_number INTEGER NOT NULL,
|
|
||||||
last_block_hash BLOB,
|
|
||||||
|
|
||||||
updated_at_ms INTEGER NOT NULL,
|
|
||||||
|
|
||||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_blockchain_state_login
|
|
||||||
ON blockchain_state (login);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_blockchain_state_updated_at
|
|
||||||
ON blockchain_state (updated_at_ms);
|
|
||||||
""");
|
|
||||||
|
|
||||||
// 6. blocks (+ line_code)
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS blocks (
|
|
||||||
login TEXT NOT NULL,
|
|
||||||
bch_name TEXT NOT NULL,
|
|
||||||
block_number INTEGER NOT NULL CHECK(block_number >= 0),
|
|
||||||
|
|
||||||
msg_type INTEGER NOT NULL,
|
|
||||||
msg_sub_type INTEGER NOT NULL,
|
|
||||||
|
|
||||||
block_bytes BLOB NOT NULL,
|
|
||||||
|
|
||||||
-- target (reply/like/edit и т.д.)
|
|
||||||
to_login TEXT,
|
|
||||||
to_bch_name TEXT,
|
|
||||||
to_block_number INTEGER CHECK(to_block_number IS NULL OR to_block_number >= 0),
|
|
||||||
to_block_hash BLOB,
|
|
||||||
|
|
||||||
-- собственные данные
|
|
||||||
block_hash BLOB NOT NULL,
|
|
||||||
block_signature BLOB NOT NULL,
|
|
||||||
|
|
||||||
-- если этот блок был изменён последним edit'ом
|
|
||||||
edited_by_block_number INTEGER CHECK(edited_by_block_number IS NULL OR edited_by_block_number >= 0),
|
|
||||||
|
|
||||||
-- линейность (опционально)
|
|
||||||
line_code INTEGER CHECK(line_code IS NULL OR line_code >= 0),
|
|
||||||
prev_line_number INTEGER CHECK(prev_line_number IS NULL OR prev_line_number >= 0),
|
|
||||||
prev_line_hash BLOB,
|
|
||||||
this_line_number INTEGER CHECK(this_line_number IS NULL OR this_line_number >= 0),
|
|
||||||
|
|
||||||
FOREIGN KEY (login) REFERENCES solana_users(login),
|
|
||||||
FOREIGN KEY (bch_name) REFERENCES blockchain_state(blockchain_name),
|
|
||||||
|
|
||||||
UNIQUE (bch_name, block_number)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_blocks_by_chain_number
|
|
||||||
ON blocks (bch_name, block_number);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_blocks_to_target
|
|
||||||
ON blocks (to_login, to_bch_name, to_block_number);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_blocks_by_line
|
|
||||||
ON blocks (bch_name, line_code, this_line_number);
|
|
||||||
""");
|
|
||||||
|
|
||||||
// 7) connections_state
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS connections_state (
|
|
||||||
login TEXT NOT NULL,
|
|
||||||
rel_type INTEGER NOT NULL,
|
|
||||||
to_login TEXT NOT NULL,
|
|
||||||
to_bch_name TEXT NOT NULL,
|
|
||||||
to_block_number INTEGER NOT NULL,
|
|
||||||
to_block_hash BLOB NOT NULL,
|
|
||||||
|
|
||||||
FOREIGN KEY (login) REFERENCES solana_users(login),
|
|
||||||
|
|
||||||
UNIQUE (login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_connections_state_login
|
|
||||||
ON connections_state (login);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_connections_state_to_login
|
|
||||||
ON connections_state (to_login);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_connections_state_pair
|
|
||||||
ON connections_state (login, to_login);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_connections_state_target
|
|
||||||
ON connections_state (login, rel_type, to_bch_name, to_block_number);
|
|
||||||
""");
|
|
||||||
|
|
||||||
// 8) message_stats
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS message_stats (
|
|
||||||
to_login TEXT NOT NULL,
|
|
||||||
to_bch_name TEXT NOT NULL,
|
|
||||||
to_block_number INTEGER NOT NULL,
|
|
||||||
to_block_hash BLOB NOT NULL,
|
|
||||||
|
|
||||||
likes_count INTEGER NOT NULL DEFAULT 0,
|
|
||||||
replies_count INTEGER NOT NULL DEFAULT 0,
|
|
||||||
edits_count INTEGER NOT NULL DEFAULT 0,
|
|
||||||
|
|
||||||
UNIQUE (
|
|
||||||
to_login,
|
|
||||||
to_bch_name,
|
|
||||||
to_block_number,
|
|
||||||
to_block_hash
|
|
||||||
)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_message_stats_target
|
|
||||||
ON message_stats (to_bch_name, to_block_number, to_block_hash);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_message_stats_login
|
|
||||||
ON message_stats (to_login);
|
|
||||||
""");
|
|
||||||
|
|
||||||
// 8.0) reactions_state (идемпотентный LIKE/UNLIKE per actor/target)
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS reactions_state (
|
|
||||||
from_login TEXT NOT NULL,
|
|
||||||
from_bch_name TEXT NOT NULL,
|
|
||||||
reaction_type INTEGER NOT NULL,
|
|
||||||
to_login TEXT NOT NULL,
|
|
||||||
to_bch_name TEXT NOT NULL,
|
|
||||||
to_block_number INTEGER NOT NULL,
|
|
||||||
to_block_hash BLOB NOT NULL,
|
|
||||||
last_sub_type INTEGER NOT NULL,
|
|
||||||
|
|
||||||
UNIQUE (
|
|
||||||
from_login,
|
|
||||||
from_bch_name,
|
|
||||||
reaction_type,
|
|
||||||
to_login,
|
|
||||||
to_bch_name,
|
|
||||||
to_block_number,
|
|
||||||
to_block_hash
|
|
||||||
)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_reactions_state_target
|
|
||||||
ON reactions_state (to_bch_name, to_block_number, to_block_hash);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_reactions_state_actor
|
|
||||||
ON reactions_state (from_login, from_bch_name, reaction_type);
|
|
||||||
""");
|
|
||||||
|
|
||||||
// 9) channel_names_state (global normalized channel names)
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS channel_names_state (
|
|
||||||
slug TEXT NOT NULL,
|
|
||||||
display_name TEXT NOT NULL,
|
|
||||||
channel_description TEXT NOT NULL DEFAULT '',
|
|
||||||
owner_login TEXT NOT NULL,
|
|
||||||
owner_bch_name TEXT NOT NULL,
|
|
||||||
channel_type_code INTEGER NOT NULL DEFAULT 1,
|
|
||||||
channel_type_version INTEGER NOT NULL DEFAULT 1,
|
|
||||||
channel_root_block_number INTEGER NOT NULL,
|
|
||||||
channel_root_block_hash BLOB NOT NULL,
|
|
||||||
created_at_ms INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_names_state_owner_type_slug
|
|
||||||
ON channel_names_state (owner_bch_name, channel_type_code, slug);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_names_state_target
|
|
||||||
ON channel_names_state (owner_bch_name, channel_root_block_number, channel_root_block_hash);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_channel_names_state_owner
|
|
||||||
ON channel_names_state (owner_login, owner_bch_name);
|
|
||||||
""");
|
|
||||||
|
|
||||||
// 9.1) chat200_state
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS chat200_state (
|
|
||||||
owner_login TEXT NOT NULL,
|
|
||||||
owner_bch_name TEXT NOT NULL,
|
|
||||||
channel_root_block_number INTEGER NOT NULL,
|
|
||||||
channel_root_block_hash BLOB NOT NULL,
|
|
||||||
channel_name TEXT NOT NULL,
|
|
||||||
channel_type_version INTEGER NOT NULL,
|
|
||||||
chat_title TEXT NOT NULL DEFAULT '',
|
|
||||||
updated_at_ms INTEGER NOT NULL,
|
|
||||||
PRIMARY KEY (owner_bch_name, channel_root_block_number)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_chat200_state_owner
|
|
||||||
ON chat200_state (owner_login, owner_bch_name);
|
|
||||||
""");
|
|
||||||
|
|
||||||
// 9.2) chat200_members_state
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS chat200_members_state (
|
|
||||||
owner_bch_name TEXT NOT NULL,
|
|
||||||
channel_root_block_number INTEGER NOT NULL,
|
|
||||||
member_login TEXT NOT NULL,
|
|
||||||
member_channel_name TEXT NOT NULL,
|
|
||||||
is_active INTEGER NOT NULL,
|
|
||||||
updated_at_ms INTEGER NOT NULL,
|
|
||||||
updated_by_block_number INTEGER NOT NULL,
|
|
||||||
PRIMARY KEY (
|
|
||||||
owner_bch_name,
|
|
||||||
channel_root_block_number,
|
|
||||||
member_login,
|
|
||||||
member_channel_name
|
|
||||||
)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_chat200_members_owner
|
|
||||||
ON chat200_members_state (owner_bch_name, channel_root_block_number, is_active);
|
|
||||||
""");
|
|
||||||
|
|
||||||
// 10) direct_messages
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS direct_messages (
|
|
||||||
message_id TEXT NOT NULL PRIMARY KEY,
|
|
||||||
from_login TEXT NOT NULL,
|
|
||||||
to_login TEXT NOT NULL,
|
|
||||||
text TEXT NOT NULL,
|
|
||||||
created_at_ms INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY (from_login) REFERENCES solana_users(login),
|
|
||||||
FOREIGN KEY (to_login) REFERENCES solana_users(login)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_direct_messages_to_login
|
|
||||||
ON direct_messages (to_login, created_at_ms);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_direct_messages_from_login
|
|
||||||
ON direct_messages (from_login, created_at_ms);
|
|
||||||
""");
|
|
||||||
|
|
||||||
// 11) user_push_tokens
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS user_push_tokens (
|
|
||||||
token_id TEXT NOT NULL PRIMARY KEY,
|
|
||||||
login TEXT NOT NULL,
|
|
||||||
session_id TEXT NOT NULL,
|
|
||||||
provider TEXT NOT NULL,
|
|
||||||
token TEXT NOT NULL,
|
|
||||||
platform TEXT,
|
|
||||||
user_agent TEXT,
|
|
||||||
updated_at_ms INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_push_tokens_login
|
|
||||||
ON user_push_tokens (login);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_push_tokens_login_session
|
|
||||||
ON user_push_tokens (login, session_id);
|
|
||||||
""");
|
|
||||||
|
|
||||||
// 11) signed_direct_message_replay (anti-replay window)
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS signed_direct_message_replay (
|
|
||||||
from_login TEXT NOT NULL,
|
|
||||||
time_ms INTEGER NOT NULL,
|
|
||||||
nonce INTEGER NOT NULL,
|
|
||||||
created_at_ms INTEGER NOT NULL,
|
|
||||||
UNIQUE (from_login, time_ms, nonce)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_signed_dm_replay_created
|
|
||||||
ON signed_direct_message_replay (created_at_ms);
|
|
||||||
""");
|
|
||||||
|
|
||||||
// 12) signed_direct_messages_history (сырой бинарный пакет + мета)
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS signed_direct_messages_history (
|
|
||||||
message_id TEXT NOT NULL PRIMARY KEY,
|
|
||||||
from_login TEXT NOT NULL,
|
|
||||||
to_login TEXT NOT NULL,
|
|
||||||
target_mode INTEGER NOT NULL,
|
|
||||||
target_session_id TEXT,
|
|
||||||
message_type INTEGER NOT NULL,
|
|
||||||
time_ms INTEGER NOT NULL,
|
|
||||||
nonce INTEGER NOT NULL,
|
|
||||||
raw_packet BLOB NOT NULL,
|
|
||||||
created_at_ms INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY (from_login) REFERENCES solana_users(login),
|
|
||||||
FOREIGN KEY (to_login) REFERENCES solana_users(login)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_signed_dm_history_to
|
|
||||||
ON signed_direct_messages_history (to_login, created_at_ms);
|
|
||||||
""");
|
|
||||||
|
|
||||||
// 13) signed_messages_v2 (универсальное хранилище блоков типов 1..8)
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS signed_messages_v2 (
|
|
||||||
message_key TEXT NOT NULL PRIMARY KEY,
|
|
||||||
base_key TEXT NOT NULL,
|
|
||||||
target_login TEXT NOT NULL,
|
|
||||||
from_login TEXT NOT NULL,
|
|
||||||
to_login TEXT NOT NULL,
|
|
||||||
time_ms INTEGER NOT NULL,
|
|
||||||
nonce INTEGER NOT NULL,
|
|
||||||
message_type INTEGER NOT NULL,
|
|
||||||
revision_time_ms INTEGER NOT NULL DEFAULT 0,
|
|
||||||
reencrypted_at_ms INTEGER NOT NULL DEFAULT 0,
|
|
||||||
raw_block BLOB NOT NULL,
|
|
||||||
created_at_ms INTEGER NOT NULL,
|
|
||||||
source_api TEXT NOT NULL,
|
|
||||||
origin_session_id TEXT,
|
|
||||||
receipt_ref_base_key TEXT,
|
|
||||||
receipt_ref_type INTEGER,
|
|
||||||
read_at_ms INTEGER,
|
|
||||||
FOREIGN KEY (from_login) REFERENCES solana_users(login),
|
|
||||||
FOREIGN KEY (to_login) REFERENCES solana_users(login)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_signed_messages_v2_target
|
|
||||||
ON signed_messages_v2 (target_login, time_ms, created_at_ms);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_signed_messages_v2_base
|
|
||||||
ON signed_messages_v2 (base_key, message_type);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_signed_messages_v2_receipt_incoming
|
|
||||||
ON signed_messages_v2 (target_login, receipt_ref_base_key)
|
|
||||||
WHERE message_type = 3 AND receipt_ref_base_key IS NOT NULL;
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_signed_messages_v2_receipt_outgoing
|
|
||||||
ON signed_messages_v2 (target_login, receipt_ref_base_key)
|
|
||||||
WHERE message_type = 4 AND receipt_ref_base_key IS NOT NULL;
|
|
||||||
""");
|
|
||||||
|
|
||||||
// 14) signed_message_session_delivery (доставка по сессиям)
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS signed_message_session_delivery (
|
|
||||||
message_key TEXT NOT NULL,
|
|
||||||
session_id TEXT NOT NULL,
|
|
||||||
delivered INTEGER NOT NULL DEFAULT 0,
|
|
||||||
delivered_at_ms INTEGER,
|
|
||||||
created_at_ms INTEGER NOT NULL,
|
|
||||||
PRIMARY KEY (message_key, session_id),
|
|
||||||
FOREIGN KEY (message_key) REFERENCES signed_messages_v2(message_key)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_signed_message_delivery_session
|
|
||||||
ON signed_message_session_delivery (session_id, delivered);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS db_schema_version (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY CHECK (id = 1),
|
|
||||||
schema_version INTEGER NOT NULL,
|
|
||||||
updated_at_ms INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
if (initializeVersionRow) {
|
|
||||||
st.executeUpdate("""
|
|
||||||
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
|
||||||
VALUES (1, 1, CAST(strftime('%s','now') AS INTEGER) * 1000)
|
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
|
||||||
schema_version = excluded.schema_version,
|
|
||||||
updated_at_ms = excluded.updated_at_ms;
|
|
||||||
""");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DatabaseTriggersInstaller.createAllTriggers(st);
|
if (inBlockComment) {
|
||||||
|
current.append(ch);
|
||||||
|
if (ch == '*' && i + 1 < sqlScript.length() && sqlScript.charAt(i + 1) == '/') {
|
||||||
|
current.append('/');
|
||||||
|
i += 1;
|
||||||
|
inBlockComment = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dollarQuoteTag != null) {
|
||||||
|
if (startsWithAt(sqlScript, i, dollarQuoteTag)) {
|
||||||
|
current.append(dollarQuoteTag);
|
||||||
|
i += dollarQuoteTag.length() - 1;
|
||||||
|
dollarQuoteTag = null;
|
||||||
|
} else {
|
||||||
|
current.append(ch);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inSingleQuote) {
|
||||||
|
current.append(ch);
|
||||||
|
if (ch == '\'') {
|
||||||
|
if (i + 1 < sqlScript.length() && sqlScript.charAt(i + 1) == '\'') {
|
||||||
|
current.append('\'');
|
||||||
|
i += 1;
|
||||||
|
} else {
|
||||||
|
inSingleQuote = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '-' && i + 1 < sqlScript.length() && sqlScript.charAt(i + 1) == '-') {
|
||||||
|
current.append("--");
|
||||||
|
i += 1;
|
||||||
|
inLineComment = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '/' && i + 1 < sqlScript.length() && sqlScript.charAt(i + 1) == '*') {
|
||||||
|
current.append("/*");
|
||||||
|
i += 1;
|
||||||
|
inBlockComment = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '\'') {
|
||||||
|
inSingleQuote = true;
|
||||||
|
current.append(ch);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String tag = readDollarQuoteTag(sqlScript, i);
|
||||||
|
if (tag != null) {
|
||||||
|
current.append(tag);
|
||||||
|
i += tag.length() - 1;
|
||||||
|
dollarQuoteTag = tag;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == ';') {
|
||||||
|
String statement = current.toString().trim();
|
||||||
|
if (!statement.isEmpty()) {
|
||||||
|
statements.add(current.toString());
|
||||||
|
}
|
||||||
|
current.setLength(0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
current.append(ch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!current.isEmpty()) {
|
||||||
|
String statement = current.toString().trim();
|
||||||
|
if (!statement.isEmpty()) {
|
||||||
|
statements.add(current.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return statements;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean startsWithAt(String sqlScript, int index, String token) {
|
||||||
|
return sqlScript.regionMatches(index, token, 0, token.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readDollarQuoteTag(String sqlScript, int index) {
|
||||||
|
if (sqlScript.charAt(index) != '$') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int end = index + 1;
|
||||||
|
while (end < sqlScript.length()) {
|
||||||
|
char current = sqlScript.charAt(end);
|
||||||
|
if (current == '$') {
|
||||||
|
return sqlScript.substring(index, end + 1);
|
||||||
|
}
|
||||||
|
if (!(Character.isLetterOrDigit(current) || current == '_')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,569 +0,0 @@
|
|||||||
package shine.db;
|
|
||||||
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.sql.ResultSet;
|
|
||||||
import java.sql.Statement;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DatabaseTriggersInstaller — устанавливает триггеры, которые поддерживают бизнес-логику БД.
|
|
||||||
*
|
|
||||||
* Мы специально сделали триггеры максимально "совместимыми":
|
|
||||||
* - НЕТ динамических сообщений в RAISE(...): только фиксированные строки.
|
|
||||||
* (Некоторые SQLite-сборки / просмотрщики падают на "||" внутри RAISE.)
|
|
||||||
* - НЕТ UPSERT "ON CONFLICT DO UPDATE" — вместо него:
|
|
||||||
* INSERT OR IGNORE + UPDATE
|
|
||||||
* (Старые SQLite не знают UPSERT.)
|
|
||||||
*
|
|
||||||
* =============================================================================
|
|
||||||
* РћРџРРЎРђРќРР• РўР РГГЕРОВ
|
|
||||||
* =============================================================================
|
|
||||||
*
|
|
||||||
* [1] trg_blocks_line_integrity_bi (BEFORE INSERT ON blocks)
|
|
||||||
* Контроль целостности "линий" (line_code / prev_line_number / prev_line_hash / this_line_number).
|
|
||||||
*
|
|
||||||
* Зачем это нужно:
|
|
||||||
* - В каналах/ветках/действиях ты хочешь иметь "линейную" последовательность,
|
|
||||||
* где каждый следующий блок явно ссылается на предыдущий блок линии
|
|
||||||
* и подтверждает, что ссылка не подменена.
|
|
||||||
*
|
|
||||||
* Когда срабатывает:
|
|
||||||
* - ТОЛЬКО если при вставке передано ХОТЯ БЫ ОДНО из line-полей.
|
|
||||||
* - Если line-поля не переданы — триггер вообще не работает (это важно).
|
|
||||||
*
|
|
||||||
* Что проверяет:
|
|
||||||
* A) line-поля допускаются только для msg_type:
|
|
||||||
* 0 (TECH), 1 (TEXT), 3 (CONNECTION), 4 (USER_PARAM)
|
|
||||||
* B) Если пришло хоть одно line-поле — обязаны прийти ВСЕ 4 (никаких "частичных")
|
|
||||||
* C) prev-блок линии существует в той же цепочке bch_name
|
|
||||||
* D) prev_hash совпадает с block_hash найденного prev-блока
|
|
||||||
* E) line_code корректный:
|
|
||||||
* - либо первый шаг после root: prev_line_number == line_code
|
|
||||||
* - либо prev уже принадлежит этой линии: p.line_code == NEW.line_code
|
|
||||||
* F) this_line_number:
|
|
||||||
* - первый шаг после root:
|
|
||||||
* TEXT: this_line_number = 0
|
|
||||||
* TECH/CONNECTION/USER_PARAM: this_line_number = 1
|
|
||||||
* - обычный шаг:
|
|
||||||
* TEXT: допускаем same или +1 (чтобы "edit" мог не двигать шаг)
|
|
||||||
* TECH/CONNECTION/USER_PARAM: строго prev.this + 1
|
|
||||||
*
|
|
||||||
* Какие ошибки кидает:
|
|
||||||
* - LINE_ERR_UNSUPPORTED_TYPE_WITH_LINE
|
|
||||||
* - LINE_ERR_PARTIAL_FIELDS
|
|
||||||
* - LINE_ERR_NO_PREV
|
|
||||||
* - LINE_ERR_PREV_HASH_MISMATCH
|
|
||||||
* - LINE_ERR_LINE_CODE_MISMATCH
|
|
||||||
* - LINE_ERR_FIRST_STEP_BAD_THIS
|
|
||||||
* - LINE_ERR_THIS_LINE_BAD_STEP
|
|
||||||
*
|
|
||||||
* [2] trg_blocks_connection_state_ai (AFTER INSERT ON blocks WHEN msg_type=3)
|
|
||||||
* Поддерживает таблицу connections_state как "текущее состояние" отношений:
|
|
||||||
* - FRIEND/CONTACT/FOLLOW -> добавить/обновить состояние
|
|
||||||
* - UNFRIEND/UNCONTACT/UNFOLLOW -> удалить соответствующее "позитивное" состояние
|
|
||||||
*
|
|
||||||
* [3] trg_blocks_message_stats_like_ai (AFTER INSERT ON blocks WHEN msg_type=2 AND sub_type=LIKE)
|
|
||||||
* Поддерживает likes_count в message_stats для цели (to_*).
|
|
||||||
*
|
|
||||||
* [4] trg_blocks_message_stats_reply_ai (AFTER INSERT ON blocks WHEN msg_type=1 AND sub_type=REPLY)
|
|
||||||
* Поддерживает replies_count в message_stats.
|
|
||||||
*
|
|
||||||
* [5] trg_blocks_edit_apply_ai (AFTER INSERT ON blocks WHEN msg_type=1 AND sub_type=EDIT)
|
|
||||||
* Логика edit:
|
|
||||||
* - помечает исходный блок edited_by_block_number = NEW.block_number
|
|
||||||
* - увеличивает edits_count в message_stats
|
|
||||||
*/
|
|
||||||
public final class DatabaseTriggersInstaller {
|
|
||||||
|
|
||||||
private DatabaseTriggersInstaller() {}
|
|
||||||
|
|
||||||
public static void createAllTriggers(Statement st) throws SQLException {
|
|
||||||
dropTriggersByPrefix(st, "trg_blocks_");
|
|
||||||
|
|
||||||
// На всякий случай убираем старые "криво названные" триггеры,
|
|
||||||
// если они когда-то попадали в БД.
|
|
||||||
st.executeUpdate("DROP TRIGGER IF EXISTS trg_block_lini_integriti_by;");
|
|
||||||
st.executeUpdate("DROP TRIGGER IF EXISTS trg_blocks_line_integrity_bi;");
|
|
||||||
|
|
||||||
st.executeUpdate("DROP TRIGGER IF EXISTS trg_blocks_connection_state_ai;");
|
|
||||||
st.executeUpdate("DROP TRIGGER IF EXISTS trg_blocks_message_stats_like_ai;");
|
|
||||||
st.executeUpdate("DROP TRIGGER IF EXISTS trg_blocks_message_stats_reply_ai;");
|
|
||||||
st.executeUpdate("DROP TRIGGER IF EXISTS trg_blocks_edit_apply_ai;");
|
|
||||||
|
|
||||||
createLineIntegrityTrigger(st);
|
|
||||||
createConnectionStateTrigger(st);
|
|
||||||
createMessageStatsLikeTrigger(st);
|
|
||||||
createMessageStatsReplyTrigger(st);
|
|
||||||
createEditApplyTrigger(st);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void dropTriggersByPrefix(Statement st, String prefix) throws SQLException {
|
|
||||||
List<String> triggerNames = new ArrayList<>();
|
|
||||||
String sql = "SELECT name FROM sqlite_master WHERE type='trigger' AND name LIKE '" + prefix + "%'";
|
|
||||||
try (ResultSet rs = st.executeQuery(sql)) {
|
|
||||||
while (rs.next()) {
|
|
||||||
String name = rs.getString("name");
|
|
||||||
if (name != null && !name.isBlank()) {
|
|
||||||
triggerNames.add(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String name : triggerNames) {
|
|
||||||
String safeName = name.replace("\"", "\"\"");
|
|
||||||
st.executeUpdate("DROP TRIGGER IF EXISTS \"" + safeName + "\"");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void createLineIntegrityTrigger(Statement st) throws SQLException {
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TRIGGER IF NOT EXISTS trg_blocks_line_integrity_bi
|
|
||||||
BEFORE INSERT ON blocks
|
|
||||||
WHEN
|
|
||||||
NEW.line_code IS NOT NULL
|
|
||||||
OR NEW.prev_line_number IS NOT NULL
|
|
||||||
OR NEW.prev_line_hash IS NOT NULL
|
|
||||||
OR NEW.this_line_number IS NOT NULL
|
|
||||||
BEGIN
|
|
||||||
SELECT RAISE(ABORT, 'LINE_ERR_UNSUPPORTED_TYPE_WITH_LINE')
|
|
||||||
WHERE NOT (NEW.msg_type IN (0, 1, 3, 4));
|
|
||||||
|
|
||||||
SELECT RAISE(ABORT, 'LINE_ERR_PARTIAL_FIELDS')
|
|
||||||
WHERE NEW.line_code IS NULL
|
|
||||||
OR NEW.prev_line_number IS NULL
|
|
||||||
OR NEW.prev_line_hash IS NULL
|
|
||||||
OR NEW.this_line_number IS NULL;
|
|
||||||
|
|
||||||
SELECT RAISE(ABORT, 'LINE_ERR_NO_PREV')
|
|
||||||
WHERE NOT EXISTS(
|
|
||||||
SELECT 1
|
|
||||||
FROM blocks p
|
|
||||||
WHERE p.bch_name = NEW.bch_name
|
|
||||||
AND p.block_number = NEW.prev_line_number
|
|
||||||
LIMIT 1
|
|
||||||
);
|
|
||||||
|
|
||||||
SELECT RAISE(ABORT, 'LINE_ERR_PREV_HASH_MISMATCH')
|
|
||||||
WHERE NOT EXISTS(
|
|
||||||
SELECT 1
|
|
||||||
FROM blocks p
|
|
||||||
WHERE p.bch_name = NEW.bch_name
|
|
||||||
AND p.block_number = NEW.prev_line_number
|
|
||||||
AND p.block_hash = NEW.prev_line_hash
|
|
||||||
LIMIT 1
|
|
||||||
);
|
|
||||||
|
|
||||||
SELECT RAISE(ABORT, 'LINE_ERR_LINE_CODE_MISMATCH')
|
|
||||||
WHERE NEW.prev_line_number <> NEW.line_code
|
|
||||||
AND NOT EXISTS(
|
|
||||||
SELECT 1
|
|
||||||
FROM blocks p
|
|
||||||
WHERE p.bch_name = NEW.bch_name
|
|
||||||
AND p.block_number = NEW.prev_line_number
|
|
||||||
AND p.line_code = NEW.line_code
|
|
||||||
LIMIT 1
|
|
||||||
);
|
|
||||||
|
|
||||||
SELECT RAISE(ABORT, 'LINE_ERR_FIRST_STEP_BAD_THIS')
|
|
||||||
WHERE NEW.prev_line_number = NEW.line_code
|
|
||||||
AND NEW.this_line_number <> (CASE WHEN NEW.msg_type = 1 THEN 0 ELSE 1 END);
|
|
||||||
|
|
||||||
SELECT RAISE(ABORT, 'LINE_ERR_THIS_LINE_BAD_STEP')
|
|
||||||
WHERE NEW.prev_line_number <> NEW.line_code
|
|
||||||
AND NOT EXISTS(
|
|
||||||
SELECT 1
|
|
||||||
FROM blocks p
|
|
||||||
WHERE p.bch_name = NEW.bch_name
|
|
||||||
AND p.block_number = NEW.prev_line_number
|
|
||||||
AND p.this_line_number IS NOT NULL
|
|
||||||
AND (
|
|
||||||
(NEW.msg_type = 1 AND
|
|
||||||
(NEW.this_line_number = p.this_line_number OR NEW.this_line_number = p.this_line_number + 1)
|
|
||||||
)
|
|
||||||
OR
|
|
||||||
(NEW.msg_type IN (0,3,4) AND NEW.this_line_number = p.this_line_number + 1)
|
|
||||||
)
|
|
||||||
LIMIT 1
|
|
||||||
);
|
|
||||||
END;
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void createConnectionStateTrigger(Statement st) throws SQLException {
|
|
||||||
int FRIEND = (int) DatabaseInitializer.CONNECTION_FRIEND;
|
|
||||||
int CONTACT = (int) DatabaseInitializer.CONNECTION_CONTACT;
|
|
||||||
int FOLLOW = (int) DatabaseInitializer.CONNECTION_FOLLOW;
|
|
||||||
int SPOUSE = (int) DatabaseInitializer.CONNECTION_SPOUSE;
|
|
||||||
int PARENT = (int) DatabaseInitializer.CONNECTION_PARENT;
|
|
||||||
int CHILD = (int) DatabaseInitializer.CONNECTION_CHILD;
|
|
||||||
int SIBLING = (int) DatabaseInitializer.CONNECTION_SIBLING;
|
|
||||||
int KNOWN = (int) DatabaseInitializer.CONNECTION_KNOWN_PERSON;
|
|
||||||
int SHINE_CONF = (int) DatabaseInitializer.CONNECTION_SHINE_CONFIRMED;
|
|
||||||
int SHINE_SEEN = (int) DatabaseInitializer.CONNECTION_SHINE_SEEN;
|
|
||||||
|
|
||||||
int UNFRIEND = (int) DatabaseInitializer.CONNECTION_UNFRIEND;
|
|
||||||
int UNCONTACT = (int) DatabaseInitializer.CONNECTION_UNCONTACT;
|
|
||||||
int UNFOLLOW = (int) DatabaseInitializer.CONNECTION_UNFOLLOW;
|
|
||||||
int UNSPOUSE = (int) DatabaseInitializer.CONNECTION_UNSPOUSE;
|
|
||||||
int UNPARENT = (int) DatabaseInitializer.CONNECTION_UNPARENT;
|
|
||||||
int UNCHILD = (int) DatabaseInitializer.CONNECTION_UNCHILD;
|
|
||||||
int UNSIBLING = (int) DatabaseInitializer.CONNECTION_UNSIBLING;
|
|
||||||
int UNKNOWN = (int) DatabaseInitializer.CONNECTION_UNKNOWN_PERSON;
|
|
||||||
int SHINE_UNCONF = (int) DatabaseInitializer.CONNECTION_SHINE_UNCONFIRMED;
|
|
||||||
int SHINE_UNSEEN = (int) DatabaseInitializer.CONNECTION_SHINE_UNSEEN;
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TRIGGER IF NOT EXISTS trg_blocks_connection_state_ai
|
|
||||||
AFTER INSERT ON blocks
|
|
||||||
WHEN NEW.msg_type = 3
|
|
||||||
BEGIN
|
|
||||||
-- FRIEND/CONTACT/FOLLOW/SPOUSE/PARENT/CHILD/SIBLING/KNOWN_PERSON/SHINE_*:
|
|
||||||
-- 1) если записи нет — создаём
|
|
||||||
INSERT OR IGNORE INTO connections_state (
|
|
||||||
login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
NEW.login,
|
|
||||||
NEW.msg_sub_type,
|
|
||||||
COALESCE(
|
|
||||||
NEW.to_login,
|
|
||||||
(
|
|
||||||
SELECT su.login
|
|
||||||
FROM solana_users su
|
|
||||||
WHERE su.blockchain_name = NEW.to_bch_name COLLATE NOCASE
|
|
||||||
LIMIT 1
|
|
||||||
),
|
|
||||||
CASE
|
|
||||||
WHEN NEW.to_bch_name IS NOT NULL
|
|
||||||
AND length(NEW.to_bch_name) > 4
|
|
||||||
AND substr(NEW.to_bch_name, length(NEW.to_bch_name) - 3, 1) = '-'
|
|
||||||
THEN substr(NEW.to_bch_name, 1, length(NEW.to_bch_name) - 4)
|
|
||||||
ELSE NULL
|
|
||||||
END
|
|
||||||
),
|
|
||||||
NEW.to_bch_name,
|
|
||||||
NEW.to_block_number,
|
|
||||||
NEW.to_block_hash
|
|
||||||
WHERE NEW.msg_sub_type IN (%d, %d, %d, %d, %d, %d, %d, %d, %d, %d)
|
|
||||||
AND COALESCE(
|
|
||||||
NEW.to_login,
|
|
||||||
(
|
|
||||||
SELECT su.login
|
|
||||||
FROM solana_users su
|
|
||||||
WHERE su.blockchain_name = NEW.to_bch_name COLLATE NOCASE
|
|
||||||
LIMIT 1
|
|
||||||
),
|
|
||||||
CASE
|
|
||||||
WHEN NEW.to_bch_name IS NOT NULL
|
|
||||||
AND length(NEW.to_bch_name) > 4
|
|
||||||
AND substr(NEW.to_bch_name, length(NEW.to_bch_name) - 3, 1) = '-'
|
|
||||||
THEN substr(NEW.to_bch_name, 1, length(NEW.to_bch_name) - 4)
|
|
||||||
ELSE NULL
|
|
||||||
END
|
|
||||||
) IS NOT NULL
|
|
||||||
AND NEW.to_bch_name IS NOT NULL;
|
|
||||||
|
|
||||||
-- 2) если запись есть — обновляем актуальные to_*
|
|
||||||
UPDATE connections_state
|
|
||||||
SET
|
|
||||||
to_bch_name = NEW.to_bch_name,
|
|
||||||
to_block_number = NEW.to_block_number,
|
|
||||||
to_block_hash = NEW.to_block_hash
|
|
||||||
WHERE login = NEW.login
|
|
||||||
AND rel_type = NEW.msg_sub_type
|
|
||||||
AND to_login = COALESCE(
|
|
||||||
NEW.to_login,
|
|
||||||
(
|
|
||||||
SELECT su.login
|
|
||||||
FROM solana_users su
|
|
||||||
WHERE su.blockchain_name = NEW.to_bch_name COLLATE NOCASE
|
|
||||||
LIMIT 1
|
|
||||||
),
|
|
||||||
CASE
|
|
||||||
WHEN NEW.to_bch_name IS NOT NULL
|
|
||||||
AND length(NEW.to_bch_name) > 4
|
|
||||||
AND substr(NEW.to_bch_name, length(NEW.to_bch_name) - 3, 1) = '-'
|
|
||||||
THEN substr(NEW.to_bch_name, 1, length(NEW.to_bch_name) - 4)
|
|
||||||
ELSE NULL
|
|
||||||
END
|
|
||||||
)
|
|
||||||
AND NEW.msg_sub_type IN (%d, %d, %d, %d, %d, %d, %d, %d, %d, %d)
|
|
||||||
AND COALESCE(
|
|
||||||
NEW.to_login,
|
|
||||||
(
|
|
||||||
SELECT su.login
|
|
||||||
FROM solana_users su
|
|
||||||
WHERE su.blockchain_name = NEW.to_bch_name COLLATE NOCASE
|
|
||||||
LIMIT 1
|
|
||||||
),
|
|
||||||
CASE
|
|
||||||
WHEN NEW.to_bch_name IS NOT NULL
|
|
||||||
AND length(NEW.to_bch_name) > 4
|
|
||||||
AND substr(NEW.to_bch_name, length(NEW.to_bch_name) - 3, 1) = '-'
|
|
||||||
THEN substr(NEW.to_bch_name, 1, length(NEW.to_bch_name) - 4)
|
|
||||||
ELSE NULL
|
|
||||||
END
|
|
||||||
) IS NOT NULL
|
|
||||||
AND NEW.to_bch_name IS NOT NULL;
|
|
||||||
|
|
||||||
-- UNFRIEND/UNCONTACT/UNFOLLOW/UNSPOUSE/UNPARENT/UNCHILD/UNSIBLING/UNKNOWN_PERSON/SHINE_UN*:
|
|
||||||
-- удаляем соответствующее "позитивное" состояние
|
|
||||||
DELETE FROM connections_state
|
|
||||||
WHERE login = NEW.login
|
|
||||||
AND to_login = COALESCE(
|
|
||||||
NEW.to_login,
|
|
||||||
(
|
|
||||||
SELECT su.login
|
|
||||||
FROM solana_users su
|
|
||||||
WHERE su.blockchain_name = NEW.to_bch_name COLLATE NOCASE
|
|
||||||
LIMIT 1
|
|
||||||
),
|
|
||||||
CASE
|
|
||||||
WHEN NEW.to_bch_name IS NOT NULL
|
|
||||||
AND length(NEW.to_bch_name) > 4
|
|
||||||
AND substr(NEW.to_bch_name, length(NEW.to_bch_name) - 3, 1) = '-'
|
|
||||||
THEN substr(NEW.to_bch_name, 1, length(NEW.to_bch_name) - 4)
|
|
||||||
ELSE NULL
|
|
||||||
END
|
|
||||||
)
|
|
||||||
AND rel_type = CASE NEW.msg_sub_type
|
|
||||||
WHEN %d THEN %d
|
|
||||||
WHEN %d THEN %d
|
|
||||||
WHEN %d THEN %d
|
|
||||||
WHEN %d THEN %d
|
|
||||||
WHEN %d THEN %d
|
|
||||||
WHEN %d THEN %d
|
|
||||||
WHEN %d THEN %d
|
|
||||||
WHEN %d THEN %d
|
|
||||||
WHEN %d THEN %d
|
|
||||||
WHEN %d THEN %d
|
|
||||||
ELSE rel_type
|
|
||||||
END
|
|
||||||
AND COALESCE(
|
|
||||||
NEW.to_login,
|
|
||||||
(
|
|
||||||
SELECT su.login
|
|
||||||
FROM solana_users su
|
|
||||||
WHERE su.blockchain_name = NEW.to_bch_name COLLATE NOCASE
|
|
||||||
LIMIT 1
|
|
||||||
),
|
|
||||||
CASE
|
|
||||||
WHEN NEW.to_bch_name IS NOT NULL
|
|
||||||
AND length(NEW.to_bch_name) > 4
|
|
||||||
AND substr(NEW.to_bch_name, length(NEW.to_bch_name) - 3, 1) = '-'
|
|
||||||
THEN substr(NEW.to_bch_name, 1, length(NEW.to_bch_name) - 4)
|
|
||||||
ELSE NULL
|
|
||||||
END
|
|
||||||
) IS NOT NULL
|
|
||||||
AND NEW.msg_sub_type IN (%d, %d, %d, %d, %d, %d, %d, %d, %d, %d);
|
|
||||||
END;
|
|
||||||
""".formatted(
|
|
||||||
FRIEND, CONTACT, FOLLOW, SPOUSE, PARENT, CHILD, SIBLING, KNOWN, SHINE_CONF, SHINE_SEEN,
|
|
||||||
FRIEND, CONTACT, FOLLOW, SPOUSE, PARENT, CHILD, SIBLING, KNOWN, SHINE_CONF, SHINE_SEEN,
|
|
||||||
|
|
||||||
UNFRIEND, FRIEND,
|
|
||||||
UNCONTACT, CONTACT,
|
|
||||||
UNFOLLOW, FOLLOW,
|
|
||||||
UNSPOUSE, SPOUSE,
|
|
||||||
UNPARENT, PARENT,
|
|
||||||
UNCHILD, CHILD,
|
|
||||||
UNSIBLING, SIBLING,
|
|
||||||
UNKNOWN, KNOWN,
|
|
||||||
SHINE_UNCONF, SHINE_CONF,
|
|
||||||
SHINE_UNSEEN, SHINE_SEEN,
|
|
||||||
|
|
||||||
UNFRIEND, UNCONTACT, UNFOLLOW, UNSPOUSE, UNPARENT, UNCHILD, UNSIBLING, UNKNOWN, SHINE_UNCONF, SHINE_UNSEEN
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void createMessageStatsLikeTrigger(Statement st) throws SQLException {
|
|
||||||
int LIKE = (int) DatabaseInitializer.REACTION_LIKE;
|
|
||||||
int UNLIKE = (int) DatabaseInitializer.REACTION_UNLIKE;
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_like_ai
|
|
||||||
AFTER INSERT ON blocks
|
|
||||||
WHEN NEW.msg_type = 2 AND NEW.msg_sub_type IN (%d, %d)
|
|
||||||
BEGIN
|
|
||||||
-- ensure target stats row exists
|
|
||||||
INSERT OR IGNORE INTO message_stats (
|
|
||||||
to_login, to_bch_name, to_block_number, to_block_hash,
|
|
||||||
likes_count, replies_count, edits_count
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
NEW.to_login, NEW.to_bch_name, NEW.to_block_number, NEW.to_block_hash,
|
|
||||||
0, 0, 0
|
|
||||||
WHERE NEW.to_login IS NOT NULL
|
|
||||||
AND NEW.to_bch_name IS NOT NULL
|
|
||||||
AND NEW.to_block_number IS NOT NULL
|
|
||||||
AND NEW.to_block_hash IS NOT NULL;
|
|
||||||
|
|
||||||
-- apply delta by state transition (none/unlike->like = +1, like->unlike = -1)
|
|
||||||
UPDATE message_stats
|
|
||||||
SET likes_count = MAX(
|
|
||||||
0,
|
|
||||||
likes_count + (
|
|
||||||
CASE
|
|
||||||
WHEN NEW.msg_sub_type = %d
|
|
||||||
AND COALESCE((
|
|
||||||
SELECT b.msg_sub_type
|
|
||||||
FROM blocks b
|
|
||||||
WHERE b.login = NEW.login
|
|
||||||
AND b.bch_name = NEW.bch_name
|
|
||||||
AND b.msg_type = 2
|
|
||||||
AND b.to_login = NEW.to_login
|
|
||||||
AND b.to_bch_name = NEW.to_bch_name
|
|
||||||
AND b.to_block_number = NEW.to_block_number
|
|
||||||
AND b.to_block_hash = NEW.to_block_hash
|
|
||||||
AND b.block_number < NEW.block_number
|
|
||||||
ORDER BY b.block_number DESC
|
|
||||||
LIMIT 1
|
|
||||||
), -1) <> %d
|
|
||||||
THEN 1
|
|
||||||
WHEN NEW.msg_sub_type = %d
|
|
||||||
AND COALESCE((
|
|
||||||
SELECT b.msg_sub_type
|
|
||||||
FROM blocks b
|
|
||||||
WHERE b.login = NEW.login
|
|
||||||
AND b.bch_name = NEW.bch_name
|
|
||||||
AND b.msg_type = 2
|
|
||||||
AND b.to_login = NEW.to_login
|
|
||||||
AND b.to_bch_name = NEW.to_bch_name
|
|
||||||
AND b.to_block_number = NEW.to_block_number
|
|
||||||
AND b.to_block_hash = NEW.to_block_hash
|
|
||||||
AND b.block_number < NEW.block_number
|
|
||||||
ORDER BY b.block_number DESC
|
|
||||||
LIMIT 1
|
|
||||||
), -1) = %d
|
|
||||||
THEN -1
|
|
||||||
ELSE 0
|
|
||||||
END
|
|
||||||
)
|
|
||||||
)
|
|
||||||
WHERE to_login = NEW.to_login
|
|
||||||
AND to_bch_name = NEW.to_bch_name
|
|
||||||
AND to_block_number = NEW.to_block_number
|
|
||||||
AND to_block_hash = NEW.to_block_hash
|
|
||||||
AND NEW.to_login IS NOT NULL
|
|
||||||
AND NEW.to_bch_name IS NOT NULL
|
|
||||||
AND NEW.to_block_number IS NOT NULL
|
|
||||||
AND NEW.to_block_hash IS NOT NULL;
|
|
||||||
|
|
||||||
-- persist latest actor->target reaction state
|
|
||||||
INSERT OR IGNORE INTO reactions_state (
|
|
||||||
from_login, from_bch_name, reaction_type,
|
|
||||||
to_login, to_bch_name, to_block_number, to_block_hash,
|
|
||||||
last_sub_type
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
NEW.login, NEW.bch_name, %d,
|
|
||||||
NEW.to_login, NEW.to_bch_name, NEW.to_block_number, NEW.to_block_hash,
|
|
||||||
NEW.msg_sub_type
|
|
||||||
WHERE NEW.to_login IS NOT NULL
|
|
||||||
AND NEW.to_bch_name IS NOT NULL
|
|
||||||
AND NEW.to_block_number IS NOT NULL
|
|
||||||
AND NEW.to_block_hash IS NOT NULL;
|
|
||||||
|
|
||||||
UPDATE reactions_state
|
|
||||||
SET last_sub_type = NEW.msg_sub_type
|
|
||||||
WHERE from_login = NEW.login
|
|
||||||
AND from_bch_name = NEW.bch_name
|
|
||||||
AND reaction_type = %d
|
|
||||||
AND to_login = NEW.to_login
|
|
||||||
AND to_bch_name = NEW.to_bch_name
|
|
||||||
AND to_block_number = NEW.to_block_number
|
|
||||||
AND to_block_hash = NEW.to_block_hash
|
|
||||||
AND NEW.to_login IS NOT NULL
|
|
||||||
AND NEW.to_bch_name IS NOT NULL
|
|
||||||
AND NEW.to_block_number IS NOT NULL
|
|
||||||
AND NEW.to_block_hash IS NOT NULL;
|
|
||||||
END;
|
|
||||||
""".formatted(
|
|
||||||
LIKE, UNLIKE,
|
|
||||||
LIKE, LIKE,
|
|
||||||
UNLIKE, LIKE,
|
|
||||||
LIKE,
|
|
||||||
LIKE
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void createMessageStatsReplyTrigger(Statement st) throws SQLException {
|
|
||||||
int REPLY = (int) DatabaseInitializer.TEXT_REPLY;
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_reply_ai
|
|
||||||
AFTER INSERT ON blocks
|
|
||||||
WHEN NEW.msg_type = 1 AND NEW.msg_sub_type = %d
|
|
||||||
BEGIN
|
|
||||||
INSERT OR IGNORE INTO message_stats (
|
|
||||||
to_login, to_bch_name, to_block_number, to_block_hash,
|
|
||||||
likes_count, replies_count, edits_count
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
NEW.to_login, NEW.to_bch_name, NEW.to_block_number, NEW.to_block_hash,
|
|
||||||
0, 0, 0
|
|
||||||
WHERE NEW.to_login IS NOT NULL
|
|
||||||
AND NEW.to_bch_name IS NOT NULL
|
|
||||||
AND NEW.to_block_number IS NOT NULL
|
|
||||||
AND NEW.to_block_hash IS NOT NULL;
|
|
||||||
|
|
||||||
UPDATE message_stats
|
|
||||||
SET replies_count = replies_count + 1
|
|
||||||
WHERE to_login = NEW.to_login
|
|
||||||
AND to_bch_name = NEW.to_bch_name
|
|
||||||
AND to_block_number = NEW.to_block_number
|
|
||||||
AND to_block_hash = NEW.to_block_hash
|
|
||||||
AND NEW.to_login IS NOT NULL
|
|
||||||
AND NEW.to_bch_name IS NOT NULL
|
|
||||||
AND NEW.to_block_number IS NOT NULL
|
|
||||||
AND NEW.to_block_hash IS NOT NULL;
|
|
||||||
END;
|
|
||||||
""".formatted(REPLY));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void createEditApplyTrigger(Statement st) throws SQLException {
|
|
||||||
int EDIT_POST = (int) DatabaseInitializer.TEXT_EDIT_POST;
|
|
||||||
int EDIT_REPLY = (int) DatabaseInitializer.TEXT_EDIT_REPLY;
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TRIGGER IF NOT EXISTS trg_blocks_edit_apply_ai
|
|
||||||
AFTER INSERT ON blocks
|
|
||||||
WHEN NEW.msg_type = 1 AND NEW.msg_sub_type IN (%d, %d)
|
|
||||||
BEGIN
|
|
||||||
-- 1) помечаем исходный блок, что его "перекрыл" этот edit
|
|
||||||
UPDATE blocks
|
|
||||||
SET edited_by_block_number = NEW.block_number
|
|
||||||
WHERE login = NEW.login
|
|
||||||
AND bch_name = NEW.bch_name
|
|
||||||
AND block_number = NEW.to_block_number
|
|
||||||
AND NEW.to_block_number IS NOT NULL;
|
|
||||||
|
|
||||||
-- 2) создаём stats-строку если её не было
|
|
||||||
INSERT OR IGNORE INTO message_stats (
|
|
||||||
to_login, to_bch_name, to_block_number, to_block_hash,
|
|
||||||
likes_count, replies_count, edits_count
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
NEW.to_login, NEW.to_bch_name, NEW.to_block_number, NEW.to_block_hash,
|
|
||||||
0, 0, 0
|
|
||||||
WHERE NEW.to_login IS NOT NULL
|
|
||||||
AND NEW.to_bch_name IS NOT NULL
|
|
||||||
AND NEW.to_block_number IS NOT NULL
|
|
||||||
AND NEW.to_block_hash IS NOT NULL;
|
|
||||||
|
|
||||||
-- 3) +1 edit
|
|
||||||
UPDATE message_stats
|
|
||||||
SET edits_count = edits_count + 1
|
|
||||||
WHERE to_login = NEW.to_login
|
|
||||||
AND to_bch_name = NEW.to_bch_name
|
|
||||||
AND to_block_number = NEW.to_block_number
|
|
||||||
AND to_block_hash = NEW.to_block_hash
|
|
||||||
AND NEW.to_login IS NOT NULL
|
|
||||||
AND NEW.to_bch_name IS NOT NULL
|
|
||||||
AND NEW.to_block_number IS NOT NULL
|
|
||||||
AND NEW.to_block_hash IS NOT NULL;
|
|
||||||
END;
|
|
||||||
""".formatted(EDIT_POST, EDIT_REPLY));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package shine.db;
|
||||||
|
|
||||||
|
import shine.db.connection.DbProvider;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Нейтральная точка входа в runtime БД сервера.
|
||||||
|
*
|
||||||
|
* Runtime-сервер теперь поддерживает только PostgreSQL.
|
||||||
|
*/
|
||||||
|
public final class DbController implements DbProvider {
|
||||||
|
|
||||||
|
private static volatile DbController instance;
|
||||||
|
|
||||||
|
private final PostgresDbController delegate;
|
||||||
|
|
||||||
|
private DbController() {
|
||||||
|
this.delegate = PostgresDbController.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DbController getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (DbController.class) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new DbController();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Connection getConnection() throws SQLException {
|
||||||
|
return delegate.getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
delegate.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package shine.db;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Нормализация ключей из разных storage-слоёв к каноническому Base64(32).
|
||||||
|
*/
|
||||||
|
public final class KeyEncodingUtil {
|
||||||
|
|
||||||
|
private static final String BASE58_ALPHABET =
|
||||||
|
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||||
|
|
||||||
|
private static final int[] BASE58_INDEXES = new int[128];
|
||||||
|
|
||||||
|
static {
|
||||||
|
for (int i = 0; i < BASE58_INDEXES.length; i++) {
|
||||||
|
BASE58_INDEXES[i] = -1;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < BASE58_ALPHABET.length(); i++) {
|
||||||
|
BASE58_INDEXES[BASE58_ALPHABET.charAt(i)] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyEncodingUtil() {}
|
||||||
|
|
||||||
|
public static String normalizeKeyToBase64_32(String rawKey) {
|
||||||
|
if (rawKey == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String value = rawKey.trim();
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] asBase64 = tryDecodeBase64_32(value);
|
||||||
|
if (asBase64 != null) {
|
||||||
|
return Base64.getEncoder().encodeToString(asBase64);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] asBase58 = tryDecodeBase58_32(value);
|
||||||
|
if (asBase58 != null) {
|
||||||
|
return Base64.getEncoder().encodeToString(asBase58);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] tryDecodeBase64_32(String value) {
|
||||||
|
try {
|
||||||
|
byte[] decoded = Base64.getDecoder().decode(value);
|
||||||
|
return decoded.length == 32 ? decoded : null;
|
||||||
|
} catch (IllegalArgumentException ignore) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] tryDecodeBase58_32(String value) {
|
||||||
|
byte[] decoded = decodeBase58(value);
|
||||||
|
return decoded.length == 32 ? decoded : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] decodeBase58(String input) {
|
||||||
|
if (input.isEmpty()) {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] input58 = new byte[input.length()];
|
||||||
|
for (int i = 0; i < input.length(); i++) {
|
||||||
|
char c = input.charAt(i);
|
||||||
|
if (c >= BASE58_INDEXES.length || BASE58_INDEXES[c] < 0) {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
input58[i] = (byte) BASE58_INDEXES[c];
|
||||||
|
}
|
||||||
|
|
||||||
|
int zeros = 0;
|
||||||
|
while (zeros < input58.length && input58[zeros] == 0) {
|
||||||
|
zeros++;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] decoded = new byte[input.length()];
|
||||||
|
int outputStart = decoded.length;
|
||||||
|
int inputStart = zeros;
|
||||||
|
while (inputStart < input58.length) {
|
||||||
|
int mod = divmod256(input58, inputStart);
|
||||||
|
if (input58[inputStart] == 0) {
|
||||||
|
inputStart++;
|
||||||
|
}
|
||||||
|
decoded[--outputStart] = (byte) mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (outputStart < decoded.length && decoded[outputStart] == 0) {
|
||||||
|
outputStart++;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] result = new byte[decoded.length - outputStart + zeros];
|
||||||
|
for (int i = 0; i < zeros; i++) {
|
||||||
|
result[i] = 0;
|
||||||
|
}
|
||||||
|
System.arraycopy(decoded, outputStart, result, zeros, decoded.length - outputStart);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int divmod256(byte[] number58, int startAt) {
|
||||||
|
int remainder = 0;
|
||||||
|
for (int i = startAt; i < number58.length; i++) {
|
||||||
|
int digit58 = number58[i] & 0xFF;
|
||||||
|
int temp = remainder * 58 + digit58;
|
||||||
|
number58[i] = (byte) (temp / 256);
|
||||||
|
remainder = temp % 256;
|
||||||
|
}
|
||||||
|
return remainder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) ===================== */
|
||||||
|
|
||||||
@@ -120,6 +135,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 — лучше добавить новые значения,
|
||||||
// не трогая уже занятые коды.
|
// не трогая уже занятые коды.
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package shine.db;
|
||||||
|
|
||||||
|
import shine.db.connection.DriverManagerDbProvider;
|
||||||
|
import utils.config.AppConfig;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public final class PostgresDbController {
|
||||||
|
|
||||||
|
private static volatile PostgresDbController instance;
|
||||||
|
|
||||||
|
private final DriverManagerDbProvider delegate;
|
||||||
|
|
||||||
|
private PostgresDbController() {
|
||||||
|
AppConfig config = AppConfig.getInstance();
|
||||||
|
String jdbcUrl = trimToNull(config.getParam("db.url"));
|
||||||
|
String dbUser = trimToNull(config.getParam("db.user"));
|
||||||
|
String dbPassword = trimToNull(config.getParam("db.password"));
|
||||||
|
|
||||||
|
if (jdbcUrl == null) {
|
||||||
|
throw new IllegalStateException("Config param 'db.url' is required and must point to PostgreSQL");
|
||||||
|
}
|
||||||
|
if (!jdbcUrl.startsWith("jdbc:postgresql:")) {
|
||||||
|
throw new IllegalStateException("Only PostgreSQL runtime is supported. Unsupported db.url=" + jdbcUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
DatabaseInitializer.ensurePostgresSchemaInitialized(jdbcUrl, dbUser, dbPassword);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException("PostgreSQL schema auto-init failed", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.delegate = new DriverManagerDbProvider(jdbcUrl, dbUser, dbPassword, connection -> {
|
||||||
|
connection.setAutoCommit(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PostgresDbController getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (PostgresDbController.class) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new PostgresDbController();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Connection getConnection() throws SQLException {
|
||||||
|
return delegate.getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String trimToNull(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
String trimmed = value.trim();
|
||||||
|
return trimmed.isEmpty() ? null : trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,846 +0,0 @@
|
|||||||
package shine.db;
|
|
||||||
|
|
||||||
import utils.config.AppConfig;
|
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.sql.DriverManager;
|
|
||||||
import java.sql.ResultSet;
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.sql.Statement;
|
|
||||||
|
|
||||||
public final class SqliteDbController {
|
|
||||||
|
|
||||||
private static volatile SqliteDbController instance;
|
|
||||||
private static final int LATEST_SCHEMA_VERSION = 12;
|
|
||||||
|
|
||||||
private final String jdbcUrl;
|
|
||||||
|
|
||||||
private SqliteDbController() {
|
|
||||||
try {
|
|
||||||
Class.forName("org.sqlite.JDBC");
|
|
||||||
} catch (ClassNotFoundException e) {
|
|
||||||
throw new RuntimeException("SQLite JDBC driver not found", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
String dbPath = AppConfig.getInstance().getParam("db.path");
|
|
||||||
if (dbPath == null || dbPath.isBlank()) {
|
|
||||||
throw new RuntimeException("Config param 'db.path' is not set in application.properties");
|
|
||||||
}
|
|
||||||
|
|
||||||
Path dbFile = Paths.get(dbPath);
|
|
||||||
|
|
||||||
if (!Files.exists(dbFile)) {
|
|
||||||
System.out.println("[DB] Файл БД не найден: " + dbFile.toAbsolutePath());
|
|
||||||
System.out.println("[DB] Создаём новую БД с помощью DatabaseInitializer...");
|
|
||||||
DatabaseInitializer.createNewDB(new String[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.jdbcUrl = "jdbc:sqlite:" + dbPath;
|
|
||||||
ensureSchemaMigrations();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SqliteDbController getInstance() {
|
|
||||||
if (instance == null) {
|
|
||||||
synchronized (SqliteDbController.class) {
|
|
||||||
if (instance == null) {
|
|
||||||
instance = new SqliteDbController();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Connection getConnection() throws SQLException {
|
|
||||||
Connection conn = DriverManager.getConnection(jdbcUrl);
|
|
||||||
conn.setAutoCommit(true);
|
|
||||||
|
|
||||||
try (Statement st = conn.createStatement()) {
|
|
||||||
st.execute("PRAGMA foreign_keys = ON");
|
|
||||||
st.execute("PRAGMA journal_mode = WAL");
|
|
||||||
st.execute("PRAGMA synchronous = NORMAL");
|
|
||||||
st.execute("PRAGMA busy_timeout = 5000");
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void close() {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ensureSchemaMigrations() {
|
|
||||||
int currentVersion = getCurrentSchemaVersion();
|
|
||||||
|
|
||||||
while (currentVersion < LATEST_SCHEMA_VERSION) {
|
|
||||||
int nextVersion = currentVersion + 1;
|
|
||||||
applyMigration(nextVersion);
|
|
||||||
currentVersion = nextVersion;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyMigration(int targetVersion) {
|
|
||||||
switch (targetVersion) {
|
|
||||||
case 1 -> migrateToV1();
|
|
||||||
case 2 -> migrateToV2();
|
|
||||||
case 3 -> migrateToV3();
|
|
||||||
case 4 -> migrateToV4();
|
|
||||||
case 5 -> migrateToV5();
|
|
||||||
case 6 -> migrateToV6();
|
|
||||||
case 7 -> migrateToV7();
|
|
||||||
case 8 -> migrateToV8();
|
|
||||||
case 9 -> migrateToV9();
|
|
||||||
case 10 -> migrateToV10();
|
|
||||||
case 11 -> migrateToV11();
|
|
||||||
case 12 -> migrateToV12();
|
|
||||||
default -> throw new RuntimeException("Unknown DB migration target version: " + targetVersion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void migrateToV1() {
|
|
||||||
try {
|
|
||||||
DatabaseInitializer.ensureSchemaV1Structure(jdbcUrl);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("DB migration to v1 failed (base schema)", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
|
||||||
Statement st = c.createStatement()) {
|
|
||||||
c.setAutoCommit(false);
|
|
||||||
try {
|
|
||||||
st.execute("PRAGMA foreign_keys = OFF");
|
|
||||||
|
|
||||||
if (tableExists(c, "connections_state") && needsConnectionsStateUpgrade(c)) {
|
|
||||||
rebuildConnectionsStateTable(st);
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureChannelNamesDescriptionColumn(c, st);
|
|
||||||
ensureChannelNamesTypeColumns(c, st);
|
|
||||||
ensureSignedMessageReceiptUniq(c, st);
|
|
||||||
DatabaseTriggersInstaller.createAllTriggers(st);
|
|
||||||
setSchemaVersion(c, 1);
|
|
||||||
|
|
||||||
st.execute("PRAGMA foreign_keys = ON");
|
|
||||||
c.commit();
|
|
||||||
} catch (Exception e) {
|
|
||||||
try { c.rollback(); } catch (Exception ignored) {}
|
|
||||||
throw new RuntimeException("DB migration to v1 failed", e);
|
|
||||||
} finally {
|
|
||||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("DB migration to v1 failed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void migrateToV2() {
|
|
||||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
|
||||||
Statement st = c.createStatement()) {
|
|
||||||
c.setAutoCommit(false);
|
|
||||||
try {
|
|
||||||
st.execute("PRAGMA foreign_keys = OFF");
|
|
||||||
st.executeUpdate("DROP TABLE IF EXISTS message_views_state");
|
|
||||||
st.executeUpdate("DROP INDEX IF EXISTS idx_message_views_state_target");
|
|
||||||
st.executeUpdate("DROP INDEX IF EXISTS idx_message_views_state_viewer_channel");
|
|
||||||
setSchemaVersion(c, 2);
|
|
||||||
st.execute("PRAGMA foreign_keys = ON");
|
|
||||||
c.commit();
|
|
||||||
} catch (Exception e) {
|
|
||||||
try { c.rollback(); } catch (Exception ignored) {}
|
|
||||||
throw new RuntimeException("DB migration to v2 failed", e);
|
|
||||||
} finally {
|
|
||||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("DB migration to v2 failed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void migrateToV3() {
|
|
||||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
|
||||||
Statement st = c.createStatement()) {
|
|
||||||
c.setAutoCommit(false);
|
|
||||||
try {
|
|
||||||
ensureChat200StateTables(st);
|
|
||||||
setSchemaVersion(c, 3);
|
|
||||||
c.commit();
|
|
||||||
} catch (Exception e) {
|
|
||||||
try { c.rollback(); } catch (Exception ignored) {}
|
|
||||||
throw new RuntimeException("DB migration to v3 failed", e);
|
|
||||||
} finally {
|
|
||||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("DB migration to v3 failed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void migrateToV4() {
|
|
||||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
|
||||||
Statement st = c.createStatement()) {
|
|
||||||
c.setAutoCommit(false);
|
|
||||||
try {
|
|
||||||
ensureActiveSessionsSessionTypeColumn(c, st);
|
|
||||||
ensureActiveSessionsClientPlatformColumn(c, st);
|
|
||||||
setSchemaVersion(c, 4);
|
|
||||||
c.commit();
|
|
||||||
} catch (Exception e) {
|
|
||||||
try { c.rollback(); } catch (Exception ignored) {}
|
|
||||||
throw new RuntimeException("DB migration to v4 failed", e);
|
|
||||||
} finally {
|
|
||||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("DB migration to v4 failed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void migrateToV5() {
|
|
||||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
|
||||||
Statement st = c.createStatement()) {
|
|
||||||
c.setAutoCommit(false);
|
|
||||||
try {
|
|
||||||
ensureEspPairingTables(st);
|
|
||||||
setSchemaVersion(c, 5);
|
|
||||||
c.commit();
|
|
||||||
} catch (Exception e) {
|
|
||||||
try { c.rollback(); } catch (Exception ignored) {}
|
|
||||||
throw new RuntimeException("DB migration to v5 failed", e);
|
|
||||||
} finally {
|
|
||||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("DB migration to v5 failed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void migrateToV6() {
|
|
||||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
|
||||||
Statement st = c.createStatement()) {
|
|
||||||
c.setAutoCommit(false);
|
|
||||||
try {
|
|
||||||
ensureSignedMessagesRevisionColumn(c, st);
|
|
||||||
setSchemaVersion(c, 6);
|
|
||||||
c.commit();
|
|
||||||
} catch (Exception e) {
|
|
||||||
try { c.rollback(); } catch (Exception ignored) {}
|
|
||||||
throw new RuntimeException("DB migration to v6 failed", e);
|
|
||||||
} finally {
|
|
||||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("DB migration to v6 failed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void migrateToV7() {
|
|
||||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
|
||||||
Statement st = c.createStatement()) {
|
|
||||||
c.setAutoCommit(false);
|
|
||||||
try {
|
|
||||||
dropDmFileTables(st);
|
|
||||||
setSchemaVersion(c, 7);
|
|
||||||
c.commit();
|
|
||||||
} catch (Exception e) {
|
|
||||||
try { c.rollback(); } catch (Exception ignored) {}
|
|
||||||
throw new RuntimeException("DB migration to v7 failed", e);
|
|
||||||
} finally {
|
|
||||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("DB migration to v7 failed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void migrateToV8() {
|
|
||||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
|
||||||
Statement st = c.createStatement()) {
|
|
||||||
c.setAutoCommit(false);
|
|
||||||
try {
|
|
||||||
ensureTestFreeAvatarUploadsTable(st);
|
|
||||||
setSchemaVersion(c, 8);
|
|
||||||
c.commit();
|
|
||||||
} catch (Exception e) {
|
|
||||||
try { c.rollback(); } catch (Exception ignored) {}
|
|
||||||
throw new RuntimeException("DB migration to v8 failed", e);
|
|
||||||
} finally {
|
|
||||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("DB migration to v8 failed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void migrateToV9() {
|
|
||||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
|
||||||
Statement st = c.createStatement()) {
|
|
||||||
c.setAutoCommit(false);
|
|
||||||
try {
|
|
||||||
ensureSyncServersTable(st);
|
|
||||||
setSchemaVersion(c, 9);
|
|
||||||
c.commit();
|
|
||||||
} catch (Exception e) {
|
|
||||||
try { c.rollback(); } catch (Exception ignored) {}
|
|
||||||
throw new RuntimeException("DB migration to v9 failed", e);
|
|
||||||
} finally {
|
|
||||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("DB migration to v9 failed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void migrateToV10() {
|
|
||||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
|
||||||
Statement st = c.createStatement()) {
|
|
||||||
c.setAutoCommit(false);
|
|
||||||
try {
|
|
||||||
ensureSignedMessagesReencryptedColumn(c, st);
|
|
||||||
setSchemaVersion(c, 10);
|
|
||||||
c.commit();
|
|
||||||
} catch (Exception e) {
|
|
||||||
try { c.rollback(); } catch (Exception ignored) {}
|
|
||||||
throw new RuntimeException("DB migration to v10 failed", e);
|
|
||||||
} finally {
|
|
||||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("DB migration to v10 failed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void migrateToV11() {
|
|
||||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
|
||||||
Statement st = c.createStatement()) {
|
|
||||||
c.setAutoCommit(false);
|
|
||||||
try {
|
|
||||||
clearLegacySignedMessagesForDmV11(c, st);
|
|
||||||
setSchemaVersion(c, 11);
|
|
||||||
c.commit();
|
|
||||||
} catch (Exception e) {
|
|
||||||
try { c.rollback(); } catch (Exception ignored) {}
|
|
||||||
throw new RuntimeException("DB migration to v11 failed", e);
|
|
||||||
} finally {
|
|
||||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("DB migration to v11 failed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void migrateToV12() {
|
|
||||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
|
||||||
Statement st = c.createStatement()) {
|
|
||||||
c.setAutoCommit(false);
|
|
||||||
try {
|
|
||||||
ensureSignedMessagesReadAtColumn(c, st);
|
|
||||||
backfillSignedMessagesReadAt(st);
|
|
||||||
setSchemaVersion(c, 12);
|
|
||||||
c.commit();
|
|
||||||
} catch (Exception e) {
|
|
||||||
try { c.rollback(); } catch (Exception ignored) {}
|
|
||||||
throw new RuntimeException("DB migration to v12 failed", e);
|
|
||||||
} finally {
|
|
||||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("DB migration to v12 failed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ensureChat200StateTables(Statement st) throws SQLException {
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS chat200_state (
|
|
||||||
owner_login TEXT NOT NULL,
|
|
||||||
owner_bch_name TEXT NOT NULL,
|
|
||||||
channel_root_block_number INTEGER NOT NULL,
|
|
||||||
channel_root_block_hash BLOB NOT NULL,
|
|
||||||
channel_name TEXT NOT NULL,
|
|
||||||
channel_type_version INTEGER NOT NULL,
|
|
||||||
chat_title TEXT NOT NULL DEFAULT '',
|
|
||||||
updated_at_ms INTEGER NOT NULL,
|
|
||||||
PRIMARY KEY (owner_bch_name, channel_root_block_number)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS chat200_members_state (
|
|
||||||
owner_bch_name TEXT NOT NULL,
|
|
||||||
channel_root_block_number INTEGER NOT NULL,
|
|
||||||
member_login TEXT NOT NULL,
|
|
||||||
member_channel_name TEXT NOT NULL,
|
|
||||||
is_active INTEGER NOT NULL,
|
|
||||||
updated_at_ms INTEGER NOT NULL,
|
|
||||||
updated_by_block_number INTEGER NOT NULL,
|
|
||||||
PRIMARY KEY (
|
|
||||||
owner_bch_name,
|
|
||||||
channel_root_block_number,
|
|
||||||
member_login,
|
|
||||||
member_channel_name
|
|
||||||
)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_chat200_state_owner
|
|
||||||
ON chat200_state (owner_login, owner_bch_name);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_chat200_members_owner
|
|
||||||
ON chat200_members_state (owner_bch_name, channel_root_block_number, is_active);
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getCurrentSchemaVersion() {
|
|
||||||
try (Connection c = DriverManager.getConnection(jdbcUrl)) {
|
|
||||||
if (!tableExists(c, DatabaseInitializer.DB_SCHEMA_VERSION_TABLE)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
try (var ps = c.prepareStatement("""
|
|
||||||
SELECT schema_version
|
|
||||||
FROM db_schema_version
|
|
||||||
WHERE id = 1
|
|
||||||
LIMIT 1
|
|
||||||
""");
|
|
||||||
ResultSet rs = ps.executeQuery()) {
|
|
||||||
if (rs.next()) {
|
|
||||||
return rs.getInt(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
} catch (SQLException e) {
|
|
||||||
throw new RuntimeException("Cannot read DB schema version", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ensureActiveSessionsSessionTypeColumn(Connection c, Statement st) throws SQLException {
|
|
||||||
if (columnExists(c, "active_sessions", "session_type")) return;
|
|
||||||
st.executeUpdate("ALTER TABLE active_sessions ADD COLUMN session_type INTEGER NOT NULL DEFAULT 1");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ensureActiveSessionsClientPlatformColumn(Connection c, Statement st) throws SQLException {
|
|
||||||
if (columnExists(c, "active_sessions", "client_platform")) return;
|
|
||||||
st.executeUpdate("ALTER TABLE active_sessions ADD COLUMN client_platform TEXT NOT NULL DEFAULT ''");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ensureEspPairingTables(Statement st) throws SQLException {
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS esp_pairing_settings (
|
|
||||||
login TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,
|
|
||||||
enabled INTEGER NOT NULL DEFAULT 0,
|
|
||||||
password_hash TEXT NOT NULL DEFAULT '',
|
|
||||||
ttl_seconds INTEGER NOT NULL DEFAULT 300,
|
|
||||||
failed_attempts INTEGER NOT NULL DEFAULT 0,
|
|
||||||
first_failed_at_ms INTEGER NOT NULL DEFAULT 0,
|
|
||||||
blocked_until_ms INTEGER NOT NULL DEFAULT 0,
|
|
||||||
updated_at_ms INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS esp_pairing_requests (
|
|
||||||
pairing_id TEXT NOT NULL PRIMARY KEY,
|
|
||||||
login TEXT NOT NULL,
|
|
||||||
requester_session_key TEXT NOT NULL,
|
|
||||||
requester_session_type INTEGER NOT NULL DEFAULT 1,
|
|
||||||
requester_client_platform TEXT NOT NULL DEFAULT '',
|
|
||||||
payload_type INTEGER NOT NULL,
|
|
||||||
status TEXT NOT NULL,
|
|
||||||
short_code TEXT NOT NULL,
|
|
||||||
fingerprint_b58 TEXT NOT NULL,
|
|
||||||
encrypted_payload TEXT,
|
|
||||||
reject_reason TEXT,
|
|
||||||
approved_by_session_id TEXT,
|
|
||||||
created_at_ms INTEGER NOT NULL,
|
|
||||||
expires_at_ms INTEGER NOT NULL,
|
|
||||||
updated_at_ms INTEGER NOT NULL,
|
|
||||||
delivered_to_homeserver INTEGER NOT NULL DEFAULT 0,
|
|
||||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_esp_pairing_requests_login_status
|
|
||||||
ON esp_pairing_requests (login, status, expires_at_ms);
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ensureSignedMessagesRevisionColumn(Connection c, Statement st) throws SQLException {
|
|
||||||
if (!tableExists(c, "signed_messages_v2")) return;
|
|
||||||
if (!columnExists(c, "signed_messages_v2", "revision_time_ms")) {
|
|
||||||
st.executeUpdate("ALTER TABLE signed_messages_v2 ADD COLUMN revision_time_ms INTEGER NOT NULL DEFAULT 0");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ensureSignedMessagesReencryptedColumn(Connection c, Statement st) throws SQLException {
|
|
||||||
if (!tableExists(c, "signed_messages_v2")) return;
|
|
||||||
if (!columnExists(c, "signed_messages_v2", "reencrypted_at_ms")) {
|
|
||||||
st.executeUpdate("ALTER TABLE signed_messages_v2 ADD COLUMN reencrypted_at_ms INTEGER NOT NULL DEFAULT 0");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ensureSignedMessagesReadAtColumn(Connection c, Statement st) throws SQLException {
|
|
||||||
if (!tableExists(c, "signed_messages_v2")) return;
|
|
||||||
if (!columnExists(c, "signed_messages_v2", "read_at_ms")) {
|
|
||||||
st.executeUpdate("ALTER TABLE signed_messages_v2 ADD COLUMN read_at_ms INTEGER");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void backfillSignedMessagesReadAt(Statement st) throws SQLException {
|
|
||||||
st.executeUpdate("""
|
|
||||||
UPDATE signed_messages_v2 AS content
|
|
||||||
SET read_at_ms = (
|
|
||||||
SELECT MIN(receipt.time_ms)
|
|
||||||
FROM signed_messages_v2 AS receipt
|
|
||||||
WHERE receipt.message_type IN (3, 4)
|
|
||||||
AND receipt.receipt_ref_base_key = content.base_key
|
|
||||||
)
|
|
||||||
WHERE content.message_type IN (1, 2)
|
|
||||||
AND (content.read_at_ms IS NULL OR content.read_at_ms <= 0)
|
|
||||||
AND EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM signed_messages_v2 AS receipt
|
|
||||||
WHERE receipt.message_type IN (3, 4)
|
|
||||||
AND receipt.receipt_ref_base_key = content.base_key
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Временная одноразовая миграция на переходе к SHiNE_DM v1:
|
|
||||||
* старые строки signed_messages_v2 больше не гарантированно совместимы
|
|
||||||
* с новым бинарным форматом и правилами tombstone/reencrypt.
|
|
||||||
* Позже эту миграцию нужно убрать, чтобы случайно не очищать рабочие DM.
|
|
||||||
*/
|
|
||||||
private static void clearLegacySignedMessagesForDmV11(Connection c, Statement st) throws SQLException {
|
|
||||||
if (tableExists(c, "signed_message_session_delivery")) {
|
|
||||||
st.executeUpdate("DELETE FROM signed_message_session_delivery");
|
|
||||||
}
|
|
||||||
if (tableExists(c, "signed_messages_v2")) {
|
|
||||||
st.executeUpdate("DELETE FROM signed_messages_v2");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void dropDmFileTables(Statement st) throws SQLException {
|
|
||||||
st.executeUpdate("DROP INDEX IF EXISTS idx_dm_message_file_links_login");
|
|
||||||
st.executeUpdate("DROP INDEX IF EXISTS idx_dm_message_file_links_message");
|
|
||||||
st.executeUpdate("DROP TABLE IF EXISTS dm_message_file_links");
|
|
||||||
st.executeUpdate("DROP TABLE IF EXISTS dm_files");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean columnExists(Connection c, String tableName, String columnName) throws SQLException {
|
|
||||||
try (Statement probe = c.createStatement();
|
|
||||||
ResultSet rs = probe.executeQuery("PRAGMA table_info(" + tableName + ")")) {
|
|
||||||
while (rs.next()) {
|
|
||||||
if (columnName.equalsIgnoreCase(rs.getString("name"))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void setSchemaVersion(Connection c, int version) throws SQLException {
|
|
||||||
try (var ps = c.prepareStatement("""
|
|
||||||
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
|
||||||
VALUES (1, ?, CAST(strftime('%s','now') AS INTEGER) * 1000)
|
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
|
||||||
schema_version = excluded.schema_version,
|
|
||||||
updated_at_ms = excluded.updated_at_ms
|
|
||||||
""")) {
|
|
||||||
ps.setInt(1, version);
|
|
||||||
ps.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ensureReactionsStateTable(Statement st) throws SQLException {
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS reactions_state (
|
|
||||||
from_login TEXT NOT NULL,
|
|
||||||
from_bch_name TEXT NOT NULL,
|
|
||||||
reaction_type INTEGER NOT NULL,
|
|
||||||
to_login TEXT NOT NULL,
|
|
||||||
to_bch_name TEXT NOT NULL,
|
|
||||||
to_block_number INTEGER NOT NULL,
|
|
||||||
to_block_hash BLOB NOT NULL,
|
|
||||||
last_sub_type INTEGER NOT NULL,
|
|
||||||
UNIQUE (
|
|
||||||
from_login,
|
|
||||||
from_bch_name,
|
|
||||||
reaction_type,
|
|
||||||
to_login,
|
|
||||||
to_bch_name,
|
|
||||||
to_block_number,
|
|
||||||
to_block_hash
|
|
||||||
)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ensureTestFreeAvatarUploadsTable(Statement st) throws SQLException {
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS test_free_avatar_uploads (
|
|
||||||
login TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,
|
|
||||||
used_count INTEGER NOT NULL DEFAULT 0,
|
|
||||||
updated_at_ms INTEGER NOT NULL,
|
|
||||||
last_tx_id TEXT NOT NULL DEFAULT '',
|
|
||||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_test_free_avatar_uploads_updated
|
|
||||||
ON test_free_avatar_uploads (updated_at_ms);
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ensureSyncServersTable(Statement st) throws SQLException {
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS sync_servers (
|
|
||||||
login TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,
|
|
||||||
server_address TEXT NOT NULL DEFAULT '',
|
|
||||||
updated_at_ms INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sync_servers_updated
|
|
||||||
ON sync_servers (updated_at_ms);
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static void createConnectionsStateTable(Statement st) throws SQLException {
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS connections_state (
|
|
||||||
login TEXT NOT NULL,
|
|
||||||
rel_type INTEGER NOT NULL,
|
|
||||||
to_login TEXT NOT NULL,
|
|
||||||
to_bch_name TEXT NOT NULL,
|
|
||||||
to_block_number INTEGER NOT NULL,
|
|
||||||
to_block_hash BLOB NOT NULL,
|
|
||||||
FOREIGN KEY (login) REFERENCES solana_users(login),
|
|
||||||
UNIQUE (login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ensureConnectionsIndexes(Statement st) throws SQLException {
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_connections_state_login
|
|
||||||
ON connections_state (login);
|
|
||||||
""");
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_connections_state_to_login
|
|
||||||
ON connections_state (to_login);
|
|
||||||
""");
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_connections_state_pair
|
|
||||||
ON connections_state (login, to_login);
|
|
||||||
""");
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_connections_state_target
|
|
||||||
ON connections_state (login, rel_type, to_bch_name, to_block_number);
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ensureReactionsIndexes(Statement st) throws SQLException {
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_reactions_state_target
|
|
||||||
ON reactions_state (to_bch_name, to_block_number, to_block_hash);
|
|
||||||
""");
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_reactions_state_actor
|
|
||||||
ON reactions_state (from_login, from_bch_name, reaction_type);
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ensureChannelNamesStateTable(Statement st) throws SQLException {
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE IF NOT EXISTS channel_names_state (
|
|
||||||
slug TEXT NOT NULL,
|
|
||||||
display_name TEXT NOT NULL,
|
|
||||||
channel_description TEXT NOT NULL DEFAULT '',
|
|
||||||
owner_login TEXT NOT NULL,
|
|
||||||
owner_bch_name TEXT NOT NULL,
|
|
||||||
channel_type_code INTEGER NOT NULL DEFAULT 1,
|
|
||||||
channel_type_version INTEGER NOT NULL DEFAULT 1,
|
|
||||||
channel_root_block_number INTEGER NOT NULL,
|
|
||||||
channel_root_block_hash BLOB NOT NULL,
|
|
||||||
created_at_ms INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ensureChannelNamesDescriptionColumn(Connection c, Statement st) throws SQLException {
|
|
||||||
boolean hasDescription = false;
|
|
||||||
try (Statement probe = c.createStatement();
|
|
||||||
ResultSet rs = probe.executeQuery("PRAGMA table_info(channel_names_state)")) {
|
|
||||||
while (rs.next()) {
|
|
||||||
String name = rs.getString("name");
|
|
||||||
if ("channel_description".equalsIgnoreCase(name)) {
|
|
||||||
hasDescription = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasDescription) {
|
|
||||||
st.executeUpdate("ALTER TABLE channel_names_state ADD COLUMN channel_description TEXT NOT NULL DEFAULT ''");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ensureChannelNamesTypeColumns(Connection c, Statement st) throws SQLException {
|
|
||||||
boolean hasTypeCode = false;
|
|
||||||
boolean hasTypeVersion = false;
|
|
||||||
|
|
||||||
try (Statement probe = c.createStatement();
|
|
||||||
ResultSet rs = probe.executeQuery("PRAGMA table_info(channel_names_state)")) {
|
|
||||||
while (rs.next()) {
|
|
||||||
String name = rs.getString("name");
|
|
||||||
if ("channel_type_code".equalsIgnoreCase(name)) hasTypeCode = true;
|
|
||||||
if ("channel_type_version".equalsIgnoreCase(name)) hasTypeVersion = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasTypeCode) {
|
|
||||||
st.executeUpdate("ALTER TABLE channel_names_state ADD COLUMN channel_type_code INTEGER NOT NULL DEFAULT 1");
|
|
||||||
}
|
|
||||||
if (!hasTypeVersion) {
|
|
||||||
st.executeUpdate("ALTER TABLE channel_names_state ADD COLUMN channel_type_version INTEGER NOT NULL DEFAULT 1");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ensureChannelNamesIndexes(Statement st) throws SQLException {
|
|
||||||
st.executeUpdate("DROP INDEX IF EXISTS uq_channel_names_state_slug");
|
|
||||||
st.executeUpdate("DROP INDEX IF EXISTS uq_channel_names_state_owner_slug");
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_names_state_owner_type_slug
|
|
||||||
ON channel_names_state (owner_bch_name, channel_type_code, slug);
|
|
||||||
""");
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_names_state_target
|
|
||||||
ON channel_names_state (owner_bch_name, channel_root_block_number, channel_root_block_hash);
|
|
||||||
""");
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_channel_names_state_owner
|
|
||||||
ON channel_names_state (owner_login, owner_bch_name);
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ensureSignedMessageReceiptUniq(Connection c, Statement st) throws SQLException {
|
|
||||||
if (!tableExists(c, "signed_messages_v2")) return;
|
|
||||||
|
|
||||||
if (tableExists(c, "signed_message_session_delivery")) {
|
|
||||||
st.executeUpdate("""
|
|
||||||
DELETE FROM signed_message_session_delivery
|
|
||||||
WHERE message_key IN (
|
|
||||||
SELECT message_key
|
|
||||||
FROM signed_messages_v2
|
|
||||||
WHERE message_type IN (3, 4)
|
|
||||||
AND receipt_ref_base_key IS NOT NULL
|
|
||||||
AND rowid NOT IN (
|
|
||||||
SELECT MIN(rowid)
|
|
||||||
FROM signed_messages_v2
|
|
||||||
WHERE message_type IN (3, 4)
|
|
||||||
AND receipt_ref_base_key IS NOT NULL
|
|
||||||
GROUP BY target_login, message_type, receipt_ref_base_key
|
|
||||||
)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
DELETE FROM signed_messages_v2
|
|
||||||
WHERE message_type IN (3, 4)
|
|
||||||
AND receipt_ref_base_key IS NOT NULL
|
|
||||||
AND rowid NOT IN (
|
|
||||||
SELECT MIN(rowid)
|
|
||||||
FROM signed_messages_v2
|
|
||||||
WHERE message_type IN (3, 4)
|
|
||||||
AND receipt_ref_base_key IS NOT NULL
|
|
||||||
GROUP BY target_login, message_type, receipt_ref_base_key
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_signed_messages_v2_receipt_incoming
|
|
||||||
ON signed_messages_v2 (target_login, receipt_ref_base_key)
|
|
||||||
WHERE message_type = 3 AND receipt_ref_base_key IS NOT NULL;
|
|
||||||
""");
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_signed_messages_v2_receipt_outgoing
|
|
||||||
ON signed_messages_v2 (target_login, receipt_ref_base_key)
|
|
||||||
WHERE message_type = 4 AND receipt_ref_base_key IS NOT NULL;
|
|
||||||
""");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void rebuildConnectionsStateTable(Statement st) throws SQLException {
|
|
||||||
st.executeUpdate("DROP TABLE IF EXISTS connections_state_v2");
|
|
||||||
st.executeUpdate("""
|
|
||||||
CREATE TABLE connections_state_v2 (
|
|
||||||
login TEXT NOT NULL,
|
|
||||||
rel_type INTEGER NOT NULL,
|
|
||||||
to_login TEXT NOT NULL,
|
|
||||||
to_bch_name TEXT NOT NULL,
|
|
||||||
to_block_number INTEGER NOT NULL,
|
|
||||||
to_block_hash BLOB NOT NULL,
|
|
||||||
FOREIGN KEY (login) REFERENCES solana_users(login),
|
|
||||||
UNIQUE (login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash)
|
|
||||||
);
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("""
|
|
||||||
INSERT OR IGNORE INTO connections_state_v2
|
|
||||||
(login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash)
|
|
||||||
SELECT
|
|
||||||
login,
|
|
||||||
rel_type,
|
|
||||||
to_login,
|
|
||||||
to_bch_name,
|
|
||||||
COALESCE(to_block_number, 0),
|
|
||||||
COALESCE(to_block_hash, zeroblob(32))
|
|
||||||
FROM connections_state
|
|
||||||
WHERE login IS NOT NULL
|
|
||||||
AND to_login IS NOT NULL
|
|
||||||
AND to_bch_name IS NOT NULL;
|
|
||||||
""");
|
|
||||||
|
|
||||||
st.executeUpdate("DROP TABLE connections_state");
|
|
||||||
st.executeUpdate("ALTER TABLE connections_state_v2 RENAME TO connections_state");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean tableExists(Connection c, String tableName) throws SQLException {
|
|
||||||
String sql = "SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1";
|
|
||||||
try (var ps = c.prepareStatement(sql)) {
|
|
||||||
ps.setString(1, tableName);
|
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
|
||||||
return rs.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean needsConnectionsStateUpgrade(Connection c) throws SQLException {
|
|
||||||
boolean toBlockNumberNotNull = false;
|
|
||||||
boolean toBlockHashNotNull = false;
|
|
||||||
|
|
||||||
try (Statement st = c.createStatement();
|
|
||||||
ResultSet rs = st.executeQuery("PRAGMA table_info(connections_state)")) {
|
|
||||||
while (rs.next()) {
|
|
||||||
String name = rs.getString("name");
|
|
||||||
int notNull = rs.getInt("notnull");
|
|
||||||
if ("to_block_number".equalsIgnoreCase(name)) {
|
|
||||||
toBlockNumberNotNull = notNull == 1;
|
|
||||||
}
|
|
||||||
if ("to_block_hash".equalsIgnoreCase(name)) {
|
|
||||||
toBlockHashNotNull = notNull == 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return !toBlockNumberNotNull || !toBlockHashNotNull;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,8 @@ public final class ChannelNameRules {
|
|||||||
private static final int MAX_DISPLAY_NAME_LENGTH = 32;
|
private static final int MAX_DISPLAY_NAME_LENGTH = 32;
|
||||||
private static final Pattern DISPLAY_ALLOWED_PATTERN =
|
private static final Pattern DISPLAY_ALLOWED_PATTERN =
|
||||||
Pattern.compile("^[A-Za-z0-9_-]+$");
|
Pattern.compile("^[A-Za-z0-9_-]+$");
|
||||||
|
private static final Pattern PUBLIC_CHANNEL_ALLOWED_PATTERN =
|
||||||
|
Pattern.compile("^[A-Za-z0-9_-]+$");
|
||||||
|
|
||||||
private ChannelNameRules() {}
|
private ChannelNameRules() {}
|
||||||
|
|
||||||
@@ -34,6 +36,14 @@ public final class ChannelNameRules {
|
|||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String requireValidPublicDisplayNameForCreate(String rawName) {
|
||||||
|
String normalized = requireValidDisplayNameForCreate(rawName);
|
||||||
|
if (!PUBLIC_CHANNEL_ALLOWED_PATTERN.matcher(normalized).matches()) {
|
||||||
|
throw new IllegalArgumentException("channelName contains unsupported characters");
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
public static String toCanonicalSlug(String rawName) {
|
public static String toCanonicalSlug(String rawName) {
|
||||||
String normalized = normalizeDisplayName(rawName);
|
String normalized = normalizeDisplayName(rawName);
|
||||||
if (normalized.isEmpty()) {
|
if (normalized.isEmpty()) {
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package shine.db.connection;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface ConnectionFactory {
|
||||||
|
Connection getConnection() throws SQLException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package shine.db.connection;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Нейтральная точка доступа к JDBC-соединениям без привязки к конкретной БД.
|
||||||
|
*
|
||||||
|
* На первом этапе переносов этот интерфейс нужен как новая опора для DAO,
|
||||||
|
* чтобы постепенно убрать прямую зависимость от DbController.
|
||||||
|
*/
|
||||||
|
public interface DbProvider extends ConnectionFactory, AutoCloseable {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Connection getConnection() throws SQLException;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default void close() throws Exception {
|
||||||
|
// По умолчанию ресурсов на уровне provider нет.
|
||||||
|
}
|
||||||
|
}
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
package shine.db.connection;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Базовый JDBC provider поверх DriverManager.
|
||||||
|
*
|
||||||
|
* Не знает ничего про конкретный доменный режим БД:
|
||||||
|
* конкретные параметры и post-connect инициализация задаются снаружи.
|
||||||
|
*/
|
||||||
|
public final class DriverManagerDbProvider implements DbProvider {
|
||||||
|
|
||||||
|
private final String jdbcUrl;
|
||||||
|
private final String user;
|
||||||
|
private final String password;
|
||||||
|
private final ConnectionInitializer initializer;
|
||||||
|
|
||||||
|
public DriverManagerDbProvider(String jdbcUrl,
|
||||||
|
String user,
|
||||||
|
String password,
|
||||||
|
ConnectionInitializer initializer) {
|
||||||
|
if (jdbcUrl == null || jdbcUrl.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("jdbcUrl is blank");
|
||||||
|
}
|
||||||
|
this.jdbcUrl = jdbcUrl;
|
||||||
|
this.user = blankToNull(user);
|
||||||
|
this.password = blankToNull(password);
|
||||||
|
this.initializer = initializer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Connection getConnection() throws SQLException {
|
||||||
|
Connection connection;
|
||||||
|
if (user == null) {
|
||||||
|
connection = DriverManager.getConnection(jdbcUrl);
|
||||||
|
} else {
|
||||||
|
connection = DriverManager.getConnection(jdbcUrl, user, password == null ? "" : password);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initializer != null) {
|
||||||
|
initializer.initialize(connection);
|
||||||
|
}
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String blankToNull(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
String trimmed = value.trim();
|
||||||
|
return trimmed.isEmpty() ? null : trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface ConnectionInitializer {
|
||||||
|
void initialize(Connection connection) throws SQLException;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.entities.ActiveSessionEntry;
|
import shine.db.entities.ActiveSessionEntry;
|
||||||
|
|
||||||
import java.sql.*;
|
import java.sql.*;
|
||||||
@@ -17,7 +17,7 @@ import java.util.List;
|
|||||||
public final class ActiveSessionsDAO {
|
public final class ActiveSessionsDAO {
|
||||||
|
|
||||||
private static volatile ActiveSessionsDAO instance;
|
private static volatile ActiveSessionsDAO instance;
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
private ActiveSessionsDAO() { }
|
private ActiveSessionsDAO() { }
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ public final class ActiveSessionsDAO {
|
|||||||
client_platform,
|
client_platform,
|
||||||
user_language
|
user_language
|
||||||
FROM active_sessions
|
FROM active_sessions
|
||||||
WHERE login = ? COLLATE NOCASE
|
WHERE LOWER(login) = LOWER(?)
|
||||||
""";
|
""";
|
||||||
|
|
||||||
List<ActiveSessionEntry> result = new ArrayList<>();
|
List<ActiveSessionEntry> result = new ArrayList<>();
|
||||||
|
|||||||
+6
-6
@@ -1,7 +1,7 @@
|
|||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.DatabaseInitializer;
|
import shine.db.DatabaseInitializer;
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
@@ -24,12 +24,12 @@ import java.sql.SQLException;
|
|||||||
* - эта схема проще и прозрачнее, чем много обратных триггеров по разным таблицам;
|
* - эта схема проще и прозрачнее, чем много обратных триггеров по разным таблицам;
|
||||||
* - если любой шаг не удался, делаем rollback и БД остаётся в исходном состоянии;
|
* - если любой шаг не удался, делаем rollback и БД остаётся в исходном состоянии;
|
||||||
* - файловые действия (.bch / .tmp_bch) сознательно НЕ входят в эту транзакцию:
|
* - файловые действия (.bch / .tmp_bch) сознательно НЕ входят в эту транзакцию:
|
||||||
* SQLite не может атомарно закоммитить и SQL, и файловую систему сразу;
|
* SQL-транзакция и файловая система не коммитятся атомарно вместе;
|
||||||
* поэтому БД-чистка делается здесь, а файловая чистка будет следующим шагом
|
* поэтому БД-чистка делается здесь, а файловая чистка будет следующим шагом
|
||||||
* отдельным recovery/resync-слоем после успешного commit.
|
* отдельным recovery/resync-слоем после успешного commit.
|
||||||
*
|
*
|
||||||
* Важный смысл текущей реализации:
|
* Важный смысл текущей реализации:
|
||||||
* - мы НЕ трогаем identity-слой (`solana_users`) и НЕ трогаем DM-таблицы;
|
* - мы НЕ трогаем current users слой (`solana_user_pda_current`) и НЕ трогаем DM-таблицы;
|
||||||
* - мы очищаем только блокчейн пользователя и derived-state, который строится из неё;
|
* - мы очищаем только блокчейн пользователя и derived-state, который строится из неё;
|
||||||
* - висячие cross-chain ссылки в чужих blocks допускаются как нормальное поведение системы.
|
* - висячие cross-chain ссылки в чужих blocks допускаются как нормальное поведение системы.
|
||||||
*/
|
*/
|
||||||
@@ -39,7 +39,7 @@ public final class BlockchainResyncCleanupDAO {
|
|||||||
|
|
||||||
private static volatile BlockchainResyncCleanupDAO instance;
|
private static volatile BlockchainResyncCleanupDAO instance;
|
||||||
|
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
private BlockchainResyncCleanupDAO() {}
|
private BlockchainResyncCleanupDAO() {}
|
||||||
|
|
||||||
@@ -307,7 +307,7 @@ public final class BlockchainResyncCleanupDAO {
|
|||||||
private int deleteConnectionsStateForLogin(Connection c, String login) throws SQLException {
|
private int deleteConnectionsStateForLogin(Connection c, String login) throws SQLException {
|
||||||
return executeDelete(c, """
|
return executeDelete(c, """
|
||||||
DELETE FROM connections_state
|
DELETE FROM connections_state
|
||||||
WHERE login = ? COLLATE NOCASE
|
WHERE LOWER(login) = LOWER(?)
|
||||||
""", login);
|
""", login);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +317,7 @@ public final class BlockchainResyncCleanupDAO {
|
|||||||
private int deleteUsersParamsForLogin(Connection c, String login) throws SQLException {
|
private int deleteUsersParamsForLogin(Connection c, String login) throws SQLException {
|
||||||
return executeDelete(c, """
|
return executeDelete(c, """
|
||||||
DELETE FROM users_params
|
DELETE FROM users_params
|
||||||
WHERE login = ? COLLATE NOCASE
|
WHERE LOWER(login) = LOWER(?)
|
||||||
""", login);
|
""", login);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.entities.BlockchainStateEntry;
|
import shine.db.entities.BlockchainStateEntry;
|
||||||
|
|
||||||
import java.sql.*;
|
import java.sql.*;
|
||||||
@@ -10,7 +10,7 @@ import java.util.List;
|
|||||||
public final class BlockchainStateDAO {
|
public final class BlockchainStateDAO {
|
||||||
|
|
||||||
private static volatile BlockchainStateDAO instance;
|
private static volatile BlockchainStateDAO instance;
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
private BlockchainStateDAO() {}
|
private BlockchainStateDAO() {}
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ public final class BlockchainStateDAO {
|
|||||||
last_block_hash,
|
last_block_hash,
|
||||||
updated_at_ms
|
updated_at_ms
|
||||||
FROM blockchain_state
|
FROM blockchain_state
|
||||||
ORDER BY blockchain_name COLLATE NOCASE
|
ORDER BY LOWER(blockchain_name)
|
||||||
""";
|
""";
|
||||||
|
|
||||||
List<BlockchainStateEntry> result = new ArrayList<>();
|
List<BlockchainStateEntry> result = new ArrayList<>();
|
||||||
@@ -142,7 +142,7 @@ public final class BlockchainStateDAO {
|
|||||||
* Строгая вставка state только если записи ещё нет.
|
* Строгая вставка state только если записи ещё нет.
|
||||||
*
|
*
|
||||||
* Нужна для recovery / resync:
|
* Нужна для recovery / resync:
|
||||||
* - identity пользователя уже может существовать в solana_users;
|
* - runtime-проекция пользователя уже может существовать в current users слое;
|
||||||
* - в таком случае нам надо восстановить только blockchain_state;
|
* - в таком случае нам надо восстановить только blockchain_state;
|
||||||
* - если запись уже есть, метод просто ничего не меняет.
|
* - если запись уже есть, метод просто ничего не меняет.
|
||||||
*/
|
*/
|
||||||
@@ -229,7 +229,7 @@ public final class BlockchainStateDAO {
|
|||||||
|
|
||||||
private static void setBytesNullable(PreparedStatement ps, int index, byte[] b) throws SQLException {
|
private static void setBytesNullable(PreparedStatement ps, int index, byte[] b) throws SQLException {
|
||||||
if (b != null) ps.setBytes(index, b);
|
if (b != null) ps.setBytes(index, b);
|
||||||
else ps.setNull(index, Types.BLOB);
|
else ps.setNull(index, Types.BINARY);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String nn(String s) { return s == null ? "" : s; }
|
private static String nn(String s) { return s == null ? "" : s; }
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package shine.db.dao;
|
|||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.entities.BlockEntry;
|
import shine.db.entities.BlockEntry;
|
||||||
|
|
||||||
import java.sql.*;
|
import java.sql.*;
|
||||||
@@ -22,7 +22,7 @@ import java.util.List;
|
|||||||
public final class BlocksDAO {
|
public final class BlocksDAO {
|
||||||
|
|
||||||
private static volatile BlocksDAO instance;
|
private static volatile BlocksDAO instance;
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
private static final Logger log = LoggerFactory.getLogger(BlocksDAO.class);
|
private static final Logger log = LoggerFactory.getLogger(BlocksDAO.class);
|
||||||
|
|
||||||
private BlocksDAO() { }
|
private BlocksDAO() { }
|
||||||
@@ -90,7 +90,7 @@ public final class BlocksDAO {
|
|||||||
else ps.setNull(i++, Types.INTEGER);
|
else ps.setNull(i++, Types.INTEGER);
|
||||||
|
|
||||||
if (e.getToBlockHash() != null) ps.setBytes(i++, e.getToBlockHash());
|
if (e.getToBlockHash() != null) ps.setBytes(i++, e.getToBlockHash());
|
||||||
else ps.setNull(i++, Types.BLOB);
|
else ps.setNull(i++, Types.BINARY);
|
||||||
|
|
||||||
ps.setBytes(i++, e.getBlockHash());
|
ps.setBytes(i++, e.getBlockHash());
|
||||||
ps.setBytes(i++, e.getBlockSignature());
|
ps.setBytes(i++, e.getBlockSignature());
|
||||||
@@ -106,7 +106,7 @@ public final class BlocksDAO {
|
|||||||
else ps.setNull(i++, Types.INTEGER);
|
else ps.setNull(i++, Types.INTEGER);
|
||||||
|
|
||||||
if (e.getPrevLineHash() != null) ps.setBytes(i++, e.getPrevLineHash());
|
if (e.getPrevLineHash() != null) ps.setBytes(i++, e.getPrevLineHash());
|
||||||
else ps.setNull(i++, Types.BLOB);
|
else ps.setNull(i++, Types.BINARY);
|
||||||
|
|
||||||
if (e.getThisLineNumber() != null) ps.setInt(i++, e.getThisLineNumber());
|
if (e.getThisLineNumber() != null) ps.setInt(i++, e.getThisLineNumber());
|
||||||
else ps.setNull(i++, Types.INTEGER);
|
else ps.setNull(i++, Types.INTEGER);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.entities.ChannelNameStateEntry;
|
import shine.db.entities.ChannelNameStateEntry;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
@@ -11,7 +11,7 @@ import java.util.List;
|
|||||||
|
|
||||||
public final class ChannelNameStateDAO {
|
public final class ChannelNameStateDAO {
|
||||||
private static volatile ChannelNameStateDAO instance;
|
private static volatile ChannelNameStateDAO instance;
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
private ChannelNameStateDAO() {}
|
private ChannelNameStateDAO() {}
|
||||||
|
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
|
import shine.db.sql.CurrentUsersSql;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
@@ -13,16 +14,14 @@ import java.util.List;
|
|||||||
* ConnectionsStateDAO — чтение текущего состояния связей из connections_state.
|
* ConnectionsStateDAO — чтение текущего состояния связей из connections_state.
|
||||||
*
|
*
|
||||||
* ВАЖНО:
|
* ВАЖНО:
|
||||||
* - login в запросах может быть в любом регистре, поэтому в WHERE используем COLLATE NOCASE
|
* - login в запросах может быть в любом регистре;
|
||||||
* - в ответах возвращаем логины в каноническом регистре через JOIN на solana_users
|
* - в ответах возвращаем логины в каноническом регистре через прямой lookup
|
||||||
*
|
* по актуальным PDA-данным и manual fallback.
|
||||||
* ПРИМЕЧАНИЕ:
|
|
||||||
* Таблица пользователей тут названа "solana_users". Если у тебя иначе — поменяй в SQL.
|
|
||||||
*/
|
*/
|
||||||
public final class ConnectionsStateDAO {
|
public final class ConnectionsStateDAO {
|
||||||
|
|
||||||
private static volatile ConnectionsStateDAO instance;
|
private static volatile ConnectionsStateDAO instance;
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
private ConnectionsStateDAO() {}
|
private ConnectionsStateDAO() {}
|
||||||
|
|
||||||
@@ -42,14 +41,17 @@ public final class ConnectionsStateDAO {
|
|||||||
String sql = """
|
String sql = """
|
||||||
SELECT COALESCE(u_login.login, u_bch.login, cs.to_login) AS friend_login
|
SELECT COALESCE(u_login.login, u_bch.login, cs.to_login) AS friend_login
|
||||||
FROM connections_state cs
|
FROM connections_state cs
|
||||||
LEFT JOIN solana_users u_login
|
LEFT JOIN %s
|
||||||
ON u_login.login = cs.to_login COLLATE NOCASE
|
ON LOWER(u_login.login) = LOWER(cs.to_login)
|
||||||
LEFT JOIN solana_users u_bch
|
LEFT JOIN %s
|
||||||
ON u_bch.blockchain_name = cs.to_bch_name COLLATE NOCASE
|
ON LOWER(u_bch.blockchain_name) = LOWER(cs.to_bch_name)
|
||||||
WHERE cs.login = ? COLLATE NOCASE
|
WHERE LOWER(cs.login) = LOWER(?)
|
||||||
AND cs.rel_type = ?
|
AND cs.rel_type = ?
|
||||||
ORDER BY friend_login
|
ORDER BY friend_login
|
||||||
""";
|
""".formatted(
|
||||||
|
CurrentUsersSql.usersSubquery("u_login"),
|
||||||
|
CurrentUsersSql.usersSubquery("u_bch")
|
||||||
|
);
|
||||||
|
|
||||||
List<String> out = new ArrayList<>();
|
List<String> out = new ArrayList<>();
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
@@ -72,17 +74,20 @@ public final class ConnectionsStateDAO {
|
|||||||
String sql = """
|
String sql = """
|
||||||
SELECT COALESCE(u_actor.login, cs.login) AS friend_login
|
SELECT COALESCE(u_actor.login, cs.login) AS friend_login
|
||||||
FROM connections_state cs
|
FROM connections_state cs
|
||||||
LEFT JOIN solana_users u_actor
|
LEFT JOIN %s
|
||||||
ON u_actor.login = cs.login COLLATE NOCASE
|
ON LOWER(u_actor.login) = LOWER(cs.login)
|
||||||
LEFT JOIN solana_users u_target
|
LEFT JOIN %s
|
||||||
ON u_target.login = ? COLLATE NOCASE
|
ON LOWER(u_target.login) = LOWER(?)
|
||||||
WHERE (
|
WHERE (
|
||||||
cs.to_login = ? COLLATE NOCASE
|
LOWER(cs.to_login) = LOWER(?)
|
||||||
OR (u_target.blockchain_name IS NOT NULL AND cs.to_bch_name = u_target.blockchain_name COLLATE NOCASE)
|
OR (u_target.blockchain_name IS NOT NULL AND LOWER(cs.to_bch_name) = LOWER(u_target.blockchain_name))
|
||||||
)
|
)
|
||||||
AND cs.rel_type = ?
|
AND cs.rel_type = ?
|
||||||
ORDER BY friend_login
|
ORDER BY friend_login
|
||||||
""";
|
""".formatted(
|
||||||
|
CurrentUsersSql.usersSubquery("u_actor"),
|
||||||
|
CurrentUsersSql.usersSubquery("u_target")
|
||||||
|
);
|
||||||
|
|
||||||
List<String> out = new ArrayList<>();
|
List<String> out = new ArrayList<>();
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
@@ -106,19 +111,19 @@ public final class ConnectionsStateDAO {
|
|||||||
String sql = """
|
String sql = """
|
||||||
SELECT u.login AS friend_login
|
SELECT u.login AS friend_login
|
||||||
FROM connections_state a
|
FROM connections_state a
|
||||||
JOIN solana_users u
|
JOIN %s
|
||||||
ON u.login = a.to_login COLLATE NOCASE
|
ON LOWER(u.login) = LOWER(a.to_login)
|
||||||
WHERE a.login = ? COLLATE NOCASE
|
WHERE LOWER(a.login) = LOWER(?)
|
||||||
AND a.rel_type = ?
|
AND a.rel_type = ?
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM connections_state b
|
FROM connections_state b
|
||||||
WHERE b.login = a.to_login COLLATE NOCASE
|
WHERE LOWER(b.login) = LOWER(a.to_login)
|
||||||
AND b.to_login = a.login COLLATE NOCASE
|
AND LOWER(b.to_login) = LOWER(a.login)
|
||||||
AND b.rel_type = a.rel_type
|
AND b.rel_type = a.rel_type
|
||||||
)
|
)
|
||||||
ORDER BY u.login
|
ORDER BY u.login
|
||||||
""";
|
""".formatted(CurrentUsersSql.usersSubquery("u"));
|
||||||
|
|
||||||
List<String> out = new ArrayList<>();
|
List<String> out = new ArrayList<>();
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
@@ -141,10 +146,22 @@ public final class ConnectionsStateDAO {
|
|||||||
String toBchName,
|
String toBchName,
|
||||||
Integer toBlockNumber,
|
Integer toBlockNumber,
|
||||||
byte[] toBlockHash) throws SQLException {
|
byte[] toBlockHash) throws SQLException {
|
||||||
|
try (PreparedStatement deletePs = c.prepareStatement("""
|
||||||
|
DELETE FROM connections_state
|
||||||
|
WHERE LOWER(login) = LOWER(?)
|
||||||
|
AND rel_type = ?
|
||||||
|
AND LOWER(to_login) = LOWER(?)
|
||||||
|
""")) {
|
||||||
|
deletePs.setString(1, login);
|
||||||
|
deletePs.setInt(2, relType);
|
||||||
|
deletePs.setString(3, toLogin);
|
||||||
|
deletePs.executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
String sql = """
|
String sql = """
|
||||||
INSERT INTO connections_state (login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash)
|
INSERT INTO connections_state (login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(login, rel_type, to_login) DO UPDATE SET
|
ON CONFLICT(login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash) DO UPDATE SET
|
||||||
to_bch_name=excluded.to_bch_name,
|
to_bch_name=excluded.to_bch_name,
|
||||||
to_block_number=excluded.to_block_number,
|
to_block_number=excluded.to_block_number,
|
||||||
to_block_hash=excluded.to_block_hash
|
to_block_hash=excluded.to_block_hash
|
||||||
|
|||||||
+57
-63
@@ -1,19 +1,22 @@
|
|||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.entities.SolanaUserEntry;
|
import shine.db.KeyEncodingUtil;
|
||||||
|
import shine.db.entities.CurrentUserEntry;
|
||||||
|
import shine.db.sql.CurrentUsersSql;
|
||||||
|
|
||||||
import java.sql.*;
|
import java.sql.*;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SolanaUsersDAO — локальная таблица пользователей из Solana.
|
* CurrentUsersDAO — совместимый runtime-доступ к текущему срезу пользователей.
|
||||||
*
|
*
|
||||||
* Таблица: solana_users
|
* Источник:
|
||||||
|
* - solana_user_pda_current
|
||||||
*
|
*
|
||||||
* Колонки:
|
* Колонки:
|
||||||
* - login TEXT PRIMARY KEY (COLLATE NOCASE)
|
* - login TEXT PRIMARY KEY
|
||||||
* - blockchain_name TEXT NOT NULL
|
* - blockchain_name TEXT NOT NULL
|
||||||
* - solana_key TEXT NOT NULL
|
* - solana_key TEXT NOT NULL
|
||||||
* - blockchain_key TEXT NOT NULL
|
* - blockchain_key TEXT NOT NULL
|
||||||
@@ -23,59 +26,32 @@ import java.util.List;
|
|||||||
* - методы с Connection НЕ закрывают соединение
|
* - методы с Connection НЕ закрывают соединение
|
||||||
* - методы без Connection сами открывают и закрывают соединение
|
* - методы без Connection сами открывают и закрывают соединение
|
||||||
*/
|
*/
|
||||||
public final class SolanaUsersDAO {
|
public final class CurrentUsersDAO {
|
||||||
|
|
||||||
private static volatile SolanaUsersDAO instance;
|
private static volatile CurrentUsersDAO instance;
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
private SolanaUsersDAO() {}
|
private CurrentUsersDAO() {}
|
||||||
|
|
||||||
public static SolanaUsersDAO getInstance() {
|
public static CurrentUsersDAO getInstance() {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
synchronized (SolanaUsersDAO.class) {
|
synchronized (CurrentUsersDAO.class) {
|
||||||
if (instance == null) instance = new SolanaUsersDAO();
|
if (instance == null) instance = new CurrentUsersDAO();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- INSERT --------------------
|
|
||||||
|
|
||||||
/** Вставка с внешним соединением. Соединение НЕ закрывает. */
|
|
||||||
public void insert(Connection c, SolanaUserEntry user) throws SQLException {
|
|
||||||
String sql = """
|
|
||||||
INSERT INTO solana_users (
|
|
||||||
login, blockchain_name, solana_key, blockchain_key, client_key
|
|
||||||
) VALUES (?, ?, ?, ?, ?)
|
|
||||||
""";
|
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
|
||||||
ps.setString(1, user.getLogin());
|
|
||||||
ps.setString(2, user.getBlockchainName());
|
|
||||||
ps.setString(3, user.getSolanaKey());
|
|
||||||
ps.setString(4, user.getBlockchainKey());
|
|
||||||
ps.setString(5, user.getClientKey());
|
|
||||||
ps.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Вставка без внешнего соединения. Сам открывает/закрывает. */
|
|
||||||
public void insert(SolanaUserEntry user) throws SQLException {
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
insert(c, user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------- EXISTS --------------------
|
// -------------------- EXISTS --------------------
|
||||||
|
|
||||||
/** Проверка существования по login (case-insensitive) с внешним соединением. Соединение НЕ закрывает. */
|
/** Проверка существования по login (case-insensitive) с внешним соединением. Соединение НЕ закрывает. */
|
||||||
public boolean existsByLogin(Connection c, String login) throws SQLException {
|
public boolean existsByLogin(Connection c, String login) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM solana_users
|
FROM %s
|
||||||
WHERE LOWER(login) = LOWER(?)
|
WHERE LOWER(login) = LOWER(?)
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""";
|
""".formatted(CurrentUsersSql.usersSubquery("su"));
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, login);
|
ps.setString(1, login);
|
||||||
@@ -96,10 +72,10 @@ public final class SolanaUsersDAO {
|
|||||||
public boolean existsByBlockchainName(Connection c, String blockchainName) throws SQLException {
|
public boolean existsByBlockchainName(Connection c, String blockchainName) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM solana_users
|
FROM %s
|
||||||
WHERE blockchain_name = ?
|
WHERE blockchain_name = ?
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""";
|
""".formatted(CurrentUsersSql.usersSubquery("su"));
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, blockchainName);
|
ps.setString(1, blockchainName);
|
||||||
@@ -119,7 +95,7 @@ public final class SolanaUsersDAO {
|
|||||||
// -------------------- SELECT --------------------
|
// -------------------- SELECT --------------------
|
||||||
|
|
||||||
/** Получить по login (case-insensitive) с внешним соединением. Соединение НЕ закрывает. */
|
/** Получить по login (case-insensitive) с внешним соединением. Соединение НЕ закрывает. */
|
||||||
public SolanaUserEntry getByLogin(Connection c, String login) throws SQLException {
|
public CurrentUserEntry getByLogin(Connection c, String login) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT
|
SELECT
|
||||||
login,
|
login,
|
||||||
@@ -127,9 +103,9 @@ public final class SolanaUsersDAO {
|
|||||||
solana_key,
|
solana_key,
|
||||||
blockchain_key,
|
blockchain_key,
|
||||||
client_key
|
client_key
|
||||||
FROM solana_users
|
FROM %s
|
||||||
WHERE LOWER(login) = LOWER(?)
|
WHERE LOWER(login) = LOWER(?)
|
||||||
""";
|
""".formatted(CurrentUsersSql.usersSubquery("su"));
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, login);
|
ps.setString(1, login);
|
||||||
@@ -141,14 +117,14 @@ public final class SolanaUsersDAO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Получить по login (case-insensitive) без внешнего соединения. Сам открывает/закрывает. */
|
/** Получить по login (case-insensitive) без внешнего соединения. Сам открывает/закрывает. */
|
||||||
public SolanaUserEntry getByLogin(String login) throws SQLException {
|
public CurrentUserEntry getByLogin(String login) throws SQLException {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
return getByLogin(c, login);
|
return getByLogin(c, login);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Получить по blockchain_name (case-sensitive) с внешним соединением. Соединение НЕ закрывает. */
|
/** Получить по blockchain_name (case-sensitive) с внешним соединением. Соединение НЕ закрывает. */
|
||||||
public SolanaUserEntry getByBlockchainName(Connection c, String blockchainName) throws SQLException {
|
public CurrentUserEntry getByBlockchainName(Connection c, String blockchainName) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT
|
SELECT
|
||||||
login,
|
login,
|
||||||
@@ -156,9 +132,9 @@ public final class SolanaUsersDAO {
|
|||||||
solana_key,
|
solana_key,
|
||||||
blockchain_key,
|
blockchain_key,
|
||||||
client_key
|
client_key
|
||||||
FROM solana_users
|
FROM %s
|
||||||
WHERE blockchain_name = ?
|
WHERE blockchain_name = ?
|
||||||
""";
|
""".formatted(CurrentUsersSql.usersSubquery("su"));
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, blockchainName);
|
ps.setString(1, blockchainName);
|
||||||
@@ -170,14 +146,19 @@ public final class SolanaUsersDAO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Получить по blockchain_name без внешнего соединения. */
|
/** Получить по blockchain_name без внешнего соединения. */
|
||||||
public SolanaUserEntry getByBlockchainName(String blockchainName) throws SQLException {
|
public CurrentUserEntry getByBlockchainName(String blockchainName) throws SQLException {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
return getByBlockchainName(c, blockchainName);
|
return getByBlockchainName(c, blockchainName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Поиск по префиксу с внешним соединением. Соединение НЕ закрывает. */
|
/** Поиск по префиксу с внешним соединением. Соединение НЕ закрывает. */
|
||||||
public List<SolanaUserEntry> searchByLoginPrefix(Connection c, String prefix) throws SQLException {
|
public List<CurrentUserEntry> searchByLoginPrefix(Connection c, String prefix) throws SQLException {
|
||||||
|
return searchByLoginPrefix(c, prefix, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Поиск по префиксу с optional-фильтром server PDA. Соединение НЕ закрывает. */
|
||||||
|
public List<CurrentUserEntry> searchByLoginPrefix(Connection c, String prefix, Boolean isServer) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT
|
SELECT
|
||||||
login,
|
login,
|
||||||
@@ -185,16 +166,24 @@ public final class SolanaUsersDAO {
|
|||||||
solana_key,
|
solana_key,
|
||||||
blockchain_key,
|
blockchain_key,
|
||||||
client_key
|
client_key
|
||||||
FROM solana_users
|
FROM %s
|
||||||
WHERE LOWER(login) LIKE ?
|
WHERE LOWER(login) LIKE ?
|
||||||
|
AND (? IS NULL OR is_server = ?)
|
||||||
ORDER BY login
|
ORDER BY login
|
||||||
LIMIT 5
|
LIMIT 5
|
||||||
""";
|
""".formatted(CurrentUsersSql.usersSubquery("su"));
|
||||||
|
|
||||||
List<SolanaUserEntry> 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.toLowerCase() + "%");
|
||||||
|
if (isServer == null) {
|
||||||
|
ps.setNull(2, Types.BOOLEAN);
|
||||||
|
ps.setNull(3, Types.BOOLEAN);
|
||||||
|
} else {
|
||||||
|
ps.setBoolean(2, isServer);
|
||||||
|
ps.setBoolean(3, isServer);
|
||||||
|
}
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
while (rs.next()) result.add(mapRow(rs));
|
while (rs.next()) result.add(mapRow(rs));
|
||||||
}
|
}
|
||||||
@@ -204,23 +193,28 @@ public final class SolanaUsersDAO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Поиск по префиксу без внешнего соединения. Сам открывает/закрывает. */
|
/** Поиск по префиксу без внешнего соединения. Сам открывает/закрывает. */
|
||||||
public List<SolanaUserEntry> searchByLoginPrefix(String prefix) throws SQLException {
|
public List<CurrentUserEntry> searchByLoginPrefix(String prefix) throws SQLException {
|
||||||
|
return searchByLoginPrefix(prefix, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Поиск по префиксу без внешнего соединения с optional-фильтром server PDA. */
|
||||||
|
public List<CurrentUserEntry> searchByLoginPrefix(String prefix, Boolean isServer) throws SQLException {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
return searchByLoginPrefix(c, prefix);
|
return searchByLoginPrefix(c, prefix, isServer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- MAPPER --------------------
|
// -------------------- MAPPER --------------------
|
||||||
|
|
||||||
private SolanaUserEntry mapRow(ResultSet rs) throws SQLException {
|
private CurrentUserEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
SolanaUserEntry e = new SolanaUserEntry();
|
CurrentUserEntry e = new CurrentUserEntry();
|
||||||
|
|
||||||
e.setLogin(rs.getString("login"));
|
e.setLogin(rs.getString("login"));
|
||||||
e.setBlockchainName(rs.getString("blockchain_name"));
|
e.setBlockchainName(rs.getString("blockchain_name"));
|
||||||
e.setSolanaKey(rs.getString("solana_key"));
|
e.setSolanaKey(KeyEncodingUtil.normalizeKeyToBase64_32(rs.getString("solana_key")));
|
||||||
e.setBlockchainKey(rs.getString("blockchain_key"));
|
e.setBlockchainKey(KeyEncodingUtil.normalizeKeyToBase64_32(rs.getString("blockchain_key")));
|
||||||
e.setClientKey(rs.getString("client_key"));
|
e.setClientKey(KeyEncodingUtil.normalizeKeyToBase64_32(rs.getString("client_key")));
|
||||||
|
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.entities.DirectMessageEntry;
|
import shine.db.entities.DirectMessageEntry;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
@@ -8,7 +8,7 @@ import java.sql.PreparedStatement;
|
|||||||
|
|
||||||
public final class DirectMessagesDAO {
|
public final class DirectMessagesDAO {
|
||||||
private static volatile DirectMessagesDAO instance;
|
private static volatile DirectMessagesDAO instance;
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
private DirectMessagesDAO() {}
|
private DirectMessagesDAO() {}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package shine.db.dao;
|
||||||
|
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.entities.DmSyncPeerStateEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public final class DmSyncPeerStateDAO {
|
||||||
|
private static volatile DmSyncPeerStateDAO instance;
|
||||||
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
|
private DmSyncPeerStateDAO() {}
|
||||||
|
|
||||||
|
public static DmSyncPeerStateDAO getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (DmSyncPeerStateDAO.class) {
|
||||||
|
if (instance == null) instance = new DmSyncPeerStateDAO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DmSyncPeerStateEntry getOrCreate(String ownerLogin, String remoteServerLogin, String remoteServerUrl) throws SQLException {
|
||||||
|
DmSyncPeerStateEntry existing = get(ownerLogin, remoteServerLogin);
|
||||||
|
if (existing != null) {
|
||||||
|
if (remoteServerUrl != null && !remoteServerUrl.isBlank()
|
||||||
|
&& !remoteServerUrl.equals(existing.getRemoteServerUrl())) {
|
||||||
|
touchRemoteUrl(ownerLogin, remoteServerLogin, remoteServerUrl);
|
||||||
|
existing.setRemoteServerUrl(remoteServerUrl);
|
||||||
|
}
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
long nowMs = System.currentTimeMillis();
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement("""
|
||||||
|
INSERT INTO dm_sync_peer_state (
|
||||||
|
owner_login, remote_server_login, remote_server_url,
|
||||||
|
cursor_stored_at_ms, cursor_message_key, bootstrap_completed,
|
||||||
|
last_sync_at_ms, last_error, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, 0, '', FALSE, NULL, NULL, ?)
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
""")) {
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setString(2, remoteServerLogin);
|
||||||
|
ps.setString(3, remoteServerUrl == null ? "" : remoteServerUrl);
|
||||||
|
ps.setLong(4, nowMs);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
existing = get(ownerLogin, remoteServerLogin);
|
||||||
|
if (existing == null) {
|
||||||
|
throw new SQLException("Failed to create dm_sync_peer_state row");
|
||||||
|
}
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateSuccess(
|
||||||
|
String ownerLogin,
|
||||||
|
String remoteServerLogin,
|
||||||
|
String remoteServerUrl,
|
||||||
|
long cursorStoredAtMs,
|
||||||
|
String cursorMessageKey,
|
||||||
|
boolean bootstrapCompleted
|
||||||
|
) throws SQLException {
|
||||||
|
long nowMs = System.currentTimeMillis();
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement("""
|
||||||
|
INSERT INTO dm_sync_peer_state (
|
||||||
|
owner_login, remote_server_login, remote_server_url,
|
||||||
|
cursor_stored_at_ms, cursor_message_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_stored_at_ms = EXCLUDED.cursor_stored_at_ms,
|
||||||
|
cursor_message_key = EXCLUDED.cursor_message_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
|
||||||
|
""")) {
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setString(2, remoteServerLogin);
|
||||||
|
ps.setString(3, remoteServerUrl == null ? "" : remoteServerUrl);
|
||||||
|
ps.setLong(4, Math.max(0L, cursorStoredAtMs));
|
||||||
|
ps.setString(5, cursorMessageKey == null ? "" : cursorMessageKey);
|
||||||
|
ps.setBoolean(6, bootstrapCompleted);
|
||||||
|
ps.setLong(7, nowMs);
|
||||||
|
ps.setLong(8, nowMs);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateError(String ownerLogin, String remoteServerLogin, String remoteServerUrl, String error) throws SQLException {
|
||||||
|
long nowMs = System.currentTimeMillis();
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement("""
|
||||||
|
INSERT INTO dm_sync_peer_state (
|
||||||
|
owner_login, remote_server_login, remote_server_url,
|
||||||
|
cursor_stored_at_ms, cursor_message_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
|
||||||
|
""")) {
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setString(2, remoteServerLogin);
|
||||||
|
ps.setString(3, remoteServerUrl == null ? "" : remoteServerUrl);
|
||||||
|
ps.setString(4, truncate(error));
|
||||||
|
ps.setLong(5, nowMs);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DmSyncPeerStateEntry get(String ownerLogin, String remoteServerLogin) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement("""
|
||||||
|
SELECT owner_login, remote_server_login, remote_server_url,
|
||||||
|
cursor_stored_at_ms, cursor_message_key, bootstrap_completed,
|
||||||
|
last_sync_at_ms, last_error, updated_at_ms
|
||||||
|
FROM dm_sync_peer_state
|
||||||
|
WHERE LOWER(owner_login) = LOWER(?)
|
||||||
|
AND LOWER(remote_server_login) = LOWER(?)
|
||||||
|
LIMIT 1
|
||||||
|
""")) {
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setString(2, remoteServerLogin);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (!rs.next()) return null;
|
||||||
|
DmSyncPeerStateEntry e = new DmSyncPeerStateEntry();
|
||||||
|
e.setOwnerLogin(rs.getString("owner_login"));
|
||||||
|
e.setRemoteServerLogin(rs.getString("remote_server_login"));
|
||||||
|
e.setRemoteServerUrl(rs.getString("remote_server_url"));
|
||||||
|
e.setCursorStoredAtMs(rs.getLong("cursor_stored_at_ms"));
|
||||||
|
e.setCursorMessageKey(rs.getString("cursor_message_key"));
|
||||||
|
e.setBootstrapCompleted(rs.getBoolean("bootstrap_completed"));
|
||||||
|
long lastSyncAt = rs.getLong("last_sync_at_ms");
|
||||||
|
e.setLastSyncAtMs(rs.wasNull() ? null : lastSyncAt);
|
||||||
|
e.setLastError(rs.getString("last_error"));
|
||||||
|
e.setUpdatedAtMs(rs.getLong("updated_at_ms"));
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void touchRemoteUrl(String ownerLogin, String remoteServerLogin, String remoteServerUrl) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement("""
|
||||||
|
UPDATE dm_sync_peer_state
|
||||||
|
SET remote_server_url = ?, updated_at_ms = ?
|
||||||
|
WHERE LOWER(owner_login) = LOWER(?)
|
||||||
|
AND LOWER(remote_server_login) = LOWER(?)
|
||||||
|
""")) {
|
||||||
|
ps.setString(1, remoteServerUrl);
|
||||||
|
ps.setLong(2, System.currentTimeMillis());
|
||||||
|
ps.setString(3, ownerLogin);
|
||||||
|
ps.setString(4, remoteServerLogin);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String truncate(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
String clean = value.trim();
|
||||||
|
if (clean.length() <= 500) return clean;
|
||||||
|
return clean.substring(0, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.entities.EspPairingRequestEntry;
|
import shine.db.entities.EspPairingRequestEntry;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
@@ -13,7 +13,7 @@ import java.util.List;
|
|||||||
public final class EspPairingRequestsDAO {
|
public final class EspPairingRequestsDAO {
|
||||||
|
|
||||||
private static volatile EspPairingRequestsDAO instance;
|
private static volatile EspPairingRequestsDAO instance;
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
private EspPairingRequestsDAO() { }
|
private EspPairingRequestsDAO() { }
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ public final class EspPairingRequestsDAO {
|
|||||||
payload_type, status, short_code, fingerprint_b58, encrypted_payload, reject_reason,
|
payload_type, status, short_code, fingerprint_b58, encrypted_payload, reject_reason,
|
||||||
approved_by_session_id, created_at_ms, expires_at_ms, updated_at_ms, delivered_to_homeserver
|
approved_by_session_id, created_at_ms, expires_at_ms, updated_at_ms, delivered_to_homeserver
|
||||||
FROM esp_pairing_requests
|
FROM esp_pairing_requests
|
||||||
WHERE login = ? COLLATE NOCASE
|
WHERE LOWER(login) = LOWER(?)
|
||||||
AND expires_at_ms > ?
|
AND expires_at_ms > ?
|
||||||
AND status = 'created'
|
AND status = 'created'
|
||||||
ORDER BY created_at_ms DESC
|
ORDER BY created_at_ms DESC
|
||||||
@@ -131,7 +131,7 @@ public final class EspPairingRequestsDAO {
|
|||||||
StringBuilder sql = new StringBuilder("""
|
StringBuilder sql = new StringBuilder("""
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM esp_pairing_requests
|
FROM esp_pairing_requests
|
||||||
WHERE login = ? COLLATE NOCASE
|
WHERE LOWER(login) = LOWER(?)
|
||||||
AND created_at_ms >= ?
|
AND created_at_ms >= ?
|
||||||
AND status IN (
|
AND status IN (
|
||||||
""");
|
""");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.entities.EspPairingSettingsEntry;
|
import shine.db.entities.EspPairingSettingsEntry;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
@@ -11,7 +11,7 @@ import java.sql.SQLException;
|
|||||||
public final class EspPairingSettingsDAO {
|
public final class EspPairingSettingsDAO {
|
||||||
|
|
||||||
private static volatile EspPairingSettingsDAO instance;
|
private static volatile EspPairingSettingsDAO instance;
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
private EspPairingSettingsDAO() { }
|
private EspPairingSettingsDAO() { }
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ public final class EspPairingSettingsDAO {
|
|||||||
String sql = """
|
String sql = """
|
||||||
SELECT login, enabled, password_hash, ttl_seconds, failed_attempts, first_failed_at_ms, blocked_until_ms, updated_at_ms
|
SELECT login, enabled, password_hash, ttl_seconds, failed_attempts, first_failed_at_ms, blocked_until_ms, updated_at_ms
|
||||||
FROM esp_pairing_settings
|
FROM esp_pairing_settings
|
||||||
WHERE login = ? COLLATE NOCASE
|
WHERE LOWER(login) = LOWER(?)
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""";
|
""";
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.entities.IpGeoCacheEntry;
|
import shine.db.entities.IpGeoCacheEntry;
|
||||||
|
|
||||||
import java.sql.*;
|
import java.sql.*;
|
||||||
@@ -20,7 +20,7 @@ import java.sql.*;
|
|||||||
public final class IpGeoCacheDAO {
|
public final class IpGeoCacheDAO {
|
||||||
|
|
||||||
private static volatile IpGeoCacheDAO instance;
|
private static volatile IpGeoCacheDAO instance;
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
private IpGeoCacheDAO() { }
|
private IpGeoCacheDAO() { }
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.entities.PushTokenEntry;
|
import shine.db.entities.PushTokenEntry;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
@@ -11,7 +11,7 @@ import java.util.List;
|
|||||||
|
|
||||||
public final class PushTokensDAO {
|
public final class PushTokensDAO {
|
||||||
private static volatile PushTokensDAO instance;
|
private static volatile PushTokensDAO instance;
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
private PushTokensDAO() {}
|
private PushTokensDAO() {}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.entities.SignedDirectMessageHistoryEntry;
|
import shine.db.entities.SignedDirectMessageHistoryEntry;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
@@ -8,7 +8,7 @@ import java.sql.PreparedStatement;
|
|||||||
|
|
||||||
public final class SignedDirectMessagesHistoryDAO {
|
public final class SignedDirectMessagesHistoryDAO {
|
||||||
private static volatile SignedDirectMessagesHistoryDAO instance;
|
private static volatile SignedDirectMessagesHistoryDAO instance;
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
private SignedDirectMessagesHistoryDAO() {}
|
private SignedDirectMessagesHistoryDAO() {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
|
|
||||||
public final class SignedDmReplayDAO {
|
public final class SignedDmReplayDAO {
|
||||||
private static volatile SignedDmReplayDAO instance;
|
private static volatile SignedDmReplayDAO instance;
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
private SignedDmReplayDAO() {}
|
private SignedDmReplayDAO() {}
|
||||||
|
|
||||||
@@ -24,10 +24,11 @@ public final class SignedDmReplayDAO {
|
|||||||
cleanupExpired(nowMs - 15L * 60L * 1000L);
|
cleanupExpired(nowMs - 15L * 60L * 1000L);
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
String sql = """
|
String sql = """
|
||||||
INSERT OR IGNORE INTO signed_direct_message_replay (
|
INSERT INTO signed_direct_message_replay (
|
||||||
from_login, time_ms, nonce, created_at_ms
|
from_login, time_ms, nonce, created_at_ms
|
||||||
) VALUES (?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?)
|
||||||
""";
|
ON CONFLICT DO NOTHING
|
||||||
|
""";
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, fromLogin);
|
ps.setString(1, fromLogin);
|
||||||
ps.setLong(2, timeMs);
|
ps.setLong(2, timeMs);
|
||||||
|
|||||||
+156
-118
@@ -1,7 +1,7 @@
|
|||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.entities.SignedMessageV2Entry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
@@ -11,9 +11,7 @@ import java.sql.Statement;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public final class SignedMessagesV2DAO {
|
public final class SignedMessagesDAO {
|
||||||
private static final int SQLITE_BUSY_MAX_RETRIES = 6;
|
|
||||||
private static final long SQLITE_BUSY_RETRY_BASE_DELAY_MS = 40L;
|
|
||||||
|
|
||||||
public enum ApplyStatus {
|
public enum ApplyStatus {
|
||||||
APPLIED,
|
APPLIED,
|
||||||
@@ -26,33 +24,34 @@ public final class SignedMessagesV2DAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static volatile SignedMessagesV2DAO instance;
|
private static volatile SignedMessagesDAO instance;
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
private SignedMessagesV2DAO() {}
|
private SignedMessagesDAO() {}
|
||||||
|
|
||||||
public static SignedMessagesV2DAO getInstance() {
|
public static SignedMessagesDAO getInstance() {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
synchronized (SignedMessagesV2DAO.class) {
|
synchronized (SignedMessagesDAO.class) {
|
||||||
if (instance == null) instance = new SignedMessagesV2DAO();
|
if (instance == null) instance = new SignedMessagesDAO();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ApplyStatus insertIfAbsent(SignedMessageV2Entry e) throws Exception {
|
public ApplyStatus insertIfAbsent(SignedMessageEntry e) throws Exception {
|
||||||
return withBusyRetry(() -> {
|
return withBusyRetry(() -> {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
if (isBlockedByConversationDelete(c, e.getFromLogin(), e.getToLogin(), e.getTimeMs())) {
|
if (isBlockedByConversationDelete(c, e.getFromLogin(), e.getToLogin(), e.getTimeMs())) {
|
||||||
return ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE;
|
return ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE;
|
||||||
}
|
}
|
||||||
String sql = """
|
String sql = """
|
||||||
INSERT OR IGNORE INTO signed_messages_v2 (
|
INSERT INTO signed_messages (
|
||||||
message_key, base_key, target_login, from_login, to_login,
|
message_key, base_key, target_login, from_login, to_login,
|
||||||
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
||||||
raw_block, created_at_ms, source_api, origin_session_id,
|
raw_block, created_at_ms, source_api, origin_session_id,
|
||||||
receipt_ref_base_key, receipt_ref_type, read_at_ms
|
receipt_ref_base_key, receipt_ref_type, read_at_ms
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
""";
|
""";
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
bindSignedMessage(ps, e);
|
bindSignedMessage(ps, e);
|
||||||
@@ -66,7 +65,7 @@ public final class SignedMessagesV2DAO {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean insertPairBothOrNothing(SignedMessageV2Entry first, SignedMessageV2Entry second) throws Exception {
|
public boolean insertPairBothOrNothing(SignedMessageEntry first, SignedMessageEntry second) throws Exception {
|
||||||
return withBusyRetry(() -> {
|
return withBusyRetry(() -> {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
boolean prevAutoCommit = c.getAutoCommit();
|
boolean prevAutoCommit = c.getAutoCommit();
|
||||||
@@ -95,7 +94,7 @@ public final class SignedMessagesV2DAO {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ApplyStatus upsertContentPair(SignedMessageV2Entry incoming, SignedMessageV2Entry outgoing) throws Exception {
|
public ApplyStatus upsertContentPair(SignedMessageEntry incoming, SignedMessageEntry outgoing) throws Exception {
|
||||||
return withBusyRetry(() -> {
|
return withBusyRetry(() -> {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
boolean prevAutoCommit = c.getAutoCommit();
|
boolean prevAutoCommit = c.getAutoCommit();
|
||||||
@@ -136,7 +135,7 @@ public final class SignedMessagesV2DAO {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ApplyStatus upsertIncomingCopy(SignedMessageV2Entry incoming) throws Exception {
|
public ApplyStatus upsertIncomingCopy(SignedMessageEntry incoming) throws Exception {
|
||||||
return withBusyRetry(() -> {
|
return withBusyRetry(() -> {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
boolean prevAutoCommit = c.getAutoCommit();
|
boolean prevAutoCommit = c.getAutoCommit();
|
||||||
@@ -173,7 +172,7 @@ public final class SignedMessagesV2DAO {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ApplyStatus applyDeleteMessage(SignedMessageV2Entry tombstone) throws Exception {
|
public ApplyStatus applyDeleteMessage(SignedMessageEntry tombstone) throws Exception {
|
||||||
return withBusyRetry(() -> {
|
return withBusyRetry(() -> {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
boolean prevAutoCommit = c.getAutoCommit();
|
boolean prevAutoCommit = c.getAutoCommit();
|
||||||
@@ -204,7 +203,7 @@ public final class SignedMessagesV2DAO {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ApplyStatus applyDeleteConversation(SignedMessageV2Entry tombstone) throws Exception {
|
public ApplyStatus applyDeleteConversation(SignedMessageEntry tombstone) throws Exception {
|
||||||
return withBusyRetry(() -> {
|
return withBusyRetry(() -> {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
boolean prevAutoCommit = c.getAutoCommit();
|
boolean prevAutoCommit = c.getAutoCommit();
|
||||||
@@ -232,7 +231,7 @@ public final class SignedMessagesV2DAO {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public SignedMessageV2Entry getByMessageKey(String messageKey) throws Exception {
|
public SignedMessageEntry getByMessageKey(String messageKey) throws Exception {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT
|
SELECT
|
||||||
@@ -240,9 +239,9 @@ public final class SignedMessagesV2DAO {
|
|||||||
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
||||||
raw_block, created_at_ms, source_api, origin_session_id,
|
raw_block, created_at_ms, source_api, origin_session_id,
|
||||||
receipt_ref_base_key, receipt_ref_type, read_at_ms
|
receipt_ref_base_key, receipt_ref_type, read_at_ms
|
||||||
FROM signed_messages_v2
|
FROM %s
|
||||||
WHERE message_key = ?
|
WHERE message_key = ?
|
||||||
""";
|
""".formatted(messagesTable());
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, messageKey);
|
ps.setString(1, messageKey);
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
@@ -253,7 +252,7 @@ public final class SignedMessagesV2DAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public SignedMessageV2Entry getLatestConversationDelete(String fromLogin, String toLogin) throws Exception {
|
public SignedMessageEntry getLatestConversationDelete(String fromLogin, String toLogin) throws Exception {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
return getLatestConversationDelete(c, fromLogin, toLogin);
|
return getLatestConversationDelete(c, fromLogin, toLogin);
|
||||||
}
|
}
|
||||||
@@ -268,9 +267,10 @@ public final class SignedMessagesV2DAO {
|
|||||||
withBusyRetry(() -> {
|
withBusyRetry(() -> {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
String sql = """
|
String sql = """
|
||||||
INSERT OR IGNORE INTO signed_message_session_delivery (
|
INSERT INTO signed_message_session_delivery (
|
||||||
message_key, session_id, delivered, delivered_at_ms, created_at_ms
|
message_key, session_id, delivered, delivered_at_ms, created_at_ms
|
||||||
) VALUES (?, ?, 0, NULL, ?)
|
) VALUES (?, ?, 0, NULL, ?)
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
""";
|
""";
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
for (String sessionId : sessionIds) {
|
for (String sessionId : sessionIds) {
|
||||||
@@ -314,22 +314,23 @@ public final class SignedMessagesV2DAO {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<SignedMessageV2Entry> listPendingForSession(String login, String sessionId) throws Exception {
|
public List<SignedMessageEntry> listPendingForSession(String login, String sessionId) throws Exception {
|
||||||
return withBusyRetry(() -> {
|
return withBusyRetry(() -> {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
String fillSql = """
|
String fillSql = """
|
||||||
INSERT OR IGNORE INTO signed_message_session_delivery (
|
INSERT INTO signed_message_session_delivery (
|
||||||
message_key, session_id, delivered, delivered_at_ms, created_at_ms
|
message_key, session_id, delivered, delivered_at_ms, created_at_ms
|
||||||
)
|
)
|
||||||
SELECT m.message_key, ?, 0, NULL, ?
|
SELECT m.message_key, ?, 0, NULL, ?
|
||||||
FROM signed_messages_v2 m
|
FROM %s m
|
||||||
WHERE (
|
WHERE (
|
||||||
(m.message_type IN (1, 3) AND m.to_login = ? COLLATE NOCASE)
|
(m.message_type IN (1, 3) AND LOWER(m.to_login) = LOWER(?))
|
||||||
OR (m.message_type IN (2, 4) AND m.from_login = ? COLLATE NOCASE)
|
OR (m.message_type IN (2, 4) AND LOWER(m.from_login) = LOWER(?))
|
||||||
OR (m.message_type IN (5, 6, 7, 8)
|
OR (m.message_type IN (5, 6, 7, 8)
|
||||||
AND (m.from_login = ? COLLATE NOCASE OR m.to_login = ? COLLATE NOCASE))
|
AND (LOWER(m.from_login) = LOWER(?) OR LOWER(m.to_login) = LOWER(?)))
|
||||||
)
|
)
|
||||||
""";
|
ON CONFLICT DO NOTHING
|
||||||
|
""".formatted(messagesTable());
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
try (PreparedStatement ps = c.prepareStatement(fillSql)) {
|
try (PreparedStatement ps = c.prepareStatement(fillSql)) {
|
||||||
ps.setString(1, sessionId);
|
ps.setString(1, sessionId);
|
||||||
@@ -347,13 +348,13 @@ public final class SignedMessagesV2DAO {
|
|||||||
m.time_ms, m.nonce, m.message_type, m.revision_time_ms, m.reencrypted_at_ms,
|
m.time_ms, m.nonce, m.message_type, m.revision_time_ms, m.reencrypted_at_ms,
|
||||||
m.raw_block, m.created_at_ms, m.source_api, m.origin_session_id,
|
m.raw_block, m.created_at_ms, m.source_api, m.origin_session_id,
|
||||||
m.receipt_ref_base_key, m.receipt_ref_type, m.read_at_ms
|
m.receipt_ref_base_key, m.receipt_ref_type, m.read_at_ms
|
||||||
FROM signed_messages_v2 m
|
FROM %s m
|
||||||
JOIN signed_message_session_delivery d
|
JOIN signed_message_session_delivery d
|
||||||
ON d.message_key = m.message_key
|
ON d.message_key = m.message_key
|
||||||
WHERE d.session_id = ? AND d.delivered = 0
|
WHERE d.session_id = ? AND d.delivered = 0
|
||||||
ORDER BY m.time_ms ASC, m.revision_time_ms ASC, m.reencrypted_at_ms ASC, m.created_at_ms ASC
|
ORDER BY m.time_ms ASC, m.revision_time_ms ASC, m.reencrypted_at_ms ASC, m.created_at_ms ASC
|
||||||
""";
|
""".formatted(messagesTable());
|
||||||
List<SignedMessageV2Entry> out = new ArrayList<>();
|
List<SignedMessageEntry> out = new ArrayList<>();
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, sessionId);
|
ps.setString(1, sessionId);
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
@@ -365,7 +366,7 @@ public final class SignedMessagesV2DAO {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<SignedMessageV2Entry> listConversationPage(
|
public List<SignedMessageEntry> listConversationPage(
|
||||||
String login,
|
String login,
|
||||||
String peerLogin,
|
String peerLogin,
|
||||||
long beforeTimeMs,
|
long beforeTimeMs,
|
||||||
@@ -379,12 +380,12 @@ public final class SignedMessagesV2DAO {
|
|||||||
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
||||||
raw_block, created_at_ms, source_api, origin_session_id,
|
raw_block, created_at_ms, source_api, origin_session_id,
|
||||||
receipt_ref_base_key, receipt_ref_type, read_at_ms
|
receipt_ref_base_key, receipt_ref_type, read_at_ms
|
||||||
FROM signed_messages_v2
|
FROM %s
|
||||||
WHERE target_login = ? COLLATE NOCASE
|
WHERE LOWER(target_login) = LOWER(?)
|
||||||
AND message_type IN (1, 2)
|
AND message_type IN (1, 2)
|
||||||
AND (
|
AND (
|
||||||
(from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
(LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
OR (from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
)
|
)
|
||||||
AND (
|
AND (
|
||||||
? <= 0
|
? <= 0
|
||||||
@@ -393,8 +394,8 @@ public final class SignedMessagesV2DAO {
|
|||||||
)
|
)
|
||||||
ORDER BY time_ms DESC, message_key DESC
|
ORDER BY time_ms DESC, message_key DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
""";
|
""".formatted(messagesTable());
|
||||||
List<SignedMessageV2Entry> out = new ArrayList<>();
|
List<SignedMessageEntry> out = new ArrayList<>();
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, login);
|
ps.setString(1, login);
|
||||||
ps.setString(2, login);
|
ps.setString(2, login);
|
||||||
@@ -415,9 +416,75 @@ public final class SignedMessagesV2DAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void upsertMessage(Connection c, SignedMessageV2Entry e) throws SQLException {
|
public SyncBatch listSyncBatch(
|
||||||
|
String ownerLogin,
|
||||||
|
long afterStoredAtMs,
|
||||||
|
String afterMessageKey,
|
||||||
|
int limit,
|
||||||
|
int maxBytes
|
||||||
|
) throws Exception {
|
||||||
|
int safeLimit = Math.max(1, Math.min(limit, 500));
|
||||||
|
int sqlLimit = safeLimit + 1;
|
||||||
|
String safeAfterMessageKey = afterMessageKey == null ? "" : afterMessageKey;
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
String sql = """
|
||||||
|
SELECT
|
||||||
|
message_key, base_key, target_login, from_login, to_login,
|
||||||
|
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
||||||
|
raw_block, created_at_ms, source_api, origin_session_id,
|
||||||
|
receipt_ref_base_key, receipt_ref_type, read_at_ms
|
||||||
|
FROM %s
|
||||||
|
WHERE (
|
||||||
|
LOWER(target_login) = LOWER(?)
|
||||||
|
OR (
|
||||||
|
message_type IN (5, 6, 7, 8)
|
||||||
|
AND (LOWER(from_login) = LOWER(?) OR LOWER(to_login) = LOWER(?))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
created_at_ms > ?
|
||||||
|
OR (created_at_ms = ? AND (? = '' OR message_key > ?))
|
||||||
|
)
|
||||||
|
ORDER BY created_at_ms ASC, message_key ASC
|
||||||
|
LIMIT ?
|
||||||
|
""".formatted(messagesTable());
|
||||||
|
List<SignedMessageEntry> out = new ArrayList<>();
|
||||||
|
boolean hasMore = false;
|
||||||
|
int usedBytes = 0;
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setString(2, ownerLogin);
|
||||||
|
ps.setString(3, ownerLogin);
|
||||||
|
ps.setLong(4, Math.max(0L, afterStoredAtMs));
|
||||||
|
ps.setLong(5, Math.max(0L, afterStoredAtMs));
|
||||||
|
ps.setString(6, safeAfterMessageKey);
|
||||||
|
ps.setString(7, safeAfterMessageKey);
|
||||||
|
ps.setInt(8, sqlLimit);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
SignedMessageEntry row = mapRow(rs);
|
||||||
|
byte[] raw = row.getRawBlock();
|
||||||
|
int rowBytes = raw == null ? 0 : raw.length;
|
||||||
|
if (out.size() >= safeLimit) {
|
||||||
|
hasMore = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!out.isEmpty() && maxBytes > 0 && usedBytes + rowBytes > maxBytes) {
|
||||||
|
hasMore = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out.add(row);
|
||||||
|
usedBytes += rowBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new SyncBatch(out, hasMore, usedBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void upsertMessage(Connection c, SignedMessageEntry e) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
INSERT INTO signed_messages_v2 (
|
INSERT INTO %s (
|
||||||
message_key, base_key, target_login, from_login, to_login,
|
message_key, base_key, target_login, from_login, to_login,
|
||||||
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
||||||
raw_block, created_at_ms, source_api, origin_session_id,
|
raw_block, created_at_ms, source_api, origin_session_id,
|
||||||
@@ -439,15 +506,15 @@ public final class SignedMessagesV2DAO {
|
|||||||
origin_session_id = excluded.origin_session_id,
|
origin_session_id = excluded.origin_session_id,
|
||||||
receipt_ref_base_key = excluded.receipt_ref_base_key,
|
receipt_ref_base_key = excluded.receipt_ref_base_key,
|
||||||
receipt_ref_type = excluded.receipt_ref_type,
|
receipt_ref_type = excluded.receipt_ref_type,
|
||||||
read_at_ms = COALESCE(signed_messages_v2.read_at_ms, excluded.read_at_ms)
|
read_at_ms = COALESCE(%s.read_at_ms, excluded.read_at_ms)
|
||||||
""";
|
""".formatted(messagesTable(), messagesTable());
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
bindSignedMessage(ps, e);
|
bindSignedMessage(ps, e);
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void markMessageReadByReceipt(Connection c, SignedMessageV2Entry entry) throws SQLException {
|
private void markMessageReadByReceipt(Connection c, SignedMessageEntry entry) throws SQLException {
|
||||||
if (entry == null) return;
|
if (entry == null) return;
|
||||||
int messageType = entry.getMessageType();
|
int messageType = entry.getMessageType();
|
||||||
if (messageType != 3 && messageType != 4) return;
|
if (messageType != 3 && messageType != 4) return;
|
||||||
@@ -456,7 +523,7 @@ public final class SignedMessagesV2DAO {
|
|||||||
long readAtMs = entry.getTimeMs();
|
long readAtMs = entry.getTimeMs();
|
||||||
if (readAtMs <= 0) return;
|
if (readAtMs <= 0) return;
|
||||||
try (PreparedStatement ps = c.prepareStatement("""
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
UPDATE signed_messages_v2
|
UPDATE %s
|
||||||
SET read_at_ms = CASE
|
SET read_at_ms = CASE
|
||||||
WHEN read_at_ms IS NULL OR read_at_ms <= 0 THEN ?
|
WHEN read_at_ms IS NULL OR read_at_ms <= 0 THEN ?
|
||||||
WHEN read_at_ms > ? THEN ?
|
WHEN read_at_ms > ? THEN ?
|
||||||
@@ -464,7 +531,7 @@ public final class SignedMessagesV2DAO {
|
|||||||
END
|
END
|
||||||
WHERE base_key = ?
|
WHERE base_key = ?
|
||||||
AND message_type IN (1, 2)
|
AND message_type IN (1, 2)
|
||||||
""")) {
|
""".formatted(messagesTable()))) {
|
||||||
ps.setLong(1, readAtMs);
|
ps.setLong(1, readAtMs);
|
||||||
ps.setLong(2, readAtMs);
|
ps.setLong(2, readAtMs);
|
||||||
ps.setLong(3, readAtMs);
|
ps.setLong(3, readAtMs);
|
||||||
@@ -476,10 +543,10 @@ public final class SignedMessagesV2DAO {
|
|||||||
private RevisionMarker getRevisionMarkerByMessageKey(Connection c, String messageKey) throws SQLException {
|
private RevisionMarker getRevisionMarkerByMessageKey(Connection c, String messageKey) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT revision_time_ms, reencrypted_at_ms
|
SELECT revision_time_ms, reencrypted_at_ms
|
||||||
FROM signed_messages_v2
|
FROM %s
|
||||||
WHERE message_key = ?
|
WHERE message_key = ?
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""";
|
""".formatted(messagesTable());
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, messageKey);
|
ps.setString(1, messageKey);
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
@@ -492,12 +559,12 @@ public final class SignedMessagesV2DAO {
|
|||||||
private RevisionMarker getCurrentContentMarker(Connection c, String baseKey) throws SQLException {
|
private RevisionMarker getCurrentContentMarker(Connection c, String baseKey) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT revision_time_ms, reencrypted_at_ms
|
SELECT revision_time_ms, reencrypted_at_ms
|
||||||
FROM signed_messages_v2
|
FROM %s
|
||||||
WHERE base_key = ?
|
WHERE base_key = ?
|
||||||
AND message_type IN (1, 2)
|
AND message_type IN (1, 2)
|
||||||
ORDER BY revision_time_ms DESC, reencrypted_at_ms DESC
|
ORDER BY revision_time_ms DESC, reencrypted_at_ms DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""";
|
""".formatted(messagesTable());
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, baseKey);
|
ps.setString(1, baseKey);
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
@@ -510,11 +577,11 @@ public final class SignedMessagesV2DAO {
|
|||||||
private boolean hasMessageDeleteTombstone(Connection c, String baseKey) throws SQLException {
|
private boolean hasMessageDeleteTombstone(Connection c, String baseKey) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM signed_messages_v2
|
FROM %s
|
||||||
WHERE base_key = ?
|
WHERE base_key = ?
|
||||||
AND message_type IN (5, 6)
|
AND message_type IN (5, 6)
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""";
|
""".formatted(messagesTable());
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, baseKey);
|
ps.setString(1, baseKey);
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
@@ -531,13 +598,13 @@ public final class SignedMessagesV2DAO {
|
|||||||
private Long getLatestConversationDeleteBoundary(Connection c, String fromLogin, String toLogin) throws SQLException {
|
private Long getLatestConversationDeleteBoundary(Connection c, String fromLogin, String toLogin) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT MAX(time_ms)
|
SELECT MAX(time_ms)
|
||||||
FROM signed_messages_v2
|
FROM %s
|
||||||
WHERE message_type IN (7, 8)
|
WHERE message_type IN (7, 8)
|
||||||
AND (
|
AND (
|
||||||
(from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
(LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
OR (from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
)
|
)
|
||||||
""";
|
""".formatted(messagesTable());
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, fromLogin);
|
ps.setString(1, fromLogin);
|
||||||
ps.setString(2, toLogin);
|
ps.setString(2, toLogin);
|
||||||
@@ -551,22 +618,22 @@ public final class SignedMessagesV2DAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SignedMessageV2Entry getLatestConversationDelete(Connection c, String fromLogin, String toLogin) throws Exception {
|
private SignedMessageEntry getLatestConversationDelete(Connection c, String fromLogin, String toLogin) throws Exception {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT
|
SELECT
|
||||||
message_key, base_key, target_login, from_login, to_login,
|
message_key, base_key, target_login, from_login, to_login,
|
||||||
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
||||||
raw_block, created_at_ms, source_api, origin_session_id,
|
raw_block, created_at_ms, source_api, origin_session_id,
|
||||||
receipt_ref_base_key, receipt_ref_type
|
receipt_ref_base_key, receipt_ref_type, read_at_ms
|
||||||
FROM signed_messages_v2
|
FROM %s
|
||||||
WHERE message_type IN (7, 8)
|
WHERE message_type IN (7, 8)
|
||||||
AND (
|
AND (
|
||||||
(from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
(LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
OR (from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
)
|
)
|
||||||
ORDER BY time_ms DESC
|
ORDER BY time_ms DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""";
|
""".formatted(messagesTable());
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, fromLogin);
|
ps.setString(1, fromLogin);
|
||||||
ps.setString(2, toLogin);
|
ps.setString(2, toLogin);
|
||||||
@@ -582,16 +649,16 @@ public final class SignedMessagesV2DAO {
|
|||||||
private void deleteMessageContentAndReceipts(Connection c, String baseKey) throws SQLException {
|
private void deleteMessageContentAndReceipts(Connection c, String baseKey) throws SQLException {
|
||||||
deleteDeliveryRowsByMessageSelection(c, """
|
deleteDeliveryRowsByMessageSelection(c, """
|
||||||
SELECT message_key
|
SELECT message_key
|
||||||
FROM signed_messages_v2
|
FROM %s
|
||||||
WHERE (base_key = ? AND message_type IN (1, 2))
|
WHERE (base_key = ? AND message_type IN (1, 2))
|
||||||
OR (receipt_ref_base_key = ? AND message_type IN (3, 4))
|
OR (receipt_ref_base_key = ? AND message_type IN (3, 4))
|
||||||
""", baseKey, baseKey);
|
""".formatted(messagesTable()), baseKey, baseKey);
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement("""
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
DELETE FROM signed_messages_v2
|
DELETE FROM %s
|
||||||
WHERE (base_key = ? AND message_type IN (1, 2))
|
WHERE (base_key = ? AND message_type IN (1, 2))
|
||||||
OR (receipt_ref_base_key = ? AND message_type IN (3, 4))
|
OR (receipt_ref_base_key = ? AND message_type IN (3, 4))
|
||||||
""")) {
|
""".formatted(messagesTable()))) {
|
||||||
ps.setString(1, baseKey);
|
ps.setString(1, baseKey);
|
||||||
ps.setString(2, baseKey);
|
ps.setString(2, baseKey);
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
@@ -601,22 +668,22 @@ public final class SignedMessagesV2DAO {
|
|||||||
private void deleteConversationHistoryBefore(Connection c, String fromLogin, String toLogin, long boundaryTimeMs) throws SQLException {
|
private void deleteConversationHistoryBefore(Connection c, String fromLogin, String toLogin, long boundaryTimeMs) throws SQLException {
|
||||||
deleteDeliveryRowsByMessageSelection(c, """
|
deleteDeliveryRowsByMessageSelection(c, """
|
||||||
SELECT message_key
|
SELECT message_key
|
||||||
FROM signed_messages_v2
|
FROM %s
|
||||||
WHERE time_ms < ?
|
WHERE time_ms < ?
|
||||||
AND (
|
AND (
|
||||||
(from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
(LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
OR (from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
)
|
)
|
||||||
""", boundaryTimeMs, fromLogin, toLogin, toLogin, fromLogin);
|
""".formatted(messagesTable()), boundaryTimeMs, fromLogin, toLogin, toLogin, fromLogin);
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement("""
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
DELETE FROM signed_messages_v2
|
DELETE FROM %s
|
||||||
WHERE time_ms < ?
|
WHERE time_ms < ?
|
||||||
AND (
|
AND (
|
||||||
(from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
(LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
OR (from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
)
|
)
|
||||||
""")) {
|
""".formatted(messagesTable()))) {
|
||||||
ps.setLong(1, boundaryTimeMs);
|
ps.setLong(1, boundaryTimeMs);
|
||||||
ps.setString(2, fromLogin);
|
ps.setString(2, fromLogin);
|
||||||
ps.setString(3, toLogin);
|
ps.setString(3, toLogin);
|
||||||
@@ -645,22 +712,22 @@ public final class SignedMessagesV2DAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int insertStrict(Connection c, SignedMessageV2Entry e) throws SQLException {
|
private int insertStrict(Connection c, SignedMessageEntry e) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
INSERT INTO signed_messages_v2 (
|
INSERT INTO %s (
|
||||||
message_key, base_key, target_login, from_login, to_login,
|
message_key, base_key, target_login, from_login, to_login,
|
||||||
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
||||||
raw_block, created_at_ms, source_api, origin_session_id,
|
raw_block, created_at_ms, source_api, origin_session_id,
|
||||||
receipt_ref_base_key, receipt_ref_type, read_at_ms
|
receipt_ref_base_key, receipt_ref_type, read_at_ms
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""";
|
""".formatted(messagesTable());
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
bindSignedMessage(ps, e);
|
bindSignedMessage(ps, e);
|
||||||
return ps.executeUpdate();
|
return ps.executeUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void bindSignedMessage(PreparedStatement ps, SignedMessageV2Entry e) throws SQLException {
|
private void bindSignedMessage(PreparedStatement ps, SignedMessageEntry e) throws SQLException {
|
||||||
ps.setString(1, e.getMessageKey());
|
ps.setString(1, e.getMessageKey());
|
||||||
ps.setString(2, e.getBaseKey());
|
ps.setString(2, e.getBaseKey());
|
||||||
ps.setString(3, e.getTargetLogin());
|
ps.setString(3, e.getTargetLogin());
|
||||||
@@ -703,43 +770,8 @@ public final class SignedMessagesV2DAO {
|
|||||||
return msg.contains("constraint") || msg.contains("unique") || msg.contains("primary key");
|
return msg.contains("constraint") || msg.contains("unique") || msg.contains("primary key");
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isBusyLock(SQLException ex) {
|
|
||||||
Throwable current = ex;
|
|
||||||
while (current != null) {
|
|
||||||
String msg = String.valueOf(current.getMessage()).toLowerCase();
|
|
||||||
if (msg.contains("sqlite_busy") || msg.contains("database is locked") || msg.contains("database table is locked")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
current = current.getCause();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sleepBeforeBusyRetry(int attempt) throws SQLException {
|
|
||||||
long delayMs = SQLITE_BUSY_RETRY_BASE_DELAY_MS * (1L << Math.min(attempt, 4));
|
|
||||||
try {
|
|
||||||
Thread.sleep(delayMs);
|
|
||||||
} catch (InterruptedException ie) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
SQLException sqlEx = new SQLException("Interrupted while retrying SQLite busy lock", ie);
|
|
||||||
throw sqlEx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private <T> T withBusyRetry(SqlWork<T> work) throws Exception {
|
private <T> T withBusyRetry(SqlWork<T> work) throws Exception {
|
||||||
SQLException lastBusy = null;
|
return work.run();
|
||||||
for (int attempt = 0; attempt < SQLITE_BUSY_MAX_RETRIES; attempt++) {
|
|
||||||
try {
|
|
||||||
return work.run();
|
|
||||||
} catch (SQLException ex) {
|
|
||||||
if (!isBusyLock(ex) || attempt >= SQLITE_BUSY_MAX_RETRIES - 1) {
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
lastBusy = ex;
|
|
||||||
sleepBeforeBusyRetry(attempt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw lastBusy == null ? new SQLException("SQLite busy retry failed") : lastBusy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int compareMarkers(RevisionMarker left, RevisionMarker right) {
|
private int compareMarkers(RevisionMarker left, RevisionMarker right) {
|
||||||
@@ -748,8 +780,12 @@ public final class SignedMessagesV2DAO {
|
|||||||
return Long.compare(left.reencryptedAtMs, right.reencryptedAtMs);
|
return Long.compare(left.reencryptedAtMs, right.reencryptedAtMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private SignedMessageV2Entry mapRow(ResultSet rs) throws Exception {
|
private String messagesTable() {
|
||||||
SignedMessageV2Entry e = new SignedMessageV2Entry();
|
return "signed_messages";
|
||||||
|
}
|
||||||
|
|
||||||
|
private SignedMessageEntry mapRow(ResultSet rs) throws Exception {
|
||||||
|
SignedMessageEntry e = new SignedMessageEntry();
|
||||||
e.setMessageKey(rs.getString("message_key"));
|
e.setMessageKey(rs.getString("message_key"));
|
||||||
e.setBaseKey(rs.getString("base_key"));
|
e.setBaseKey(rs.getString("base_key"));
|
||||||
e.setTargetLogin(rs.getString("target_login"));
|
e.setTargetLogin(rs.getString("target_login"));
|
||||||
@@ -773,11 +809,13 @@ public final class SignedMessagesV2DAO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private record RevisionMarker(long revisionTimeMs, long reencryptedAtMs) {
|
private record RevisionMarker(long revisionTimeMs, long reencryptedAtMs) {
|
||||||
private static RevisionMarker of(SignedMessageV2Entry entry) {
|
private static RevisionMarker of(SignedMessageEntry entry) {
|
||||||
return new RevisionMarker(entry.getRevisionTimeMs(), entry.getReencryptedAtMs());
|
return new RevisionMarker(entry.getRevisionTimeMs(), entry.getReencryptedAtMs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record SyncBatch(List<SignedMessageEntry> items, boolean hasMore, int rawBytes) {}
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
private interface SqlWork<T> {
|
private interface SqlWork<T> {
|
||||||
T run() throws Exception;
|
T run() throws Exception;
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package shine.db.dao;
|
||||||
|
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.KeyEncodingUtil;
|
||||||
|
import shine.db.entities.SolanaUserPdaCurrentEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Доступ к актуальной PostgreSQL-таблице пользователей, поддерживаемой sync-модулем.
|
||||||
|
*/
|
||||||
|
public final class SolanaUserPdaCurrentDAO {
|
||||||
|
|
||||||
|
private static volatile SolanaUserPdaCurrentDAO instance;
|
||||||
|
|
||||||
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
|
private SolanaUserPdaCurrentDAO() {}
|
||||||
|
|
||||||
|
public static SolanaUserPdaCurrentDAO getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (SolanaUserPdaCurrentDAO.class) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new SolanaUserPdaCurrentDAO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SolanaUserPdaCurrentEntry getByBlockchainName(String blockchainName) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
return getByBlockchainName(c, blockchainName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SolanaUserPdaCurrentEntry getByBlockchainName(Connection c, String blockchainName) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT
|
||||||
|
login,
|
||||||
|
blockchain_name,
|
||||||
|
blockchain_key,
|
||||||
|
paid_limit_bytes
|
||||||
|
FROM solana_user_pda_current
|
||||||
|
WHERE blockchain_name = ?
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, blockchainName);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (!rs.next()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return mapRow(rs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SolanaUserPdaCurrentEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
|
SolanaUserPdaCurrentEntry entry = new SolanaUserPdaCurrentEntry();
|
||||||
|
entry.setLogin(rs.getString("login"));
|
||||||
|
entry.setBlockchainName(rs.getString("blockchain_name"));
|
||||||
|
entry.setBlockchainKey(KeyEncodingUtil.normalizeKeyToBase64_32(rs.getString("blockchain_key")));
|
||||||
|
entry.setPaidLimitBytes(rs.getLong("paid_limit_bytes"));
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.MsgSubType;
|
import shine.db.MsgSubType;
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
|
|
||||||
import java.sql.*;
|
import java.sql.*;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -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) — для текста превью
|
||||||
*
|
*
|
||||||
@@ -25,7 +25,7 @@ import java.util.List;
|
|||||||
public final class SubscriptionsDAO {
|
public final class SubscriptionsDAO {
|
||||||
|
|
||||||
private static volatile SubscriptionsDAO instance;
|
private static volatile SubscriptionsDAO instance;
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
private SubscriptionsDAO() {}
|
private SubscriptionsDAO() {}
|
||||||
|
|
||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.entities.SyncServerEntry;
|
import shine.db.entities.SyncServerEntry;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
@@ -17,7 +17,7 @@ import java.util.List;
|
|||||||
public final class SyncServersDAO {
|
public final class SyncServersDAO {
|
||||||
|
|
||||||
private static volatile SyncServersDAO instance;
|
private static volatile SyncServersDAO instance;
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
private SyncServersDAO() {}
|
private SyncServersDAO() {}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.entities.TestFreeAvatarUploadEntry;
|
import shine.db.entities.TestFreeAvatarUploadEntry;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
@@ -11,7 +11,7 @@ import java.sql.SQLException;
|
|||||||
public final class TestFreeAvatarUploadsDAO {
|
public final class TestFreeAvatarUploadsDAO {
|
||||||
|
|
||||||
private static volatile TestFreeAvatarUploadsDAO instance;
|
private static volatile TestFreeAvatarUploadsDAO instance;
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
private TestFreeAvatarUploadsDAO() {
|
private TestFreeAvatarUploadsDAO() {
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ public final class TestFreeAvatarUploadsDAO {
|
|||||||
String sql = """
|
String sql = """
|
||||||
SELECT login, used_count, updated_at_ms, last_tx_id
|
SELECT login, used_count, updated_at_ms, last_tx_id
|
||||||
FROM test_free_avatar_uploads
|
FROM test_free_avatar_uploads
|
||||||
WHERE login = ? COLLATE NOCASE
|
WHERE LOWER(login) = LOWER(?)
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""";
|
""";
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
|||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
package shine.db.dao;
|
||||||
|
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.KeyEncodingUtil;
|
||||||
|
import shine.db.entities.UserAccessServerRouteEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DAO локальной routing-проекции access servers пользователей.
|
||||||
|
*/
|
||||||
|
public final class UserAccessServersCurrentDAO {
|
||||||
|
|
||||||
|
private static volatile UserAccessServersCurrentDAO instance;
|
||||||
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
|
private UserAccessServersCurrentDAO() {}
|
||||||
|
|
||||||
|
public static UserAccessServersCurrentDAO getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (UserAccessServersCurrentDAO.class) {
|
||||||
|
if (instance == null) instance = new UserAccessServersCurrentDAO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserAccessServerRouteEntry> listByUserLogin(String userLogin) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
return listByUserLogin(c, userLogin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserAccessServerRouteEntry> listByUserLogin(Connection c, String userLogin) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT user_login, server_login, server_url, server_client_key
|
||||||
|
FROM user_access_servers_current
|
||||||
|
WHERE LOWER(user_login) = LOWER(?)
|
||||||
|
ORDER BY server_login
|
||||||
|
""";
|
||||||
|
List<UserAccessServerRouteEntry> result = new ArrayList<>();
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, userLogin);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
result.add(mapRow(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> listUserLoginsByServerLogin(String serverLogin) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT user_login
|
||||||
|
FROM user_access_servers_current
|
||||||
|
WHERE LOWER(server_login) = LOWER(?)
|
||||||
|
ORDER BY user_login
|
||||||
|
""";
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, serverLogin);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
result.add(rs.getString("user_login"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserAccessServerRouteEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
|
UserAccessServerRouteEntry entry = new UserAccessServerRouteEntry();
|
||||||
|
entry.setUserLogin(rs.getString("user_login"));
|
||||||
|
entry.setServerLogin(rs.getString("server_login"));
|
||||||
|
entry.setServerUrl(rs.getString("server_url"));
|
||||||
|
entry.setServerClientKey(KeyEncodingUtil.normalizeKeyToBase64_32(rs.getString("server_client_key")));
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
package shine.db.dao;
|
|
||||||
|
|
||||||
import shine.db.SqliteDbController;
|
|
||||||
import shine.db.entities.SolanaUserEntry;
|
|
||||||
|
|
||||||
import java.sql.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserCreateDAO — атомарное добавление пользователя:
|
|
||||||
* - solana_users (login, blockchain_name, solana_key, blockchain_key, client_key)
|
|
||||||
* - blockchain_state (blockchain_name, login, blockchain_key, size_limit, ... last_block_number=-1 ...)
|
|
||||||
*
|
|
||||||
* ВАЖНО:
|
|
||||||
* - только INSERT (без перезаписи существующих записей)
|
|
||||||
* - если login или blockchainName заняты — возвращаем false (пользователь уже есть/занято)
|
|
||||||
*/
|
|
||||||
public final class UserCreateDAO {
|
|
||||||
|
|
||||||
private static volatile UserCreateDAO instance;
|
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
|
||||||
private final SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
|
|
||||||
|
|
||||||
private UserCreateDAO() {}
|
|
||||||
|
|
||||||
public static UserCreateDAO getInstance() {
|
|
||||||
if (instance == null) {
|
|
||||||
synchronized (UserCreateDAO.class) {
|
|
||||||
if (instance == null) instance = new UserCreateDAO();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return true если добавили; false если занято (login уже есть или blockchainName уже существует).
|
|
||||||
*/
|
|
||||||
public boolean insertUserWithBlockchain(
|
|
||||||
String login,
|
|
||||||
String blockchainName,
|
|
||||||
String solanaKey,
|
|
||||||
String blockchainKey,
|
|
||||||
String clientKey,
|
|
||||||
long sizeLimit,
|
|
||||||
long nowMs
|
|
||||||
) throws SQLException {
|
|
||||||
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
boolean oldAuto = c.getAutoCommit();
|
|
||||||
c.setAutoCommit(false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1) solana_users
|
|
||||||
SolanaUserEntry u = new SolanaUserEntry();
|
|
||||||
u.setLogin(login);
|
|
||||||
u.setBlockchainName(blockchainName);
|
|
||||||
u.setSolanaKey(solanaKey);
|
|
||||||
u.setBlockchainKey(blockchainKey);
|
|
||||||
u.setClientKey(clientKey);
|
|
||||||
|
|
||||||
usersDao.insert(c, u); // если login занят (NOCASE) или blockchainName (unique) -> constraint
|
|
||||||
|
|
||||||
// 2) blockchain_state — строго INSERT, без UPSERT (иначе можно перезаписать существующую цепочку)
|
|
||||||
insertBlockchainStateStrict(
|
|
||||||
c,
|
|
||||||
blockchainName,
|
|
||||||
login,
|
|
||||||
blockchainKey,
|
|
||||||
sizeLimit,
|
|
||||||
nowMs
|
|
||||||
);
|
|
||||||
|
|
||||||
c.commit();
|
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (SQLException e) {
|
|
||||||
c.rollback();
|
|
||||||
|
|
||||||
String msg = e.getMessage() == null ? "" : e.getMessage().toLowerCase();
|
|
||||||
if (msg.contains("constraint")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
c.setAutoCommit(oldAuto);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void insertBlockchainStateStrict(
|
|
||||||
Connection c,
|
|
||||||
String blockchainName,
|
|
||||||
String login,
|
|
||||||
String blockchainKey,
|
|
||||||
long sizeLimit,
|
|
||||||
long nowMs
|
|
||||||
) throws SQLException {
|
|
||||||
|
|
||||||
String sql = """
|
|
||||||
INSERT INTO blockchain_state (
|
|
||||||
blockchain_name,
|
|
||||||
login,
|
|
||||||
blockchain_key,
|
|
||||||
size_limit,
|
|
||||||
file_size_bytes,
|
|
||||||
last_block_number,
|
|
||||||
last_block_hash,
|
|
||||||
updated_at_ms
|
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
""";
|
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
|
||||||
int i = 1;
|
|
||||||
ps.setString(i++, blockchainName);
|
|
||||||
ps.setString(i++, login);
|
|
||||||
ps.setString(i++, blockchainKey);
|
|
||||||
|
|
||||||
ps.setLong(i++, sizeLimit);
|
|
||||||
ps.setLong(i++, 0L);
|
|
||||||
|
|
||||||
ps.setInt(i++, -1);
|
|
||||||
ps.setNull(i++, Types.BLOB); // старт: блоков ещё нет
|
|
||||||
ps.setLong(i++, nowMs);
|
|
||||||
|
|
||||||
ps.executeUpdate(); // если blockchainName занят -> constraint (PK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package shine.db.dao;
|
package shine.db.dao;
|
||||||
|
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.entities.UserParamEntry;
|
import shine.db.entities.UserParamEntry;
|
||||||
|
|
||||||
import java.sql.*;
|
import java.sql.*;
|
||||||
@@ -21,7 +21,7 @@ import java.util.List;
|
|||||||
public final class UserParamsDAO {
|
public final class UserParamsDAO {
|
||||||
|
|
||||||
private static volatile UserParamsDAO instance;
|
private static volatile UserParamsDAO instance;
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
private UserParamsDAO() { }
|
private UserParamsDAO() { }
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ public final class UserParamsDAO {
|
|||||||
client_key,
|
client_key,
|
||||||
signature
|
signature
|
||||||
FROM users_params
|
FROM users_params
|
||||||
WHERE login = ? COLLATE NOCASE AND param = ?
|
WHERE LOWER(login) = LOWER(?) AND param = ?
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""";
|
""";
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ public final class UserParamsDAO {
|
|||||||
client_key,
|
client_key,
|
||||||
signature
|
signature
|
||||||
FROM users_params
|
FROM users_params
|
||||||
WHERE login = ? COLLATE NOCASE
|
WHERE LOWER(login) = LOWER(?)
|
||||||
ORDER BY time_ms DESC
|
ORDER BY time_ms DESC
|
||||||
""";
|
""";
|
||||||
|
|
||||||
|
|||||||
+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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-7
@@ -3,18 +3,19 @@ package shine.db.entities;
|
|||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SolanaUserEntry — локальная запись пользователя из Solana.
|
* CurrentUserEntry — локальная runtime-проекция текущего состояния пользователя.
|
||||||
*
|
*
|
||||||
* Таблица: solana_users
|
* Источник:
|
||||||
|
* - solana_user_pda_current
|
||||||
*
|
*
|
||||||
* Поля:
|
* Поля:
|
||||||
* - login — PRIMARY KEY (TEXT) (case-insensitive на уровне COLLATE NOCASE)
|
* - login — PRIMARY KEY (TEXT)
|
||||||
* - blockchain_name — TEXT NOT NULL
|
* - blockchain_name — TEXT NOT NULL
|
||||||
* - solana_key — TEXT NOT NULL
|
* - solana_key — TEXT NOT NULL
|
||||||
* - blockchain_key — TEXT NOT NULL
|
* - blockchain_key — TEXT NOT NULL
|
||||||
* - client_key — TEXT NOT NULL
|
* - client_key — TEXT NOT NULL
|
||||||
*/
|
*/
|
||||||
public class SolanaUserEntry {
|
public class CurrentUserEntry {
|
||||||
|
|
||||||
private String login;
|
private String login;
|
||||||
|
|
||||||
@@ -29,9 +30,9 @@ public class SolanaUserEntry {
|
|||||||
/** Ключ устройства (публичный ключ устройства) */
|
/** Ключ устройства (публичный ключ устройства) */
|
||||||
private String clientKey;
|
private String clientKey;
|
||||||
|
|
||||||
public SolanaUserEntry() {}
|
public CurrentUserEntry() {}
|
||||||
|
|
||||||
public SolanaUserEntry(String login,
|
public CurrentUserEntry(String login,
|
||||||
String blockchainName,
|
String blockchainName,
|
||||||
String solanaKey,
|
String solanaKey,
|
||||||
String blockchainKey,
|
String blockchainKey,
|
||||||
@@ -81,4 +82,4 @@ public class SolanaUserEntry {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
package shine.db.entities;
|
||||||
|
|
||||||
|
public class DmSyncPeerStateEntry {
|
||||||
|
private String ownerLogin;
|
||||||
|
private String remoteServerLogin;
|
||||||
|
private String remoteServerUrl;
|
||||||
|
private long cursorStoredAtMs;
|
||||||
|
private String cursorMessageKey;
|
||||||
|
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 getCursorStoredAtMs() { return cursorStoredAtMs; }
|
||||||
|
public void setCursorStoredAtMs(long cursorStoredAtMs) { this.cursorStoredAtMs = cursorStoredAtMs; }
|
||||||
|
public String getCursorMessageKey() { return cursorMessageKey; }
|
||||||
|
public void setCursorMessageKey(String cursorMessageKey) { this.cursorMessageKey = cursorMessageKey; }
|
||||||
|
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; }
|
||||||
|
}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
package shine.db.entities;
|
package shine.db.entities;
|
||||||
|
|
||||||
public class SignedMessageV2Entry {
|
public class SignedMessageEntry {
|
||||||
private String messageKey;
|
private String messageKey;
|
||||||
private String baseKey;
|
private String baseKey;
|
||||||
private String targetLogin;
|
private String targetLogin;
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
package shine.db.entities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Минимальный срез текущей записи пользователя из PostgreSQL sync-таблицы.
|
||||||
|
*
|
||||||
|
* Источник: solana_user_pda_current
|
||||||
|
*/
|
||||||
|
public final class SolanaUserPdaCurrentEntry {
|
||||||
|
|
||||||
|
private String login;
|
||||||
|
private String blockchainName;
|
||||||
|
private String blockchainKey;
|
||||||
|
private long paidLimitBytes;
|
||||||
|
|
||||||
|
public String getLogin() {
|
||||||
|
return login;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLogin(String login) {
|
||||||
|
this.login = login;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBlockchainName() {
|
||||||
|
return blockchainName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBlockchainName(String blockchainName) {
|
||||||
|
this.blockchainName = blockchainName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBlockchainKey() {
|
||||||
|
return blockchainKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBlockchainKey(String blockchainKey) {
|
||||||
|
this.blockchainKey = blockchainKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getPaidLimitBytes() {
|
||||||
|
return paidLimitBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPaidLimitBytes(long paidLimitBytes) {
|
||||||
|
this.paidLimitBytes = paidLimitBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
package shine.db.entities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Локальная routing-проекция access server пользователя.
|
||||||
|
*
|
||||||
|
* Источник:
|
||||||
|
* - user_access_servers_current
|
||||||
|
*/
|
||||||
|
public class UserAccessServerRouteEntry {
|
||||||
|
|
||||||
|
private String userLogin;
|
||||||
|
private String serverLogin;
|
||||||
|
private String serverUrl;
|
||||||
|
private String serverClientKey;
|
||||||
|
|
||||||
|
public String getUserLogin() { return userLogin; }
|
||||||
|
public void setUserLogin(String userLogin) { this.userLogin = userLogin; }
|
||||||
|
|
||||||
|
public String getServerLogin() { return serverLogin; }
|
||||||
|
public void setServerLogin(String serverLogin) { this.serverLogin = serverLogin; }
|
||||||
|
|
||||||
|
public String getServerUrl() { return serverUrl; }
|
||||||
|
public void setServerUrl(String serverUrl) { this.serverUrl = serverUrl; }
|
||||||
|
|
||||||
|
public String getServerClientKey() { return serverClientKey; }
|
||||||
|
public void setServerClientKey(String serverClientKey) { this.serverClientKey = serverClientKey; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package shine.db.sql;
|
||||||
|
|
||||||
|
public final class CurrentUsersSql {
|
||||||
|
|
||||||
|
private CurrentUsersSql() {}
|
||||||
|
|
||||||
|
public static String usersSubquery(String alias) {
|
||||||
|
if (alias == null || alias.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("alias is blank");
|
||||||
|
}
|
||||||
|
return """
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
current_users.login AS login,
|
||||||
|
current_users.blockchain_name AS blockchain_name,
|
||||||
|
current_users.client_key AS solana_key,
|
||||||
|
current_users.blockchain_key AS blockchain_key,
|
||||||
|
current_users.client_key AS client_key,
|
||||||
|
current_users.is_server AS is_server
|
||||||
|
FROM solana_user_pda_current current_users
|
||||||
|
) %s
|
||||||
|
""".formatted(alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_access_servers_current (
|
||||||
|
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_url TEXT NOT NULL,
|
||||||
|
server_client_key TEXT NOT NULL,
|
||||||
|
user_record_number INTEGER NOT NULL,
|
||||||
|
user_updated_at_ms BIGINT NOT NULL,
|
||||||
|
server_record_number INTEGER NOT NULL,
|
||||||
|
server_updated_at_ms BIGINT NOT NULL,
|
||||||
|
refreshed_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (user_login, server_login)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_access_servers_user
|
||||||
|
ON user_access_servers_current(user_login);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_access_servers_server
|
||||||
|
ON user_access_servers_current(server_login);
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_refresh_user_access_servers_for_user(p_user_login TEXT)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF p_user_login IS NULL OR btrim(p_user_login) = '' THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
DELETE FROM user_access_servers_current
|
||||||
|
WHERE LOWER(user_login) = LOWER(p_user_login);
|
||||||
|
|
||||||
|
INSERT INTO user_access_servers_current (
|
||||||
|
user_login,
|
||||||
|
server_login,
|
||||||
|
server_url,
|
||||||
|
server_client_key,
|
||||||
|
user_record_number,
|
||||||
|
user_updated_at_ms,
|
||||||
|
server_record_number,
|
||||||
|
server_updated_at_ms,
|
||||||
|
refreshed_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
u.login,
|
||||||
|
s.login,
|
||||||
|
s.server_address,
|
||||||
|
s.client_key,
|
||||||
|
u.record_number,
|
||||||
|
u.updated_at_ms,
|
||||||
|
s.record_number,
|
||||||
|
s.updated_at_ms,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
FROM solana_user_pda_current u
|
||||||
|
CROSS JOIN LATERAL (
|
||||||
|
SELECT login_value
|
||||||
|
FROM jsonb_array_elements_text(
|
||||||
|
CASE
|
||||||
|
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||||
|
ELSE u.access_servers_json::jsonb
|
||||||
|
END
|
||||||
|
) WITH ORDINALITY AS access_server(login_value, ord)
|
||||||
|
WHERE ord <= 2
|
||||||
|
) AS access_server
|
||||||
|
JOIN solana_user_pda_current s
|
||||||
|
ON LOWER(s.login) = LOWER(btrim(access_server.login_value))
|
||||||
|
AND s.is_server = TRUE
|
||||||
|
AND btrim(COALESCE(s.server_address, '')) <> ''
|
||||||
|
WHERE LOWER(u.login) = LOWER(p_user_login)
|
||||||
|
ON CONFLICT (user_login, server_login) DO UPDATE SET
|
||||||
|
server_url = EXCLUDED.server_url,
|
||||||
|
server_client_key = EXCLUDED.server_client_key,
|
||||||
|
user_record_number = EXCLUDED.user_record_number,
|
||||||
|
user_updated_at_ms = EXCLUDED.user_updated_at_ms,
|
||||||
|
server_record_number = EXCLUDED.server_record_number,
|
||||||
|
server_updated_at_ms = EXCLUDED.server_updated_at_ms,
|
||||||
|
refreshed_at_ms = EXCLUDED.refreshed_at_ms;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_refresh_user_access_servers_for_server(p_server_login TEXT)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
affected_user RECORD;
|
||||||
|
BEGIN
|
||||||
|
IF p_server_login IS NULL OR btrim(p_server_login) = '' THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
DELETE FROM user_access_servers_current
|
||||||
|
WHERE LOWER(server_login) = LOWER(p_server_login);
|
||||||
|
|
||||||
|
FOR affected_user IN
|
||||||
|
SELECT u.login
|
||||||
|
FROM solana_user_pda_current u
|
||||||
|
CROSS JOIN LATERAL (
|
||||||
|
SELECT login_value
|
||||||
|
FROM jsonb_array_elements_text(
|
||||||
|
CASE
|
||||||
|
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||||
|
ELSE u.access_servers_json::jsonb
|
||||||
|
END
|
||||||
|
) WITH ORDINALITY AS access_server(login_value, ord)
|
||||||
|
WHERE ord <= 2
|
||||||
|
) AS access_server
|
||||||
|
WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login)
|
||||||
|
LOOP
|
||||||
|
PERFORM shine_refresh_user_access_servers_for_user(affected_user.login);
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_refresh_user_access_servers_all()
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
affected_user RECORD;
|
||||||
|
BEGIN
|
||||||
|
TRUNCATE TABLE user_access_servers_current;
|
||||||
|
|
||||||
|
FOR affected_user IN
|
||||||
|
SELECT login
|
||||||
|
FROM solana_user_pda_current
|
||||||
|
LOOP
|
||||||
|
PERFORM shine_refresh_user_access_servers_for_user(affected_user.login);
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION trg_refresh_user_access_servers_from_user_pda_row()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF TG_OP = 'DELETE' THEN
|
||||||
|
PERFORM shine_refresh_user_access_servers_for_user(OLD.login);
|
||||||
|
PERFORM shine_refresh_user_access_servers_for_server(OLD.login);
|
||||||
|
RETURN OLD;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF TG_OP = 'UPDATE' AND LOWER(OLD.login) <> LOWER(NEW.login) THEN
|
||||||
|
PERFORM shine_refresh_user_access_servers_for_user(OLD.login);
|
||||||
|
PERFORM shine_refresh_user_access_servers_for_server(OLD.login);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
PERFORM shine_refresh_user_access_servers_for_user(NEW.login);
|
||||||
|
PERFORM shine_refresh_user_access_servers_for_server(NEW.login);
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION trg_refresh_user_access_servers_from_user_pda_truncate()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
TRUNCATE TABLE user_access_servers_current;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_refresh_user_access_servers_row ON solana_user_pda_current;
|
||||||
|
CREATE TRIGGER trg_refresh_user_access_servers_row
|
||||||
|
AFTER INSERT OR UPDATE OR DELETE ON solana_user_pda_current
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION trg_refresh_user_access_servers_from_user_pda_row();
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_refresh_user_access_servers_truncate ON solana_user_pda_current;
|
||||||
|
CREATE TRIGGER trg_refresh_user_access_servers_truncate
|
||||||
|
AFTER TRUNCATE ON solana_user_pda_current
|
||||||
|
FOR EACH STATEMENT
|
||||||
|
EXECUTE FUNCTION trg_refresh_user_access_servers_from_user_pda_truncate();
|
||||||
|
|
||||||
|
SELECT shine_refresh_user_access_servers_all();
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 2, 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 INDEX IF NOT EXISTS idx_signed_messages_sync_cursor
|
||||||
|
ON signed_messages(target_login, created_at_ms, message_key);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_signed_messages_sync_cursor_lower
|
||||||
|
ON signed_messages(LOWER(target_login), created_at_ms, message_key);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_signed_messages_delete_from_sync_cursor
|
||||||
|
ON signed_messages(from_login, created_at_ms, message_key)
|
||||||
|
WHERE message_type IN (5, 6, 7, 8);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_signed_messages_delete_to_sync_cursor
|
||||||
|
ON signed_messages(to_login, created_at_ms, message_key)
|
||||||
|
WHERE message_type IN (5, 6, 7, 8);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_signed_messages_delete_from_sync_cursor_lower
|
||||||
|
ON signed_messages(LOWER(from_login), created_at_ms, message_key)
|
||||||
|
WHERE message_type IN (5, 6, 7, 8);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_signed_messages_delete_to_sync_cursor_lower
|
||||||
|
ON signed_messages(LOWER(to_login), created_at_ms, message_key)
|
||||||
|
WHERE message_type IN (5, 6, 7, 8);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_sync_peer_state (
|
||||||
|
owner_login TEXT NOT NULL,
|
||||||
|
remote_server_login TEXT NOT NULL,
|
||||||
|
remote_server_url TEXT NOT NULL,
|
||||||
|
cursor_stored_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
cursor_message_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_dm_sync_peer_state_owner
|
||||||
|
ON dm_sync_peer_state(owner_login);
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 3, 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,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;
|
||||||
File diff suppressed because it is too large
Load Diff
+10
-10
@@ -1,7 +1,7 @@
|
|||||||
package server.logic.ws_protocol.JSON;
|
package server.logic.ws_protocol.JSON;
|
||||||
|
|
||||||
import org.eclipse.jetty.websocket.api.Session;
|
import org.eclipse.jetty.websocket.api.Session;
|
||||||
import shine.db.entities.SolanaUserEntry;
|
import shine.db.entities.CurrentUserEntry;
|
||||||
import shine.db.entities.ActiveSessionEntry;
|
import shine.db.entities.ActiveSessionEntry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,8 +25,8 @@ public class ConnectionContext {
|
|||||||
public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1; // выполнен challenge (AuthChallenge или SessionChallenge)
|
public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1; // выполнен challenge (AuthChallenge или SessionChallenge)
|
||||||
public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь
|
public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь
|
||||||
|
|
||||||
// Полный пользователь из БД (solana_users)
|
// Полный пользователь из runtime БД (current users / solana_user_pda_current)
|
||||||
private SolanaUserEntry solanaUserEntry;
|
private CurrentUserEntry currentUserEntry;
|
||||||
|
|
||||||
// Активная сессия из БД (active_sessions)
|
// Активная сессия из БД (active_sessions)
|
||||||
private ActiveSessionEntry activeSessionEntry;
|
private ActiveSessionEntry activeSessionEntry;
|
||||||
@@ -89,12 +89,12 @@ public class ConnectionContext {
|
|||||||
|
|
||||||
// --- SolanaUser / ActiveSession ---
|
// --- SolanaUser / ActiveSession ---
|
||||||
|
|
||||||
public SolanaUserEntry getSolanaUser() {
|
public CurrentUserEntry getCurrentUser() {
|
||||||
return solanaUserEntry;
|
return currentUserEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSolanaUser(SolanaUserEntry solanaUserEntry) {
|
public void setCurrentUser(CurrentUserEntry currentUserEntry) {
|
||||||
this.solanaUserEntry = solanaUserEntry;
|
this.currentUserEntry = currentUserEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ActiveSessionEntry getActiveSession() {
|
public ActiveSessionEntry getActiveSession() {
|
||||||
@@ -108,7 +108,7 @@ public class ConnectionContext {
|
|||||||
// --- Удобный геттер для логина ---
|
// --- Удобный геттер для логина ---
|
||||||
|
|
||||||
public String getLogin() {
|
public String getLogin() {
|
||||||
return solanaUserEntry != null ? solanaUserEntry.getLogin() : null;
|
return currentUserEntry != null ? currentUserEntry.getLogin() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- sessionId ---
|
// --- sessionId ---
|
||||||
@@ -176,7 +176,7 @@ public class ConnectionContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void reset() {
|
public void reset() {
|
||||||
solanaUserEntry = null;
|
currentUserEntry = null;
|
||||||
activeSessionEntry = null;
|
activeSessionEntry = null;
|
||||||
|
|
||||||
sessionId = null;
|
sessionId = null;
|
||||||
@@ -198,4 +198,4 @@ public class ConnectionContext {
|
|||||||
", authenticationStatus=" + authenticationStatus +
|
", authenticationStatus=" + authenticationStatus +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-5
@@ -43,9 +43,6 @@ import server.logic.ws_protocol.JSON.handlers.blockchain.Net_GetBlockchainBlock_
|
|||||||
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_GetBlockchainBlock_Request;
|
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_GetBlockchainBlock_Request;
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_GetUser_Handler;
|
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_GetUser_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_TestGetFreeAvatarQuota_Handler;
|
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_TestGetFreeAvatarQuota_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_TestUploadFreeAvatar_Handler;
|
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_TestUploadFreeAvatar_Handler;
|
||||||
@@ -70,6 +67,7 @@ import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriend
|
|||||||
import server.logic.ws_protocol.JSON.handlers.channels.ChannelNamesStateBootstrapper;
|
import server.logic.ws_protocol.JSON.handlers.channels.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;
|
||||||
@@ -78,6 +76,7 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelsC
|
|||||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelMessages_Request;
|
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_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;
|
||||||
@@ -91,6 +90,7 @@ 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_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_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;
|
||||||
@@ -103,6 +103,7 @@ 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_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_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;
|
||||||
@@ -142,7 +143,6 @@ public final class JsonHandlerRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final Map<String, JsonMessageHandler> HANDLERS = Map.ofEntries(
|
private static final Map<String, JsonMessageHandler> HANDLERS = Map.ofEntries(
|
||||||
Map.entry("AddUser", new Net_AddUser_Handler()),
|
|
||||||
Map.entry("GetUser", new Net_GetUser_Handler()),
|
Map.entry("GetUser", new Net_GetUser_Handler()),
|
||||||
Map.entry("SearchUsers", new Net_SearchUsers_Handler()),
|
Map.entry("SearchUsers", new Net_SearchUsers_Handler()),
|
||||||
Map.entry("TestGetFreeAvatarQuota", new Net_TestGetFreeAvatarQuota_Handler()),
|
Map.entry("TestGetFreeAvatarQuota", new Net_TestGetFreeAvatarQuota_Handler()),
|
||||||
@@ -186,6 +186,7 @@ public final class JsonHandlerRegistry {
|
|||||||
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()),
|
||||||
@@ -202,6 +203,7 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("ReceiveIncomingMessage", new Net_ReceiveIncomingMessage_Handler()),
|
Map.entry("ReceiveIncomingMessage", new Net_ReceiveIncomingMessage_Handler()),
|
||||||
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("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()),
|
||||||
@@ -223,7 +225,6 @@ public final class JsonHandlerRegistry {
|
|||||||
);
|
);
|
||||||
|
|
||||||
private static final Map<String, Class<? extends Net_Request>> REQUEST_TYPES = Map.ofEntries(
|
private static final Map<String, Class<? extends Net_Request>> REQUEST_TYPES = Map.ofEntries(
|
||||||
Map.entry("AddUser", Net_AddUser_Request.class),
|
|
||||||
Map.entry("GetUser", Net_GetUser_Request.class),
|
Map.entry("GetUser", Net_GetUser_Request.class),
|
||||||
Map.entry("SearchUsers", Net_SearchUsers_Request.class),
|
Map.entry("SearchUsers", Net_SearchUsers_Request.class),
|
||||||
Map.entry("TestGetFreeAvatarQuota", Net_TestGetFreeAvatarQuota_Request.class),
|
Map.entry("TestGetFreeAvatarQuota", Net_TestGetFreeAvatarQuota_Request.class),
|
||||||
@@ -267,6 +268,7 @@ public final class JsonHandlerRegistry {
|
|||||||
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),
|
||||||
@@ -283,6 +285,7 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("ReceiveIncomingMessage", Net_ReceiveIncomingMessage_Request.class),
|
Map.entry("ReceiveIncomingMessage", Net_ReceiveIncomingMessage_Request.class),
|
||||||
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("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),
|
||||||
|
|||||||
+6
-22
@@ -9,8 +9,8 @@ import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Re
|
|||||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Response;
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_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.SolanaUsersDAO;
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
import shine.db.entities.SolanaUserEntry;
|
import shine.db.entities.CurrentUserEntry;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ import java.security.SecureRandom;
|
|||||||
*
|
*
|
||||||
* Что делает:
|
* Что делает:
|
||||||
* 1) Проверяет login.
|
* 1) Проверяет login.
|
||||||
* 2) Находит пользователя (solana_users).
|
* 2) Находит пользователя в текущем PDA snapshot.
|
||||||
* 3) Пишет solanaUser в ctx, ставит AUTH_STATUS_AUTH_IN_PROGRESS.
|
* 3) Пишет solanaUser в ctx, ставит AUTH_STATUS_AUTH_IN_PROGRESS.
|
||||||
* 4) Генерирует authNonce (base64url(32)) и сохраняет в ctx.authNonce.
|
* 4) Генерирует authNonce (base64url(32)) и сохраняет в ctx.authNonce.
|
||||||
*/
|
*/
|
||||||
@@ -60,33 +60,17 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SolanaUserEntry solanaUserEntry = SolanaUsersDAO.getInstance().getByLogin(login);
|
CurrentUserEntry solanaUserEntry = CurrentUsersDAO.getInstance().getByLogin(login);
|
||||||
if (solanaUserEntry == null) {
|
|
||||||
try {
|
|
||||||
solanaUserEntry = SolanaUserPdaImportService.findOrImportByLogin(login);
|
|
||||||
if (solanaUserEntry != null) {
|
|
||||||
log.info("AuthChallenge: пользователь {} импортирован из Solana PDA", solanaUserEntry.getLogin());
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("AuthChallenge: ошибка lazy-import пользователя {} из Solana", login, e);
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.SERVER_DATA_ERROR,
|
|
||||||
"SOLANA_IMPORT_FAILED",
|
|
||||||
"Ошибка проверки пользователя в Solana"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (solanaUserEntry == null) {
|
if (solanaUserEntry == null) {
|
||||||
return NetExceptionResponseFactory.error(
|
return NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
WireCodes.Status.UNVERIFIED,
|
WireCodes.Status.UNVERIFIED,
|
||||||
"UNKNOWN_USER",
|
"UNKNOWN_USER",
|
||||||
"Пользователь с таким логином не найден"
|
"Пользователь с таким логином не найден"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.setSolanaUser(solanaUserEntry);
|
ctx.setCurrentUser(solanaUserEntry);
|
||||||
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS);
|
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS);
|
||||||
|
|
||||||
byte[] buf = new byte[32];
|
byte[] buf = new byte[32];
|
||||||
|
|||||||
+4
-4
@@ -18,7 +18,7 @@ import server.logic.ws_protocol.WireCodes;
|
|||||||
import server.ws.WsConnectionUtils;
|
import server.ws.WsConnectionUtils;
|
||||||
import shine.db.dao.ActiveSessionsDAO;
|
import shine.db.dao.ActiveSessionsDAO;
|
||||||
import shine.db.entities.ActiveSessionEntry;
|
import shine.db.entities.ActiveSessionEntry;
|
||||||
import shine.db.entities.SolanaUserEntry;
|
import shine.db.entities.CurrentUserEntry;
|
||||||
|
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
|
|||||||
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
||||||
Net_CloseActiveSession_Request req = (Net_CloseActiveSession_Request) baseReq;
|
Net_CloseActiveSession_Request req = (Net_CloseActiveSession_Request) baseReq;
|
||||||
|
|
||||||
if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
|
if (ctx == null || ctx.getCurrentUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
|
||||||
return NetExceptionResponseFactory.error(
|
return NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
WireCodes.Status.UNVERIFIED,
|
WireCodes.Status.UNVERIFIED,
|
||||||
@@ -51,7 +51,7 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SolanaUserEntry user = ctx.getSolanaUser();
|
CurrentUserEntry user = ctx.getCurrentUser();
|
||||||
String currentLogin = user.getLogin();
|
String currentLogin = user.getLogin();
|
||||||
|
|
||||||
String targetSessionId = req.getSessionId();
|
String targetSessionId = req.getSessionId();
|
||||||
@@ -152,4 +152,4 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -15,9 +15,9 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
|||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import server.ws.WsConnectionUtils;
|
import server.ws.WsConnectionUtils;
|
||||||
import shine.db.dao.ActiveSessionsDAO;
|
import shine.db.dao.ActiveSessionsDAO;
|
||||||
import shine.db.dao.SolanaUsersDAO;
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
import shine.db.entities.ActiveSessionEntry;
|
import shine.db.entities.ActiveSessionEntry;
|
||||||
import shine.db.entities.SolanaUserEntry;
|
import shine.db.entities.CurrentUserEntry;
|
||||||
import shine.geo.ClientInfoService;
|
import shine.geo.ClientInfoService;
|
||||||
import shine.geo.GeoLookupService;
|
import shine.geo.GeoLookupService;
|
||||||
import utils.crypto.Ed25519Util;
|
import utils.crypto.Ed25519Util;
|
||||||
@@ -58,7 +58,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
Net_CreateAuthSession_Request req = (Net_CreateAuthSession_Request) baseReq;
|
Net_CreateAuthSession_Request req = (Net_CreateAuthSession_Request) baseReq;
|
||||||
|
|
||||||
if (ctx == null
|
if (ctx == null
|
||||||
|| ctx.getSolanaUser() == null
|
|| ctx.getCurrentUser() == null
|
||||||
|| ctx.getAuthNonce() == null
|
|| ctx.getAuthNonce() == null
|
||||||
|| ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
|
|| ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
SolanaUserEntry userFromContext = ctx.getSolanaUser();
|
CurrentUserEntry userFromContext = ctx.getCurrentUser();
|
||||||
String loginFromContext = userFromContext.getLogin();
|
String loginFromContext = userFromContext.getLogin();
|
||||||
String loginFromReq = req.getLogin();
|
String loginFromReq = req.getLogin();
|
||||||
if (loginFromReq == null || loginFromReq.isBlank()) {
|
if (loginFromReq == null || loginFromReq.isBlank()) {
|
||||||
@@ -97,9 +97,9 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
SolanaUserEntry user;
|
CurrentUserEntry user;
|
||||||
try {
|
try {
|
||||||
user = SolanaUsersDAO.getInstance().getByLogin(loginFromContext);
|
user = CurrentUsersDAO.getInstance().getByLogin(loginFromContext);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
Net_Response err = NetExceptionResponseFactory.error(
|
Net_Response err = NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
|
|||||||
+3
-3
@@ -14,7 +14,7 @@ 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.ActiveSessionsDAO;
|
||||||
import shine.db.entities.ActiveSessionEntry;
|
import shine.db.entities.ActiveSessionEntry;
|
||||||
import shine.db.entities.SolanaUserEntry;
|
import shine.db.entities.CurrentUserEntry;
|
||||||
import shine.geo.GeoLookupService;
|
import shine.geo.GeoLookupService;
|
||||||
|
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
@@ -36,7 +36,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
|
|||||||
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
||||||
Net_ListSessions_Request req = (Net_ListSessions_Request) baseReq;
|
Net_ListSessions_Request req = (Net_ListSessions_Request) baseReq;
|
||||||
|
|
||||||
if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
|
if (ctx == null || ctx.getCurrentUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
|
||||||
return NetExceptionResponseFactory.error(
|
return NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
WireCodes.Status.UNVERIFIED,
|
WireCodes.Status.UNVERIFIED,
|
||||||
@@ -45,7 +45,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SolanaUserEntry user = ctx.getSolanaUser();
|
CurrentUserEntry user = ctx.getCurrentUser();
|
||||||
String currentLogin = user.getLogin();
|
String currentLogin = user.getLogin();
|
||||||
|
|
||||||
List<ActiveSessionEntry> sessions;
|
List<ActiveSessionEntry> sessions;
|
||||||
|
|||||||
+5
-5
@@ -14,9 +14,9 @@ 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.ActiveSessionsDAO;
|
||||||
import shine.db.dao.SolanaUsersDAO;
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
import shine.db.entities.ActiveSessionEntry;
|
import shine.db.entities.ActiveSessionEntry;
|
||||||
import shine.db.entities.SolanaUserEntry;
|
import shine.db.entities.CurrentUserEntry;
|
||||||
import shine.geo.ClientInfoService;
|
import shine.geo.ClientInfoService;
|
||||||
import shine.geo.GeoLookupService;
|
import shine.geo.GeoLookupService;
|
||||||
import utils.crypto.Ed25519Util;
|
import utils.crypto.Ed25519Util;
|
||||||
@@ -188,9 +188,9 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler {
|
|||||||
ctx.setSessionLoginNonceExpiresAtMs(0);
|
ctx.setSessionLoginNonceExpiresAtMs(0);
|
||||||
|
|
||||||
// подтягиваем пользователя
|
// подтягиваем пользователя
|
||||||
SolanaUserEntry user;
|
CurrentUserEntry user;
|
||||||
try {
|
try {
|
||||||
user = SolanaUsersDAO.getInstance().getByLogin(session.getLogin());
|
user = CurrentUsersDAO.getInstance().getByLogin(session.getLogin());
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
return NetExceptionResponseFactory.error(
|
return NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
@@ -294,7 +294,7 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
// ctx
|
// ctx
|
||||||
ctx.setActiveSession(session);
|
ctx.setActiveSession(session);
|
||||||
ctx.setSolanaUser(user);
|
ctx.setCurrentUser(user);
|
||||||
ctx.setSessionId(sessionId);
|
ctx.setSessionId(sessionId);
|
||||||
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
|
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -15,10 +15,10 @@ import server.logic.ws_protocol.JSON.utils.NetIdGenerator;
|
|||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import shine.db.dao.EspPairingRequestsDAO;
|
import shine.db.dao.EspPairingRequestsDAO;
|
||||||
import shine.db.dao.EspPairingSettingsDAO;
|
import shine.db.dao.EspPairingSettingsDAO;
|
||||||
import shine.db.dao.SolanaUsersDAO;
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
import shine.db.entities.EspPairingRequestEntry;
|
import shine.db.entities.EspPairingRequestEntry;
|
||||||
import shine.db.entities.EspPairingSettingsEntry;
|
import shine.db.entities.EspPairingSettingsEntry;
|
||||||
import shine.db.entities.SolanaUserEntry;
|
import shine.db.entities.CurrentUserEntry;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ public class Net_StartEspPairing_Handler implements JsonMessageHandler {
|
|||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_PASSWORD_HASH_FORMAT", "passwordHash должен быть пустым или иметь формат sha256$<64 hex>");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_PASSWORD_HASH_FORMAT", "passwordHash должен быть пустым или иметь формат sha256$<64 hex>");
|
||||||
}
|
}
|
||||||
|
|
||||||
SolanaUserEntry user = SolanaUsersDAO.getInstance().getByLogin(login);
|
CurrentUserEntry user = CurrentUsersDAO.getInstance().getByLogin(login);
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
return NetExceptionResponseFactory.error(req, 422, "PAIRING_NOT_AVAILABLE", "Для этого login pairing недоступен");
|
return NetExceptionResponseFactory.error(req, 422, "PAIRING_NOT_AVAILABLE", "Для этого login pairing недоступен");
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-34
@@ -4,9 +4,6 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import shine.db.dao.SolanaUsersDAO;
|
|
||||||
import shine.db.dao.UserCreateDAO;
|
|
||||||
import shine.db.entities.SolanaUserEntry;
|
|
||||||
import utils.config.SolanaProgramsConfig;
|
import utils.config.SolanaProgramsConfig;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
@@ -22,8 +19,7 @@ import java.util.Locale;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazy-import пользователя из Solana PDA в локальную БД сервера.
|
* Чтение пользовательских и серверных PDA из Solana.
|
||||||
* Используется при входе, если в solana_users нет записи по login.
|
|
||||||
*/
|
*/
|
||||||
public final class SolanaUserPdaImportService {
|
public final class SolanaUserPdaImportService {
|
||||||
|
|
||||||
@@ -31,37 +27,10 @@ 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() {}
|
||||||
|
|
||||||
public static SolanaUserEntry findOrImportByLogin(String loginRaw) throws Exception {
|
|
||||||
String login = normalizeLogin(loginRaw);
|
|
||||||
if (login == null) return null;
|
|
||||||
|
|
||||||
SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
|
|
||||||
SolanaUserEntry existing = usersDao.getByLogin(login);
|
|
||||||
if (existing != null) return existing;
|
|
||||||
|
|
||||||
ParsedSolanaUser parsed = fetchFromSolana(login);
|
|
||||||
if (parsed == null) return null;
|
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
|
||||||
long sizeLimit = parsed.paidLimitBytes > 0 ? parsed.paidLimitBytes : 100_000L;
|
|
||||||
boolean inserted = UserCreateDAO.getInstance().insertUserWithBlockchain(
|
|
||||||
parsed.login,
|
|
||||||
parsed.blockchainName,
|
|
||||||
parsed.clientKeyB64, // в текущей модели solanaKey = clientKey
|
|
||||||
parsed.blockchainKeyB64,
|
|
||||||
parsed.clientKeyB64,
|
|
||||||
sizeLimit,
|
|
||||||
now
|
|
||||||
);
|
|
||||||
if (!inserted) {
|
|
||||||
return usersDao.getByLogin(login);
|
|
||||||
}
|
|
||||||
return usersDao.getByLogin(login);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SessionTypeCheckResult checkSessionTypeAgainstPda(String loginRaw, String sessionKeyApi, int requestedSessionType) throws Exception {
|
public static SessionTypeCheckResult checkSessionTypeAgainstPda(String loginRaw, String sessionKeyApi, int requestedSessionType) throws Exception {
|
||||||
String login = normalizeLogin(loginRaw);
|
String login = normalizeLogin(loginRaw);
|
||||||
if (login == null) return SessionTypeCheckResult.noRecord();
|
if (login == null) return SessionTypeCheckResult.noRecord();
|
||||||
@@ -119,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());
|
||||||
}
|
}
|
||||||
@@ -266,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++);
|
||||||
|
|||||||
+4
-40
@@ -33,46 +33,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
1. Добавление пользователя (AddUser)
|
1. Пользователь уже существует в актуальном PDA snapshot
|
||||||
|
|
||||||
Назначение: создать локальную запись пользователя с двумя ключами — solanaKey и clientKey.
|
Назначение: перед началом auth-flow сервер уже должен видеть пользователя
|
||||||
|
в актуальном `solana_user_pda_current`.
|
||||||
📤 Запрос клиента
|
|
||||||
{
|
|
||||||
"op": "AddUser",
|
|
||||||
"requestId": "req-1",
|
|
||||||
"payload": {
|
|
||||||
"login": "anya4",
|
|
||||||
"loginId": 100212,
|
|
||||||
"bchId": 4222,
|
|
||||||
"solanaKey": "BASE64_LOGIN_KEY",
|
|
||||||
"clientKey": "BASE64_DEVICE_KEY",
|
|
||||||
"bchLimit": 1000000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
🖥 Действия сервера
|
|
||||||
|
|
||||||
Проверяет корректность данных.
|
|
||||||
|
|
||||||
Вставляет запись в таблицу:
|
|
||||||
|
|
||||||
CREATE TABLE solana_users (
|
|
||||||
login TEXT NOT NULL,
|
|
||||||
loginId INTEGER PRIMARY KEY,
|
|
||||||
bchId INTEGER NOT NULL,
|
|
||||||
solanaKey TEXT,
|
|
||||||
clientKey TEXT,
|
|
||||||
bchLimit INTEGER
|
|
||||||
);
|
|
||||||
|
|
||||||
📥 Ответ
|
|
||||||
{
|
|
||||||
"op": "AddUser",
|
|
||||||
"requestId": "req-1",
|
|
||||||
"status": 200,
|
|
||||||
"payload": { "ok": true }
|
|
||||||
}
|
|
||||||
|
|
||||||
2. Шаг 1 — запрос временного пароля сессии
|
2. Шаг 1 — запрос временного пароля сессии
|
||||||
AuthChallenge
|
AuthChallenge
|
||||||
@@ -173,4 +137,4 @@ pushAuthKey TEXT
|
|||||||
}
|
}
|
||||||
|
|
||||||
📘 Итоговая схема создания сессии
|
📘 Итоговая схема создания сессии
|
||||||
AddUser → AuthChallenge → CreateAuthSession → Session Created
|
PDA snapshot ready → AuthChallenge → CreateAuthSession → Session Created
|
||||||
|
|||||||
+184
-15
@@ -6,6 +6,7 @@ import blockchain.MsgSubType;
|
|||||||
import blockchain.body.BodyHasLine;
|
import blockchain.body.BodyHasLine;
|
||||||
import blockchain.body.BodyHasTarget;
|
import blockchain.body.BodyHasTarget;
|
||||||
import blockchain.body.CreateChannelBody;
|
import blockchain.body.CreateChannelBody;
|
||||||
|
import blockchain.body.StatusActionBody;
|
||||||
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 +22,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;
|
||||||
@@ -166,6 +168,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,20 +282,22 @@ 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 channelTypeVersion = Short.toUnsignedInt(createChannelBody.channelTypeVersion);
|
||||||
final String normalizedName;
|
final String normalizedName;
|
||||||
final String slug;
|
final String slug;
|
||||||
try {
|
try {
|
||||||
normalizedName = ChannelNameRules.requireValidDisplayNameForCreate(createChannelBody.channelName);
|
normalizedName = channelTypeCode == (CreateChannelBody.CHANNEL_TYPE_PERSONAL & 0xFFFF)
|
||||||
|
? ChannelNameRules.requireValidDisplayNameForCreate(createChannelBody.channelName)
|
||||||
|
: ChannelNameRules.requireValidPublicDisplayNameForCreate(createChannelBody.channelName);
|
||||||
slug = ChannelNameRules.toCanonicalSlug(normalizedName);
|
slug = ChannelNameRules.toCanonicalSlug(normalizedName);
|
||||||
} catch (IllegalArgumentException badName) {
|
} catch (IllegalArgumentException badName) {
|
||||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_channel_name", serverLastNum, serverLastHashHex);
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_channel_name", serverLastNum, serverLastHashHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
int channelTypeCode = Short.toUnsignedInt(createChannelBody.channelTypeCode);
|
|
||||||
int channelTypeVersion = Short.toUnsignedInt(createChannelBody.channelTypeVersion);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (channelNameStateDAO.existsByOwnerTypeAndSlug(blockchainName, channelTypeCode, slug)) {
|
if (channelNameStateDAO.existsByOwnerTypeAndSlug(blockchainName, channelTypeCode, slug)) {
|
||||||
return new AddBlockResult(409, "channel_name_already_exists", serverLastNum, serverLastHashHex);
|
return new AddBlockResult(409, "channel_name_already_exists", serverLastNum, serverLastHashHex);
|
||||||
@@ -301,14 +308,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);
|
||||||
@@ -328,6 +357,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
|
||||||
@@ -484,7 +573,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
dbWriter.appendBlockAndState(blockchainName, block, st, be, upsertedParam, channelNameStateEntry);
|
dbWriter.appendBlockAndState(blockchainName, block, st, be, upsertedParam, channelNameStateEntry, channelMetaUpdateEntry);
|
||||||
|
|
||||||
if (chat200CreateSeed != null) {
|
if (chat200CreateSeed != null) {
|
||||||
upsertChat200StateFromCreate(chat200CreateSeed);
|
upsertChat200StateFromCreate(chat200CreateSeed);
|
||||||
@@ -543,8 +632,88 @@ 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.SqliteDbController.getInstance().getConnection();
|
try (Connection c = shine.db.DbController.getInstance().getConnection();
|
||||||
PreparedStatement ps = c.prepareStatement("""
|
PreparedStatement ps = c.prepareStatement("""
|
||||||
INSERT INTO chat200_state (
|
INSERT INTO chat200_state (
|
||||||
owner_login, owner_bch_name, channel_root_block_number, channel_root_block_hash,
|
owner_login, owner_bch_name, channel_root_block_number, channel_root_block_hash,
|
||||||
@@ -586,7 +755,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
long updatedAtMs = block.timestamp * 1000L;
|
long updatedAtMs = block.timestamp * 1000L;
|
||||||
if ("desc".equals(cmd.command)) {
|
if ("desc".equals(cmd.command)) {
|
||||||
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection();
|
try (Connection c = shine.db.DbController.getInstance().getConnection();
|
||||||
PreparedStatement ps = c.prepareStatement("""
|
PreparedStatement ps = c.prepareStatement("""
|
||||||
UPDATE chat200_state
|
UPDATE chat200_state
|
||||||
SET chat_title = ?, updated_at_ms = ?
|
SET chat_title = ?, updated_at_ms = ?
|
||||||
@@ -606,7 +775,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
String memberChannel = cmd.arg2 == null ? "" : cmd.arg2.trim();
|
String memberChannel = cmd.arg2 == null ? "" : cmd.arg2.trim();
|
||||||
if (memberLogin.isBlank() || memberChannel.isBlank()) return;
|
if (memberLogin.isBlank() || memberChannel.isBlank()) return;
|
||||||
|
|
||||||
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection();
|
try (Connection c = shine.db.DbController.getInstance().getConnection();
|
||||||
PreparedStatement ps = c.prepareStatement("""
|
PreparedStatement ps = c.prepareStatement("""
|
||||||
INSERT INTO chat200_members_state (
|
INSERT INTO chat200_members_state (
|
||||||
owner_bch_name, channel_root_block_number, member_login, member_channel_name,
|
owner_bch_name, channel_root_block_number, member_login, member_channel_name,
|
||||||
@@ -630,7 +799,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isChat200Channel(String ownerBch, int rootBlockNumber) throws Exception {
|
private boolean isChat200Channel(String ownerBch, int rootBlockNumber) throws Exception {
|
||||||
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection();
|
try (Connection c = shine.db.DbController.getInstance().getConnection();
|
||||||
PreparedStatement ps = c.prepareStatement("""
|
PreparedStatement ps = c.prepareStatement("""
|
||||||
SELECT channel_type_code
|
SELECT channel_type_code
|
||||||
FROM channel_names_state
|
FROM channel_names_state
|
||||||
|
|||||||
+7
-2
@@ -58,7 +58,8 @@ public final class BlockchainWriter {
|
|||||||
BlockchainStateEntry st,
|
BlockchainStateEntry st,
|
||||||
BlockEntry be,
|
BlockEntry be,
|
||||||
UserParamEntry userParamEntry,
|
UserParamEntry userParamEntry,
|
||||||
ChannelNameStateEntry channelNameStateEntry) throws SQLException {
|
ChannelNameStateEntry channelNameStateEntry,
|
||||||
|
ChannelNameStateEntry channelMetaUpdateEntry) throws SQLException {
|
||||||
|
|
||||||
long nowMs = System.currentTimeMillis();
|
long nowMs = System.currentTimeMillis();
|
||||||
byte[] blockBytes = block.toBytes();
|
byte[] blockBytes = block.toBytes();
|
||||||
@@ -68,7 +69,7 @@ public final class BlockchainWriter {
|
|||||||
prepareWriteArtifacts(blockchainName, block.blockNumber, blockHashHex, candidateBytes);
|
prepareWriteArtifacts(blockchainName, block.blockNumber, blockHashHex, candidateBytes);
|
||||||
|
|
||||||
boolean committed = false;
|
boolean committed = false;
|
||||||
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) {
|
try (Connection c = shine.db.DbController.getInstance().getConnection()) {
|
||||||
c.setAutoCommit(false);
|
c.setAutoCommit(false);
|
||||||
try {
|
try {
|
||||||
// 1) insert block
|
// 1) insert block
|
||||||
@@ -91,6 +92,10 @@ public final class BlockchainWriter {
|
|||||||
channelNameStateDAO.insert(c, channelNameStateEntry);
|
channelNameStateDAO.insert(c, channelNameStateEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (channelMetaUpdateEntry != null) {
|
||||||
|
channelNameStateDAO.updateMeta(c, channelMetaUpdateEntry);
|
||||||
|
}
|
||||||
|
|
||||||
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) {}
|
||||||
|
}
|
||||||
+75
-4
@@ -1,10 +1,12 @@
|
|||||||
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.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.channels.ChannelNameRules;
|
import shine.db.channels.ChannelNameRules;
|
||||||
import shine.db.dao.ChannelNameStateDAO;
|
import shine.db.dao.ChannelNameStateDAO;
|
||||||
import shine.db.entities.ChannelNameStateEntry;
|
import shine.db.entities.ChannelNameStateEntry;
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -48,7 +51,7 @@ public final class ChannelNamesStateBootstrapper {
|
|||||||
ORDER BY bch_name, block_number
|
ORDER BY bch_name, block_number
|
||||||
""";
|
""";
|
||||||
|
|
||||||
try (Connection c = SqliteDbController.getInstance().getConnection()) {
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
c.setAutoCommit(false);
|
c.setAutoCommit(false);
|
||||||
try {
|
try {
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
@@ -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)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+218
-63
@@ -8,6 +8,7 @@ import blockchain.body.TextLineBody;
|
|||||||
import blockchain.body.TextReplyBody;
|
import blockchain.body.TextReplyBody;
|
||||||
import shine.db.channels.ChannelNameRules;
|
import shine.db.channels.ChannelNameRules;
|
||||||
import shine.db.MsgSubType;
|
import shine.db.MsgSubType;
|
||||||
|
import shine.db.sql.CurrentUsersSql;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
@@ -21,16 +22,16 @@ 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";
|
||||||
|
|
||||||
private ChannelsReadSupport() {}
|
private ChannelsReadSupport() {}
|
||||||
|
|
||||||
static String canonicalLogin(Connection c, String anyCaseLogin) throws SQLException {
|
static String canonicalLogin(Connection c, String anyCaseLogin) throws SQLException {
|
||||||
String sql = "SELECT login FROM solana_users WHERE login = ? COLLATE NOCASE LIMIT 1";
|
String sql = ("SELECT login FROM " + CurrentUsersSql.usersSubquery("su")
|
||||||
|
+ " WHERE LOWER(login) = LOWER(?) LIMIT 1");
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, anyCaseLogin);
|
ps.setString(1, anyCaseLogin);
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
@@ -45,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);
|
||||||
@@ -66,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;
|
||||||
@@ -89,13 +127,17 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -104,9 +146,9 @@ final class ChannelsReadSupport {
|
|||||||
|
|
||||||
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
|
||||||
""";
|
""";
|
||||||
@@ -115,7 +157,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();
|
||||||
@@ -131,7 +177,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=?
|
||||||
@@ -167,6 +213,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;
|
||||||
}
|
}
|
||||||
@@ -181,7 +231,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)) {
|
||||||
@@ -189,8 +239,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()) {
|
||||||
@@ -255,21 +309,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).
|
||||||
@@ -285,8 +365,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) {
|
||||||
@@ -304,8 +383,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 "";
|
||||||
@@ -361,45 +438,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 {
|
||||||
@@ -411,7 +540,7 @@ final class ChannelsReadSupport {
|
|||||||
String partnerBchSql = """
|
String partnerBchSql = """
|
||||||
SELECT blockchain_name
|
SELECT blockchain_name
|
||||||
FROM blockchain_state
|
FROM blockchain_state
|
||||||
WHERE login = ? COLLATE NOCASE
|
WHERE LOWER(login) = LOWER(?)
|
||||||
ORDER BY blockchain_name
|
ORDER BY blockchain_name
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""";
|
""";
|
||||||
@@ -462,7 +591,7 @@ final class ChannelsReadSupport {
|
|||||||
String sql = """
|
String sql = """
|
||||||
SELECT msg_sub_type
|
SELECT msg_sub_type
|
||||||
FROM blocks
|
FROM blocks
|
||||||
WHERE login = ? COLLATE NOCASE
|
WHERE LOWER(login) = LOWER(?)
|
||||||
AND msg_type = ?
|
AND msg_type = ?
|
||||||
AND to_bch_name = ?
|
AND to_bch_name = ?
|
||||||
AND to_block_number = ?
|
AND to_block_number = ?
|
||||||
@@ -504,6 +633,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;
|
||||||
@@ -524,19 +669,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 {
|
||||||
|
|||||||
+30
-3
@@ -10,7 +10,7 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelMe
|
|||||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelMessages_Response;
|
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelMessages_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.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.MsgSubType;
|
import shine.db.MsgSubType;
|
||||||
import utils.blockchain.BlockchainNameUtil;
|
import utils.blockchain.BlockchainNameUtil;
|
||||||
import blockchain.body.CreateChannelBody;
|
import blockchain.body.CreateChannelBody;
|
||||||
@@ -39,7 +39,7 @@ public class Net_GetChannelMessages_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
boolean asc = req.getSort() == null || !"desc".equalsIgnoreCase(req.getSort());
|
boolean asc = req.getSort() == null || !"desc".equalsIgnoreCase(req.getSort());
|
||||||
|
|
||||||
try (Connection c = SqliteDbController.getInstance().getConnection()) {
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
String viewerLogin = ctx != null ? ctx.getLogin() : null;
|
String viewerLogin = ctx != null ? ctx.getLogin() : null;
|
||||||
if (viewerLogin == null || viewerLogin.isBlank()) {
|
if (viewerLogin == null || viewerLogin.isBlank()) {
|
||||||
viewerLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin());
|
viewerLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin());
|
||||||
@@ -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);
|
||||||
|
|||||||
+6
-6
@@ -10,7 +10,7 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelsC
|
|||||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelsCounters_Response;
|
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelsCounters_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.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.MsgSubType;
|
import shine.db.MsgSubType;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
@@ -26,7 +26,7 @@ public class Net_GetChannelsCounters_Handler implements JsonMessageHandler {
|
|||||||
if (req.getLogin() == null || req.getLogin().isBlank()) {
|
if (req.getLogin() == null || req.getLogin().isBlank()) {
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля: login");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля: login");
|
||||||
}
|
}
|
||||||
try (Connection c = SqliteDbController.getInstance().getConnection()) {
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
String canonicalLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin().trim());
|
String canonicalLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin().trim());
|
||||||
if (canonicalLogin == null) {
|
if (canonicalLogin == null) {
|
||||||
return NetExceptionResponseFactory.error(req, 404, "user_not_found", "Пользователь не найден");
|
return NetExceptionResponseFactory.error(req, 404, "user_not_found", "Пользователь не найден");
|
||||||
@@ -52,7 +52,7 @@ public class Net_GetChannelsCounters_Handler implements JsonMessageHandler {
|
|||||||
String sql = """
|
String sql = """
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM connections_state
|
FROM connections_state
|
||||||
WHERE login = ? COLLATE NOCASE
|
WHERE LOWER(login) = LOWER(?)
|
||||||
AND rel_type = ?
|
AND rel_type = ?
|
||||||
""";
|
""";
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
@@ -68,7 +68,7 @@ public class Net_GetChannelsCounters_Handler implements JsonMessageHandler {
|
|||||||
String sql = """
|
String sql = """
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM channel_names_state
|
FROM channel_names_state
|
||||||
WHERE owner_login = ? COLLATE NOCASE
|
WHERE LOWER(owner_login) = LOWER(?)
|
||||||
AND channel_type_code = ?
|
AND channel_type_code = ?
|
||||||
""";
|
""";
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
@@ -81,7 +81,7 @@ public class Net_GetChannelsCounters_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private int countMyChannels(Connection c, String login) throws Exception {
|
private int countMyChannels(Connection c, String login) throws Exception {
|
||||||
String bchCountSql = "SELECT COUNT(*) FROM blockchain_state WHERE login = ? COLLATE NOCASE";
|
String bchCountSql = "SELECT COUNT(*) FROM blockchain_state WHERE LOWER(login) = LOWER(?)";
|
||||||
int stories = 0;
|
int stories = 0;
|
||||||
try (PreparedStatement ps = c.prepareStatement(bchCountSql)) {
|
try (PreparedStatement ps = c.prepareStatement(bchCountSql)) {
|
||||||
ps.setString(1, login);
|
ps.setString(1, login);
|
||||||
@@ -92,7 +92,7 @@ public class Net_GetChannelsCounters_Handler implements JsonMessageHandler {
|
|||||||
String namedSql = """
|
String namedSql = """
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM channel_names_state
|
FROM channel_names_state
|
||||||
WHERE owner_login = ? COLLATE NOCASE
|
WHERE LOWER(owner_login) = LOWER(?)
|
||||||
AND channel_type_code IN (1,100,200)
|
AND channel_type_code IN (1,100,200)
|
||||||
""";
|
""";
|
||||||
int named = 0;
|
int named = 0;
|
||||||
|
|||||||
+3
-4
@@ -10,7 +10,7 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetGroupDial
|
|||||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetGroupDialog_Response;
|
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetGroupDialog_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.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.channels.ChannelNameRules;
|
import shine.db.channels.ChannelNameRules;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
@@ -33,7 +33,7 @@ public class Net_GetGroupDialog_Handler implements JsonMessageHandler {
|
|||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля group");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля group");
|
||||||
}
|
}
|
||||||
|
|
||||||
try (Connection c = SqliteDbController.getInstance().getConnection()) {
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
Net_GetGroupDialog_Response resp = new Net_GetGroupDialog_Response();
|
Net_GetGroupDialog_Response resp = new Net_GetGroupDialog_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
resp.setRequestId(req.getRequestId());
|
resp.setRequestId(req.getRequestId());
|
||||||
@@ -115,7 +115,7 @@ public class Net_GetGroupDialog_Handler implements JsonMessageHandler {
|
|||||||
String canonicalLogin = ChannelsReadSupport.canonicalLogin(c, ref.memberLogin);
|
String canonicalLogin = ChannelsReadSupport.canonicalLogin(c, ref.memberLogin);
|
||||||
if (canonicalLogin == null || canonicalLogin.isBlank()) return null;
|
if (canonicalLogin == null || canonicalLogin.isBlank()) return null;
|
||||||
|
|
||||||
String bchSql = "SELECT blockchain_name FROM blockchain_state WHERE login = ? COLLATE NOCASE ORDER BY blockchain_name LIMIT 1";
|
String bchSql = "SELECT blockchain_name FROM blockchain_state WHERE LOWER(login) = LOWER(?) ORDER BY blockchain_name LIMIT 1";
|
||||||
String memberBch = null;
|
String memberBch = null;
|
||||||
try (PreparedStatement ps = c.prepareStatement(bchSql)) {
|
try (PreparedStatement ps = c.prepareStatement(bchSql)) {
|
||||||
ps.setString(1, canonicalLogin);
|
ps.setString(1, canonicalLogin);
|
||||||
@@ -214,4 +214,3 @@ public class Net_GetGroupDialog_Handler implements JsonMessageHandler {
|
|||||||
String text;
|
String text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+71
-17
@@ -12,11 +12,12 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetMessageTh
|
|||||||
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.MsgSubType;
|
||||||
import shine.db.SqliteDbController;
|
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;
|
||||||
@@ -28,21 +29,21 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
|
|||||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||||
Net_GetMessageThread_Request req = (Net_GetMessageThread_Request) baseRequest;
|
Net_GetMessageThread_Request req = (Net_GetMessageThread_Request) baseRequest;
|
||||||
if (req.getMessage() == null || req.getMessage().getBlockchainName() == null || req.getMessage().getBlockNumber() == null) {
|
if (req.getMessage() == null || req.getMessage().getBlockchainName() == null || req.getMessage().getBlockNumber() == null) {
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля message");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля message");
|
||||||
}
|
}
|
||||||
|
|
||||||
int depthUp = req.getDepthUp() == null ? 20 : Math.max(0, req.getDepthUp());
|
int depthUp = req.getDepthUp() == null ? 20 : Math.max(0, req.getDepthUp());
|
||||||
int depthDown = req.getDepthDown() == null ? 2 : Math.max(0, req.getDepthDown());
|
int depthDown = req.getDepthDown() == null ? 2 : Math.max(0, req.getDepthDown());
|
||||||
int childLimit = req.getLimitChildrenPerNode() == null ? 50 : Math.max(1, req.getLimitChildrenPerNode());
|
int childLimit = req.getLimitChildrenPerNode() == null ? 50 : Math.max(1, req.getLimitChildrenPerNode());
|
||||||
|
|
||||||
try (Connection c = SqliteDbController.getInstance().getConnection()) {
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
String viewerLogin = ctx != null ? ctx.getLogin() : null;
|
String viewerLogin = ctx != null ? ctx.getLogin() : null;
|
||||||
if (viewerLogin == null || viewerLogin.isBlank()) {
|
if (viewerLogin == null || viewerLogin.isBlank()) {
|
||||||
viewerLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin());
|
viewerLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin());
|
||||||
}
|
}
|
||||||
PostRow focusRow = findByNumber(c, req.getMessage().getBlockchainName(), req.getMessage().getBlockNumber());
|
PostRow focusRow = findFocusRow(c, req.getMessage());
|
||||||
if (focusRow == null) {
|
if (focusRow == null) {
|
||||||
return NetExceptionResponseFactory.error(req, 404, "message_not_found", "Сообщение не найдено");
|
return NetExceptionResponseFactory.error(req, 404, "message_not_found", "Сообщение не найдено");
|
||||||
}
|
}
|
||||||
|
|
||||||
Net_GetMessageThread_Response resp = new Net_GetMessageThread_Response();
|
Net_GetMessageThread_Response resp = new Net_GetMessageThread_Response();
|
||||||
@@ -67,13 +68,29 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
|
|||||||
return resp;
|
return resp;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("GetMessageThread failed", e);
|
log.error("GetMessageThread failed", e);
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "internal_error", "Внутренняя ошибка сервера");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "internal_error", "Внутренняя ошибка сервера");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PostRow findFocusRow(Connection c, Net_GetMessageThread_Request.MessageSelector selector) throws Exception {
|
||||||
|
String blockchainName = String.valueOf(selector.getBlockchainName() == null ? "" : selector.getBlockchainName()).trim();
|
||||||
|
int blockNumber = selector.getBlockNumber();
|
||||||
|
byte[] blockHash = parseOptionalHash(selector.getBlockHash());
|
||||||
|
|
||||||
|
if (blockHash != null) {
|
||||||
|
PostRow exact = findByNumberAndHash(c, blockchainName, blockNumber, blockHash);
|
||||||
|
if (exact != null) return exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
PostRow byNumber = findByNumber(c, blockchainName, blockNumber);
|
||||||
|
if (byNumber != null) return byNumber;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
||||||
@@ -84,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,6 +145,33 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PostRow findByNumberAndHash(Connection c, String bchName, int blockNumber, byte[] blockHash) throws Exception {
|
||||||
|
String sql = """
|
||||||
|
SELECT login,bch_name,block_number,block_hash,block_bytes,to_bch_name,to_block_number,to_block_hash,line_code,msg_sub_type,this_line_number
|
||||||
|
FROM blocks
|
||||||
|
WHERE bch_name=? AND block_number=? AND block_hash=?
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, bchName);
|
||||||
|
ps.setInt(2, blockNumber);
|
||||||
|
ps.setBytes(3, blockHash);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
return rs.next() ? mapRow(rs) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] parseOptionalHash(String hex) {
|
||||||
|
String value = String.valueOf(hex == null ? "" : hex).trim();
|
||||||
|
if (value.isEmpty()) return null;
|
||||||
|
try {
|
||||||
|
return ChannelsReadSupport.hexToBytes(value);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private PostRow mapRow(ResultSet rs) throws Exception {
|
private PostRow mapRow(ResultSet rs) throws Exception {
|
||||||
PostRow row = new PostRow();
|
PostRow row = new PostRow();
|
||||||
row.login = rs.getString("login");
|
row.login = rs.getString("login");
|
||||||
@@ -165,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();
|
||||||
@@ -190,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-4
@@ -10,7 +10,7 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListGroupCha
|
|||||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListGroupChats200_Response;
|
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListGroupChats200_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.SqliteDbController;
|
import shine.db.DbController;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
@@ -27,7 +27,7 @@ public class Net_ListGroupChats200_Handler implements JsonMessageHandler {
|
|||||||
if (req.getLogin() == null || req.getLogin().isBlank()) {
|
if (req.getLogin() == null || req.getLogin().isBlank()) {
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля: login");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля: login");
|
||||||
}
|
}
|
||||||
try (Connection c = SqliteDbController.getInstance().getConnection()) {
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
String canonicalLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin().trim());
|
String canonicalLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin().trim());
|
||||||
if (canonicalLogin == null) {
|
if (canonicalLogin == null) {
|
||||||
return NetExceptionResponseFactory.error(req, 404, "user_not_found", "Пользователь не найден");
|
return NetExceptionResponseFactory.error(req, 404, "user_not_found", "Пользователь не найден");
|
||||||
@@ -59,7 +59,7 @@ public class Net_ListGroupChats200_Handler implements JsonMessageHandler {
|
|||||||
) m
|
) m
|
||||||
ON m.owner_bch_name = s.owner_bch_name
|
ON m.owner_bch_name = s.owner_bch_name
|
||||||
AND m.channel_root_block_number = s.channel_root_block_number
|
AND m.channel_root_block_number = s.channel_root_block_number
|
||||||
WHERE s.owner_login = ? COLLATE NOCASE
|
WHERE LOWER(s.owner_login) = LOWER(?)
|
||||||
ORDER BY s.updated_at_ms DESC, s.channel_root_block_number DESC
|
ORDER BY s.updated_at_ms DESC, s.channel_root_block_number DESC
|
||||||
""";
|
""";
|
||||||
List<Net_ListGroupChats200_Response.Row> out = new ArrayList<>();
|
List<Net_ListGroupChats200_Response.Row> out = new ArrayList<>();
|
||||||
@@ -83,4 +83,3 @@ public class Net_ListGroupChats200_Handler implements JsonMessageHandler {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -11,7 +11,7 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListSubscrip
|
|||||||
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.MsgSubType;
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
@@ -29,7 +29,7 @@ public class Net_ListSubscriptionsFeed_Handler implements JsonMessageHandler {
|
|||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля: login");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля: login");
|
||||||
}
|
}
|
||||||
|
|
||||||
try (Connection c = SqliteDbController.getInstance().getConnection()) {
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
String canonicalLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin().trim());
|
String canonicalLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin().trim());
|
||||||
if (canonicalLogin == null) {
|
if (canonicalLogin == null) {
|
||||||
return NetExceptionResponseFactory.error(req, 404, "user_not_found", "Пользователь не найден");
|
return NetExceptionResponseFactory.error(req, 404, "user_not_found", "Пользователь не найден");
|
||||||
@@ -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);
|
||||||
|
|||||||
+11
-6
@@ -11,7 +11,7 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_MarkChannelM
|
|||||||
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.MsgSubType;
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
@@ -37,7 +37,7 @@ public class Net_MarkChannelMessagesSeen_Handler implements JsonMessageHandler {
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
try (Connection c = SqliteDbController.getInstance().getConnection()) {
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
String viewerLogin = ctx != null ? ctx.getLogin() : null;
|
String viewerLogin = ctx != null ? ctx.getLogin() : null;
|
||||||
if (viewerLogin == null || viewerLogin.isBlank()) {
|
if (viewerLogin == null || viewerLogin.isBlank()) {
|
||||||
viewerLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin());
|
viewerLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin());
|
||||||
@@ -58,15 +58,16 @@ 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 = ?" : "");
|
||||||
|
|
||||||
String insertSql = """
|
String insertSql = """
|
||||||
INSERT OR IGNORE INTO message_views_state (
|
INSERT INTO message_views_state (
|
||||||
viewer_login, to_bch_name, to_block_number, to_block_hash, first_seen_at_ms
|
viewer_login, to_bch_name, to_block_number, to_block_hash, first_seen_at_ms
|
||||||
) VALUES (?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
""";
|
""";
|
||||||
|
|
||||||
int seenAccepted = 0;
|
int seenAccepted = 0;
|
||||||
@@ -97,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;
|
||||||
@@ -132,4 +138,3 @@ public class Net_MarkChannelMessagesSeen_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+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; }
|
||||||
|
|
||||||
|
|||||||
+8
-6
@@ -9,6 +9,7 @@ import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_AddCloseF
|
|||||||
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.MsgSubType;
|
||||||
|
import shine.db.sql.CurrentUsersSql;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
@@ -31,7 +32,7 @@ public class Net_AddCloseFriend_Handler implements JsonMessageHandler {
|
|||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "Нельзя добавить себя");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "Нельзя добавить себя");
|
||||||
}
|
}
|
||||||
|
|
||||||
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) {
|
try (Connection c = shine.db.DbController.getInstance().getConnection()) {
|
||||||
String canonicalTo = findCanonicalLogin(c, toLogin);
|
String canonicalTo = findCanonicalLogin(c, toLogin);
|
||||||
if (canonicalTo == null) {
|
if (canonicalTo == null) {
|
||||||
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
||||||
@@ -42,8 +43,7 @@ public class Net_AddCloseFriend_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Idempotent insert for close-friend relation.
|
// Idempotent insert for close-friend relation.
|
||||||
// Using INSERT OR IGNORE avoids ON CONFLICT(column list) mismatches
|
// Rely on PostgreSQL ON CONFLICT DO NOTHING.
|
||||||
// across DB instances with different UNIQUE schemas.
|
|
||||||
insertCloseFriendIgnoreDuplicate(c, from, canonicalTo, targetBch);
|
insertCloseFriendIgnoreDuplicate(c, from, canonicalTo, targetBch);
|
||||||
|
|
||||||
Net_AddCloseFriend_Response resp = new Net_AddCloseFriend_Response();
|
Net_AddCloseFriend_Response resp = new Net_AddCloseFriend_Response();
|
||||||
@@ -58,7 +58,8 @@ public class Net_AddCloseFriend_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String findCanonicalLogin(Connection c, String login) throws Exception {
|
private String findCanonicalLogin(Connection c, String login) throws Exception {
|
||||||
String sql = "SELECT login FROM solana_users WHERE login = ? COLLATE NOCASE LIMIT 1";
|
String sql = "SELECT login FROM " + CurrentUsersSql.usersSubquery("su")
|
||||||
|
+ " WHERE LOWER(login) = LOWER(?) LIMIT 1";
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, login);
|
ps.setString(1, login);
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
@@ -68,7 +69,7 @@ public class Net_AddCloseFriend_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String findPrimaryBlockchain(Connection c, String login) throws Exception {
|
private String findPrimaryBlockchain(Connection c, String login) throws Exception {
|
||||||
String sql = "SELECT blockchain_name FROM blockchain_state WHERE login = ? COLLATE NOCASE ORDER BY blockchain_name LIMIT 1";
|
String sql = "SELECT blockchain_name FROM blockchain_state WHERE LOWER(login) = LOWER(?) ORDER BY blockchain_name LIMIT 1";
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, login);
|
ps.setString(1, login);
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
@@ -82,10 +83,11 @@ public class Net_AddCloseFriend_Handler implements JsonMessageHandler {
|
|||||||
String toLogin,
|
String toLogin,
|
||||||
String toBchName) throws Exception {
|
String toBchName) throws Exception {
|
||||||
String sql = """
|
String sql = """
|
||||||
INSERT OR IGNORE INTO connections_state (
|
INSERT INTO connections_state (
|
||||||
login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash
|
login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT (login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash) DO NOTHING
|
||||||
""";
|
""";
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
|||||||
+8
-10
@@ -11,8 +11,9 @@ import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriend
|
|||||||
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.MsgSubType;
|
||||||
import shine.db.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.dao.ConnectionsStateDAO;
|
import shine.db.dao.ConnectionsStateDAO;
|
||||||
|
import shine.db.sql.CurrentUsersSql;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
@@ -26,10 +27,7 @@ import java.util.List;
|
|||||||
*
|
*
|
||||||
* ВАЖНО:
|
* ВАЖНО:
|
||||||
* - login в запросе может быть любым регистром
|
* - login в запросе может быть любым регистром
|
||||||
* - в ответе возвращаем канонический регистр (как в solana_users.login)
|
* - в ответе возвращаем канонический регистр логина.
|
||||||
*
|
|
||||||
* ПРИМЕЧАНИЕ:
|
|
||||||
* Таблица пользователей тут названа "solana_users". Если у тебя иначе — поменяй SQL.
|
|
||||||
*/
|
*/
|
||||||
public class Net_GetFriendsLists_Handler implements JsonMessageHandler {
|
public class Net_GetFriendsLists_Handler implements JsonMessageHandler {
|
||||||
|
|
||||||
@@ -51,12 +49,12 @@ public class Net_GetFriendsLists_Handler implements JsonMessageHandler {
|
|||||||
final String loginAnyCase = req.getLogin().trim();
|
final String loginAnyCase = req.getLogin().trim();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SqliteDbController db = SqliteDbController.getInstance();
|
DbController db = DbController.getInstance();
|
||||||
ConnectionsStateDAO dao = ConnectionsStateDAO.getInstance();
|
ConnectionsStateDAO dao = ConnectionsStateDAO.getInstance();
|
||||||
|
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
|
|
||||||
// 1) Канонизируем login через solana_users (NOCASE)
|
// 1) Канонизируем login через current users слой (NOCASE)
|
||||||
String canonicalLogin = findCanonicalLogin(c, loginAnyCase);
|
String canonicalLogin = findCanonicalLogin(c, loginAnyCase);
|
||||||
if (canonicalLogin == null) {
|
if (canonicalLogin == null) {
|
||||||
return NetExceptionResponseFactory.error(
|
return NetExceptionResponseFactory.error(
|
||||||
@@ -99,10 +97,10 @@ public class Net_GetFriendsLists_Handler implements JsonMessageHandler {
|
|||||||
private String findCanonicalLogin(Connection c, String loginAnyCase) throws Exception {
|
private String findCanonicalLogin(Connection c, String loginAnyCase) throws Exception {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT login
|
SELECT login
|
||||||
FROM solana_users
|
FROM %s
|
||||||
WHERE login = ? COLLATE NOCASE
|
WHERE LOWER(login) = LOWER(?)
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""";
|
""".formatted(CurrentUsersSql.usersSubquery("su"));
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, loginAnyCase);
|
ps.setString(1, loginAnyCase);
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
|||||||
+9
-7
@@ -10,6 +10,7 @@ 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.MsgSubType;
|
||||||
import shine.db.dao.ConnectionsStateDAO;
|
import shine.db.dao.ConnectionsStateDAO;
|
||||||
|
import shine.db.sql.CurrentUsersSql;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
@@ -39,7 +40,7 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) {
|
try (Connection c = shine.db.DbController.getInstance().getConnection()) {
|
||||||
String canonicalLogin = findCanonicalLogin(c, requestedLogin);
|
String canonicalLogin = findCanonicalLogin(c, requestedLogin);
|
||||||
if (canonicalLogin == null) {
|
if (canonicalLogin == null) {
|
||||||
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
||||||
@@ -113,7 +114,8 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String findCanonicalLogin(Connection c, String loginAnyCase) throws Exception {
|
private String findCanonicalLogin(Connection c, String loginAnyCase) throws Exception {
|
||||||
String sql = "SELECT login FROM solana_users WHERE login = ? COLLATE NOCASE LIMIT 1";
|
String sql = "SELECT login FROM " + CurrentUsersSql.usersSubquery("su")
|
||||||
|
+ " WHERE LOWER(login) = LOWER(?) LIMIT 1";
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, loginAnyCase);
|
ps.setString(1, loginAnyCase);
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
@@ -168,19 +170,19 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
|
|||||||
MAX(CASE WHEN up.param = 'official' THEN up.value END) AS official_value,
|
MAX(CASE WHEN up.param = 'official' THEN up.value END) AS official_value,
|
||||||
MAX(CASE WHEN up.param = 'shine' THEN up.value END) AS shine_value,
|
MAX(CASE WHEN up.param = 'shine' THEN up.value END) AS shine_value,
|
||||||
MAX(CASE WHEN up.param = 'ava' THEN up.value END) AS avatar_value
|
MAX(CASE WHEN up.param = 'ava' THEN up.value END) AS avatar_value
|
||||||
FROM solana_users su
|
FROM %s
|
||||||
LEFT JOIN users_params up
|
LEFT JOIN users_params up
|
||||||
ON up.login = su.login COLLATE NOCASE
|
ON LOWER(up.login) = LOWER(su.login)
|
||||||
AND up.param IN ('gender', 'official', 'shine', 'ava')
|
AND up.param IN ('gender', 'official', 'shine', 'ava')
|
||||||
WHERE su.login COLLATE NOCASE IN (%s)
|
WHERE LOWER(su.login) IN (%s)
|
||||||
GROUP BY su.login
|
GROUP BY su.login
|
||||||
ORDER BY su.login
|
ORDER BY su.login
|
||||||
""".formatted(String.join(", ", placeholders));
|
""".formatted(CurrentUsersSql.usersSubquery("su"), String.join(", ", placeholders));
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
int i = 1;
|
int i = 1;
|
||||||
for (String login : logins) {
|
for (String login : logins) {
|
||||||
ps.setString(i, login);
|
ps.setString(i, normKey(login));
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ public class Net_ListContacts_Handler implements JsonMessageHandler {
|
|||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "NOT_AUTHENTICATED", "Требуется авторизация");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "NOT_AUTHENTICATED", "Требуется авторизация");
|
||||||
}
|
}
|
||||||
|
|
||||||
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) {
|
try (Connection c = shine.db.DbController.getInstance().getConnection()) {
|
||||||
List<String> contacts = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, ctx.getLogin(), MsgSubType.CONNECTION_CONTACT);
|
List<String> contacts = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, ctx.getLogin(), MsgSubType.CONNECTION_CONTACT);
|
||||||
Net_ListContacts_Response resp = new Net_ListContacts_Response();
|
Net_ListContacts_Response resp = new Net_ListContacts_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
|
|||||||
+6
-6
@@ -11,19 +11,19 @@ import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetSyncUserPro
|
|||||||
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.BlockchainStateDAO;
|
import shine.db.dao.BlockchainStateDAO;
|
||||||
import shine.db.dao.SolanaUsersDAO;
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
import shine.db.entities.BlockchainStateEntry;
|
import shine.db.entities.BlockchainStateEntry;
|
||||||
import shine.db.entities.SolanaUserEntry;
|
import shine.db.entities.CurrentUserEntry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GetSyncUserProfile — server-to-server профиль пользователя для межсерверной синхронизации.
|
* GetSyncUserProfile — server-to-server профиль пользователя для межсерверной синхронизации.
|
||||||
* Нужен, чтобы принимающий сервер мог создать локальные solana_users + blockchain_state
|
* Нужен, чтобы принимающий сервер мог создать локальную runtime-проекцию пользователя
|
||||||
* без прямого запроса в Solana RPC.
|
* и blockchain_state без прямого запроса в Solana RPC.
|
||||||
*/
|
*/
|
||||||
public final class Net_GetSyncUserProfile_Handler implements JsonMessageHandler {
|
public final class Net_GetSyncUserProfile_Handler implements JsonMessageHandler {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(Net_GetSyncUserProfile_Handler.class);
|
private static final Logger log = LoggerFactory.getLogger(Net_GetSyncUserProfile_Handler.class);
|
||||||
private final SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
|
private final CurrentUsersDAO usersDAO = CurrentUsersDAO.getInstance();
|
||||||
private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
|
private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -41,7 +41,7 @@ public final class Net_GetSyncUserProfile_Handler implements JsonMessageHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SolanaUserEntry user = usersDAO.getByLogin(login);
|
CurrentUserEntry user = usersDAO.getByLogin(login);
|
||||||
|
|
||||||
Net_GetSyncUserProfile_Response resp = new Net_GetSyncUserProfile_Response();
|
Net_GetSyncUserProfile_Response resp = new Net_GetSyncUserProfile_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
|
|||||||
-26
@@ -1,26 +0,0 @@
|
|||||||
package server.logic.ws_protocol.JSON.handlers.tempToTest;
|
|
||||||
|
|
||||||
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.tempToTest.entyties.Net_AddUser_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AddUser отключен: регистрация работает через Solana.
|
|
||||||
*/
|
|
||||||
public class Net_AddUser_Handler implements JsonMessageHandler {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
|
||||||
Net_AddUser_Request req = (Net_AddUser_Request) baseRequest;
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
410,
|
|
||||||
"ADD_USER_DISABLED",
|
|
||||||
"Серверная регистрация AddUser отключена. Используйте регистрацию через Solana."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+4
-8
@@ -5,16 +5,15 @@ import org.slf4j.LoggerFactory;
|
|||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.SolanaUserPdaImportService;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Request;
|
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Response;
|
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_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.BlockchainStateDAO;
|
import shine.db.dao.BlockchainStateDAO;
|
||||||
import shine.db.dao.SolanaUsersDAO;
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
import shine.db.entities.BlockchainStateEntry;
|
import shine.db.entities.BlockchainStateEntry;
|
||||||
import shine.db.entities.SolanaUserEntry;
|
import shine.db.entities.CurrentUserEntry;
|
||||||
|
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -38,14 +37,11 @@ public class Net_GetUser_Handler implements JsonMessageHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
|
CurrentUsersDAO usersDAO = CurrentUsersDAO.getInstance();
|
||||||
BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
|
BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SolanaUserEntry u = usersDAO.getByLogin(req.getLogin());
|
CurrentUserEntry u = usersDAO.getByLogin(req.getLogin());
|
||||||
if (u == null) {
|
|
||||||
u = SolanaUserPdaImportService.findOrImportByLogin(req.getLogin());
|
|
||||||
}
|
|
||||||
|
|
||||||
Net_GetUser_Response resp = new Net_GetUser_Response();
|
Net_GetUser_Response resp = new Net_GetUser_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
|
|||||||
+7
-6
@@ -10,8 +10,8 @@ import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUser
|
|||||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUsers_Response;
|
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUsers_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.SolanaUsersDAO;
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
import shine.db.entities.SolanaUserEntry;
|
import shine.db.entities.CurrentUserEntry;
|
||||||
|
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -35,13 +35,14 @@ public class Net_SearchUsers_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String prefix = req.getPrefix().trim();
|
String prefix = req.getPrefix().trim();
|
||||||
|
Boolean isServer = req.getIsServer();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SolanaUsersDAO dao = SolanaUsersDAO.getInstance();
|
CurrentUsersDAO dao = CurrentUsersDAO.getInstance();
|
||||||
List<SolanaUserEntry> users = dao.searchByLoginPrefix(prefix); // case-insensitive + LIMIT 5
|
List<CurrentUserEntry> users = dao.searchByLoginPrefix(prefix, isServer); // case-insensitive + LIMIT 5
|
||||||
|
|
||||||
List<String> logins = new ArrayList<>();
|
List<String> logins = new ArrayList<>();
|
||||||
for (SolanaUserEntry u : users) {
|
for (CurrentUserEntry u : users) {
|
||||||
if (u != null && u.getLogin() != null) {
|
if (u != null && u.getLogin() != null) {
|
||||||
logins.add(u.getLogin()); // регистр как в БД
|
logins.add(u.getLogin()); // регистр как в БД
|
||||||
}
|
}
|
||||||
@@ -53,7 +54,7 @@ public class Net_SearchUsers_Handler implements JsonMessageHandler {
|
|||||||
resp.setStatus(WireCodes.Status.OK);
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
resp.setLogins(logins);
|
resp.setLogins(logins);
|
||||||
|
|
||||||
log.info("✅ SearchUsers ok: prefix='{}' -> {}", prefix, logins.size());
|
log.info("✅ SearchUsers ok: prefix='{}', isServer={} -> {}", prefix, isServer, logins.size());
|
||||||
return resp;
|
return resp;
|
||||||
|
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
|
|||||||
-58
@@ -1,58 +0,0 @@
|
|||||||
package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Запрос AddUser — временная/тестовая регистрация локального пользователя.
|
|
||||||
*
|
|
||||||
* Клиент отправляет:
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* "op": "AddUser",
|
|
||||||
* "requestId": "test-add-1",
|
|
||||||
* "payload": {
|
|
||||||
* "login": "anya",
|
|
||||||
* "blockchainName": "anya-001",
|
|
||||||
* "solanaKey": "base64-ed25519-public-key-login",
|
|
||||||
* "blockchainKey": "base64-ed25519-public-key-blockchain",
|
|
||||||
* "clientKey": "base64-ed25519-public-key-device",
|
|
||||||
* "bchLimit": 1000000
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Все поля лежат внутри payload.
|
|
||||||
*/
|
|
||||||
public class Net_AddUser_Request extends Net_Request {
|
|
||||||
|
|
||||||
private String login;
|
|
||||||
private String blockchainName;
|
|
||||||
|
|
||||||
/** Ключ пользователя Solana (публичный ключ логина) */
|
|
||||||
private String solanaKey;
|
|
||||||
|
|
||||||
/** Ключ блокчейна (публичный ключ блокчейна) */
|
|
||||||
private String blockchainKey;
|
|
||||||
|
|
||||||
/** Ключ устройства (публичный ключ устройства) */
|
|
||||||
private String clientKey;
|
|
||||||
|
|
||||||
private Integer bchLimit;
|
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
|
||||||
public void setLogin(String login) { this.login = login; }
|
|
||||||
|
|
||||||
public String getBlockchainName() { return blockchainName; }
|
|
||||||
public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
|
|
||||||
|
|
||||||
public String getSolanaKey() { return solanaKey; }
|
|
||||||
public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; }
|
|
||||||
|
|
||||||
public String getBlockchainKey() { return blockchainKey; }
|
|
||||||
public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
|
|
||||||
|
|
||||||
public String getClientKey() { return clientKey; }
|
|
||||||
public void setClientKey(String clientKey) { this.clientKey = clientKey; }
|
|
||||||
|
|
||||||
public Integer getBchLimit() { return bchLimit; }
|
|
||||||
public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; }
|
|
||||||
}
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
// file: server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_AddUser_Response.java
|
|
||||||
package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Успешный ответ на AddUser.
|
|
||||||
*
|
|
||||||
* Сейчас дополнительных полей нет — достаточно status=200.
|
|
||||||
*
|
|
||||||
* Пример:
|
|
||||||
* {
|
|
||||||
* "op": "AddUser",
|
|
||||||
* "requestId": "test-add-1",
|
|
||||||
* "status": 200,
|
|
||||||
* "payload": { }
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public class Net_AddUser_Response extends Net_Response {
|
|
||||||
// При необходимости сюда можно добавить, например, флаг created/updated и т.п.
|
|
||||||
}
|
|
||||||
+4
-1
@@ -18,7 +18,10 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|||||||
public class Net_SearchUsers_Request extends Net_Request {
|
public class Net_SearchUsers_Request extends Net_Request {
|
||||||
|
|
||||||
private String prefix;
|
private String prefix;
|
||||||
|
private Boolean isServer;
|
||||||
|
|
||||||
public String getPrefix() { return prefix; }
|
public String getPrefix() { return prefix; }
|
||||||
public void setPrefix(String prefix) { this.prefix = prefix; }
|
public void setPrefix(String prefix) { this.prefix = prefix; }
|
||||||
}
|
public Boolean getIsServer() { return isServer; }
|
||||||
|
public void setIsServer(Boolean isServer) { this.isServer = isServer; }
|
||||||
|
}
|
||||||
|
|||||||
+2
-2
@@ -10,7 +10,7 @@ import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserPar
|
|||||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Response;
|
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_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.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.dao.UserParamsDAO;
|
import shine.db.dao.UserParamsDAO;
|
||||||
import shine.db.entities.UserParamEntry;
|
import shine.db.entities.UserParamEntry;
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ public class Net_GetUserParam_Handler implements JsonMessageHandler {
|
|||||||
String param = req.getParam().trim();
|
String param = req.getParam().trim();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SqliteDbController db = SqliteDbController.getInstance();
|
DbController db = DbController.getInstance();
|
||||||
UserParamsDAO dao = UserParamsDAO.getInstance();
|
UserParamsDAO dao = UserParamsDAO.getInstance();
|
||||||
|
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
|
|||||||
+5
-5
@@ -10,10 +10,10 @@ import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserPa
|
|||||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Response;
|
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_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.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.dao.SolanaUsersDAO;
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
import shine.db.dao.UserParamsDAO;
|
import shine.db.dao.UserParamsDAO;
|
||||||
import shine.db.entities.SolanaUserEntry;
|
import shine.db.entities.CurrentUserEntry;
|
||||||
import shine.db.entities.UserParamEntry;
|
import shine.db.entities.UserParamEntry;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
@@ -50,7 +50,7 @@ public class Net_ListUserParams_Handler implements JsonMessageHandler {
|
|||||||
String login = req.getLogin().trim();
|
String login = req.getLogin().trim();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SqliteDbController db = SqliteDbController.getInstance();
|
DbController db = DbController.getInstance();
|
||||||
UserParamsDAO dao = UserParamsDAO.getInstance();
|
UserParamsDAO dao = UserParamsDAO.getInstance();
|
||||||
|
|
||||||
List<UserParamEntry> entries;
|
List<UserParamEntry> entries;
|
||||||
@@ -63,7 +63,7 @@ public class Net_ListUserParams_Handler implements JsonMessageHandler {
|
|||||||
resp.setRequestId(req.getRequestId());
|
resp.setRequestId(req.getRequestId());
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
|
||||||
SolanaUserEntry user = SolanaUsersDAO.getInstance().getByLogin(login);
|
CurrentUserEntry user = CurrentUsersDAO.getInstance().getByLogin(login);
|
||||||
resp.setLogin(user != null && user.getLogin() != null ? user.getLogin() : login);
|
resp.setLogin(user != null && user.getLogin() != null ? user.getLogin() : login);
|
||||||
|
|
||||||
List<Net_ListUserParams_Response.Item> items = new ArrayList<>();
|
List<Net_ListUserParams_Response.Item> items = new ArrayList<>();
|
||||||
|
|||||||
+6
-6
@@ -11,10 +11,10 @@ import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUser
|
|||||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Response;
|
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_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.SqliteDbController;
|
import shine.db.DbController;
|
||||||
import shine.db.dao.SolanaUsersDAO;
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
import shine.db.dao.UserParamsDAO;
|
import shine.db.dao.UserParamsDAO;
|
||||||
import shine.db.entities.SolanaUserEntry;
|
import shine.db.entities.CurrentUserEntry;
|
||||||
import shine.db.entities.UserParamEntry;
|
import shine.db.entities.UserParamEntry;
|
||||||
import utils.config.ShineSignatureConstants;
|
import utils.config.ShineSignatureConstants;
|
||||||
import utils.crypto.Ed25519Util;
|
import utils.crypto.Ed25519Util;
|
||||||
@@ -104,13 +104,13 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- DB checks + upsert ----------------
|
// ---------------- DB checks + upsert ----------------
|
||||||
SqliteDbController db = SqliteDbController.getInstance();
|
DbController db = DbController.getInstance();
|
||||||
SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
|
CurrentUsersDAO usersDAO = CurrentUsersDAO.getInstance();
|
||||||
UserParamsDAO paramsDAO = UserParamsDAO.getInstance();
|
UserParamsDAO paramsDAO = UserParamsDAO.getInstance();
|
||||||
|
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
// 1) user exists
|
// 1) user exists
|
||||||
SolanaUserEntry user = usersDAO.getByLogin(c, login);
|
CurrentUserEntry user = usersDAO.getByLogin(c, login);
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
return NetExceptionResponseFactory.error(
|
return NetExceptionResponseFactory.error(
|
||||||
req,
|
req,
|
||||||
|
|||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages;
|
||||||
|
|
||||||
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
|
public final class DmSyncApplySupport {
|
||||||
|
private DmSyncApplySupport() {}
|
||||||
|
|
||||||
|
public static ApplyResult applySyncedBlob(String ownerLogin, String blobB64) throws Exception {
|
||||||
|
if (ownerLogin == null || ownerLogin.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("EMPTY_OWNER_LOGIN");
|
||||||
|
}
|
||||||
|
if (blobB64 == null || blobB64.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("EMPTY_BLOB");
|
||||||
|
}
|
||||||
|
|
||||||
|
SignedMessageBlock block = SignedMessagesCore.parseFromB64(blobB64);
|
||||||
|
SignedMessagesCore.verifyUsersAndSignature(block);
|
||||||
|
SignedMessageEntry entry = SignedMessagesCore.toEntry(block, "DmSyncBatch", null);
|
||||||
|
String owner = ownerLogin.trim();
|
||||||
|
boolean ownerMatchesTarget = entry.getTargetLogin().equalsIgnoreCase(owner);
|
||||||
|
boolean ownerMatchesDeletePair = block.isDeleteType()
|
||||||
|
&& (entry.getFromLogin().equalsIgnoreCase(owner) || entry.getToLogin().equalsIgnoreCase(owner));
|
||||||
|
if (!ownerMatchesTarget && !ownerMatchesDeletePair) {
|
||||||
|
throw new IllegalArgumentException("TARGET_LOGIN_MISMATCH");
|
||||||
|
}
|
||||||
|
|
||||||
|
SignedMessagesDAO.ApplyStatus status;
|
||||||
|
if (block.isContentType()) {
|
||||||
|
status = SignedMessagesDAO.getInstance().upsertIncomingCopy(entry);
|
||||||
|
} else if (block.isReadReceiptType()) {
|
||||||
|
status = SignedMessagesDAO.getInstance().insertIfAbsent(entry);
|
||||||
|
} else if (block.isMessageDeleteType()) {
|
||||||
|
status = SignedMessagesDAO.getInstance().applyDeleteMessage(entry);
|
||||||
|
} else if (block.isConversationDeleteType()) {
|
||||||
|
status = SignedMessagesDAO.getInstance().applyDeleteConversation(entry);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("BAD_MESSAGE_TYPE");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ApplyResult(entry.getMessageKey(), entry.getBaseKey(), entry.getMessageType(), status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ApplyResult(
|
||||||
|
String messageKey,
|
||||||
|
String baseKey,
|
||||||
|
int messageType,
|
||||||
|
SignedMessagesDAO.ApplyStatus status
|
||||||
|
) {}
|
||||||
|
}
|
||||||
+2
-2
@@ -8,7 +8,7 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_AckSessionDelivery_Re
|
|||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_AckSessionDelivery_Response;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_AckSessionDelivery_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.SignedMessagesV2DAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
|
|
||||||
public class Net_AckSessionDelivery_Handler implements JsonMessageHandler {
|
public class Net_AckSessionDelivery_Handler implements JsonMessageHandler {
|
||||||
@Override
|
@Override
|
||||||
@@ -22,7 +22,7 @@ public class Net_AckSessionDelivery_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String messageKey = req.getMessageKey().trim();
|
String messageKey = req.getMessageKey().trim();
|
||||||
SignedMessagesV2DAO.getInstance().markDelivered(messageKey, ctx.getSessionId(), System.currentTimeMillis());
|
SignedMessagesDAO.getInstance().markDelivered(messageKey, ctx.getSessionId(), System.currentTimeMillis());
|
||||||
|
|
||||||
Net_AckSessionDelivery_Response resp = new Net_AckSessionDelivery_Response();
|
Net_AckSessionDelivery_Response resp = new Net_AckSessionDelivery_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
|
|||||||
+25
-6
@@ -15,9 +15,9 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
|||||||
import server.logic.ws_protocol.JSON.utils.NetIdGenerator;
|
import server.logic.ws_protocol.JSON.utils.NetIdGenerator;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import shine.db.dao.ActiveSessionsDAO;
|
import shine.db.dao.ActiveSessionsDAO;
|
||||||
import shine.db.dao.SolanaUsersDAO;
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
import shine.db.entities.ActiveSessionEntry;
|
import shine.db.entities.ActiveSessionEntry;
|
||||||
import shine.db.entities.SolanaUserEntry;
|
import shine.db.entities.CurrentUserEntry;
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -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,11 +39,14 @@ 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 обязательны");
|
||||||
}
|
}
|
||||||
|
|
||||||
SolanaUserEntry targetUser = SolanaUsersDAO.getInstance().getByLogin(toRequest);
|
CurrentUserEntry targetUser = CurrentUsersDAO.getInstance().getByLogin(toRequest);
|
||||||
if (targetUser == null) {
|
if (targetUser == null) {
|
||||||
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
||||||
}
|
}
|
||||||
@@ -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)) {
|
||||||
|
|||||||
+3
-3
@@ -17,9 +17,9 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
|||||||
import server.logic.ws_protocol.JSON.utils.NetIdGenerator;
|
import server.logic.ws_protocol.JSON.utils.NetIdGenerator;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import shine.db.dao.ActiveSessionsDAO;
|
import shine.db.dao.ActiveSessionsDAO;
|
||||||
import shine.db.dao.SolanaUsersDAO;
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
import shine.db.entities.ActiveSessionEntry;
|
import shine.db.entities.ActiveSessionEntry;
|
||||||
import shine.db.entities.SolanaUserEntry;
|
import shine.db.entities.CurrentUserEntry;
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -50,7 +50,7 @@ public class Net_CallSignalToSession_Handler implements JsonMessageHandler {
|
|||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "toLogin/targetSessionId/callId/type обязательны");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "toLogin/targetSessionId/callId/type обязательны");
|
||||||
}
|
}
|
||||||
|
|
||||||
SolanaUserEntry targetUser = SolanaUsersDAO.getInstance().getByLogin(toRequest);
|
CurrentUserEntry targetUser = CurrentUsersDAO.getInstance().getByLogin(toRequest);
|
||||||
if (targetUser == null) {
|
if (targetUser == null) {
|
||||||
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -9,8 +9,8 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_DeleteConversation_Re
|
|||||||
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.DmFederationService;
|
import server.sync.DmFederationService;
|
||||||
import shine.db.dao.SignedMessagesV2DAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
import shine.db.entities.SignedMessageV2Entry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
public class Net_DeleteConversation_Handler implements JsonMessageHandler {
|
public class Net_DeleteConversation_Handler implements JsonMessageHandler {
|
||||||
@Override
|
@Override
|
||||||
@@ -38,8 +38,8 @@ public class Net_DeleteConversation_Handler implements JsonMessageHandler {
|
|||||||
return NetExceptionResponseFactory.error(req, status, code, "Сообщение не прошло проверку");
|
return NetExceptionResponseFactory.error(req, status, code, "Сообщение не прошло проверку");
|
||||||
}
|
}
|
||||||
|
|
||||||
SignedMessageV2Entry entry = SignedMessagesCore.toEntry(block, "DeleteConversation", null);
|
SignedMessageEntry entry = SignedMessagesCore.toEntry(block, "DeleteConversation", null);
|
||||||
SignedMessagesV2DAO.ApplyStatus status = SignedMessagesV2DAO.getInstance().applyDeleteConversation(entry);
|
SignedMessagesDAO.ApplyStatus status = SignedMessagesDAO.getInstance().applyDeleteConversation(entry);
|
||||||
|
|
||||||
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
||||||
if (status.applied()) {
|
if (status.applied()) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user