SHA256
Compare commits
37
Commits
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
01321f5200 | ||
|
|
3a5851939e | ||
|
|
7d929e626e | ||
|
|
d8e0c77951 | ||
|
|
015fade4c0 | ||
|
|
da14b51e80 | ||
|
|
7a5ac01d1c | ||
|
|
5e6d64e965 | ||
|
|
c425fa41aa | ||
|
|
781157299f | ||
|
|
0f30317bb4 | ||
|
|
1f1bc0a7b9 | ||
|
|
60c8a6608a | ||
|
|
86adaf8c6b | ||
|
|
731688d16e | ||
|
|
63b66c48d8 | ||
|
|
ea19e511c0 | ||
|
|
f5e401cff8 | ||
|
|
bc8c7f318b | ||
|
|
585750007d | ||
|
|
6e80ac976a | ||
|
|
c70f18fcf4 | ||
|
|
d3e6aa2be2 | ||
|
|
b7a869c514 | ||
|
|
19362b950a | ||
|
|
0a4c31fb36 | ||
|
|
fef7694b48 | ||
|
|
60206e21df | ||
|
|
b9b77c66ce | ||
|
|
745a0e39d7 | ||
|
|
23bdadab56 | ||
|
|
47a4844ce8 | ||
|
|
8741be6cba | ||
|
|
4656a03ea9 | ||
|
|
5763eb828e | ||
|
|
6a5c20a165 | ||
|
|
fac166f186 |
@@ -98,14 +98,9 @@ public final class MsgSubType {
|
|||||||
/* ===================== CONNECTION (msg_type=3) ===================== */
|
/* ===================== CONNECTION (msg_type=3) ===================== */
|
||||||
|
|
||||||
/** Добавить в близкие друзья (close friend). */
|
/** Добавить в близкие друзья (close friend). */
|
||||||
public static final short CONNECTION_FRIEND = 10;
|
public static final short CONNECTION_CLOSE_FRIEND = 10;
|
||||||
/** Удалить из близких друзей (close friend). */
|
/** Удалить из близких друзей (close friend). */
|
||||||
public static final short CONNECTION_UNFRIEND = 11;
|
public static final short CONNECTION_UNCLOSE_FRIEND = 11;
|
||||||
|
|
||||||
/** Alias: добавить в close friend (то же значение, что CONNECTION_FRIEND). */
|
|
||||||
public static final short CONNECTION_CLOSE_FRIEND = CONNECTION_FRIEND;
|
|
||||||
/** Alias: удалить из close friend (то же значение, что CONNECTION_UNFRIEND). */
|
|
||||||
public static final short CONNECTION_UNCLOSE_FRIEND = CONNECTION_UNFRIEND;
|
|
||||||
|
|
||||||
/** Добавить в контакты. */
|
/** Добавить в контакты. */
|
||||||
public static final short CONNECTION_CONTACT = 20;
|
public static final short CONNECTION_CONTACT = 20;
|
||||||
|
|||||||
+3
-3
@@ -66,7 +66,7 @@ import java.util.Objects;
|
|||||||
* toBlockHash32=hash32(CREATE_CHANNEL)
|
* toBlockHash32=hash32(CREATE_CHANNEL)
|
||||||
*
|
*
|
||||||
* 3) ЗАПРЕТЫ ВАЛИДАЦИИ (желательно на сервере/в БД):
|
* 3) ЗАПРЕТЫ ВАЛИДАЦИИ (желательно на сервере/в БД):
|
||||||
* - CONNECTION_FRIEND/CONTACT не могут ссылаться на не-HEADER (toBlockNumber != 0 запрещено).
|
* - CONNECTION_CLOSE_FRIEND/CONTACT не могут ссылаться на не-HEADER (toBlockNumber != 0 запрещено).
|
||||||
* - FOLLOW на канал "X" не может ссылаться на произвольный пост внутри канала:
|
* - FOLLOW на канал "X" не может ссылаться на произвольный пост внутри канала:
|
||||||
* разрешено ТОЛЬКО на ROOT (HEADER или CREATE_CHANNEL).
|
* разрешено ТОЛЬКО на ROOT (HEADER или CREATE_CHANNEL).
|
||||||
*
|
*
|
||||||
@@ -183,8 +183,8 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasL
|
|||||||
|
|
||||||
private static boolean isValidSubType(short st) {
|
private static boolean isValidSubType(short st) {
|
||||||
int v = st & 0xFFFF;
|
int v = st & 0xFFFF;
|
||||||
return v == (MsgSubType.CONNECTION_FRIEND & 0xFFFF)
|
return v == (MsgSubType.CONNECTION_CLOSE_FRIEND & 0xFFFF)
|
||||||
|| v == (MsgSubType.CONNECTION_UNFRIEND & 0xFFFF)
|
|| v == (MsgSubType.CONNECTION_UNCLOSE_FRIEND & 0xFFFF)
|
||||||
|| v == (MsgSubType.CONNECTION_CONTACT & 0xFFFF)
|
|| v == (MsgSubType.CONNECTION_CONTACT & 0xFFFF)
|
||||||
|| v == (MsgSubType.CONNECTION_UNCONTACT & 0xFFFF)
|
|| v == (MsgSubType.CONNECTION_UNCONTACT & 0xFFFF)
|
||||||
|| v == (MsgSubType.CONNECTION_FOLLOW & 0xFFFF)
|
|| v == (MsgSubType.CONNECTION_FOLLOW & 0xFFFF)
|
||||||
|
|||||||
+3
@@ -14,6 +14,9 @@ public final class ShineSignatureConstants {
|
|||||||
/** Подписываемые данные параметра пользователя: prefix + login + param + time_ms + value */
|
/** Подписываемые данные параметра пользователя: prefix + login + param + time_ms + value */
|
||||||
public static final String USER_PARAMETER_PREFIX = "SHiNe/UserParameter:";
|
public static final String USER_PARAMETER_PREFIX = "SHiNe/UserParameter:";
|
||||||
|
|
||||||
|
/** Подписываемые данные пользовательских настроек: prefix + login + type + key + time_ms + value_text + value_num */
|
||||||
|
public static final String USER_SETTINGS_PREFIX = "SHiNe/UserSettings:";
|
||||||
|
|
||||||
/** TAG в HeaderBody (genesis). ASCII "SHiNe". */
|
/** TAG в HeaderBody (genesis). ASCII "SHiNe". */
|
||||||
public static final String BLOCKCHAIN_HEADER_TAG = "SHiNe";
|
public static final String BLOCKCHAIN_HEADER_TAG = "SHiNe";
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ public final class DatabaseInitializer {
|
|||||||
public static final int SCHEMA_VERSION_6 = 6;
|
public static final int SCHEMA_VERSION_6 = 6;
|
||||||
public static final int SCHEMA_VERSION_7 = 7;
|
public static final int SCHEMA_VERSION_7 = 7;
|
||||||
public static final int SCHEMA_VERSION_8 = 8;
|
public static final int SCHEMA_VERSION_8 = 8;
|
||||||
|
public static final int SCHEMA_VERSION_9 = 9;
|
||||||
|
public static final int SCHEMA_VERSION_10 = 10;
|
||||||
|
public static final int SCHEMA_VERSION_11 = 11;
|
||||||
|
public static final int SCHEMA_VERSION_12 = 12;
|
||||||
|
public static final int SCHEMA_VERSION_13 = 13;
|
||||||
|
public static final int SCHEMA_VERSION_14 = 14;
|
||||||
|
public static final int SCHEMA_VERSION_15 = 15;
|
||||||
public static final String POSTGRES_SCHEMA_RESOURCE = "postgres/schema_v1.sql";
|
public static final String POSTGRES_SCHEMA_RESOURCE = "postgres/schema_v1.sql";
|
||||||
public static final String POSTGRES_MIGRATION_V2_RESOURCE = "postgres/migration_v2.sql";
|
public static final String POSTGRES_MIGRATION_V2_RESOURCE = "postgres/migration_v2.sql";
|
||||||
public static final String POSTGRES_MIGRATION_V3_RESOURCE = "postgres/migration_v3.sql";
|
public static final String POSTGRES_MIGRATION_V3_RESOURCE = "postgres/migration_v3.sql";
|
||||||
@@ -33,6 +40,13 @@ public final class DatabaseInitializer {
|
|||||||
public static final String POSTGRES_MIGRATION_V6_RESOURCE = "postgres/migration_v6.sql";
|
public static final String POSTGRES_MIGRATION_V6_RESOURCE = "postgres/migration_v6.sql";
|
||||||
public static final String POSTGRES_MIGRATION_V7_RESOURCE = "postgres/migration_v7.sql";
|
public static final String POSTGRES_MIGRATION_V7_RESOURCE = "postgres/migration_v7.sql";
|
||||||
public static final String POSTGRES_MIGRATION_V8_RESOURCE = "postgres/migration_v8.sql";
|
public static final String POSTGRES_MIGRATION_V8_RESOURCE = "postgres/migration_v8.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V9_RESOURCE = "postgres/migration_v9.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V10_RESOURCE = "postgres/migration_v10.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V11_RESOURCE = "postgres/migration_v11.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V12_RESOURCE = "postgres/migration_v12.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V13_RESOURCE = "postgres/migration_v13.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V14_RESOURCE = "postgres/migration_v14.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V15_RESOURCE = "postgres/migration_v15.sql";
|
||||||
|
|
||||||
private DatabaseInitializer() {}
|
private DatabaseInitializer() {}
|
||||||
|
|
||||||
@@ -51,10 +65,8 @@ public final class DatabaseInitializer {
|
|||||||
public static final short REACTION_LIKE = 1;
|
public static final short REACTION_LIKE = 1;
|
||||||
public static final short REACTION_UNLIKE = 2;
|
public static final short REACTION_UNLIKE = 2;
|
||||||
|
|
||||||
public static final short CONNECTION_FRIEND = 10;
|
public static final short CONNECTION_CLOSE_FRIEND = 10;
|
||||||
public static final short CONNECTION_UNFRIEND = 11;
|
public static final short CONNECTION_UNCLOSE_FRIEND = 11;
|
||||||
public static final short CONNECTION_CLOSE_FRIEND = CONNECTION_FRIEND;
|
|
||||||
public static final short CONNECTION_UNCLOSE_FRIEND = CONNECTION_UNFRIEND;
|
|
||||||
|
|
||||||
public static final short CONNECTION_CONTACT = 20;
|
public static final short CONNECTION_CONTACT = 20;
|
||||||
public static final short CONNECTION_UNCONTACT = 21;
|
public static final short CONNECTION_UNCONTACT = 21;
|
||||||
@@ -124,6 +136,35 @@ public final class DatabaseInitializer {
|
|||||||
}
|
}
|
||||||
if (currentVersion < SCHEMA_VERSION_8) {
|
if (currentVersion < SCHEMA_VERSION_8) {
|
||||||
runSqlScript(conn, POSTGRES_MIGRATION_V8_RESOURCE);
|
runSqlScript(conn, POSTGRES_MIGRATION_V8_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_8;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_9) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V9_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_9;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_10) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V10_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_10;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_11) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V11_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_11;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_12) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V12_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_12;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_13) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V13_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_13;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_14) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V14_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_14;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_15) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V15_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_15;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package shine.db;
|
package shine.db;
|
||||||
|
|
||||||
import shine.db.connection.DbProvider;
|
import shine.db.connection.DbProvider;
|
||||||
|
import shine.db.dao.DmDialogStateDAO;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
@@ -15,6 +16,7 @@ public final class DbController implements DbProvider {
|
|||||||
private static volatile DbController instance;
|
private static volatile DbController instance;
|
||||||
|
|
||||||
private final PostgresDbController delegate;
|
private final PostgresDbController delegate;
|
||||||
|
private volatile boolean dmDialogStateBootstrapped;
|
||||||
|
|
||||||
private DbController() {
|
private DbController() {
|
||||||
this.delegate = PostgresDbController.getInstance();
|
this.delegate = PostgresDbController.getInstance();
|
||||||
@@ -24,7 +26,9 @@ public final class DbController implements DbProvider {
|
|||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
synchronized (DbController.class) {
|
synchronized (DbController.class) {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
instance = new DbController();
|
DbController created = new DbController();
|
||||||
|
instance = created;
|
||||||
|
created.bootstrapDmDialogStateIfNeeded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,4 +44,18 @@ public final class DbController implements DbProvider {
|
|||||||
public void close() {
|
public void close() {
|
||||||
delegate.close();
|
delegate.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void bootstrapDmDialogStateIfNeeded() {
|
||||||
|
if (dmDialogStateBootstrapped) return;
|
||||||
|
synchronized (this) {
|
||||||
|
if (dmDialogStateBootstrapped) return;
|
||||||
|
try (Connection connection = delegate.getConnection()) {
|
||||||
|
DmDialogStateDAO.getInstance().bootstrapIfEmpty(connection);
|
||||||
|
dmDialogStateBootstrapped = true;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
instance = null;
|
||||||
|
throw new RuntimeException("DM dialog state bootstrap failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,23 +58,17 @@ public final class MsgSubType {
|
|||||||
/* ===================== CONNECTION (msg_type=3) ===================== */
|
/* ===================== CONNECTION (msg_type=3) ===================== */
|
||||||
/**
|
/**
|
||||||
* Совпадает с ConnectionBody:
|
* Совпадает с ConnectionBody:
|
||||||
* SET: CLOSE_FRIEND(=FRIEND)=10, CONTACT=20, FOLLOW=30, SPOUSE=40, PARENT=50, CHILD=52, SIBLING=54,
|
* SET: CLOSE_FRIEND=10, CONTACT=20, FOLLOW=30, SPOUSE=40, PARENT=50, CHILD=52, SIBLING=54,
|
||||||
* KNOWN_PERSON=60, SHINE_CONFIRMED=70, SHINE_SEEN=74
|
* KNOWN_PERSON=60, SHINE_CONFIRMED=70, SHINE_SEEN=74
|
||||||
* UNSET: UNCLOSE_FRIEND(=UNFRIEND)=11, UNCONTACT=21, UNFOLLOW=31, UNSPOUSE=41, UNPARENT=51, UNCHILD=53, UNSIBLING=55,
|
* UNSET: UNCLOSE_FRIEND=11, UNCONTACT=21, UNFOLLOW=31, UNSPOUSE=41, UNPARENT=51, UNCHILD=53, UNSIBLING=55,
|
||||||
* UNKNOWN_PERSON=61, SHINE_UNCONFIRMED=71, SHINE_UNSEEN=75
|
* UNKNOWN_PERSON=61, SHINE_UNCONFIRMED=71, SHINE_UNSEEN=75
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Добавить в близкие друзья (close friend). */
|
/** Добавить в близкие друзья (close friend). */
|
||||||
public static final short CONNECTION_FRIEND = 10;
|
public static final short CONNECTION_CLOSE_FRIEND = 10;
|
||||||
|
|
||||||
/** Удалить из близких друзей (close friend). */
|
/** Удалить из близких друзей (close friend). */
|
||||||
public static final short CONNECTION_UNFRIEND = 11;
|
public static final short CONNECTION_UNCLOSE_FRIEND = 11;
|
||||||
|
|
||||||
/** Alias: добавить в close friend (то же значение, что CONNECTION_FRIEND). */
|
|
||||||
public static final short CONNECTION_CLOSE_FRIEND = CONNECTION_FRIEND;
|
|
||||||
|
|
||||||
/** Alias: удалить из close friend (то же значение, что CONNECTION_UNFRIEND). */
|
|
||||||
public static final short CONNECTION_UNCLOSE_FRIEND = CONNECTION_UNFRIEND;
|
|
||||||
|
|
||||||
/** Добавить в контакты. */
|
/** Добавить в контакты. */
|
||||||
public static final short CONNECTION_CONTACT = 20;
|
public static final short CONNECTION_CONTACT = 20;
|
||||||
|
|||||||
+102
-6
@@ -99,6 +99,8 @@ public final class BlockchainResyncCleanupDAO {
|
|||||||
int deletedBlocks = deleteBlocksForChain(c, blockchainName);
|
int deletedBlocks = deleteBlocksForChain(c, blockchainName);
|
||||||
int deletedBlockchainState = deleteBlockchainStateForChain(c, blockchainName);
|
int deletedBlockchainState = deleteBlockchainStateForChain(c, blockchainName);
|
||||||
|
|
||||||
|
rebuildStatsState(c);
|
||||||
|
|
||||||
c.commit();
|
c.commit();
|
||||||
|
|
||||||
return new CleanupResult(
|
return new CleanupResult(
|
||||||
@@ -185,10 +187,10 @@ public final class BlockchainResyncCleanupDAO {
|
|||||||
private int decreaseForeignLikesCount(Connection c, String blockchainName) throws SQLException {
|
private int decreaseForeignLikesCount(Connection c, String blockchainName) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
UPDATE message_stats
|
UPDATE message_stats
|
||||||
SET likes_count = MAX(
|
SET likes_count = GREATEST(
|
||||||
0,
|
0,
|
||||||
likes_count - (
|
likes_count - COALESCE((
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)::int
|
||||||
FROM reactions_state rs
|
FROM reactions_state rs
|
||||||
WHERE rs.from_bch_name = ?
|
WHERE rs.from_bch_name = ?
|
||||||
AND rs.reaction_type = ?
|
AND rs.reaction_type = ?
|
||||||
@@ -198,7 +200,7 @@ public final class BlockchainResyncCleanupDAO {
|
|||||||
AND rs.to_block_number = message_stats.to_block_number
|
AND rs.to_block_number = message_stats.to_block_number
|
||||||
AND rs.to_block_hash = message_stats.to_block_hash
|
AND rs.to_block_hash = message_stats.to_block_hash
|
||||||
AND rs.to_bch_name <> ?
|
AND rs.to_bch_name <> ?
|
||||||
)
|
), 0)
|
||||||
)
|
)
|
||||||
WHERE EXISTS (
|
WHERE EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
@@ -237,10 +239,10 @@ public final class BlockchainResyncCleanupDAO {
|
|||||||
private int decreaseForeignRepliesCount(Connection c, String blockchainName) throws SQLException {
|
private int decreaseForeignRepliesCount(Connection c, String blockchainName) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
UPDATE message_stats
|
UPDATE message_stats
|
||||||
SET replies_count = MAX(
|
SET replies_count = GREATEST(
|
||||||
0,
|
0,
|
||||||
replies_count - COALESCE((
|
replies_count - COALESCE((
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)::int
|
||||||
FROM blocks b
|
FROM blocks b
|
||||||
WHERE b.bch_name = ?
|
WHERE b.bch_name = ?
|
||||||
AND b.msg_type = 1
|
AND b.msg_type = 1
|
||||||
@@ -366,6 +368,100 @@ public final class BlockchainResyncCleanupDAO {
|
|||||||
""", blockchainName);
|
""", blockchainName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void rebuildStatsState(Connection c) throws SQLException {
|
||||||
|
try (PreparedStatement truncate = c.prepareStatement("""
|
||||||
|
TRUNCATE TABLE user_stats_state, channel_stats_state
|
||||||
|
""")) {
|
||||||
|
truncate.executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
u.login,
|
||||||
|
COALESCE(own.owned_public_channels_count, 0),
|
||||||
|
COALESCE(fu.following_users_count, 0),
|
||||||
|
COALESCE(fc.following_channels_count, 0),
|
||||||
|
COALESCE(cf.close_friends_count, 0),
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
FROM solana_user_pda_current u
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT owner_login, COUNT(*)::INTEGER AS owned_public_channels_count
|
||||||
|
FROM channel_names_state
|
||||||
|
WHERE channel_type_code = 1
|
||||||
|
GROUP BY owner_login
|
||||||
|
) own ON LOWER(own.owner_login) = LOWER(u.login)
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT login, COUNT(*)::INTEGER AS following_users_count
|
||||||
|
FROM connections_state
|
||||||
|
WHERE rel_type = 30
|
||||||
|
AND to_block_number = 0
|
||||||
|
GROUP BY login
|
||||||
|
) fu ON LOWER(fu.login) = LOWER(u.login)
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT cs.login, COUNT(*)::INTEGER AS following_channels_count
|
||||||
|
FROM connections_state cs
|
||||||
|
JOIN channel_names_state cn
|
||||||
|
ON cn.owner_bch_name = cs.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = cs.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = cs.to_block_hash
|
||||||
|
WHERE cs.rel_type = 30
|
||||||
|
AND cn.channel_type_code = 1
|
||||||
|
GROUP BY cs.login
|
||||||
|
) fc ON LOWER(fc.login) = LOWER(u.login)
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT login, COUNT(*)::INTEGER AS close_friends_count
|
||||||
|
FROM connections_state
|
||||||
|
WHERE rel_type = 10
|
||||||
|
GROUP BY login
|
||||||
|
) cf ON LOWER(cf.login) = LOWER(u.login)
|
||||||
|
""")) {
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
cn.owner_bch_name,
|
||||||
|
cn.channel_root_block_number,
|
||||||
|
cn.channel_root_block_hash,
|
||||||
|
cn.owner_login,
|
||||||
|
cn.channel_type_code,
|
||||||
|
COUNT(DISTINCT cs.login)::INTEGER AS subscribers_count,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
FROM channel_names_state cn
|
||||||
|
LEFT JOIN connections_state cs
|
||||||
|
ON cs.rel_type = 30
|
||||||
|
AND cs.to_bch_name = cn.owner_bch_name
|
||||||
|
AND cs.to_block_number = cn.channel_root_block_number
|
||||||
|
AND cs.to_block_hash = cn.channel_root_block_hash
|
||||||
|
WHERE cn.channel_type_code = 1
|
||||||
|
GROUP BY
|
||||||
|
cn.owner_bch_name,
|
||||||
|
cn.channel_root_block_number,
|
||||||
|
cn.channel_root_block_hash,
|
||||||
|
cn.owner_login,
|
||||||
|
cn.channel_type_code
|
||||||
|
""")) {
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private int executeDelete(Connection c, String sql, String value) throws SQLException {
|
private int executeDelete(Connection c, String sql, String value) throws SQLException {
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, value);
|
ps.setString(1, value);
|
||||||
|
|||||||
@@ -67,6 +67,36 @@ public final class ConnectionsStateDAO {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasOutgoingByRelTypeCanonical(Connection c, String loginAnyCase, String peerLoginAnyCase, int relType) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT 1
|
||||||
|
FROM connections_state cs
|
||||||
|
LEFT JOIN %s
|
||||||
|
ON LOWER(u_login.login) = LOWER(cs.to_login)
|
||||||
|
LEFT JOIN %s
|
||||||
|
ON LOWER(u_bch.blockchain_name) = LOWER(cs.to_bch_name)
|
||||||
|
WHERE LOWER(cs.login) = LOWER(?)
|
||||||
|
AND cs.rel_type = ?
|
||||||
|
AND (
|
||||||
|
LOWER(cs.to_login) = LOWER(?)
|
||||||
|
OR LOWER(COALESCE(u_login.login, u_bch.login, cs.to_login)) = LOWER(?)
|
||||||
|
)
|
||||||
|
LIMIT 1
|
||||||
|
""".formatted(
|
||||||
|
CurrentUsersSql.usersSubquery("u_login"),
|
||||||
|
CurrentUsersSql.usersSubquery("u_bch")
|
||||||
|
);
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, loginAnyCase);
|
||||||
|
ps.setInt(2, relType);
|
||||||
|
ps.setString(3, peerLoginAnyCase);
|
||||||
|
ps.setString(4, peerLoginAnyCase);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
return rs.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Incoming: список логинов (канонических), кто поставил relType пользователю login.
|
* Incoming: список логинов (канонических), кто поставил relType пользователю login.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
package shine.db.dao;
|
|
||||||
|
|
||||||
import shine.db.DbController;
|
|
||||||
import shine.db.entities.DirectMessageEntry;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.sql.PreparedStatement;
|
|
||||||
|
|
||||||
public final class DirectMessagesDAO {
|
|
||||||
private static volatile DirectMessagesDAO instance;
|
|
||||||
private final DbController db = DbController.getInstance();
|
|
||||||
|
|
||||||
private DirectMessagesDAO() {}
|
|
||||||
|
|
||||||
public static DirectMessagesDAO getInstance() {
|
|
||||||
if (instance == null) {
|
|
||||||
synchronized (DirectMessagesDAO.class) {
|
|
||||||
if (instance == null) instance = new DirectMessagesDAO();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void insert(DirectMessageEntry entry) throws Exception {
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
String sql = """
|
|
||||||
INSERT INTO direct_messages (
|
|
||||||
message_id, from_login, to_login, text, created_at_ms
|
|
||||||
) VALUES (?, ?, ?, ?, ?)
|
|
||||||
""";
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
|
||||||
ps.setString(1, entry.getMessageId());
|
|
||||||
ps.setString(2, entry.getFromLogin());
|
|
||||||
ps.setString(3, entry.getToLogin());
|
|
||||||
ps.setString(4, entry.getText());
|
|
||||||
ps.setLong(5, entry.getCreatedAtMs());
|
|
||||||
ps.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean existsFromTo(String fromLogin, String toLogin) throws Exception {
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
String sql = "SELECT 1 FROM direct_messages WHERE from_login = ? AND to_login = ? LIMIT 1";
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
|
||||||
ps.setString(1, fromLogin);
|
|
||||||
ps.setString(2, toLogin);
|
|
||||||
return ps.executeQuery().next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,518 @@
|
|||||||
|
package shine.db.dao;
|
||||||
|
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.entities.DmDeliveryStateEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Types;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/** Хранилище изменяемого состояния межсерверной доставки DM-пары. */
|
||||||
|
public final class DmDeliveryStateDAO {
|
||||||
|
private static volatile DmDeliveryStateDAO instance;
|
||||||
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
|
private DmDeliveryStateDAO() {}
|
||||||
|
|
||||||
|
public static DmDeliveryStateDAO getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (DmDeliveryStateDAO.class) {
|
||||||
|
if (instance == null) instance = new DmDeliveryStateDAO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DmDeliveryStateEntry upsertPair(
|
||||||
|
String outgoingMessageKey,
|
||||||
|
String eventId,
|
||||||
|
String baseKey,
|
||||||
|
String fromLogin,
|
||||||
|
String toLogin,
|
||||||
|
String incomingMessageKey,
|
||||||
|
long createdAtMs,
|
||||||
|
long expiresAtMs,
|
||||||
|
int initialState,
|
||||||
|
String deliveredServerLogin,
|
||||||
|
String routesHash,
|
||||||
|
boolean assistImmediately
|
||||||
|
) throws SQLException {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
int safeState = normalizeState(initialState, deliveredServerLogin);
|
||||||
|
if (expiresAtMs <= now && safeState == DmDeliveryStateEntry.PENDING_NONE) {
|
||||||
|
safeState = DmDeliveryStateEntry.FAILED_FINAL;
|
||||||
|
}
|
||||||
|
Long nextAttemptAt = safeState == DmDeliveryStateEntry.ACCEPTED
|
||||||
|
? now
|
||||||
|
: null;
|
||||||
|
String safeDeliveredLogin = safeState == DmDeliveryStateEntry.DELIVERED_ONE
|
||||||
|
? normalize(deliveredServerLogin)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO dm_delivery_state (
|
||||||
|
outgoing_message_key, event_id, base_key, from_login, to_login,
|
||||||
|
incoming_message_key, created_at_ms, delivery_expires_at_ms,
|
||||||
|
delivery_state, delivered_server_login, recipient_routes_hash,
|
||||||
|
attempt_index, next_attempt_at_ms, last_attempt_at_ms, last_error, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, NULL, NULL, ?)
|
||||||
|
ON CONFLICT (outgoing_message_key) DO UPDATE SET
|
||||||
|
event_id = EXCLUDED.event_id,
|
||||||
|
base_key = EXCLUDED.base_key,
|
||||||
|
from_login = EXCLUDED.from_login,
|
||||||
|
to_login = EXCLUDED.to_login,
|
||||||
|
incoming_message_key = EXCLUDED.incoming_message_key,
|
||||||
|
created_at_ms = CASE
|
||||||
|
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.created_at_ms
|
||||||
|
ELSE EXCLUDED.created_at_ms
|
||||||
|
END,
|
||||||
|
delivery_expires_at_ms = CASE
|
||||||
|
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.delivery_expires_at_ms
|
||||||
|
ELSE EXCLUDED.delivery_expires_at_ms
|
||||||
|
END,
|
||||||
|
delivery_state = CASE
|
||||||
|
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.delivery_state
|
||||||
|
ELSE EXCLUDED.delivery_state
|
||||||
|
END,
|
||||||
|
delivered_server_login = CASE
|
||||||
|
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.delivered_server_login
|
||||||
|
ELSE EXCLUDED.delivered_server_login
|
||||||
|
END,
|
||||||
|
recipient_routes_hash = CASE
|
||||||
|
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN COALESCE(dm_delivery_state.recipient_routes_hash, EXCLUDED.recipient_routes_hash)
|
||||||
|
ELSE EXCLUDED.recipient_routes_hash
|
||||||
|
END,
|
||||||
|
attempt_index = CASE
|
||||||
|
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.attempt_index
|
||||||
|
ELSE 0
|
||||||
|
END,
|
||||||
|
next_attempt_at_ms = CASE
|
||||||
|
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.next_attempt_at_ms
|
||||||
|
ELSE EXCLUDED.next_attempt_at_ms
|
||||||
|
END,
|
||||||
|
last_attempt_at_ms = CASE
|
||||||
|
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.last_attempt_at_ms
|
||||||
|
ELSE NULL
|
||||||
|
END,
|
||||||
|
last_error = CASE
|
||||||
|
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.last_error
|
||||||
|
ELSE NULL
|
||||||
|
END,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, outgoingMessageKey);
|
||||||
|
ps.setString(2, eventId);
|
||||||
|
ps.setString(3, baseKey);
|
||||||
|
ps.setString(4, normalize(fromLogin));
|
||||||
|
ps.setString(5, normalize(toLogin));
|
||||||
|
ps.setString(6, incomingMessageKey);
|
||||||
|
ps.setLong(7, createdAtMs);
|
||||||
|
ps.setLong(8, expiresAtMs);
|
||||||
|
ps.setInt(9, safeState);
|
||||||
|
setNullableString(ps, 10, safeDeliveredLogin);
|
||||||
|
setNullableString(ps, 11, normalizeBlank(routesHash));
|
||||||
|
setNullableLong(ps, 12, nextAttemptAt);
|
||||||
|
ps.setLong(13, now);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DmDeliveryStateEntry result;
|
||||||
|
if (initialState != DmDeliveryStateEntry.PENDING_NONE || !isBlank(deliveredServerLogin)) {
|
||||||
|
result = mergeRemote(eventId, initialState, deliveredServerLogin, routesHash, now);
|
||||||
|
} else {
|
||||||
|
result = getByEventId(eventId);
|
||||||
|
}
|
||||||
|
if (result != null && result.getDeliveryExpiresAtMs() <= now
|
||||||
|
&& result.getDeliveryState() == DmDeliveryStateEntry.PENDING_NONE) {
|
||||||
|
return finishAtExpiry(eventId, now);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DmDeliveryStateEntry getByEventId(String eventId) throws SQLException {
|
||||||
|
if (isBlank(eventId)) return null;
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(selectColumns() + " WHERE event_id = ?")) {
|
||||||
|
ps.setString(1, eventId.trim());
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
return rs.next() ? mapRow(rs) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DmDeliveryStateEntry getByOutgoingMessageKey(String outgoingMessageKey) throws SQLException {
|
||||||
|
if (isBlank(outgoingMessageKey)) return null;
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(selectColumns() + " WHERE outgoing_message_key = ?")) {
|
||||||
|
ps.setString(1, outgoingMessageKey.trim());
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
return rs.next() ? mapRow(rs) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, DmDeliveryStateEntry> listByOutgoingMessageKeys(List<String> keys) throws SQLException {
|
||||||
|
Map<String, DmDeliveryStateEntry> out = new HashMap<>();
|
||||||
|
if (keys == null || keys.isEmpty()) return out;
|
||||||
|
String placeholders = String.join(",", java.util.Collections.nCopies(keys.size(), "?"));
|
||||||
|
String sql = selectColumns() + " WHERE outgoing_message_key IN (" + placeholders + ")";
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
for (int i = 0; i < keys.size(); i++) ps.setString(i + 1, keys.get(i));
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
DmDeliveryStateEntry row = mapRow(rs);
|
||||||
|
out.put(row.getOutgoingMessageKey(), row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DmDeliveryStateEntry> listDue(long nowMs, int limit) throws SQLException {
|
||||||
|
String sql = selectColumns() + """
|
||||||
|
WHERE delivery_state = 0
|
||||||
|
AND next_attempt_at_ms IS NOT NULL
|
||||||
|
AND next_attempt_at_ms <= ?
|
||||||
|
ORDER BY next_attempt_at_ms ASC, created_at_ms ASC
|
||||||
|
LIMIT ?
|
||||||
|
""";
|
||||||
|
List<DmDeliveryStateEntry> out = new ArrayList<>();
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setLong(1, nowMs);
|
||||||
|
ps.setInt(2, Math.max(1, limit));
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) out.add(mapRow(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean claimAttempt(DmDeliveryStateEntry row, long nowMs, Long nextAttemptAtMs) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
UPDATE dm_delivery_state
|
||||||
|
SET attempt_index = attempt_index + 1,
|
||||||
|
next_attempt_at_ms = ?,
|
||||||
|
last_attempt_at_ms = ?,
|
||||||
|
last_error = NULL,
|
||||||
|
updated_at_ms = ?
|
||||||
|
WHERE outgoing_message_key = ?
|
||||||
|
AND event_id = ?
|
||||||
|
AND attempt_index = ?
|
||||||
|
AND delivery_state = 0
|
||||||
|
AND next_attempt_at_ms IS NOT NULL
|
||||||
|
AND next_attempt_at_ms <= ?
|
||||||
|
""";
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
long networkLeaseUntilMs = nowMs + 60_000L;
|
||||||
|
Long claimedNextAttemptAtMs = nextAttemptAtMs == null
|
||||||
|
? networkLeaseUntilMs
|
||||||
|
: Math.max(nextAttemptAtMs, networkLeaseUntilMs);
|
||||||
|
setNullableLong(ps, 1, claimedNextAttemptAtMs);
|
||||||
|
ps.setLong(2, nowMs);
|
||||||
|
ps.setLong(3, nowMs);
|
||||||
|
ps.setString(4, row.getOutgoingMessageKey());
|
||||||
|
ps.setString(5, row.getEventId());
|
||||||
|
ps.setInt(6, row.getAttemptIndex());
|
||||||
|
ps.setLong(7, nowMs);
|
||||||
|
return ps.executeUpdate() == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DmDeliveryStateEntry mergeRemote(
|
||||||
|
String eventId,
|
||||||
|
int remoteState,
|
||||||
|
String remoteDeliveredLogin,
|
||||||
|
String remoteRoutesHash,
|
||||||
|
long nowMs
|
||||||
|
) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
boolean previousAutoCommit = c.getAutoCommit();
|
||||||
|
c.setAutoCommit(false);
|
||||||
|
try {
|
||||||
|
DmDeliveryStateEntry current = getByEventIdForUpdate(c, eventId);
|
||||||
|
if (current == null) {
|
||||||
|
c.rollback();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MergeResult merged = merge(
|
||||||
|
current.getDeliveryState(),
|
||||||
|
current.getDeliveredServerLogin(),
|
||||||
|
current.getRecipientRoutesHash(),
|
||||||
|
remoteState,
|
||||||
|
remoteDeliveredLogin,
|
||||||
|
remoteRoutesHash
|
||||||
|
);
|
||||||
|
updateMutableState(c, current, merged.state(), merged.deliveredLogin(),
|
||||||
|
merged.routesHash(), current.getNextAttemptAtMs(), current.getLastError(), nowMs);
|
||||||
|
c.commit();
|
||||||
|
return getByEventId(eventId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
try { c.rollback(); } catch (Exception ignored) {}
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
c.setAutoCommit(previousAutoCommit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DmDeliveryStateEntry updateAfterAttempt(
|
||||||
|
String eventId,
|
||||||
|
int attemptedState,
|
||||||
|
String deliveredServerLogin,
|
||||||
|
String routesHash,
|
||||||
|
Long nextAttemptAtMs,
|
||||||
|
String lastError,
|
||||||
|
long nowMs
|
||||||
|
) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
boolean previousAutoCommit = c.getAutoCommit();
|
||||||
|
c.setAutoCommit(false);
|
||||||
|
try {
|
||||||
|
DmDeliveryStateEntry current = getByEventIdForUpdate(c, eventId);
|
||||||
|
if (current == null) {
|
||||||
|
c.rollback();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MergeResult merged = merge(
|
||||||
|
current.getDeliveryState(), current.getDeliveredServerLogin(), current.getRecipientRoutesHash(),
|
||||||
|
attemptedState, deliveredServerLogin, routesHash
|
||||||
|
);
|
||||||
|
Long effectiveNext = merged.state() == DmDeliveryStateEntry.ACCEPTED
|
||||||
|
? nextAttemptAtMs
|
||||||
|
: null;
|
||||||
|
updateMutableState(c, current, merged.state(), merged.deliveredLogin(),
|
||||||
|
merged.routesHash(), effectiveNext, lastError, nowMs);
|
||||||
|
c.commit();
|
||||||
|
return getByEventId(eventId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
try { c.rollback(); } catch (Exception ignored) {}
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
c.setAutoCommit(previousAutoCommit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DmDeliveryStateEntry finishAtExpiry(String eventId, long nowMs) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
String sql = """
|
||||||
|
UPDATE dm_delivery_state
|
||||||
|
SET delivery_state = CASE WHEN delivery_state = 0 THEN 3 ELSE delivery_state END,
|
||||||
|
delivered_server_login = CASE WHEN delivery_state = 0 THEN NULL ELSE delivered_server_login END,
|
||||||
|
next_attempt_at_ms = NULL,
|
||||||
|
last_error = CASE WHEN delivery_state = 0 THEN 'DELIVERY_EXPIRED' ELSE last_error END,
|
||||||
|
updated_at_ms = ?
|
||||||
|
WHERE event_id = ? AND delivery_state = 0
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setLong(1, nowMs);
|
||||||
|
ps.setString(2, eventId);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getByEventId(eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read-only peer confirmed that at least one recipient server accepted the message. */
|
||||||
|
public DmDeliveryStateEntry markDeliveredFromPeer(String eventId, long nowMs) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement("""
|
||||||
|
UPDATE dm_delivery_state
|
||||||
|
SET delivery_state = 1,
|
||||||
|
delivered_server_login = NULL,
|
||||||
|
next_attempt_at_ms = NULL,
|
||||||
|
last_error = NULL,
|
||||||
|
updated_at_ms = ?
|
||||||
|
WHERE event_id = ? AND delivery_state = 0
|
||||||
|
""")) {
|
||||||
|
ps.setLong(1, nowMs);
|
||||||
|
ps.setString(2, eventId);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
return getByEventId(eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int removeByBaseKey(String baseKey) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement("DELETE FROM dm_delivery_state WHERE base_key = ?")) {
|
||||||
|
ps.setString(1, baseKey);
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int removeConversationBefore(String fromLogin, String toLogin, long boundaryTimeMs) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
DELETE FROM dm_delivery_state
|
||||||
|
WHERE created_at_ms < ?
|
||||||
|
AND ((LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
|
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?)))
|
||||||
|
""";
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setLong(1, boundaryTimeMs);
|
||||||
|
ps.setString(2, fromLogin);
|
||||||
|
ps.setString(3, toLogin);
|
||||||
|
ps.setString(4, toLogin);
|
||||||
|
ps.setString(5, fromLogin);
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int removeMissingMessages() throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
DELETE FROM dm_delivery_state d
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM signed_messages m WHERE m.message_key = d.outgoing_message_key
|
||||||
|
)
|
||||||
|
""";
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DmDeliveryStateEntry getByEventIdForUpdate(Connection c, String eventId) throws SQLException {
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(selectColumns() + " WHERE event_id = ? FOR UPDATE")) {
|
||||||
|
ps.setString(1, eventId);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
return rs.next() ? mapRow(rs) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMutableState(
|
||||||
|
Connection c,
|
||||||
|
DmDeliveryStateEntry current,
|
||||||
|
int state,
|
||||||
|
String deliveredLogin,
|
||||||
|
String routesHash,
|
||||||
|
Long nextAttemptAtMs,
|
||||||
|
String lastError,
|
||||||
|
long nowMs
|
||||||
|
) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
UPDATE dm_delivery_state
|
||||||
|
SET delivery_state = ?,
|
||||||
|
delivered_server_login = ?,
|
||||||
|
recipient_routes_hash = ?,
|
||||||
|
next_attempt_at_ms = ?,
|
||||||
|
last_error = ?,
|
||||||
|
updated_at_ms = ?
|
||||||
|
WHERE outgoing_message_key = ? AND event_id = ?
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setInt(1, state);
|
||||||
|
setNullableString(ps, 2, state == DmDeliveryStateEntry.DELIVERED_ONE ? normalize(deliveredLogin) : null);
|
||||||
|
setNullableString(ps, 3, normalizeBlank(routesHash));
|
||||||
|
setNullableLong(ps, 4, nextAttemptAtMs);
|
||||||
|
setNullableString(ps, 5, normalizeBlank(lastError));
|
||||||
|
ps.setLong(6, nowMs);
|
||||||
|
ps.setString(7, current.getOutgoingMessageKey());
|
||||||
|
ps.setString(8, current.getEventId());
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MergeResult merge(
|
||||||
|
int localState,
|
||||||
|
String localLogin,
|
||||||
|
String localHash,
|
||||||
|
int remoteStateRaw,
|
||||||
|
String remoteLoginRaw,
|
||||||
|
String remoteHashRaw
|
||||||
|
) {
|
||||||
|
int remoteState = normalizeState(remoteStateRaw, remoteLoginRaw);
|
||||||
|
String remoteLogin = normalize(remoteLoginRaw);
|
||||||
|
String localNormalizedLogin = normalize(localLogin);
|
||||||
|
String localRoutesHash = normalizeBlank(localHash);
|
||||||
|
String remoteRoutesHash = normalizeBlank(remoteHashRaw);
|
||||||
|
String resultHash = remoteRoutesHash != null ? remoteRoutesHash : localRoutesHash;
|
||||||
|
|
||||||
|
if (localState == DmDeliveryStateEntry.DELIVERED_ONE || localState == DmDeliveryStateEntry.DELIVERED_ALL) {
|
||||||
|
return new MergeResult(DmDeliveryStateEntry.DELIVERED, localNormalizedLogin,
|
||||||
|
localRoutesHash != null ? localRoutesHash : remoteRoutesHash);
|
||||||
|
}
|
||||||
|
if (remoteState == DmDeliveryStateEntry.DELIVERED_ONE || remoteState == DmDeliveryStateEntry.DELIVERED_ALL) {
|
||||||
|
return new MergeResult(DmDeliveryStateEntry.DELIVERED, remoteLogin, resultHash);
|
||||||
|
}
|
||||||
|
if (localState == DmDeliveryStateEntry.FAILED_FINAL || remoteState == DmDeliveryStateEntry.FAILED_FINAL) {
|
||||||
|
return new MergeResult(DmDeliveryStateEntry.FAILED_FINAL, null, resultHash);
|
||||||
|
}
|
||||||
|
return new MergeResult(DmDeliveryStateEntry.PENDING_NONE, null, resultHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int normalizeState(int state, String deliveredLogin) {
|
||||||
|
if (state < DmDeliveryStateEntry.PENDING_NONE || state > DmDeliveryStateEntry.FAILED_FINAL) {
|
||||||
|
return DmDeliveryStateEntry.PENDING_NONE;
|
||||||
|
}
|
||||||
|
if (state == DmDeliveryStateEntry.DELIVERED_ALL) return DmDeliveryStateEntry.DELIVERED;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String selectColumns() {
|
||||||
|
return """
|
||||||
|
SELECT outgoing_message_key, event_id, base_key, from_login, to_login,
|
||||||
|
incoming_message_key, created_at_ms, delivery_expires_at_ms,
|
||||||
|
delivery_state, delivered_server_login, recipient_routes_hash,
|
||||||
|
attempt_index, next_attempt_at_ms, last_attempt_at_ms, last_error, updated_at_ms
|
||||||
|
FROM dm_delivery_state
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
private DmDeliveryStateEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
|
DmDeliveryStateEntry e = new DmDeliveryStateEntry();
|
||||||
|
e.setOutgoingMessageKey(rs.getString("outgoing_message_key"));
|
||||||
|
e.setEventId(rs.getString("event_id"));
|
||||||
|
e.setBaseKey(rs.getString("base_key"));
|
||||||
|
e.setFromLogin(rs.getString("from_login"));
|
||||||
|
e.setToLogin(rs.getString("to_login"));
|
||||||
|
e.setIncomingMessageKey(rs.getString("incoming_message_key"));
|
||||||
|
e.setCreatedAtMs(rs.getLong("created_at_ms"));
|
||||||
|
e.setDeliveryExpiresAtMs(rs.getLong("delivery_expires_at_ms"));
|
||||||
|
e.setDeliveryState(rs.getInt("delivery_state"));
|
||||||
|
e.setDeliveredServerLogin(rs.getString("delivered_server_login"));
|
||||||
|
e.setRecipientRoutesHash(rs.getString("recipient_routes_hash"));
|
||||||
|
e.setAttemptIndex(rs.getInt("attempt_index"));
|
||||||
|
long next = rs.getLong("next_attempt_at_ms");
|
||||||
|
e.setNextAttemptAtMs(rs.wasNull() ? null : next);
|
||||||
|
long last = rs.getLong("last_attempt_at_ms");
|
||||||
|
e.setLastAttemptAtMs(rs.wasNull() ? null : last);
|
||||||
|
e.setLastError(rs.getString("last_error"));
|
||||||
|
e.setUpdatedAtMs(rs.getLong("updated_at_ms"));
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setNullableString(PreparedStatement ps, int index, String value) throws SQLException {
|
||||||
|
if (isBlank(value)) ps.setNull(index, Types.VARCHAR);
|
||||||
|
else ps.setString(index, value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setNullableLong(PreparedStatement ps, int index, Long value) throws SQLException {
|
||||||
|
if (value == null) ps.setNull(index, Types.BIGINT);
|
||||||
|
else ps.setLong(index, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalize(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
String normalized = value.trim().toLowerCase();
|
||||||
|
return normalized.isEmpty() ? null : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeBlank(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
String normalized = value.trim();
|
||||||
|
return normalized.isEmpty() ? null : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isBlank(String value) {
|
||||||
|
return value == null || value.isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record MergeResult(int state, String deliveredLogin, String routesHash) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,453 @@
|
|||||||
|
package shine.db.dao;
|
||||||
|
|
||||||
|
import shine.db.MsgSubType;
|
||||||
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class DmDialogStateDAO {
|
||||||
|
private static volatile DmDialogStateDAO instance;
|
||||||
|
|
||||||
|
private DmDialogStateDAO() {}
|
||||||
|
|
||||||
|
public static DmDialogStateDAO getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (DmDialogStateDAO.class) {
|
||||||
|
if (instance == null) instance = new DmDialogStateDAO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void bootstrapIfEmpty(Connection c) throws SQLException {
|
||||||
|
if (c == null) return;
|
||||||
|
if (hasAnyRow(c)) return;
|
||||||
|
|
||||||
|
boolean prevAutoCommit = c.getAutoCommit();
|
||||||
|
c.setAutoCommit(false);
|
||||||
|
try {
|
||||||
|
for (PairItem pair : listConversationPairs(c)) {
|
||||||
|
refreshConversationPair(c, pair.ownerLogin(), pair.peerLogin());
|
||||||
|
}
|
||||||
|
c.commit();
|
||||||
|
} catch (Exception e) {
|
||||||
|
try { c.rollback(); } catch (Exception ignored) {}
|
||||||
|
if (e instanceof SQLException sqlEx) throw sqlEx;
|
||||||
|
throw new SQLException("Failed to bootstrap dm_dialog_state", e);
|
||||||
|
} finally {
|
||||||
|
c.setAutoCommit(prevAutoCommit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshConversationPair(Connection c, String ownerLogin, String peerLogin) throws SQLException {
|
||||||
|
String cleanOwner = normalize(ownerLogin);
|
||||||
|
String cleanPeer = normalize(peerLogin);
|
||||||
|
if (cleanOwner.isEmpty() || cleanPeer.isEmpty()) return;
|
||||||
|
if (cleanOwner.equalsIgnoreCase(cleanPeer)) return;
|
||||||
|
refreshConversation(c, cleanOwner, cleanPeer);
|
||||||
|
refreshConversation(c, cleanPeer, cleanOwner);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DialogSummary> listInboxDialogs(Connection c, String ownerLogin) throws SQLException {
|
||||||
|
String cleanOwner = normalize(ownerLogin);
|
||||||
|
if (cleanOwner.isEmpty()) return List.of();
|
||||||
|
|
||||||
|
Map<String, DialogSummary> byPeer = new LinkedHashMap<>();
|
||||||
|
List<String> contacts = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, cleanOwner, MsgSubType.CONNECTION_CONTACT);
|
||||||
|
List<String> closeFriends = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, cleanOwner, MsgSubType.CONNECTION_CLOSE_FRIEND);
|
||||||
|
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
|
SELECT owner_login, peer_login, relation_flag, last_message_blob_b64,
|
||||||
|
last_message_time_ms, unread_count, last_read_receipt_time_ms,
|
||||||
|
updated_at_ms
|
||||||
|
FROM dm_dialog_state
|
||||||
|
WHERE LOWER(owner_login) = LOWER(?)
|
||||||
|
""")) {
|
||||||
|
ps.setString(1, cleanOwner);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
DialogSummary row = new DialogSummary(
|
||||||
|
rs.getString("owner_login"),
|
||||||
|
rs.getString("peer_login"),
|
||||||
|
normalizeRelationFlag(rs.getString("relation_flag")),
|
||||||
|
rs.getString("last_message_blob_b64"),
|
||||||
|
rs.getLong("last_message_time_ms"),
|
||||||
|
rs.getInt("unread_count"),
|
||||||
|
rs.getLong("last_read_receipt_time_ms"),
|
||||||
|
rs.getLong("updated_at_ms"),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
byPeer.put(normKey(row.peerLogin()), row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String peer : contacts) {
|
||||||
|
addOrUpdateSummary(byPeer, cleanOwner, peer, "contact", false);
|
||||||
|
}
|
||||||
|
for (String peer : closeFriends) {
|
||||||
|
addOrUpdateSummary(byPeer, cleanOwner, peer, "close_friend", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DialogSummary row : new ArrayList<>(byPeer.values())) {
|
||||||
|
String relationFlag = resolveRelationFlag(c, cleanOwner, row.peerLogin());
|
||||||
|
byPeer.put(normKey(row.peerLogin()), new DialogSummary(
|
||||||
|
row.ownerLogin(),
|
||||||
|
row.peerLogin(),
|
||||||
|
relationFlag,
|
||||||
|
row.lastMessageBlobB64(),
|
||||||
|
row.lastMessageTimeMs(),
|
||||||
|
row.unreadCount(),
|
||||||
|
row.lastReadReceiptTimeMs(),
|
||||||
|
row.updatedAtMs(),
|
||||||
|
row.hasDialog()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DialogSummary> out = new ArrayList<>(byPeer.values());
|
||||||
|
out.sort((a, b) -> {
|
||||||
|
int cmp = Long.compare(b.lastMessageTimeMs(), a.lastMessageTimeMs());
|
||||||
|
if (cmp != 0) return cmp;
|
||||||
|
return normalize(a.peerLogin()).compareToIgnoreCase(normalize(b.peerLogin()));
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshFromEntry(Connection c, SignedMessageEntry entry) throws SQLException {
|
||||||
|
if (entry == null) return;
|
||||||
|
refreshConversationPair(c, entry.getFromLogin(), entry.getToLogin());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshConversation(Connection c, String ownerLogin, String peerLogin) throws SQLException {
|
||||||
|
DialogSummary summary = loadConversationSummary(c, ownerLogin, peerLogin);
|
||||||
|
upsert(c, summary);
|
||||||
|
if (summary.lastReadReceiptTimeMs() > 0) {
|
||||||
|
syncMessagesToWatermark(c, summary.ownerLogin(), summary.peerLogin(), summary.lastReadReceiptTimeMs());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DialogSummary loadConversationSummary(Connection c, String ownerLogin, String peerLogin) throws SQLException {
|
||||||
|
String cleanOwner = normalize(ownerLogin);
|
||||||
|
String cleanPeer = normalize(peerLogin);
|
||||||
|
if (cleanOwner.isEmpty() || cleanPeer.isEmpty() || cleanOwner.equalsIgnoreCase(cleanPeer)) {
|
||||||
|
return new DialogSummary(cleanOwner, cleanPeer, "none", "", 0L, 0, 0L, System.currentTimeMillis(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
String relationFlag = resolveRelationFlag(c, cleanOwner, cleanPeer);
|
||||||
|
long existingWatermark = loadExistingWatermark(c, cleanOwner, cleanPeer);
|
||||||
|
|
||||||
|
String latestSql = """
|
||||||
|
SELECT raw_block, time_ms
|
||||||
|
FROM signed_messages
|
||||||
|
WHERE (
|
||||||
|
(LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
|
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
|
)
|
||||||
|
AND message_type IN (1, 2)
|
||||||
|
ORDER BY time_ms DESC, revision_time_ms DESC, reencrypted_at_ms DESC, created_at_ms DESC, message_key DESC
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
String unreadSql = """
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM signed_messages
|
||||||
|
WHERE LOWER(from_login) = LOWER(?)
|
||||||
|
AND LOWER(to_login) = LOWER(?)
|
||||||
|
AND message_type = 1
|
||||||
|
AND time_ms > ?
|
||||||
|
AND (read_at_ms IS NULL OR read_at_ms <= 0)
|
||||||
|
""";
|
||||||
|
String contentReadSql = """
|
||||||
|
SELECT MAX(read_at_ms)
|
||||||
|
FROM signed_messages
|
||||||
|
WHERE (
|
||||||
|
(LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
|
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
|
)
|
||||||
|
AND message_type IN (1, 2)
|
||||||
|
AND read_at_ms IS NOT NULL
|
||||||
|
AND read_at_ms > 0
|
||||||
|
""";
|
||||||
|
String receiptWatermarkSql = """
|
||||||
|
SELECT MAX(time_ms)
|
||||||
|
FROM signed_messages
|
||||||
|
WHERE (
|
||||||
|
(LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
|
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
|
)
|
||||||
|
AND message_type IN (3, 4)
|
||||||
|
""";
|
||||||
|
|
||||||
|
String lastMessageBlobB64 = "";
|
||||||
|
long lastMessageTimeMs = 0L;
|
||||||
|
int unreadCount = 0;
|
||||||
|
long lastReadReceiptTimeMs = existingWatermark;
|
||||||
|
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(latestSql)) {
|
||||||
|
ps.setString(1, cleanOwner);
|
||||||
|
ps.setString(2, cleanPeer);
|
||||||
|
ps.setString(3, cleanPeer);
|
||||||
|
ps.setString(4, cleanOwner);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
lastMessageTimeMs = rs.getLong("time_ms");
|
||||||
|
byte[] rawBlock = rs.getBytes("raw_block");
|
||||||
|
if (rawBlock != null && rawBlock.length > 0) {
|
||||||
|
lastMessageBlobB64 = Base64.getEncoder().encodeToString(rawBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(contentReadSql)) {
|
||||||
|
ps.setString(1, cleanOwner);
|
||||||
|
ps.setString(2, cleanPeer);
|
||||||
|
ps.setString(3, cleanPeer);
|
||||||
|
ps.setString(4, cleanOwner);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
long value = rs.getLong(1);
|
||||||
|
if (!rs.wasNull()) lastReadReceiptTimeMs = Math.max(lastReadReceiptTimeMs, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(receiptWatermarkSql)) {
|
||||||
|
ps.setString(1, cleanOwner);
|
||||||
|
ps.setString(2, cleanPeer);
|
||||||
|
ps.setString(3, cleanPeer);
|
||||||
|
ps.setString(4, cleanOwner);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
long value = rs.getLong(1);
|
||||||
|
if (!rs.wasNull()) lastReadReceiptTimeMs = Math.max(lastReadReceiptTimeMs, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(unreadSql)) {
|
||||||
|
ps.setString(1, cleanPeer);
|
||||||
|
ps.setString(2, cleanOwner);
|
||||||
|
ps.setLong(3, lastReadReceiptTimeMs);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) unreadCount = rs.getInt(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DialogSummary(
|
||||||
|
cleanOwner,
|
||||||
|
cleanPeer,
|
||||||
|
relationFlag,
|
||||||
|
lastMessageBlobB64,
|
||||||
|
lastMessageTimeMs,
|
||||||
|
unreadCount,
|
||||||
|
lastReadReceiptTimeMs,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
lastMessageTimeMs > 0 || unreadCount > 0 || lastReadReceiptTimeMs > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void upsert(Connection c, DialogSummary summary) throws SQLException {
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
|
INSERT INTO dm_dialog_state (
|
||||||
|
owner_login, peer_login, relation_flag, last_message_blob_b64,
|
||||||
|
last_message_time_ms, unread_count, last_read_receipt_time_ms, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT (owner_login, peer_login) DO UPDATE SET
|
||||||
|
relation_flag = EXCLUDED.relation_flag,
|
||||||
|
last_message_blob_b64 = EXCLUDED.last_message_blob_b64,
|
||||||
|
last_message_time_ms = EXCLUDED.last_message_time_ms,
|
||||||
|
unread_count = EXCLUDED.unread_count,
|
||||||
|
last_read_receipt_time_ms = EXCLUDED.last_read_receipt_time_ms,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms
|
||||||
|
""")) {
|
||||||
|
ps.setString(1, summary.ownerLogin());
|
||||||
|
ps.setString(2, summary.peerLogin());
|
||||||
|
ps.setString(3, normalizeRelationFlag(summary.relationFlag()));
|
||||||
|
ps.setString(4, summary.lastMessageBlobB64());
|
||||||
|
ps.setLong(5, summary.lastMessageTimeMs());
|
||||||
|
ps.setInt(6, summary.unreadCount());
|
||||||
|
ps.setLong(7, summary.lastReadReceiptTimeMs());
|
||||||
|
ps.setLong(8, summary.updatedAtMs());
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveRelationFlag(Connection c, String ownerLogin, String peerLogin) throws SQLException {
|
||||||
|
if (ConnectionsStateDAO.getInstance().hasOutgoingByRelTypeCanonical(c, ownerLogin, peerLogin, MsgSubType.CONNECTION_CLOSE_FRIEND)) {
|
||||||
|
return "close_friend";
|
||||||
|
}
|
||||||
|
if (ConnectionsStateDAO.getInstance().hasOutgoingByRelTypeCanonical(c, ownerLogin, peerLogin, MsgSubType.CONNECTION_CONTACT)) {
|
||||||
|
return "contact";
|
||||||
|
}
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasAnyRow(Connection c) throws SQLException {
|
||||||
|
try (Statement st = c.createStatement();
|
||||||
|
ResultSet rs = st.executeQuery("SELECT 1 FROM dm_dialog_state LIMIT 1")) {
|
||||||
|
return rs.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PairItem> listConversationPairs(Connection c) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT DISTINCT owner_login, peer_login
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
m.target_login AS owner_login,
|
||||||
|
CASE
|
||||||
|
WHEN LOWER(m.target_login) = LOWER(m.to_login) THEN m.from_login
|
||||||
|
ELSE m.to_login
|
||||||
|
END AS peer_login
|
||||||
|
FROM signed_messages m
|
||||||
|
WHERE m.message_type IN (1, 2, 3, 4, 5, 6, 7, 8)
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT m.from_login AS owner_login, m.to_login AS peer_login
|
||||||
|
FROM signed_messages m
|
||||||
|
WHERE m.message_type IN (5, 6, 7, 8)
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT m.to_login AS owner_login, m.from_login AS peer_login
|
||||||
|
FROM signed_messages m
|
||||||
|
WHERE m.message_type IN (5, 6, 7, 8)
|
||||||
|
) pairs
|
||||||
|
WHERE owner_login IS NOT NULL
|
||||||
|
AND peer_login IS NOT NULL
|
||||||
|
AND BTRIM(owner_login) <> ''
|
||||||
|
AND BTRIM(peer_login) <> ''
|
||||||
|
AND LOWER(owner_login) <> LOWER(peer_login)
|
||||||
|
ORDER BY owner_login, peer_login
|
||||||
|
""";
|
||||||
|
List<PairItem> out = new ArrayList<>();
|
||||||
|
try (Statement st = c.createStatement();
|
||||||
|
ResultSet rs = st.executeQuery(sql)) {
|
||||||
|
while (rs.next()) {
|
||||||
|
String ownerLogin = rs.getString("owner_login");
|
||||||
|
String peerLogin = rs.getString("peer_login");
|
||||||
|
if (normalize(ownerLogin).isEmpty() || normalize(peerLogin).isEmpty()) continue;
|
||||||
|
out.add(new PairItem(ownerLogin, peerLogin));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addOrUpdateSummary(Map<String, DialogSummary> map, String ownerLogin, String peerLogin, String relationFlag, boolean hasDialog) {
|
||||||
|
String owner = normalize(ownerLogin);
|
||||||
|
String peer = normalize(peerLogin);
|
||||||
|
if (owner.isEmpty() || peer.isEmpty() || owner.equalsIgnoreCase(peer)) return;
|
||||||
|
String key = normKey(peer);
|
||||||
|
DialogSummary current = map.get(key);
|
||||||
|
if (current == null) {
|
||||||
|
map.put(key, new DialogSummary(owner, peer, relationFlag, "", 0L, 0, 0L, System.currentTimeMillis(), hasDialog));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String nextRelation = current.relationFlag();
|
||||||
|
if ("close_friend".equalsIgnoreCase(relationFlag) || "close_friend".equalsIgnoreCase(nextRelation)) {
|
||||||
|
nextRelation = "close_friend";
|
||||||
|
} else if ("contact".equalsIgnoreCase(relationFlag) || "contact".equalsIgnoreCase(nextRelation)) {
|
||||||
|
nextRelation = "contact";
|
||||||
|
} else {
|
||||||
|
nextRelation = normalizeRelationFlag(nextRelation);
|
||||||
|
}
|
||||||
|
map.put(key, new DialogSummary(
|
||||||
|
current.ownerLogin().isEmpty() ? owner : current.ownerLogin(),
|
||||||
|
peer,
|
||||||
|
nextRelation,
|
||||||
|
current.lastMessageBlobB64(),
|
||||||
|
current.lastMessageTimeMs(),
|
||||||
|
current.unreadCount(),
|
||||||
|
current.lastReadReceiptTimeMs(),
|
||||||
|
current.updatedAtMs(),
|
||||||
|
current.hasDialog() || hasDialog
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncMessagesToWatermark(Connection c, String ownerLogin, String peerLogin, long watermark) throws SQLException {
|
||||||
|
if (watermark <= 0) return;
|
||||||
|
String cleanOwner = normalize(ownerLogin);
|
||||||
|
String cleanPeer = normalize(peerLogin);
|
||||||
|
if (cleanOwner.isEmpty() || cleanPeer.isEmpty() || cleanOwner.equalsIgnoreCase(cleanPeer)) return;
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
|
UPDATE signed_messages
|
||||||
|
SET read_at_ms = CASE
|
||||||
|
WHEN read_at_ms IS NULL OR read_at_ms <= 0 THEN ?
|
||||||
|
WHEN read_at_ms > ? THEN read_at_ms
|
||||||
|
ELSE read_at_ms
|
||||||
|
END
|
||||||
|
WHERE (
|
||||||
|
(LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
|
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||||
|
)
|
||||||
|
AND message_type IN (1, 2)
|
||||||
|
AND time_ms <= ?
|
||||||
|
""")) {
|
||||||
|
ps.setLong(1, watermark);
|
||||||
|
ps.setLong(2, watermark);
|
||||||
|
ps.setString(3, cleanOwner);
|
||||||
|
ps.setString(4, cleanPeer);
|
||||||
|
ps.setString(5, cleanPeer);
|
||||||
|
ps.setString(6, cleanOwner);
|
||||||
|
ps.setLong(7, watermark);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long loadExistingWatermark(Connection c, String ownerLogin, String peerLogin) throws SQLException {
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
|
SELECT last_read_receipt_time_ms
|
||||||
|
FROM dm_dialog_state
|
||||||
|
WHERE LOWER(owner_login) = LOWER(?)
|
||||||
|
AND LOWER(peer_login) = LOWER(?)
|
||||||
|
LIMIT 1
|
||||||
|
""")) {
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setString(2, peerLogin);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (!rs.next()) return 0L;
|
||||||
|
long value = rs.getLong(1);
|
||||||
|
return rs.wasNull() ? 0L : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalize(String value) {
|
||||||
|
return value == null ? "" : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normKey(String value) {
|
||||||
|
return normalize(value).toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeRelationFlag(String value) {
|
||||||
|
String clean = normalize(value).toLowerCase(Locale.ROOT);
|
||||||
|
if ("close_friend".equals(clean) || "contact".equals(clean)) return clean;
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DialogSummary(
|
||||||
|
String ownerLogin,
|
||||||
|
String peerLogin,
|
||||||
|
String relationFlag,
|
||||||
|
String lastMessageBlobB64,
|
||||||
|
long lastMessageTimeMs,
|
||||||
|
int unreadCount,
|
||||||
|
long lastReadReceiptTimeMs,
|
||||||
|
long updatedAtMs,
|
||||||
|
boolean hasDialog
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private record PairItem(String ownerLogin, String peerLogin) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
package shine.db.dao;
|
||||||
|
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.entities.DmSyncOutboxEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Types;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Outbox событий DM для единственного второго access-сервера пользователя. */
|
||||||
|
public final class DmSyncOutboxDAO {
|
||||||
|
private static volatile DmSyncOutboxDAO instance;
|
||||||
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
|
private DmSyncOutboxDAO() {}
|
||||||
|
|
||||||
|
public static DmSyncOutboxDAO getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (DmSyncOutboxDAO.class) {
|
||||||
|
if (instance == null) instance = new DmSyncOutboxDAO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void upsert(
|
||||||
|
String ownerLogin,
|
||||||
|
String primaryMessageKey,
|
||||||
|
String eventId,
|
||||||
|
String secondaryMessageKey,
|
||||||
|
boolean synced,
|
||||||
|
long createdAtMs
|
||||||
|
) throws SQLException {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO dm_sync_outbox (
|
||||||
|
owner_login, primary_message_key, event_id, secondary_message_key,
|
||||||
|
synced, created_at_ms, updated_at_ms
|
||||||
|
) VALUES (LOWER(?), ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT (owner_login, primary_message_key) DO UPDATE SET
|
||||||
|
event_id = EXCLUDED.event_id,
|
||||||
|
secondary_message_key = EXCLUDED.secondary_message_key,
|
||||||
|
synced = CASE
|
||||||
|
WHEN dm_sync_outbox.event_id = EXCLUDED.event_id
|
||||||
|
THEN dm_sync_outbox.synced OR EXCLUDED.synced
|
||||||
|
ELSE EXCLUDED.synced
|
||||||
|
END,
|
||||||
|
created_at_ms = CASE
|
||||||
|
WHEN dm_sync_outbox.event_id = EXCLUDED.event_id
|
||||||
|
THEN dm_sync_outbox.created_at_ms
|
||||||
|
ELSE EXCLUDED.created_at_ms
|
||||||
|
END,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms
|
||||||
|
""";
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setString(2, primaryMessageKey);
|
||||||
|
ps.setString(3, eventId);
|
||||||
|
if (secondaryMessageKey == null || secondaryMessageKey.isBlank()) ps.setNull(4, Types.VARCHAR);
|
||||||
|
else ps.setString(4, secondaryMessageKey.trim());
|
||||||
|
ps.setBoolean(5, synced);
|
||||||
|
ps.setLong(6, createdAtMs);
|
||||||
|
ps.setLong(7, now);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DmSyncOutboxEntry> listUnsynced(String ownerLogin, int limit) throws SQLException {
|
||||||
|
return listUnsynced(ownerLogin, 0L, "", limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DmSyncOutboxEntry> listUnsynced(
|
||||||
|
String ownerLogin, long afterCreatedAtMs, String afterPrimaryMessageKey, int limit
|
||||||
|
) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT owner_login, primary_message_key, event_id, secondary_message_key,
|
||||||
|
synced, created_at_ms, updated_at_ms
|
||||||
|
FROM dm_sync_outbox
|
||||||
|
WHERE LOWER(owner_login) = LOWER(?) AND synced = FALSE
|
||||||
|
AND (created_at_ms > ? OR (created_at_ms = ? AND primary_message_key > ?))
|
||||||
|
ORDER BY created_at_ms ASC, primary_message_key ASC
|
||||||
|
LIMIT ?
|
||||||
|
""";
|
||||||
|
List<DmSyncOutboxEntry> out = new ArrayList<>();
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setLong(2, Math.max(0L, afterCreatedAtMs));
|
||||||
|
ps.setLong(3, Math.max(0L, afterCreatedAtMs));
|
||||||
|
ps.setString(4, afterPrimaryMessageKey == null ? "" : afterPrimaryMessageKey);
|
||||||
|
ps.setInt(5, Math.max(1, limit));
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) out.add(mapRow(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasUnsyncedForServer(String serverLogin) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT 1
|
||||||
|
FROM dm_sync_outbox o
|
||||||
|
JOIN user_access_servers_current r
|
||||||
|
ON LOWER(r.user_login) = LOWER(o.owner_login)
|
||||||
|
WHERE o.synced = FALSE AND LOWER(r.server_login) = LOWER(?)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM user_access_servers_current peer
|
||||||
|
WHERE LOWER(peer.user_login) = LOWER(o.owner_login)
|
||||||
|
AND LOWER(peer.server_login) <> LOWER(?)
|
||||||
|
)
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, serverLogin);
|
||||||
|
ps.setString(2, serverLogin);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
return rs.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> listUnsyncedOwnersForServer(String serverLogin) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT DISTINCT o.owner_login
|
||||||
|
FROM dm_sync_outbox o
|
||||||
|
JOIN user_access_servers_current r
|
||||||
|
ON LOWER(r.user_login) = LOWER(o.owner_login)
|
||||||
|
WHERE o.synced = FALSE AND LOWER(r.server_login) = LOWER(?)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM user_access_servers_current peer
|
||||||
|
WHERE LOWER(peer.user_login) = LOWER(o.owner_login)
|
||||||
|
AND LOWER(peer.server_login) <> LOWER(?)
|
||||||
|
)
|
||||||
|
ORDER BY o.owner_login
|
||||||
|
""";
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, serverLogin);
|
||||||
|
ps.setString(2, serverLogin);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) result.add(rs.getString("owner_login"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int markSynced(String ownerLogin, String eventId) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
UPDATE dm_sync_outbox
|
||||||
|
SET synced = TRUE, updated_at_ms = ?
|
||||||
|
WHERE LOWER(owner_login) = LOWER(?) AND event_id = ?
|
||||||
|
""";
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setLong(1, System.currentTimeMillis());
|
||||||
|
ps.setString(2, ownerLogin);
|
||||||
|
ps.setString(3, eventId);
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int markSynced(String ownerLogin, List<String> eventIds) throws SQLException {
|
||||||
|
if (eventIds == null || eventIds.isEmpty()) return 0;
|
||||||
|
int total = 0;
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement("""
|
||||||
|
UPDATE dm_sync_outbox
|
||||||
|
SET synced = TRUE, updated_at_ms = ?
|
||||||
|
WHERE LOWER(owner_login) = LOWER(?) AND event_id = ?
|
||||||
|
""")) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
for (String eventId : eventIds) {
|
||||||
|
if (eventId == null || eventId.isBlank()) continue;
|
||||||
|
ps.setLong(1, now);
|
||||||
|
ps.setString(2, ownerLogin);
|
||||||
|
ps.setString(3, eventId.trim());
|
||||||
|
ps.addBatch();
|
||||||
|
}
|
||||||
|
for (int changed : ps.executeBatch()) if (changed > 0) total += changed;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int markAllUnsynced(String ownerLogin) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
UPDATE dm_sync_outbox
|
||||||
|
SET synced = FALSE, updated_at_ms = ?
|
||||||
|
WHERE LOWER(owner_login) = LOWER(?)
|
||||||
|
""";
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setLong(1, System.currentTimeMillis());
|
||||||
|
ps.setString(2, ownerLogin);
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int markAllUnsynced() throws SQLException {
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement("UPDATE dm_sync_outbox SET synced = FALSE, updated_at_ms = ?")) {
|
||||||
|
ps.setLong(1, System.currentTimeMillis());
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int removeMissingMessages() throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
DELETE FROM dm_sync_outbox o
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM signed_messages m WHERE m.message_key = o.primary_message_key
|
||||||
|
)
|
||||||
|
""";
|
||||||
|
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DmSyncOutboxEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
|
DmSyncOutboxEntry e = new DmSyncOutboxEntry();
|
||||||
|
e.setOwnerLogin(rs.getString("owner_login"));
|
||||||
|
e.setPrimaryMessageKey(rs.getString("primary_message_key"));
|
||||||
|
e.setEventId(rs.getString("event_id"));
|
||||||
|
e.setSecondaryMessageKey(rs.getString("secondary_message_key"));
|
||||||
|
e.setSynced(rs.getBoolean("synced"));
|
||||||
|
e.setCreatedAtMs(rs.getLong("created_at_ms"));
|
||||||
|
e.setUpdatedAtMs(rs.getLong("updated_at_ms"));
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
-47
@@ -1,47 +0,0 @@
|
|||||||
package shine.db.dao;
|
|
||||||
|
|
||||||
import shine.db.DbController;
|
|
||||||
import shine.db.entities.SignedDirectMessageHistoryEntry;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.sql.PreparedStatement;
|
|
||||||
|
|
||||||
public final class SignedDirectMessagesHistoryDAO {
|
|
||||||
private static volatile SignedDirectMessagesHistoryDAO instance;
|
|
||||||
private final DbController db = DbController.getInstance();
|
|
||||||
|
|
||||||
private SignedDirectMessagesHistoryDAO() {}
|
|
||||||
|
|
||||||
public static SignedDirectMessagesHistoryDAO getInstance() {
|
|
||||||
if (instance == null) {
|
|
||||||
synchronized (SignedDirectMessagesHistoryDAO.class) {
|
|
||||||
if (instance == null) instance = new SignedDirectMessagesHistoryDAO();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void insert(SignedDirectMessageHistoryEntry e) throws Exception {
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
String sql = """
|
|
||||||
INSERT INTO signed_direct_messages_history (
|
|
||||||
message_id, from_login, to_login, target_mode, target_session_id,
|
|
||||||
message_type, time_ms, nonce, raw_packet, created_at_ms
|
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
""";
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
|
||||||
ps.setString(1, e.getMessageId());
|
|
||||||
ps.setString(2, e.getFromLogin());
|
|
||||||
ps.setString(3, e.getToLogin());
|
|
||||||
ps.setInt(4, e.getTargetMode());
|
|
||||||
ps.setString(5, e.getTargetSessionId());
|
|
||||||
ps.setInt(6, e.getMessageType());
|
|
||||||
ps.setLong(7, e.getTimeMs());
|
|
||||||
ps.setLong(8, e.getNonce());
|
|
||||||
ps.setBytes(9, e.getRawPacket());
|
|
||||||
ps.setLong(10, e.getCreatedAtMs());
|
|
||||||
ps.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package shine.db.dao;
|
|
||||||
|
|
||||||
import shine.db.DbController;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.sql.PreparedStatement;
|
|
||||||
|
|
||||||
public final class SignedDmReplayDAO {
|
|
||||||
private static volatile SignedDmReplayDAO instance;
|
|
||||||
private final DbController db = DbController.getInstance();
|
|
||||||
|
|
||||||
private SignedDmReplayDAO() {}
|
|
||||||
|
|
||||||
public static SignedDmReplayDAO getInstance() {
|
|
||||||
if (instance == null) {
|
|
||||||
synchronized (SignedDmReplayDAO.class) {
|
|
||||||
if (instance == null) instance = new SignedDmReplayDAO();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean registerUnique(String fromLogin, long timeMs, long nonce, long nowMs) throws Exception {
|
|
||||||
cleanupExpired(nowMs - 15L * 60L * 1000L);
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
String sql = """
|
|
||||||
INSERT INTO signed_direct_message_replay (
|
|
||||||
from_login, time_ms, nonce, created_at_ms
|
|
||||||
) VALUES (?, ?, ?, ?)
|
|
||||||
ON CONFLICT DO NOTHING
|
|
||||||
""";
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
|
||||||
ps.setString(1, fromLogin);
|
|
||||||
ps.setLong(2, timeMs);
|
|
||||||
ps.setLong(3, nonce);
|
|
||||||
ps.setLong(4, nowMs);
|
|
||||||
return ps.executeUpdate() > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cleanupExpired(long minCreatedAtMs) throws Exception {
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
String sql = "DELETE FROM signed_direct_message_replay WHERE created_at_ms < ?";
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
|
||||||
ps.setLong(1, minCreatedAtMs);
|
|
||||||
ps.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -58,6 +58,7 @@ public final class SignedMessagesDAO {
|
|||||||
ApplyStatus status = ps.executeUpdate() > 0 ? ApplyStatus.APPLIED : ApplyStatus.DUPLICATE_OR_OLDER;
|
ApplyStatus status = ps.executeUpdate() > 0 ? ApplyStatus.APPLIED : ApplyStatus.DUPLICATE_OR_OLDER;
|
||||||
if (status.applied()) {
|
if (status.applied()) {
|
||||||
markMessageReadByReceipt(c, e);
|
markMessageReadByReceipt(c, e);
|
||||||
|
DmDialogStateDAO.getInstance().refreshFromEntry(c, e);
|
||||||
}
|
}
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
@@ -76,6 +77,7 @@ public final class SignedMessagesDAO {
|
|||||||
if (insertedFirst == 1 && insertedSecond == 1) {
|
if (insertedFirst == 1 && insertedSecond == 1) {
|
||||||
markMessageReadByReceipt(c, first);
|
markMessageReadByReceipt(c, first);
|
||||||
markMessageReadByReceipt(c, second);
|
markMessageReadByReceipt(c, second);
|
||||||
|
DmDialogStateDAO.getInstance().refreshConversationPair(c, first.getFromLogin(), first.getToLogin());
|
||||||
c.commit();
|
c.commit();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -122,6 +124,7 @@ public final class SignedMessagesDAO {
|
|||||||
markMessageReadByReceipt(c, outgoing);
|
markMessageReadByReceipt(c, outgoing);
|
||||||
resetDeliveryRows(c, incoming.getMessageKey());
|
resetDeliveryRows(c, incoming.getMessageKey());
|
||||||
resetDeliveryRows(c, outgoing.getMessageKey());
|
resetDeliveryRows(c, outgoing.getMessageKey());
|
||||||
|
DmDialogStateDAO.getInstance().refreshConversationPair(c, incoming.getFromLogin(), incoming.getToLogin());
|
||||||
|
|
||||||
c.commit();
|
c.commit();
|
||||||
return ApplyStatus.APPLIED;
|
return ApplyStatus.APPLIED;
|
||||||
@@ -160,6 +163,7 @@ public final class SignedMessagesDAO {
|
|||||||
upsertMessage(c, incoming);
|
upsertMessage(c, incoming);
|
||||||
markMessageReadByReceipt(c, incoming);
|
markMessageReadByReceipt(c, incoming);
|
||||||
resetDeliveryRows(c, incoming.getMessageKey());
|
resetDeliveryRows(c, incoming.getMessageKey());
|
||||||
|
DmDialogStateDAO.getInstance().refreshConversationPair(c, incoming.getFromLogin(), incoming.getToLogin());
|
||||||
c.commit();
|
c.commit();
|
||||||
return ApplyStatus.APPLIED;
|
return ApplyStatus.APPLIED;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -190,6 +194,7 @@ public final class SignedMessagesDAO {
|
|||||||
deleteMessageContentAndReceipts(c, tombstone.getBaseKey());
|
deleteMessageContentAndReceipts(c, tombstone.getBaseKey());
|
||||||
upsertMessage(c, tombstone);
|
upsertMessage(c, tombstone);
|
||||||
resetDeliveryRows(c, tombstone.getMessageKey());
|
resetDeliveryRows(c, tombstone.getMessageKey());
|
||||||
|
DmDialogStateDAO.getInstance().refreshConversationPair(c, tombstone.getFromLogin(), tombstone.getToLogin());
|
||||||
|
|
||||||
c.commit();
|
c.commit();
|
||||||
return ApplyStatus.APPLIED;
|
return ApplyStatus.APPLIED;
|
||||||
@@ -218,6 +223,7 @@ public final class SignedMessagesDAO {
|
|||||||
deleteConversationHistoryBefore(c, tombstone.getFromLogin(), tombstone.getToLogin(), tombstone.getTimeMs());
|
deleteConversationHistoryBefore(c, tombstone.getFromLogin(), tombstone.getToLogin(), tombstone.getTimeMs());
|
||||||
upsertMessage(c, tombstone);
|
upsertMessage(c, tombstone);
|
||||||
resetDeliveryRows(c, tombstone.getMessageKey());
|
resetDeliveryRows(c, tombstone.getMessageKey());
|
||||||
|
DmDialogStateDAO.getInstance().refreshConversationPair(c, tombstone.getFromLogin(), tombstone.getToLogin());
|
||||||
|
|
||||||
c.commit();
|
c.commit();
|
||||||
return ApplyStatus.APPLIED;
|
return ApplyStatus.APPLIED;
|
||||||
|
|||||||
+29
@@ -75,6 +75,35 @@ public final class UserAccessServersCurrentDAO {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает уникальные физические серверы из текущей routing-проекции.
|
||||||
|
* Используется транспортным пулом: одно WSS-соединение держится на сервер,
|
||||||
|
* а не на каждого пользователя этого сервера.
|
||||||
|
*/
|
||||||
|
public List<UserAccessServerRouteEntry> listDistinctServers() throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT DISTINCT ON (LOWER(server_login))
|
||||||
|
server_login, server_url
|
||||||
|
FROM user_access_servers_current
|
||||||
|
WHERE server_login IS NOT NULL AND BTRIM(server_login) <> ''
|
||||||
|
AND server_url IS NOT NULL AND BTRIM(server_url) <> ''
|
||||||
|
ORDER BY LOWER(server_login), server_login, server_url
|
||||||
|
""";
|
||||||
|
List<UserAccessServerRouteEntry> result = new ArrayList<>();
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql);
|
||||||
|
ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
UserAccessServerRouteEntry entry = new UserAccessServerRouteEntry();
|
||||||
|
entry.setUserLogin("");
|
||||||
|
entry.setServerLogin(rs.getString("server_login"));
|
||||||
|
entry.setServerUrl(rs.getString("server_url"));
|
||||||
|
result.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private UserAccessServerRouteEntry mapRow(ResultSet rs) throws SQLException {
|
private UserAccessServerRouteEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
UserAccessServerRouteEntry entry = new UserAccessServerRouteEntry();
|
UserAccessServerRouteEntry entry = new UserAccessServerRouteEntry();
|
||||||
entry.setUserLogin(rs.getString("user_login"));
|
entry.setUserLogin(rs.getString("user_login"));
|
||||||
|
|||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
package shine.db.dao;
|
||||||
|
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.entities.UserNotificationEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Types;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class UserNotificationsStateDAO {
|
||||||
|
private static volatile UserNotificationsStateDAO instance;
|
||||||
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
|
private UserNotificationsStateDAO() {}
|
||||||
|
|
||||||
|
public static UserNotificationsStateDAO getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (UserNotificationsStateDAO.class) {
|
||||||
|
if (instance == null) instance = new UserNotificationsStateDAO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int upsert(Connection c, UserNotificationEntry e) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO user_notifications_state (
|
||||||
|
owner_login, notification_kind, created_at_ms, source_login, source_bch_name,
|
||||||
|
source_block_number, source_block_hash, target_login, target_bch_name,
|
||||||
|
target_block_number, target_block_hash, source_msg_sub_type, source_text
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(owner_login, notification_kind, source_bch_name, source_block_number, source_block_hash) DO UPDATE SET
|
||||||
|
created_at_ms = EXCLUDED.created_at_ms,
|
||||||
|
source_login = EXCLUDED.source_login,
|
||||||
|
target_login = EXCLUDED.target_login,
|
||||||
|
target_bch_name = EXCLUDED.target_bch_name,
|
||||||
|
target_block_number = EXCLUDED.target_block_number,
|
||||||
|
target_block_hash = EXCLUDED.target_block_hash,
|
||||||
|
source_msg_sub_type = EXCLUDED.source_msg_sub_type,
|
||||||
|
source_text = EXCLUDED.source_text
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
int i = 1;
|
||||||
|
ps.setString(i++, e.getOwnerLogin());
|
||||||
|
ps.setString(i++, e.getNotificationKind());
|
||||||
|
ps.setLong(i++, e.getCreatedAtMs());
|
||||||
|
ps.setString(i++, e.getSourceLogin());
|
||||||
|
ps.setString(i++, e.getSourceBchName());
|
||||||
|
ps.setInt(i++, e.getSourceBlockNumber());
|
||||||
|
ps.setBytes(i++, e.getSourceBlockHash());
|
||||||
|
if (e.getTargetLogin() != null) ps.setString(i++, e.getTargetLogin()); else ps.setNull(i++, Types.VARCHAR);
|
||||||
|
if (e.getTargetBchName() != null) ps.setString(i++, e.getTargetBchName()); else ps.setNull(i++, Types.VARCHAR);
|
||||||
|
if (e.getTargetBlockNumber() != null) ps.setInt(i++, e.getTargetBlockNumber()); else ps.setNull(i++, Types.INTEGER);
|
||||||
|
if (e.getTargetBlockHash() != null) ps.setBytes(i++, e.getTargetBlockHash()); else ps.setNull(i++, Types.BINARY);
|
||||||
|
ps.setInt(i++, e.getSourceMsgSubType());
|
||||||
|
ps.setString(i++, e.getSourceText() == null ? "" : e.getSourceText());
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserNotificationEntry> listByOwnerAndKind(Connection c, String ownerLogin, String kind, int limit) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT owner_login, notification_kind, created_at_ms, source_login, source_bch_name,
|
||||||
|
source_block_number, source_block_hash, target_login, target_bch_name,
|
||||||
|
target_block_number, target_block_hash, source_msg_sub_type, source_text
|
||||||
|
FROM user_notifications_state
|
||||||
|
WHERE owner_login = ? AND notification_kind = ?
|
||||||
|
ORDER BY created_at_ms DESC, source_block_number DESC
|
||||||
|
LIMIT ?
|
||||||
|
""";
|
||||||
|
List<UserNotificationEntry> out = new ArrayList<>();
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setString(2, kind);
|
||||||
|
ps.setInt(3, Math.max(1, limit));
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) out.add(mapRow(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UserNotificationEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
|
UserNotificationEntry e = new UserNotificationEntry();
|
||||||
|
e.setOwnerLogin(rs.getString("owner_login"));
|
||||||
|
e.setNotificationKind(rs.getString("notification_kind"));
|
||||||
|
e.setCreatedAtMs(rs.getLong("created_at_ms"));
|
||||||
|
e.setSourceLogin(rs.getString("source_login"));
|
||||||
|
e.setSourceBchName(rs.getString("source_bch_name"));
|
||||||
|
e.setSourceBlockNumber(rs.getInt("source_block_number"));
|
||||||
|
e.setSourceBlockHash(rs.getBytes("source_block_hash"));
|
||||||
|
e.setTargetLogin(rs.getString("target_login"));
|
||||||
|
e.setTargetBchName(rs.getString("target_bch_name"));
|
||||||
|
Object targetBlockNumber = rs.getObject("target_block_number");
|
||||||
|
e.setTargetBlockNumber(targetBlockNumber == null ? null : ((Number) targetBlockNumber).intValue());
|
||||||
|
e.setTargetBlockHash(rs.getBytes("target_block_hash"));
|
||||||
|
e.setSourceMsgSubType(rs.getInt("source_msg_sub_type"));
|
||||||
|
e.setSourceText(rs.getString("source_text"));
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
package shine.db.dao;
|
||||||
|
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.entities.UserSettingEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Types;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserSettingsDAO — хранение пользовательских настроек.
|
||||||
|
*
|
||||||
|
* Правило:
|
||||||
|
* - уникальность: login + setting_type + setting_key
|
||||||
|
* - запись обновляется только если time_ms новее
|
||||||
|
* - synced=true означает, что значение уже дошло до второго сервера
|
||||||
|
*/
|
||||||
|
public final class UserSettingsDAO {
|
||||||
|
|
||||||
|
private static volatile UserSettingsDAO instance;
|
||||||
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
|
private UserSettingsDAO() {}
|
||||||
|
|
||||||
|
public static UserSettingsDAO getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (UserSettingsDAO.class) {
|
||||||
|
if (instance == null) instance = new UserSettingsDAO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int upsertIfNewer(Connection c, UserSettingEntry e) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO user_settings (
|
||||||
|
login, setting_type, setting_key, time_ms,
|
||||||
|
value_text, value_num, client_key, signature, synced
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT (login, setting_type, setting_key)
|
||||||
|
DO UPDATE SET
|
||||||
|
time_ms = EXCLUDED.time_ms,
|
||||||
|
value_text = EXCLUDED.value_text,
|
||||||
|
value_num = EXCLUDED.value_num,
|
||||||
|
client_key = EXCLUDED.client_key,
|
||||||
|
signature = EXCLUDED.signature,
|
||||||
|
synced = EXCLUDED.synced
|
||||||
|
WHERE user_settings.time_ms < EXCLUDED.time_ms
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, e.getLogin());
|
||||||
|
ps.setInt(2, e.getSettingType());
|
||||||
|
ps.setString(3, e.getSettingKey());
|
||||||
|
ps.setLong(4, e.getTimeMs());
|
||||||
|
ps.setString(5, e.getValueText() == null ? "" : e.getValueText());
|
||||||
|
ps.setLong(6, e.getValueNum());
|
||||||
|
|
||||||
|
if (e.getClientKey() == null || e.getClientKey().isBlank()) ps.setNull(7, Types.VARCHAR);
|
||||||
|
else ps.setString(7, e.getClientKey());
|
||||||
|
|
||||||
|
if (e.getSignature() == null || e.getSignature().isBlank()) ps.setNull(8, Types.VARCHAR);
|
||||||
|
else ps.setString(8, e.getSignature());
|
||||||
|
|
||||||
|
ps.setBoolean(9, e.isSynced());
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int upsertIfNewer(UserSettingEntry e) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
return upsertIfNewer(c, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserSettingEntry getByLoginTypeKey(Connection c, String login, int settingType, String settingKey) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT login, setting_type, setting_key, time_ms, value_text, value_num, client_key, signature, synced
|
||||||
|
FROM user_settings
|
||||||
|
WHERE LOWER(login) = LOWER(?) AND setting_type = ? AND setting_key = ?
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, login);
|
||||||
|
ps.setInt(2, settingType);
|
||||||
|
ps.setString(3, settingKey);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
return rs.next() ? mapRow(rs) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserSettingEntry getByLoginTypeKey(String login, int settingType, String settingKey) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
return getByLoginTypeKey(c, login, settingType, settingKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserSettingEntry> getByLogin(Connection c, String login) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT login, setting_type, setting_key, time_ms, value_text, value_num, client_key, signature, synced
|
||||||
|
FROM user_settings
|
||||||
|
WHERE LOWER(login) = LOWER(?)
|
||||||
|
ORDER BY time_ms DESC, setting_type ASC, setting_key ASC
|
||||||
|
""";
|
||||||
|
List<UserSettingEntry> out = new ArrayList<>();
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, login);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) out.add(mapRow(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserSettingEntry> getByLogin(String login) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
return getByLogin(c, login);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserSettingEntry> listNewerThan(Connection c, String login, long afterTimeMs, String afterSettingKey, int limit) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT login, setting_type, setting_key, time_ms, value_text, value_num, client_key, signature, synced
|
||||||
|
FROM user_settings
|
||||||
|
WHERE LOWER(login) = LOWER(?)
|
||||||
|
AND (
|
||||||
|
time_ms > ?
|
||||||
|
OR (time_ms = ? AND setting_key > ?)
|
||||||
|
)
|
||||||
|
ORDER BY time_ms ASC, setting_key ASC
|
||||||
|
LIMIT ?
|
||||||
|
""";
|
||||||
|
List<UserSettingEntry> out = new ArrayList<>();
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, login);
|
||||||
|
ps.setLong(2, Math.max(0L, afterTimeMs));
|
||||||
|
ps.setLong(3, Math.max(0L, afterTimeMs));
|
||||||
|
ps.setString(4, afterSettingKey == null ? "" : afterSettingKey);
|
||||||
|
ps.setInt(5, Math.max(1, limit));
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) out.add(mapRow(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UserSettingEntry> listUnsyncedByLogin(Connection c, String login, int limit) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT login, setting_type, setting_key, time_ms, value_text, value_num, client_key, signature, synced
|
||||||
|
FROM user_settings
|
||||||
|
WHERE LOWER(login) = LOWER(?)
|
||||||
|
AND synced = FALSE
|
||||||
|
ORDER BY time_ms ASC, setting_key ASC
|
||||||
|
LIMIT ?
|
||||||
|
""";
|
||||||
|
List<UserSettingEntry> out = new ArrayList<>();
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, login);
|
||||||
|
ps.setInt(2, Math.max(1, limit));
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) out.add(mapRow(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int markSynced(Connection c, String login, int settingType, String settingKey) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
UPDATE user_settings
|
||||||
|
SET synced = TRUE
|
||||||
|
WHERE LOWER(login) = LOWER(?)
|
||||||
|
AND setting_type = ?
|
||||||
|
AND setting_key = ?
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, login);
|
||||||
|
ps.setInt(2, settingType);
|
||||||
|
ps.setString(3, settingKey);
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int markAllUnsynced(Connection c, String login) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
UPDATE user_settings
|
||||||
|
SET synced = FALSE
|
||||||
|
WHERE LOWER(login) = LOWER(?)
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, login);
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int markAllUnsynced() throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
return markAllUnsynced(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int markAllUnsynced(Connection c) throws SQLException {
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("UPDATE user_settings SET synced = FALSE")) {
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UserSettingEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
|
UserSettingEntry e = new UserSettingEntry();
|
||||||
|
e.setLogin(rs.getString("login"));
|
||||||
|
e.setSettingType(rs.getInt("setting_type"));
|
||||||
|
e.setSettingKey(rs.getString("setting_key"));
|
||||||
|
e.setTimeMs(rs.getLong("time_ms"));
|
||||||
|
e.setValueText(rs.getString("value_text"));
|
||||||
|
e.setValueNum(rs.getLong("value_num"));
|
||||||
|
|
||||||
|
String clientKey = rs.getString("client_key");
|
||||||
|
if (rs.wasNull()) clientKey = null;
|
||||||
|
e.setClientKey(clientKey);
|
||||||
|
|
||||||
|
String signature = rs.getString("signature");
|
||||||
|
if (rs.wasNull()) signature = null;
|
||||||
|
e.setSignature(signature);
|
||||||
|
|
||||||
|
e.setSynced(rs.getBoolean("synced"));
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
package shine.db.dao;
|
||||||
|
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.entities.UserSettingsSyncPeerStateEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public final class UserSettingsSyncPeerStateDAO {
|
||||||
|
|
||||||
|
private static volatile UserSettingsSyncPeerStateDAO instance;
|
||||||
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
|
private UserSettingsSyncPeerStateDAO() {}
|
||||||
|
|
||||||
|
public static UserSettingsSyncPeerStateDAO getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (UserSettingsSyncPeerStateDAO.class) {
|
||||||
|
if (instance == null) instance = new UserSettingsSyncPeerStateDAO();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserSettingsSyncPeerStateEntry getOrCreate(Connection c, String ownerLogin, String remoteServerLogin, String remoteServerUrl) throws SQLException {
|
||||||
|
UserSettingsSyncPeerStateEntry existing = get(c, ownerLogin, remoteServerLogin);
|
||||||
|
if (existing != null) return existing;
|
||||||
|
long nowMs = System.currentTimeMillis();
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO user_settings_sync_peer_state (
|
||||||
|
owner_login, remote_server_login, remote_server_url,
|
||||||
|
cursor_time_ms, cursor_setting_key, bootstrap_completed,
|
||||||
|
last_sync_at_ms, last_error, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, 0, '', FALSE, NULL, NULL, ?)
|
||||||
|
ON CONFLICT (owner_login, remote_server_login) DO UPDATE SET
|
||||||
|
remote_server_url = EXCLUDED.remote_server_url,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setString(2, remoteServerLogin);
|
||||||
|
ps.setString(3, remoteServerUrl);
|
||||||
|
ps.setLong(4, nowMs);
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
return get(c, ownerLogin, remoteServerLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserSettingsSyncPeerStateEntry get(Connection c, String ownerLogin, String remoteServerLogin) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT owner_login, remote_server_login, remote_server_url, cursor_time_ms, cursor_setting_key,
|
||||||
|
bootstrap_completed, last_sync_at_ms, last_error, updated_at_ms
|
||||||
|
FROM user_settings_sync_peer_state
|
||||||
|
WHERE LOWER(owner_login) = LOWER(?) AND LOWER(remote_server_login) = LOWER(?)
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setString(2, remoteServerLogin);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
return rs.next() ? mapRow(rs) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int updateSuccess(String ownerLogin, String remoteServerLogin, String remoteServerUrl, long cursorTimeMs, String cursorSettingKey, boolean bootstrapCompleted) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO user_settings_sync_peer_state (
|
||||||
|
owner_login, remote_server_login, remote_server_url,
|
||||||
|
cursor_time_ms, cursor_setting_key, bootstrap_completed,
|
||||||
|
last_sync_at_ms, last_error, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, NULL, ?)
|
||||||
|
ON CONFLICT (owner_login, remote_server_login) DO UPDATE SET
|
||||||
|
remote_server_url = EXCLUDED.remote_server_url,
|
||||||
|
cursor_time_ms = EXCLUDED.cursor_time_ms,
|
||||||
|
cursor_setting_key = EXCLUDED.cursor_setting_key,
|
||||||
|
bootstrap_completed = EXCLUDED.bootstrap_completed,
|
||||||
|
last_sync_at_ms = EXCLUDED.last_sync_at_ms,
|
||||||
|
last_error = NULL,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
long nowMs = System.currentTimeMillis();
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setString(2, remoteServerLogin);
|
||||||
|
ps.setString(3, remoteServerUrl);
|
||||||
|
ps.setLong(4, Math.max(0L, cursorTimeMs));
|
||||||
|
ps.setString(5, cursorSettingKey == null ? "" : cursorSettingKey);
|
||||||
|
ps.setBoolean(6, bootstrapCompleted);
|
||||||
|
ps.setLong(7, nowMs);
|
||||||
|
ps.setLong(8, nowMs);
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int updateError(String ownerLogin, String remoteServerLogin, String remoteServerUrl, String error) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO user_settings_sync_peer_state (
|
||||||
|
owner_login, remote_server_login, remote_server_url,
|
||||||
|
cursor_time_ms, cursor_setting_key, bootstrap_completed,
|
||||||
|
last_sync_at_ms, last_error, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, 0, '', FALSE, NULL, ?, ?)
|
||||||
|
ON CONFLICT (owner_login, remote_server_login) DO UPDATE SET
|
||||||
|
remote_server_url = EXCLUDED.remote_server_url,
|
||||||
|
last_error = EXCLUDED.last_error,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
long nowMs = System.currentTimeMillis();
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
ps.setString(2, remoteServerLogin);
|
||||||
|
ps.setString(3, remoteServerUrl);
|
||||||
|
ps.setString(4, error);
|
||||||
|
ps.setLong(5, nowMs);
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int clearBootstrap(String ownerLogin, String remoteServerLogin, String remoteServerUrl) throws SQLException {
|
||||||
|
return updateSuccess(ownerLogin, remoteServerLogin, remoteServerUrl, 0L, "", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int deleteAllForOwner(String ownerLogin) throws SQLException {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("DELETE FROM user_settings_sync_peer_state WHERE LOWER(owner_login) = LOWER(?)")) {
|
||||||
|
ps.setString(1, ownerLogin);
|
||||||
|
return ps.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UserSettingsSyncPeerStateEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
|
UserSettingsSyncPeerStateEntry e = new UserSettingsSyncPeerStateEntry();
|
||||||
|
e.setOwnerLogin(rs.getString("owner_login"));
|
||||||
|
e.setRemoteServerLogin(rs.getString("remote_server_login"));
|
||||||
|
e.setRemoteServerUrl(rs.getString("remote_server_url"));
|
||||||
|
e.setCursorTimeMs(rs.getLong("cursor_time_ms"));
|
||||||
|
e.setCursorSettingKey(rs.getString("cursor_setting_key"));
|
||||||
|
e.setBootstrapCompleted(rs.getBoolean("bootstrap_completed"));
|
||||||
|
long lastSyncAtMs = rs.getLong("last_sync_at_ms");
|
||||||
|
e.setLastSyncAtMs(rs.wasNull() ? null : lastSyncAtMs);
|
||||||
|
e.setLastError(rs.getString("last_error"));
|
||||||
|
e.setUpdatedAtMs(rs.getLong("updated_at_ms"));
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package shine.db.entities;
|
|
||||||
|
|
||||||
public class DirectMessageEntry {
|
|
||||||
private String messageId;
|
|
||||||
private String fromLogin;
|
|
||||||
private String toLogin;
|
|
||||||
private String text;
|
|
||||||
private long createdAtMs;
|
|
||||||
|
|
||||||
public String getMessageId() { return messageId; }
|
|
||||||
public void setMessageId(String messageId) { this.messageId = messageId; }
|
|
||||||
|
|
||||||
public String getFromLogin() { return fromLogin; }
|
|
||||||
public void setFromLogin(String fromLogin) { this.fromLogin = fromLogin; }
|
|
||||||
|
|
||||||
public String getToLogin() { return toLogin; }
|
|
||||||
public void setToLogin(String toLogin) { this.toLogin = toLogin; }
|
|
||||||
|
|
||||||
public String getText() { return text; }
|
|
||||||
public void setText(String text) { this.text = text; }
|
|
||||||
|
|
||||||
public long getCreatedAtMs() { return createdAtMs; }
|
|
||||||
public void setCreatedAtMs(long createdAtMs) { this.createdAtMs = createdAtMs; }
|
|
||||||
}
|
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
package shine.db.entities;
|
||||||
|
|
||||||
|
public class DmDeliveryStateEntry {
|
||||||
|
public static final int ACCEPTED = 0;
|
||||||
|
public static final int DELIVERED = 1;
|
||||||
|
public static final int FAILED = 3;
|
||||||
|
|
||||||
|
// Совместимость со строками БД, созданными ранней экспериментальной миграцией.
|
||||||
|
public static final int PENDING_NONE = ACCEPTED;
|
||||||
|
public static final int DELIVERED_ONE = DELIVERED;
|
||||||
|
public static final int DELIVERED_ALL = 2;
|
||||||
|
public static final int FAILED_FINAL = FAILED;
|
||||||
|
|
||||||
|
private String outgoingMessageKey;
|
||||||
|
private String eventId;
|
||||||
|
private String baseKey;
|
||||||
|
private String fromLogin;
|
||||||
|
private String toLogin;
|
||||||
|
private String incomingMessageKey;
|
||||||
|
private long createdAtMs;
|
||||||
|
private long deliveryExpiresAtMs;
|
||||||
|
private int deliveryState;
|
||||||
|
private String deliveredServerLogin;
|
||||||
|
private String recipientRoutesHash;
|
||||||
|
private int attemptIndex;
|
||||||
|
private Long nextAttemptAtMs;
|
||||||
|
private Long lastAttemptAtMs;
|
||||||
|
private String lastError;
|
||||||
|
private long updatedAtMs;
|
||||||
|
|
||||||
|
public String getOutgoingMessageKey() { return outgoingMessageKey; }
|
||||||
|
public void setOutgoingMessageKey(String value) { this.outgoingMessageKey = value; }
|
||||||
|
public String getEventId() { return eventId; }
|
||||||
|
public void setEventId(String value) { this.eventId = value; }
|
||||||
|
public String getBaseKey() { return baseKey; }
|
||||||
|
public void setBaseKey(String value) { this.baseKey = value; }
|
||||||
|
public String getFromLogin() { return fromLogin; }
|
||||||
|
public void setFromLogin(String value) { this.fromLogin = value; }
|
||||||
|
public String getToLogin() { return toLogin; }
|
||||||
|
public void setToLogin(String value) { this.toLogin = value; }
|
||||||
|
public String getIncomingMessageKey() { return incomingMessageKey; }
|
||||||
|
public void setIncomingMessageKey(String value) { this.incomingMessageKey = value; }
|
||||||
|
public long getCreatedAtMs() { return createdAtMs; }
|
||||||
|
public void setCreatedAtMs(long value) { this.createdAtMs = value; }
|
||||||
|
public long getDeliveryExpiresAtMs() { return deliveryExpiresAtMs; }
|
||||||
|
public void setDeliveryExpiresAtMs(long value) { this.deliveryExpiresAtMs = value; }
|
||||||
|
public int getDeliveryState() { return deliveryState; }
|
||||||
|
public void setDeliveryState(int value) { this.deliveryState = value; }
|
||||||
|
public String getDeliveredServerLogin() { return deliveredServerLogin; }
|
||||||
|
public void setDeliveredServerLogin(String value) { this.deliveredServerLogin = value; }
|
||||||
|
public String getRecipientRoutesHash() { return recipientRoutesHash; }
|
||||||
|
public void setRecipientRoutesHash(String value) { this.recipientRoutesHash = value; }
|
||||||
|
public int getAttemptIndex() { return attemptIndex; }
|
||||||
|
public void setAttemptIndex(int value) { this.attemptIndex = value; }
|
||||||
|
public Long getNextAttemptAtMs() { return nextAttemptAtMs; }
|
||||||
|
public void setNextAttemptAtMs(Long value) { this.nextAttemptAtMs = value; }
|
||||||
|
public Long getLastAttemptAtMs() { return lastAttemptAtMs; }
|
||||||
|
public void setLastAttemptAtMs(Long value) { this.lastAttemptAtMs = value; }
|
||||||
|
public String getLastError() { return lastError; }
|
||||||
|
public void setLastError(String value) { this.lastError = value; }
|
||||||
|
public long getUpdatedAtMs() { return updatedAtMs; }
|
||||||
|
public void setUpdatedAtMs(long value) { this.updatedAtMs = value; }
|
||||||
|
|
||||||
|
public String deliveryStateCode() {
|
||||||
|
return switch (deliveryState) {
|
||||||
|
case DELIVERED_ONE, DELIVERED_ALL -> "delivered";
|
||||||
|
case FAILED_FINAL -> "failed";
|
||||||
|
default -> "accepted";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int parseStateCode(String value) {
|
||||||
|
if (value == null) return ACCEPTED;
|
||||||
|
return switch (value.trim().toLowerCase()) {
|
||||||
|
case "delivered", "delivered_one", "delivered_all" -> DELIVERED;
|
||||||
|
case "failed", "failed_final" -> FAILED;
|
||||||
|
default -> ACCEPTED;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDelivered() {
|
||||||
|
return deliveryState == DELIVERED_ONE || deliveryState == DELIVERED_ALL;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package shine.db.entities;
|
||||||
|
|
||||||
|
public class DmSyncOutboxEntry {
|
||||||
|
private String ownerLogin;
|
||||||
|
private String primaryMessageKey;
|
||||||
|
private String eventId;
|
||||||
|
private String secondaryMessageKey;
|
||||||
|
private boolean synced;
|
||||||
|
private long createdAtMs;
|
||||||
|
private long updatedAtMs;
|
||||||
|
|
||||||
|
public String getOwnerLogin() { return ownerLogin; }
|
||||||
|
public void setOwnerLogin(String value) { this.ownerLogin = value; }
|
||||||
|
public String getPrimaryMessageKey() { return primaryMessageKey; }
|
||||||
|
public void setPrimaryMessageKey(String value) { this.primaryMessageKey = value; }
|
||||||
|
public String getEventId() { return eventId; }
|
||||||
|
public void setEventId(String value) { this.eventId = value; }
|
||||||
|
public String getSecondaryMessageKey() { return secondaryMessageKey; }
|
||||||
|
public void setSecondaryMessageKey(String value) { this.secondaryMessageKey = value; }
|
||||||
|
public boolean isSynced() { return synced; }
|
||||||
|
public void setSynced(boolean value) { this.synced = value; }
|
||||||
|
public long getCreatedAtMs() { return createdAtMs; }
|
||||||
|
public void setCreatedAtMs(long value) { this.createdAtMs = value; }
|
||||||
|
public long getUpdatedAtMs() { return updatedAtMs; }
|
||||||
|
public void setUpdatedAtMs(long value) { this.updatedAtMs = value; }
|
||||||
|
}
|
||||||
-35
@@ -1,35 +0,0 @@
|
|||||||
package shine.db.entities;
|
|
||||||
|
|
||||||
public class SignedDirectMessageHistoryEntry {
|
|
||||||
private String messageId;
|
|
||||||
private String fromLogin;
|
|
||||||
private String toLogin;
|
|
||||||
private int targetMode;
|
|
||||||
private String targetSessionId;
|
|
||||||
private int messageType;
|
|
||||||
private long timeMs;
|
|
||||||
private long nonce;
|
|
||||||
private byte[] rawPacket;
|
|
||||||
private long createdAtMs;
|
|
||||||
|
|
||||||
public String getMessageId() { return messageId; }
|
|
||||||
public void setMessageId(String messageId) { this.messageId = messageId; }
|
|
||||||
public String getFromLogin() { return fromLogin; }
|
|
||||||
public void setFromLogin(String fromLogin) { this.fromLogin = fromLogin; }
|
|
||||||
public String getToLogin() { return toLogin; }
|
|
||||||
public void setToLogin(String toLogin) { this.toLogin = toLogin; }
|
|
||||||
public int getTargetMode() { return targetMode; }
|
|
||||||
public void setTargetMode(int targetMode) { this.targetMode = targetMode; }
|
|
||||||
public String getTargetSessionId() { return targetSessionId; }
|
|
||||||
public void setTargetSessionId(String targetSessionId) { this.targetSessionId = targetSessionId; }
|
|
||||||
public int getMessageType() { return messageType; }
|
|
||||||
public void setMessageType(int messageType) { this.messageType = messageType; }
|
|
||||||
public long getTimeMs() { return timeMs; }
|
|
||||||
public void setTimeMs(long timeMs) { this.timeMs = timeMs; }
|
|
||||||
public long getNonce() { return nonce; }
|
|
||||||
public void setNonce(long nonce) { this.nonce = nonce; }
|
|
||||||
public byte[] getRawPacket() { return rawPacket; }
|
|
||||||
public void setRawPacket(byte[] rawPacket) { this.rawPacket = rawPacket; }
|
|
||||||
public long getCreatedAtMs() { return createdAtMs; }
|
|
||||||
public void setCreatedAtMs(long createdAtMs) { this.createdAtMs = createdAtMs; }
|
|
||||||
}
|
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
package shine.db.entities;
|
||||||
|
|
||||||
|
public class UserNotificationEntry {
|
||||||
|
private String ownerLogin;
|
||||||
|
private String notificationKind;
|
||||||
|
private long createdAtMs;
|
||||||
|
private String sourceLogin;
|
||||||
|
private String sourceBchName;
|
||||||
|
private int sourceBlockNumber;
|
||||||
|
private byte[] sourceBlockHash;
|
||||||
|
private String targetLogin;
|
||||||
|
private String targetBchName;
|
||||||
|
private Integer targetBlockNumber;
|
||||||
|
private byte[] targetBlockHash;
|
||||||
|
private int sourceMsgSubType;
|
||||||
|
private String sourceText;
|
||||||
|
|
||||||
|
public String getOwnerLogin() { return ownerLogin; }
|
||||||
|
public void setOwnerLogin(String ownerLogin) { this.ownerLogin = ownerLogin; }
|
||||||
|
public String getNotificationKind() { return notificationKind; }
|
||||||
|
public void setNotificationKind(String notificationKind) { this.notificationKind = notificationKind; }
|
||||||
|
public long getCreatedAtMs() { return createdAtMs; }
|
||||||
|
public void setCreatedAtMs(long createdAtMs) { this.createdAtMs = createdAtMs; }
|
||||||
|
public String getSourceLogin() { return sourceLogin; }
|
||||||
|
public void setSourceLogin(String sourceLogin) { this.sourceLogin = sourceLogin; }
|
||||||
|
public String getSourceBchName() { return sourceBchName; }
|
||||||
|
public void setSourceBchName(String sourceBchName) { this.sourceBchName = sourceBchName; }
|
||||||
|
public int getSourceBlockNumber() { return sourceBlockNumber; }
|
||||||
|
public void setSourceBlockNumber(int sourceBlockNumber) { this.sourceBlockNumber = sourceBlockNumber; }
|
||||||
|
public byte[] getSourceBlockHash() { return sourceBlockHash; }
|
||||||
|
public void setSourceBlockHash(byte[] sourceBlockHash) { this.sourceBlockHash = sourceBlockHash; }
|
||||||
|
public String getTargetLogin() { return targetLogin; }
|
||||||
|
public void setTargetLogin(String targetLogin) { this.targetLogin = targetLogin; }
|
||||||
|
public String getTargetBchName() { return targetBchName; }
|
||||||
|
public void setTargetBchName(String targetBchName) { this.targetBchName = targetBchName; }
|
||||||
|
public Integer getTargetBlockNumber() { return targetBlockNumber; }
|
||||||
|
public void setTargetBlockNumber(Integer targetBlockNumber) { this.targetBlockNumber = targetBlockNumber; }
|
||||||
|
public byte[] getTargetBlockHash() { return targetBlockHash; }
|
||||||
|
public void setTargetBlockHash(byte[] targetBlockHash) { this.targetBlockHash = targetBlockHash; }
|
||||||
|
public int getSourceMsgSubType() { return sourceMsgSubType; }
|
||||||
|
public void setSourceMsgSubType(int sourceMsgSubType) { this.sourceMsgSubType = sourceMsgSubType; }
|
||||||
|
public String getSourceText() { return sourceText; }
|
||||||
|
public void setSourceText(String sourceText) { this.sourceText = sourceText; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package shine.db.entities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserSettingEntry — одна пользовательская настройка.
|
||||||
|
*
|
||||||
|
* Таблица: user_settings
|
||||||
|
* - login TEXT NOT NULL
|
||||||
|
* - setting_type INTEGER NOT NULL
|
||||||
|
* - setting_key TEXT NOT NULL
|
||||||
|
* - time_ms BIGINT NOT NULL
|
||||||
|
* - value_text TEXT NOT NULL
|
||||||
|
* - value_num BIGINT NOT NULL
|
||||||
|
* - client_key TEXT NOT NULL
|
||||||
|
* - signature TEXT NOT NULL
|
||||||
|
* - synced BOOLEAN NOT NULL
|
||||||
|
*/
|
||||||
|
public class UserSettingEntry {
|
||||||
|
private String login;
|
||||||
|
private int settingType;
|
||||||
|
private String settingKey;
|
||||||
|
private long timeMs;
|
||||||
|
private String valueText;
|
||||||
|
private long valueNum;
|
||||||
|
private String clientKey;
|
||||||
|
private String signature;
|
||||||
|
private boolean synced;
|
||||||
|
|
||||||
|
public UserSettingEntry() {}
|
||||||
|
|
||||||
|
public UserSettingEntry(String login, int settingType, String settingKey, long timeMs, String valueText, long valueNum, String clientKey, String signature, boolean synced) {
|
||||||
|
this.login = login;
|
||||||
|
this.settingType = settingType;
|
||||||
|
this.settingKey = settingKey;
|
||||||
|
this.timeMs = timeMs;
|
||||||
|
this.valueText = valueText;
|
||||||
|
this.valueNum = valueNum;
|
||||||
|
this.clientKey = clientKey;
|
||||||
|
this.signature = signature;
|
||||||
|
this.synced = synced;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
|
public int getSettingType() { return settingType; }
|
||||||
|
public void setSettingType(int settingType) { this.settingType = settingType; }
|
||||||
|
|
||||||
|
public String getSettingKey() { return settingKey; }
|
||||||
|
public void setSettingKey(String settingKey) { this.settingKey = settingKey; }
|
||||||
|
|
||||||
|
public long getTimeMs() { return timeMs; }
|
||||||
|
public void setTimeMs(long timeMs) { this.timeMs = timeMs; }
|
||||||
|
|
||||||
|
public String getValueText() { return valueText; }
|
||||||
|
public void setValueText(String valueText) { this.valueText = valueText; }
|
||||||
|
|
||||||
|
public long getValueNum() { return valueNum; }
|
||||||
|
public void setValueNum(long valueNum) { this.valueNum = valueNum; }
|
||||||
|
|
||||||
|
public String getClientKey() { return clientKey; }
|
||||||
|
public void setClientKey(String clientKey) { this.clientKey = clientKey; }
|
||||||
|
|
||||||
|
public String getSignature() { return signature; }
|
||||||
|
public void setSignature(String signature) { this.signature = signature; }
|
||||||
|
|
||||||
|
public boolean isSynced() { return synced; }
|
||||||
|
public void setSynced(boolean synced) { this.synced = synced; }
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package shine.db.entities;
|
||||||
|
|
||||||
|
public class UserSettingsSyncPeerStateEntry {
|
||||||
|
private String ownerLogin;
|
||||||
|
private String remoteServerLogin;
|
||||||
|
private String remoteServerUrl;
|
||||||
|
private long cursorTimeMs;
|
||||||
|
private String cursorSettingKey;
|
||||||
|
private boolean bootstrapCompleted;
|
||||||
|
private Long lastSyncAtMs;
|
||||||
|
private String lastError;
|
||||||
|
private long updatedAtMs;
|
||||||
|
|
||||||
|
public String getOwnerLogin() { return ownerLogin; }
|
||||||
|
public void setOwnerLogin(String ownerLogin) { this.ownerLogin = ownerLogin; }
|
||||||
|
|
||||||
|
public String getRemoteServerLogin() { return remoteServerLogin; }
|
||||||
|
public void setRemoteServerLogin(String remoteServerLogin) { this.remoteServerLogin = remoteServerLogin; }
|
||||||
|
|
||||||
|
public String getRemoteServerUrl() { return remoteServerUrl; }
|
||||||
|
public void setRemoteServerUrl(String remoteServerUrl) { this.remoteServerUrl = remoteServerUrl; }
|
||||||
|
|
||||||
|
public long getCursorTimeMs() { return cursorTimeMs; }
|
||||||
|
public void setCursorTimeMs(long cursorTimeMs) { this.cursorTimeMs = cursorTimeMs; }
|
||||||
|
|
||||||
|
public String getCursorSettingKey() { return cursorSettingKey; }
|
||||||
|
public void setCursorSettingKey(String cursorSettingKey) { this.cursorSettingKey = cursorSettingKey; }
|
||||||
|
|
||||||
|
public boolean isBootstrapCompleted() { return bootstrapCompleted; }
|
||||||
|
public void setBootstrapCompleted(boolean bootstrapCompleted) { this.bootstrapCompleted = bootstrapCompleted; }
|
||||||
|
|
||||||
|
public Long getLastSyncAtMs() { return lastSyncAtMs; }
|
||||||
|
public void setLastSyncAtMs(Long lastSyncAtMs) { this.lastSyncAtMs = lastSyncAtMs; }
|
||||||
|
|
||||||
|
public String getLastError() { return lastError; }
|
||||||
|
public void setLastError(String lastError) { this.lastError = lastError; }
|
||||||
|
|
||||||
|
public long getUpdatedAtMs() { return updatedAtMs; }
|
||||||
|
public void setUpdatedAtMs(long updatedAtMs) { this.updatedAtMs = updatedAtMs; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_notifications_state (
|
||||||
|
owner_login TEXT NOT NULL REFERENCES solana_user_pda_current(normalized_login),
|
||||||
|
notification_kind TEXT NOT NULL CHECK (notification_kind IN ('reply', 'connection')),
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
source_login TEXT NOT NULL,
|
||||||
|
source_bch_name TEXT NOT NULL,
|
||||||
|
source_block_number INTEGER NOT NULL CHECK (source_block_number >= 0),
|
||||||
|
source_block_hash BYTEA NOT NULL,
|
||||||
|
target_login TEXT,
|
||||||
|
target_bch_name TEXT,
|
||||||
|
target_block_number INTEGER,
|
||||||
|
target_block_hash BYTEA,
|
||||||
|
source_msg_sub_type INTEGER NOT NULL,
|
||||||
|
source_text TEXT NOT NULL DEFAULT '',
|
||||||
|
UNIQUE (owner_login, notification_kind, source_bch_name, source_block_number, source_block_hash)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_notifications_state_owner_kind_time
|
||||||
|
ON user_notifications_state(owner_login, notification_kind, created_at_ms DESC, source_block_number DESC);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_notifications_state_owner_time
|
||||||
|
ON user_notifications_state(owner_login, created_at_ms DESC);
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 10, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_dialog_state (
|
||||||
|
owner_login TEXT NOT NULL,
|
||||||
|
peer_login TEXT NOT NULL,
|
||||||
|
relation_flag TEXT NOT NULL DEFAULT 'none',
|
||||||
|
last_message_blob_b64 TEXT NOT NULL DEFAULT '',
|
||||||
|
last_message_key TEXT NOT NULL DEFAULT '',
|
||||||
|
last_message_type INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_message_time_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
last_message_revision_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
unread_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_read_receipt_time_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (owner_login, peer_login)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE IF EXISTS dm_dialog_state
|
||||||
|
ADD COLUMN IF NOT EXISTS last_message_blob_b64 TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_dialog_state_owner_last_time
|
||||||
|
ON dm_dialog_state(owner_login, last_message_time_ms DESC, peer_login);
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 11, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_delivery_state (
|
||||||
|
outgoing_message_key TEXT PRIMARY KEY,
|
||||||
|
event_id TEXT NOT NULL UNIQUE,
|
||||||
|
base_key TEXT NOT NULL,
|
||||||
|
from_login TEXT NOT NULL,
|
||||||
|
to_login TEXT NOT NULL,
|
||||||
|
incoming_message_key TEXT NOT NULL,
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
delivery_expires_at_ms BIGINT NOT NULL,
|
||||||
|
delivery_state INTEGER NOT NULL DEFAULT 0 CHECK (delivery_state IN (0, 1, 2, 3)),
|
||||||
|
delivered_server_login TEXT,
|
||||||
|
recipient_routes_hash TEXT,
|
||||||
|
attempt_index INTEGER NOT NULL DEFAULT 0,
|
||||||
|
next_attempt_at_ms BIGINT,
|
||||||
|
last_attempt_at_ms BIGINT,
|
||||||
|
last_error TEXT,
|
||||||
|
updated_at_ms BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_delivery_state_due
|
||||||
|
ON dm_delivery_state(next_attempt_at_ms, delivery_state)
|
||||||
|
WHERE delivery_state = 0 AND next_attempt_at_ms IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_delivery_state_base
|
||||||
|
ON dm_delivery_state(base_key, from_login);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_sync_outbox (
|
||||||
|
owner_login TEXT NOT NULL,
|
||||||
|
primary_message_key TEXT NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
secondary_message_key TEXT,
|
||||||
|
synced BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (owner_login, primary_message_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_sync_outbox_unsynced
|
||||||
|
ON dm_sync_outbox(owner_login, created_at_ms, primary_message_key)
|
||||||
|
WHERE synced = FALSE;
|
||||||
|
|
||||||
|
-- До v12 факт межсерверной доставки не сохранялся. Старые исходящие пары
|
||||||
|
-- считаем уже доставленными, чтобы обновление не вызвало повторную рассылку.
|
||||||
|
INSERT INTO dm_delivery_state (
|
||||||
|
outgoing_message_key, event_id, base_key, from_login, to_login,
|
||||||
|
incoming_message_key, created_at_ms, delivery_expires_at_ms,
|
||||||
|
delivery_state, delivered_server_login, recipient_routes_hash,
|
||||||
|
attempt_index, next_attempt_at_ms, last_attempt_at_ms, last_error, updated_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
outgoing.message_key,
|
||||||
|
outgoing.message_key || ':' || outgoing.revision_time_ms || ':' || outgoing.reencrypted_at_ms,
|
||||||
|
outgoing.base_key,
|
||||||
|
outgoing.from_login,
|
||||||
|
outgoing.to_login,
|
||||||
|
incoming.message_key,
|
||||||
|
outgoing.created_at_ms,
|
||||||
|
outgoing.created_at_ms + 3600000,
|
||||||
|
2,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
9,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
'MIGRATED_AS_DELIVERED',
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
FROM signed_messages outgoing
|
||||||
|
JOIN signed_messages incoming
|
||||||
|
ON incoming.base_key = outgoing.base_key
|
||||||
|
AND incoming.message_type = outgoing.message_type - 1
|
||||||
|
WHERE outgoing.message_type IN (2, 4)
|
||||||
|
ON CONFLICT (outgoing_message_key) DO NOTHING;
|
||||||
|
|
||||||
|
-- Историю до v12 считаем подтверждённой, иначе само обновление вызовет
|
||||||
|
-- массовую повторную передачу. При замене сервера общий reset вернёт FALSE.
|
||||||
|
INSERT INTO dm_sync_outbox (
|
||||||
|
owner_login, primary_message_key, event_id, secondary_message_key,
|
||||||
|
synced, created_at_ms, updated_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
LOWER(outgoing.from_login),
|
||||||
|
outgoing.message_key,
|
||||||
|
outgoing.message_key || ':' || outgoing.revision_time_ms || ':' || outgoing.reencrypted_at_ms,
|
||||||
|
incoming.message_key,
|
||||||
|
TRUE,
|
||||||
|
outgoing.created_at_ms,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
FROM signed_messages outgoing
|
||||||
|
JOIN signed_messages incoming
|
||||||
|
ON incoming.base_key = outgoing.base_key
|
||||||
|
AND incoming.message_type = outgoing.message_type - 1
|
||||||
|
WHERE outgoing.message_type IN (2, 4)
|
||||||
|
ON CONFLICT (owner_login, primary_message_key) DO NOTHING;
|
||||||
|
|
||||||
|
-- Входящая копия синхронизируется отдельно между серверами получателя.
|
||||||
|
INSERT INTO dm_sync_outbox (
|
||||||
|
owner_login, primary_message_key, event_id, secondary_message_key,
|
||||||
|
synced, created_at_ms, updated_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
LOWER(incoming.to_login),
|
||||||
|
incoming.message_key,
|
||||||
|
incoming.message_key || ':' || incoming.revision_time_ms || ':' || incoming.reencrypted_at_ms,
|
||||||
|
NULL,
|
||||||
|
TRUE,
|
||||||
|
incoming.created_at_ms,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
FROM signed_messages incoming
|
||||||
|
WHERE incoming.message_type IN (1, 3)
|
||||||
|
ON CONFLICT (owner_login, primary_message_key) DO NOTHING;
|
||||||
|
|
||||||
|
-- Tombstone принадлежит обоим участникам и должен попасть на второй сервер
|
||||||
|
-- каждого из них. Для self-DM конфликт безопасно схлопывается.
|
||||||
|
INSERT INTO dm_sync_outbox (
|
||||||
|
owner_login, primary_message_key, event_id, secondary_message_key,
|
||||||
|
synced, created_at_ms, updated_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
LOWER(owner_login),
|
||||||
|
tombstone.message_key,
|
||||||
|
tombstone.message_key || ':' || tombstone.revision_time_ms || ':' || tombstone.reencrypted_at_ms,
|
||||||
|
NULL,
|
||||||
|
TRUE,
|
||||||
|
tombstone.created_at_ms,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
FROM signed_messages tombstone
|
||||||
|
CROSS JOIN LATERAL (VALUES (tombstone.from_login), (tombstone.to_login)) owners(owner_login)
|
||||||
|
WHERE tombstone.message_type IN (5, 6, 7, 8)
|
||||||
|
ON CONFLICT (owner_login, primary_message_key) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 12, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Нормализация экспериментальной схемы доставки v12:
|
||||||
|
-- 0=accepted, 1=delivered, 3=failed. Старое delivered_all (2)
|
||||||
|
-- объединяется с delivered, поскольку ACK одного сервера теперь достаточен.
|
||||||
|
UPDATE dm_delivery_state
|
||||||
|
SET delivery_state = 1,
|
||||||
|
delivered_server_login = NULL,
|
||||||
|
next_attempt_at_ms = NULL,
|
||||||
|
updated_at_ms = CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
WHERE delivery_state = 2;
|
||||||
|
|
||||||
|
-- Для незавершённых записей перестраиваем только будущую очередь. Повторная
|
||||||
|
-- передача безопасна благодаря messageKey и идемпотентному приёму.
|
||||||
|
UPDATE dm_delivery_state
|
||||||
|
SET delivery_expires_at_ms = created_at_ms + 3600000,
|
||||||
|
attempt_index = CASE
|
||||||
|
WHEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) >= created_at_ms + 3600000 THEN 4
|
||||||
|
WHEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) >= created_at_ms + 1500000 THEN 3
|
||||||
|
WHEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) >= created_at_ms + 300000 THEN 2
|
||||||
|
ELSE 1
|
||||||
|
END,
|
||||||
|
next_attempt_at_ms = CASE
|
||||||
|
WHEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) >= created_at_ms + 3600000
|
||||||
|
THEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
WHEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) >= created_at_ms + 1500000
|
||||||
|
THEN created_at_ms + 3600000
|
||||||
|
WHEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) >= created_at_ms + 300000
|
||||||
|
THEN created_at_ms + 1500000
|
||||||
|
ELSE created_at_ms + 30000
|
||||||
|
END,
|
||||||
|
updated_at_ms = CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
WHERE delivery_state = 0;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_dm_delivery_state_due;
|
||||||
|
CREATE INDEX idx_dm_delivery_state_due
|
||||||
|
ON dm_delivery_state(next_attempt_at_ms, delivery_state)
|
||||||
|
WHERE delivery_state = 0 AND next_attempt_at_ms IS NOT NULL;
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 13, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,620 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_stats_state (
|
||||||
|
login TEXT PRIMARY KEY REFERENCES solana_user_pda_current(login) ON DELETE CASCADE,
|
||||||
|
owned_public_channels_count INTEGER NOT NULL DEFAULT 0 CHECK (owned_public_channels_count >= 0),
|
||||||
|
following_users_count INTEGER NOT NULL DEFAULT 0 CHECK (following_users_count >= 0),
|
||||||
|
following_channels_count INTEGER NOT NULL DEFAULT 0 CHECK (following_channels_count >= 0),
|
||||||
|
close_friends_count INTEGER NOT NULL DEFAULT 0 CHECK (close_friends_count >= 0),
|
||||||
|
updated_at_ms BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_stats_state_following_users_count
|
||||||
|
ON user_stats_state (following_users_count);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS channel_stats_state (
|
||||||
|
owner_bch_name TEXT NOT NULL,
|
||||||
|
channel_root_block_number INTEGER NOT NULL CHECK (channel_root_block_number >= 0),
|
||||||
|
channel_root_block_hash BYTEA NOT NULL,
|
||||||
|
owner_login TEXT NOT NULL,
|
||||||
|
channel_type_code INTEGER NOT NULL DEFAULT 1,
|
||||||
|
subscribers_count INTEGER NOT NULL DEFAULT 0 CHECK (subscribers_count >= 0),
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (owner_bch_name, channel_root_block_number, channel_root_block_hash)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_channel_stats_state_owner_login
|
||||||
|
ON channel_stats_state (owner_login);
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_user_stats_state_ai()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
now_ms BIGINT;
|
||||||
|
BEGIN
|
||||||
|
now_ms := CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT);
|
||||||
|
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
now_ms
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO NOTHING;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_channel_names_state_stats_ai()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
now_ms BIGINT;
|
||||||
|
public_subscribers_count INTEGER;
|
||||||
|
BEGIN
|
||||||
|
now_ms := CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT);
|
||||||
|
|
||||||
|
IF NEW.owner_login IS NULL OR btrim(NEW.owner_login) = '' THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NEW.channel_type_code = 1 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.owner_login,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
now_ms
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
owned_public_channels_count = user_stats_state.owned_public_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
SELECT COUNT(*)::INTEGER
|
||||||
|
INTO public_subscribers_count
|
||||||
|
FROM connections_state cs
|
||||||
|
JOIN channel_names_state cn
|
||||||
|
ON cn.owner_bch_name = cs.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = cs.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = cs.to_block_hash
|
||||||
|
WHERE cs.rel_type = 30
|
||||||
|
AND cn.owner_bch_name = NEW.owner_bch_name
|
||||||
|
AND cn.channel_root_block_number = NEW.channel_root_block_number
|
||||||
|
AND cn.channel_root_block_hash = NEW.channel_root_block_hash
|
||||||
|
AND cn.channel_type_code = 1;
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.owner_bch_name,
|
||||||
|
NEW.channel_root_block_number,
|
||||||
|
NEW.channel_root_block_hash,
|
||||||
|
NEW.owner_login,
|
||||||
|
NEW.channel_type_code,
|
||||||
|
COALESCE(public_subscribers_count, 0),
|
||||||
|
now_ms
|
||||||
|
)
|
||||||
|
ON CONFLICT (owner_bch_name, channel_root_block_number, channel_root_block_hash) DO UPDATE SET
|
||||||
|
owner_login = EXCLUDED.owner_login,
|
||||||
|
channel_type_code = EXCLUDED.channel_type_code,
|
||||||
|
subscribers_count = EXCLUDED.subscribers_count,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSE
|
||||||
|
WITH pending AS (
|
||||||
|
SELECT cs.login, COUNT(*)::INTEGER AS cnt
|
||||||
|
FROM connections_state cs
|
||||||
|
WHERE cs.rel_type = 30
|
||||||
|
AND cs.to_bch_name = NEW.owner_bch_name
|
||||||
|
AND cs.to_block_number = NEW.channel_root_block_number
|
||||||
|
AND cs.to_block_hash = NEW.channel_root_block_hash
|
||||||
|
GROUP BY cs.login
|
||||||
|
)
|
||||||
|
UPDATE user_stats_state us
|
||||||
|
SET following_channels_count = GREATEST(0, us.following_channels_count - pending.cnt),
|
||||||
|
updated_at_ms = now_ms
|
||||||
|
FROM pending
|
||||||
|
WHERE us.login = pending.login;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_blocks_connection_state_ai()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
resolved_login TEXT;
|
||||||
|
positive_rel_type INTEGER;
|
||||||
|
existed_before BOOLEAN;
|
||||||
|
target_channel_type INTEGER;
|
||||||
|
BEGIN
|
||||||
|
IF NEW.msg_type <> 3 THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
resolved_login := shine_resolve_login(NEW.to_login, NEW.to_bch_name);
|
||||||
|
|
||||||
|
IF NEW.msg_sub_type IN (10, 20, 30, 40, 50, 52, 54, 60, 70, 74) THEN
|
||||||
|
IF resolved_login IS NULL OR NEW.to_bch_name IS NULL THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM connections_state
|
||||||
|
WHERE login = NEW.login
|
||||||
|
AND rel_type = NEW.msg_sub_type
|
||||||
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'))
|
||||||
|
)
|
||||||
|
INTO existed_before;
|
||||||
|
|
||||||
|
IF NOT existed_before THEN
|
||||||
|
IF NEW.msg_sub_type = 10 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
close_friends_count = user_stats_state.close_friends_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF NEW.msg_sub_type = 30 THEN
|
||||||
|
IF NEW.to_block_number IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = user_stats_state.following_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF NEW.to_block_number = 0 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_users_count = user_stats_state.following_users_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSE
|
||||||
|
SELECT cn.channel_type_code
|
||||||
|
INTO target_channel_type
|
||||||
|
FROM channel_names_state cn
|
||||||
|
WHERE cn.owner_bch_name = NEW.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = NEW.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = NEW.to_block_hash
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF target_channel_type = 1 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = user_stats_state.following_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.to_bch_name,
|
||||||
|
NEW.to_block_number,
|
||||||
|
NEW.to_block_hash,
|
||||||
|
resolved_login,
|
||||||
|
target_channel_type,
|
||||||
|
1,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (owner_bch_name, channel_root_block_number, channel_root_block_hash) DO UPDATE SET
|
||||||
|
owner_login = EXCLUDED.owner_login,
|
||||||
|
channel_type_code = EXCLUDED.channel_type_code,
|
||||||
|
subscribers_count = channel_stats_state.subscribers_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF target_channel_type IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = user_stats_state.following_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
DELETE FROM connections_state
|
||||||
|
WHERE login = NEW.login
|
||||||
|
AND rel_type = NEW.msg_sub_type
|
||||||
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'));
|
||||||
|
|
||||||
|
INSERT INTO connections_state (
|
||||||
|
login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
NEW.msg_sub_type,
|
||||||
|
resolved_login,
|
||||||
|
NEW.to_bch_name,
|
||||||
|
COALESCE(NEW.to_block_number, 0),
|
||||||
|
COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'))
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
positive_rel_type := CASE NEW.msg_sub_type
|
||||||
|
WHEN 11 THEN 10
|
||||||
|
WHEN 21 THEN 20
|
||||||
|
WHEN 31 THEN 30
|
||||||
|
WHEN 41 THEN 40
|
||||||
|
WHEN 51 THEN 50
|
||||||
|
WHEN 53 THEN 52
|
||||||
|
WHEN 55 THEN 54
|
||||||
|
WHEN 61 THEN 60
|
||||||
|
WHEN 71 THEN 70
|
||||||
|
WHEN 75 THEN 74
|
||||||
|
ELSE NULL
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF positive_rel_type IS NULL OR resolved_login IS NULL THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM connections_state
|
||||||
|
WHERE login = NEW.login
|
||||||
|
AND rel_type = positive_rel_type
|
||||||
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'))
|
||||||
|
)
|
||||||
|
INTO existed_before;
|
||||||
|
|
||||||
|
IF NOT existed_before THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF positive_rel_type = 10 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
close_friends_count = GREATEST(0, user_stats_state.close_friends_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF positive_rel_type = 30 THEN
|
||||||
|
IF NEW.to_block_number IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = GREATEST(0, user_stats_state.following_channels_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF NEW.to_block_number = 0 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_users_count = GREATEST(0, user_stats_state.following_users_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSE
|
||||||
|
SELECT cn.channel_type_code
|
||||||
|
INTO target_channel_type
|
||||||
|
FROM channel_names_state cn
|
||||||
|
WHERE cn.owner_bch_name = NEW.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = NEW.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = NEW.to_block_hash
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF target_channel_type = 1 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = GREATEST(0, user_stats_state.following_channels_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.to_bch_name,
|
||||||
|
NEW.to_block_number,
|
||||||
|
NEW.to_block_hash,
|
||||||
|
resolved_login,
|
||||||
|
target_channel_type,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (owner_bch_name, channel_root_block_number, channel_root_block_hash) DO UPDATE SET
|
||||||
|
owner_login = EXCLUDED.owner_login,
|
||||||
|
channel_type_code = EXCLUDED.channel_type_code,
|
||||||
|
subscribers_count = GREATEST(0, channel_stats_state.subscribers_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF target_channel_type IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = GREATEST(0, user_stats_state.following_channels_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
DELETE FROM connections_state
|
||||||
|
WHERE login = NEW.login
|
||||||
|
AND rel_type = positive_rel_type
|
||||||
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'));
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_user_stats_state_ai ON solana_user_pda_current;
|
||||||
|
CREATE TRIGGER trg_user_stats_state_ai
|
||||||
|
AFTER INSERT ON solana_user_pda_current
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION shine_user_stats_state_ai();
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_channel_names_state_stats_ai ON channel_names_state;
|
||||||
|
CREATE TRIGGER trg_channel_names_state_stats_ai
|
||||||
|
AFTER INSERT ON channel_names_state
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION shine_channel_names_state_stats_ai();
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_blocks_connection_state_ai ON blocks;
|
||||||
|
CREATE TRIGGER trg_blocks_connection_state_ai
|
||||||
|
AFTER INSERT ON blocks
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION shine_blocks_connection_state_ai();
|
||||||
|
|
||||||
|
TRUNCATE TABLE user_stats_state, channel_stats_state;
|
||||||
|
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
u.login,
|
||||||
|
COALESCE(own.owned_public_channels_count, 0) AS owned_public_channels_count,
|
||||||
|
COALESCE(fu.following_users_count, 0) AS following_users_count,
|
||||||
|
COALESCE(fc.following_channels_count, 0) AS following_channels_count,
|
||||||
|
COALESCE(cf.close_friends_count, 0) AS close_friends_count,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) AS updated_at_ms
|
||||||
|
FROM solana_user_pda_current u
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT owner_login, COUNT(*)::INTEGER AS owned_public_channels_count
|
||||||
|
FROM channel_names_state
|
||||||
|
WHERE channel_type_code = 1
|
||||||
|
GROUP BY owner_login
|
||||||
|
) own ON LOWER(own.owner_login) = LOWER(u.login)
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT login, COUNT(*)::INTEGER AS following_users_count
|
||||||
|
FROM connections_state
|
||||||
|
WHERE rel_type = 30
|
||||||
|
AND to_block_number = 0
|
||||||
|
GROUP BY login
|
||||||
|
) fu ON LOWER(fu.login) = LOWER(u.login)
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT cs.login, COUNT(*)::INTEGER AS following_channels_count
|
||||||
|
FROM connections_state cs
|
||||||
|
JOIN channel_names_state cn
|
||||||
|
ON cn.owner_bch_name = cs.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = cs.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = cs.to_block_hash
|
||||||
|
WHERE cs.rel_type = 30
|
||||||
|
AND cn.channel_type_code = 1
|
||||||
|
GROUP BY cs.login
|
||||||
|
) fc ON LOWER(fc.login) = LOWER(u.login)
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT login, COUNT(*)::INTEGER AS close_friends_count
|
||||||
|
FROM connections_state
|
||||||
|
WHERE rel_type = 10
|
||||||
|
GROUP BY login
|
||||||
|
) cf ON LOWER(cf.login) = LOWER(u.login);
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
cn.owner_bch_name,
|
||||||
|
cn.channel_root_block_number,
|
||||||
|
cn.channel_root_block_hash,
|
||||||
|
cn.owner_login,
|
||||||
|
cn.channel_type_code,
|
||||||
|
COUNT(DISTINCT cs.login)::INTEGER AS subscribers_count,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) AS updated_at_ms
|
||||||
|
FROM channel_names_state cn
|
||||||
|
LEFT JOIN connections_state cs
|
||||||
|
ON cs.rel_type = 30
|
||||||
|
AND cs.to_bch_name = cn.owner_bch_name
|
||||||
|
AND cs.to_block_number = cn.channel_root_block_number
|
||||||
|
AND cs.to_block_hash = cn.channel_root_block_hash
|
||||||
|
WHERE cn.channel_type_code = 1
|
||||||
|
GROUP BY
|
||||||
|
cn.owner_bch_name,
|
||||||
|
cn.channel_root_block_number,
|
||||||
|
cn.channel_root_block_hash,
|
||||||
|
cn.owner_login,
|
||||||
|
cn.channel_type_code;
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 14, 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,105 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_channel_names_state_stats_ai()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
now_ms BIGINT;
|
||||||
|
public_subscribers_count INTEGER;
|
||||||
|
BEGIN
|
||||||
|
now_ms := CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT);
|
||||||
|
|
||||||
|
IF NEW.owner_login IS NULL OR btrim(NEW.owner_login) = '' THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NEW.channel_type_code = 1 THEN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM solana_user_pda_current su
|
||||||
|
WHERE su.login = NEW.owner_login
|
||||||
|
) THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.owner_login,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
now_ms
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
owned_public_channels_count = user_stats_state.owned_public_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT COUNT(*)::INTEGER
|
||||||
|
INTO public_subscribers_count
|
||||||
|
FROM connections_state cs
|
||||||
|
JOIN channel_names_state cn
|
||||||
|
ON cn.owner_bch_name = cs.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = cs.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = cs.to_block_hash
|
||||||
|
WHERE cs.rel_type = 30
|
||||||
|
AND cn.owner_bch_name = NEW.owner_bch_name
|
||||||
|
AND cn.channel_root_block_number = NEW.channel_root_block_number
|
||||||
|
AND cn.channel_root_block_hash = NEW.channel_root_block_hash
|
||||||
|
AND cn.channel_type_code = 1;
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.owner_bch_name,
|
||||||
|
NEW.channel_root_block_number,
|
||||||
|
NEW.channel_root_block_hash,
|
||||||
|
NEW.owner_login,
|
||||||
|
NEW.channel_type_code,
|
||||||
|
COALESCE(public_subscribers_count, 0),
|
||||||
|
now_ms
|
||||||
|
)
|
||||||
|
ON CONFLICT (owner_bch_name, channel_root_block_number, channel_root_block_hash) DO UPDATE SET
|
||||||
|
owner_login = EXCLUDED.owner_login,
|
||||||
|
channel_type_code = EXCLUDED.channel_type_code,
|
||||||
|
subscribers_count = EXCLUDED.subscribers_count,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSE
|
||||||
|
WITH pending AS (
|
||||||
|
SELECT cs.login, COUNT(*)::INTEGER AS cnt
|
||||||
|
FROM connections_state cs
|
||||||
|
WHERE cs.rel_type = 30
|
||||||
|
AND cs.to_bch_name = NEW.owner_bch_name
|
||||||
|
AND cs.to_block_number = NEW.channel_root_block_number
|
||||||
|
AND cs.to_block_hash = NEW.channel_root_block_hash
|
||||||
|
GROUP BY cs.login
|
||||||
|
)
|
||||||
|
UPDATE user_stats_state us
|
||||||
|
SET following_channels_count = GREATEST(0, us.following_channels_count - pending.cnt),
|
||||||
|
updated_at_ms = now_ms
|
||||||
|
FROM pending
|
||||||
|
WHERE us.login = pending.login;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 15, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_settings (
|
||||||
|
login TEXT NOT NULL REFERENCES solana_user_pda_current(login),
|
||||||
|
setting_type INTEGER NOT NULL,
|
||||||
|
setting_key TEXT NOT NULL,
|
||||||
|
time_ms BIGINT NOT NULL,
|
||||||
|
value_text TEXT NOT NULL DEFAULT '',
|
||||||
|
value_num BIGINT NOT NULL DEFAULT 0,
|
||||||
|
client_key TEXT NOT NULL DEFAULT '',
|
||||||
|
signature TEXT NOT NULL DEFAULT '',
|
||||||
|
synced BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
UNIQUE (login, setting_type, setting_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_settings_login
|
||||||
|
ON user_settings(login);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_settings_synced
|
||||||
|
ON user_settings(login, synced, time_ms);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_settings_type_key
|
||||||
|
ON user_settings(setting_type, setting_key);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_settings_sync_peer_state (
|
||||||
|
owner_login TEXT NOT NULL,
|
||||||
|
remote_server_login TEXT NOT NULL,
|
||||||
|
remote_server_url TEXT NOT NULL,
|
||||||
|
cursor_time_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
cursor_setting_key TEXT NOT NULL DEFAULT '',
|
||||||
|
bootstrap_completed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
last_sync_at_ms BIGINT,
|
||||||
|
last_error TEXT,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (owner_login, remote_server_login)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_settings_sync_peer_state_owner
|
||||||
|
ON user_settings_sync_peer_state(owner_login);
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 9, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS db_schema_version (
|
|||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
VALUES (1, 8, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
VALUES (1, 13, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
schema_version = EXCLUDED.schema_version,
|
schema_version = EXCLUDED.schema_version,
|
||||||
updated_at_ms = EXCLUDED.updated_at_ms;
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
@@ -291,6 +291,12 @@ AFTER TRUNCATE ON solana_user_pda_current
|
|||||||
FOR EACH STATEMENT
|
FOR EACH STATEMENT
|
||||||
EXECUTE FUNCTION trg_refresh_user_access_servers_from_user_pda_truncate();
|
EXECUTE FUNCTION trg_refresh_user_access_servers_from_user_pda_truncate();
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_user_stats_state_ai ON solana_user_pda_current;
|
||||||
|
CREATE TRIGGER trg_user_stats_state_ai
|
||||||
|
AFTER INSERT ON solana_user_pda_current
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION shine_user_stats_state_ai();
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS solana_user_pda_history (
|
CREATE TABLE IF NOT EXISTS solana_user_pda_history (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
tx_signature TEXT NOT NULL,
|
tx_signature TEXT NOT NULL,
|
||||||
@@ -401,6 +407,44 @@ CREATE TABLE IF NOT EXISTS users_params (
|
|||||||
CREATE INDEX IF NOT EXISTS idx_users_params_login
|
CREATE INDEX IF NOT EXISTS idx_users_params_login
|
||||||
ON users_params(login);
|
ON users_params(login);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_settings (
|
||||||
|
login TEXT NOT NULL REFERENCES solana_user_pda_current(login),
|
||||||
|
setting_type INTEGER NOT NULL,
|
||||||
|
setting_key TEXT NOT NULL,
|
||||||
|
time_ms BIGINT NOT NULL,
|
||||||
|
value_text TEXT NOT NULL DEFAULT '',
|
||||||
|
value_num BIGINT NOT NULL DEFAULT 0,
|
||||||
|
client_key TEXT NOT NULL DEFAULT '',
|
||||||
|
signature TEXT NOT NULL DEFAULT '',
|
||||||
|
synced BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
UNIQUE (login, setting_type, setting_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_settings_login
|
||||||
|
ON user_settings(login);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_settings_synced
|
||||||
|
ON user_settings(login, synced, time_ms);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_settings_type_key
|
||||||
|
ON user_settings(setting_type, setting_key);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_settings_sync_peer_state (
|
||||||
|
owner_login TEXT NOT NULL,
|
||||||
|
remote_server_login TEXT NOT NULL,
|
||||||
|
remote_server_url TEXT NOT NULL,
|
||||||
|
cursor_time_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
cursor_setting_key TEXT NOT NULL DEFAULT '',
|
||||||
|
bootstrap_completed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
last_sync_at_ms BIGINT,
|
||||||
|
last_error TEXT,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (owner_login, remote_server_login)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_settings_sync_peer_state_owner
|
||||||
|
ON user_settings_sync_peer_state(owner_login);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ip_geo_cache (
|
CREATE TABLE IF NOT EXISTS ip_geo_cache (
|
||||||
ip TEXT PRIMARY KEY,
|
ip TEXT PRIMARY KEY,
|
||||||
geo TEXT,
|
geo TEXT,
|
||||||
@@ -567,6 +611,32 @@ CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_names_state_target
|
|||||||
CREATE INDEX IF NOT EXISTS idx_channel_names_state_owner
|
CREATE INDEX IF NOT EXISTS idx_channel_names_state_owner
|
||||||
ON channel_names_state (owner_login, owner_bch_name);
|
ON channel_names_state (owner_login, owner_bch_name);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_stats_state (
|
||||||
|
login TEXT PRIMARY KEY REFERENCES solana_user_pda_current(login) ON DELETE CASCADE,
|
||||||
|
owned_public_channels_count INTEGER NOT NULL DEFAULT 0 CHECK (owned_public_channels_count >= 0),
|
||||||
|
following_users_count INTEGER NOT NULL DEFAULT 0 CHECK (following_users_count >= 0),
|
||||||
|
following_channels_count INTEGER NOT NULL DEFAULT 0 CHECK (following_channels_count >= 0),
|
||||||
|
close_friends_count INTEGER NOT NULL DEFAULT 0 CHECK (close_friends_count >= 0),
|
||||||
|
updated_at_ms BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_stats_state_following_users_count
|
||||||
|
ON user_stats_state (following_users_count);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS channel_stats_state (
|
||||||
|
owner_bch_name TEXT NOT NULL,
|
||||||
|
channel_root_block_number INTEGER NOT NULL CHECK (channel_root_block_number >= 0),
|
||||||
|
channel_root_block_hash BYTEA NOT NULL,
|
||||||
|
owner_login TEXT NOT NULL,
|
||||||
|
channel_type_code INTEGER NOT NULL DEFAULT 1,
|
||||||
|
subscribers_count INTEGER NOT NULL DEFAULT 0 CHECK (subscribers_count >= 0),
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (owner_bch_name, channel_root_block_number, channel_root_block_hash)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_channel_stats_state_owner_login
|
||||||
|
ON channel_stats_state (owner_login);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS chat200_state (
|
CREATE TABLE IF NOT EXISTS chat200_state (
|
||||||
owner_login TEXT NOT NULL,
|
owner_login TEXT NOT NULL,
|
||||||
owner_bch_name TEXT NOT NULL,
|
owner_bch_name TEXT NOT NULL,
|
||||||
@@ -713,6 +783,47 @@ CREATE TABLE IF NOT EXISTS signed_message_session_delivery (
|
|||||||
CREATE INDEX IF NOT EXISTS idx_signed_message_delivery_session
|
CREATE INDEX IF NOT EXISTS idx_signed_message_delivery_session
|
||||||
ON signed_message_session_delivery(session_id, delivered);
|
ON signed_message_session_delivery(session_id, delivered);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_notifications_state (
|
||||||
|
owner_login TEXT NOT NULL REFERENCES solana_user_pda_current(normalized_login),
|
||||||
|
notification_kind TEXT NOT NULL CHECK (notification_kind IN ('reply', 'connection')),
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
source_login TEXT NOT NULL,
|
||||||
|
source_bch_name TEXT NOT NULL,
|
||||||
|
source_block_number INTEGER NOT NULL CHECK (source_block_number >= 0),
|
||||||
|
source_block_hash BYTEA NOT NULL,
|
||||||
|
target_login TEXT,
|
||||||
|
target_bch_name TEXT,
|
||||||
|
target_block_number INTEGER,
|
||||||
|
target_block_hash BYTEA,
|
||||||
|
source_msg_sub_type INTEGER NOT NULL,
|
||||||
|
source_text TEXT NOT NULL DEFAULT '',
|
||||||
|
UNIQUE (owner_login, notification_kind, source_bch_name, source_block_number, source_block_hash)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_notifications_state_owner_kind_time
|
||||||
|
ON user_notifications_state(owner_login, notification_kind, created_at_ms DESC, source_block_number DESC);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_notifications_state_owner_time
|
||||||
|
ON user_notifications_state(owner_login, created_at_ms DESC);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_dialog_state (
|
||||||
|
owner_login TEXT NOT NULL,
|
||||||
|
peer_login TEXT NOT NULL,
|
||||||
|
relation_flag TEXT NOT NULL DEFAULT 'none',
|
||||||
|
last_message_blob_b64 TEXT NOT NULL DEFAULT '',
|
||||||
|
last_message_key TEXT NOT NULL DEFAULT '',
|
||||||
|
last_message_type INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_message_time_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
last_message_revision_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
unread_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_read_receipt_time_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (owner_login, peer_login)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_dialog_state_owner_last_time
|
||||||
|
ON dm_dialog_state(owner_login, last_message_time_ms DESC, peer_login);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS dm_sync_peer_state (
|
CREATE TABLE IF NOT EXISTS dm_sync_peer_state (
|
||||||
owner_login TEXT NOT NULL,
|
owner_login TEXT NOT NULL,
|
||||||
remote_server_login TEXT NOT NULL,
|
remote_server_login TEXT NOT NULL,
|
||||||
@@ -729,6 +840,51 @@ CREATE TABLE IF NOT EXISTS dm_sync_peer_state (
|
|||||||
CREATE INDEX IF NOT EXISTS idx_dm_sync_peer_state_owner
|
CREATE INDEX IF NOT EXISTS idx_dm_sync_peer_state_owner
|
||||||
ON dm_sync_peer_state(owner_login);
|
ON dm_sync_peer_state(owner_login);
|
||||||
|
|
||||||
|
-- Изменяемое состояние доставки исходящей пары. Подписанные блоки остаются
|
||||||
|
-- неизменяемыми; эта таблица описывает только сетевую доставку пары 1/2 или 3/4.
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_delivery_state (
|
||||||
|
outgoing_message_key TEXT PRIMARY KEY,
|
||||||
|
event_id TEXT NOT NULL UNIQUE,
|
||||||
|
base_key TEXT NOT NULL,
|
||||||
|
from_login TEXT NOT NULL,
|
||||||
|
to_login TEXT NOT NULL,
|
||||||
|
incoming_message_key TEXT NOT NULL,
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
delivery_expires_at_ms BIGINT NOT NULL,
|
||||||
|
delivery_state INTEGER NOT NULL DEFAULT 0 CHECK (delivery_state IN (0, 1, 2, 3)),
|
||||||
|
delivered_server_login TEXT,
|
||||||
|
recipient_routes_hash TEXT,
|
||||||
|
attempt_index INTEGER NOT NULL DEFAULT 0,
|
||||||
|
next_attempt_at_ms BIGINT,
|
||||||
|
last_attempt_at_ms BIGINT,
|
||||||
|
last_error TEXT,
|
||||||
|
updated_at_ms BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_delivery_state_due
|
||||||
|
ON dm_delivery_state(next_attempt_at_ms, delivery_state)
|
||||||
|
WHERE delivery_state = 0 AND next_attempt_at_ms IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_delivery_state_base
|
||||||
|
ON dm_delivery_state(base_key, from_login);
|
||||||
|
|
||||||
|
-- У одного пользователя теперь максимум один второй access-сервер, поэтому
|
||||||
|
-- достаточно одного флага ACK на событие. Курсор по времени больше не нужен.
|
||||||
|
CREATE TABLE IF NOT EXISTS dm_sync_outbox (
|
||||||
|
owner_login TEXT NOT NULL,
|
||||||
|
primary_message_key TEXT NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
secondary_message_key TEXT,
|
||||||
|
synced BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (owner_login, primary_message_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dm_sync_outbox_unsynced
|
||||||
|
ON dm_sync_outbox(owner_login, created_at_ms, primary_message_key)
|
||||||
|
WHERE synced = FALSE;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS message_views_state (
|
CREATE TABLE IF NOT EXISTS message_views_state (
|
||||||
viewer_login TEXT NOT NULL,
|
viewer_login TEXT NOT NULL,
|
||||||
to_bch_name TEXT NOT NULL,
|
to_bch_name TEXT NOT NULL,
|
||||||
@@ -797,6 +953,132 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_user_stats_state_ai()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
now_ms BIGINT;
|
||||||
|
BEGIN
|
||||||
|
now_ms := CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT);
|
||||||
|
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
now_ms
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO NOTHING;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_channel_names_state_stats_ai()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
now_ms BIGINT;
|
||||||
|
public_subscribers_count INTEGER;
|
||||||
|
BEGIN
|
||||||
|
now_ms := CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT);
|
||||||
|
|
||||||
|
IF NEW.owner_login IS NULL OR btrim(NEW.owner_login) = '' THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NEW.channel_type_code = 1 THEN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM solana_user_pda_current su
|
||||||
|
WHERE su.login = NEW.owner_login
|
||||||
|
) THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.owner_login,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
now_ms
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
owned_public_channels_count = user_stats_state.owned_public_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT COUNT(*)::INTEGER
|
||||||
|
INTO public_subscribers_count
|
||||||
|
FROM connections_state cs
|
||||||
|
JOIN channel_names_state cn
|
||||||
|
ON cn.owner_bch_name = cs.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = cs.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = cs.to_block_hash
|
||||||
|
WHERE cs.rel_type = 30
|
||||||
|
AND cn.owner_bch_name = NEW.owner_bch_name
|
||||||
|
AND cn.channel_root_block_number = NEW.channel_root_block_number
|
||||||
|
AND cn.channel_root_block_hash = NEW.channel_root_block_hash
|
||||||
|
AND cn.channel_type_code = 1;
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.owner_bch_name,
|
||||||
|
NEW.channel_root_block_number,
|
||||||
|
NEW.channel_root_block_hash,
|
||||||
|
NEW.owner_login,
|
||||||
|
NEW.channel_type_code,
|
||||||
|
COALESCE(public_subscribers_count, 0),
|
||||||
|
now_ms
|
||||||
|
)
|
||||||
|
ON CONFLICT (owner_bch_name, channel_root_block_number, channel_root_block_hash) DO UPDATE SET
|
||||||
|
owner_login = EXCLUDED.owner_login,
|
||||||
|
channel_type_code = EXCLUDED.channel_type_code,
|
||||||
|
subscribers_count = EXCLUDED.subscribers_count,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSE
|
||||||
|
WITH pending AS (
|
||||||
|
SELECT cs.login, COUNT(*)::INTEGER AS cnt
|
||||||
|
FROM connections_state cs
|
||||||
|
WHERE cs.rel_type = 30
|
||||||
|
AND cs.to_bch_name = NEW.owner_bch_name
|
||||||
|
AND cs.to_block_number = NEW.channel_root_block_number
|
||||||
|
AND cs.to_block_hash = NEW.channel_root_block_hash
|
||||||
|
GROUP BY cs.login
|
||||||
|
)
|
||||||
|
UPDATE user_stats_state us
|
||||||
|
SET following_channels_count = GREATEST(0, us.following_channels_count - pending.cnt),
|
||||||
|
updated_at_ms = now_ms
|
||||||
|
FROM pending
|
||||||
|
WHERE us.login = pending.login;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION shine_blocks_line_integrity_bi()
|
CREATE OR REPLACE FUNCTION shine_blocks_line_integrity_bi()
|
||||||
RETURNS TRIGGER
|
RETURNS TRIGGER
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
@@ -875,6 +1157,8 @@ AS $$
|
|||||||
DECLARE
|
DECLARE
|
||||||
resolved_login TEXT;
|
resolved_login TEXT;
|
||||||
positive_rel_type INTEGER;
|
positive_rel_type INTEGER;
|
||||||
|
existed_before BOOLEAN;
|
||||||
|
target_channel_type INTEGER;
|
||||||
BEGIN
|
BEGIN
|
||||||
IF NEW.msg_type <> 3 THEN
|
IF NEW.msg_type <> 3 THEN
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
@@ -887,10 +1171,159 @@ BEGIN
|
|||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM connections_state
|
||||||
|
WHERE login = NEW.login
|
||||||
|
AND rel_type = NEW.msg_sub_type
|
||||||
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'))
|
||||||
|
)
|
||||||
|
INTO existed_before;
|
||||||
|
|
||||||
|
IF NOT existed_before THEN
|
||||||
|
IF NEW.msg_sub_type = 10 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
close_friends_count = user_stats_state.close_friends_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF NEW.msg_sub_type = 30 THEN
|
||||||
|
IF NEW.to_block_number IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = user_stats_state.following_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF NEW.to_block_number = 0 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_users_count = user_stats_state.following_users_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSE
|
||||||
|
SELECT cn.channel_type_code
|
||||||
|
INTO target_channel_type
|
||||||
|
FROM channel_names_state cn
|
||||||
|
WHERE cn.owner_bch_name = NEW.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = NEW.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = NEW.to_block_hash
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF target_channel_type = 1 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = user_stats_state.following_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.to_bch_name,
|
||||||
|
NEW.to_block_number,
|
||||||
|
NEW.to_block_hash,
|
||||||
|
resolved_login,
|
||||||
|
target_channel_type,
|
||||||
|
1,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (owner_bch_name, channel_root_block_number, channel_root_block_hash) DO UPDATE SET
|
||||||
|
owner_login = EXCLUDED.owner_login,
|
||||||
|
channel_type_code = EXCLUDED.channel_type_code,
|
||||||
|
subscribers_count = channel_stats_state.subscribers_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF target_channel_type IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = user_stats_state.following_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
DELETE FROM connections_state
|
DELETE FROM connections_state
|
||||||
WHERE login = NEW.login
|
WHERE login = NEW.login
|
||||||
AND rel_type = NEW.msg_sub_type
|
AND rel_type = NEW.msg_sub_type
|
||||||
AND to_login = resolved_login;
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'));
|
||||||
|
|
||||||
INSERT 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
|
||||||
@@ -924,10 +1357,161 @@ BEGIN
|
|||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM connections_state
|
||||||
|
WHERE login = NEW.login
|
||||||
|
AND rel_type = positive_rel_type
|
||||||
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'))
|
||||||
|
)
|
||||||
|
INTO existed_before;
|
||||||
|
|
||||||
|
IF NOT existed_before THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF positive_rel_type = 10 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
close_friends_count = GREATEST(0, user_stats_state.close_friends_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF positive_rel_type = 30 THEN
|
||||||
|
IF NEW.to_block_number IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = GREATEST(0, user_stats_state.following_channels_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF NEW.to_block_number = 0 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_users_count = GREATEST(0, user_stats_state.following_users_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSE
|
||||||
|
SELECT cn.channel_type_code
|
||||||
|
INTO target_channel_type
|
||||||
|
FROM channel_names_state cn
|
||||||
|
WHERE cn.owner_bch_name = NEW.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = NEW.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = NEW.to_block_hash
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF target_channel_type = 1 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = GREATEST(0, user_stats_state.following_channels_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.to_bch_name,
|
||||||
|
NEW.to_block_number,
|
||||||
|
NEW.to_block_hash,
|
||||||
|
resolved_login,
|
||||||
|
target_channel_type,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (owner_bch_name, channel_root_block_number, channel_root_block_hash) DO UPDATE SET
|
||||||
|
owner_login = EXCLUDED.owner_login,
|
||||||
|
channel_type_code = EXCLUDED.channel_type_code,
|
||||||
|
subscribers_count = GREATEST(0, channel_stats_state.subscribers_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF target_channel_type IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = GREATEST(0, user_stats_state.following_channels_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
DELETE FROM connections_state
|
DELETE FROM connections_state
|
||||||
WHERE login = NEW.login
|
WHERE login = NEW.login
|
||||||
AND rel_type = positive_rel_type
|
AND rel_type = positive_rel_type
|
||||||
AND to_login = resolved_login;
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'));
|
||||||
|
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END;
|
END;
|
||||||
@@ -1107,4 +1691,10 @@ AFTER INSERT ON blocks
|
|||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
EXECUTE FUNCTION shine_blocks_edit_apply_ai();
|
EXECUTE FUNCTION shine_blocks_edit_apply_ai();
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_channel_names_state_stats_ai ON channel_names_state;
|
||||||
|
CREATE TRIGGER trg_channel_names_state_stats_ai
|
||||||
|
AFTER INSERT ON channel_names_state
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION shine_channel_names_state_stats_ai();
|
||||||
|
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|||||||
+26
@@ -4,6 +4,8 @@ import org.eclipse.jetty.websocket.api.Session;
|
|||||||
import shine.db.entities.CurrentUserEntry;
|
import shine.db.entities.CurrentUserEntry;
|
||||||
import shine.db.entities.ActiveSessionEntry;
|
import shine.db.entities.ActiveSessionEntry;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ConnectionContext — контекст состояния одного WebSocket-соединения.
|
* ConnectionContext — контекст состояния одного WebSocket-соединения.
|
||||||
* Живёт ровно столько же, сколько живёт подключение.
|
* Живёт ровно столько же, сколько живёт подключение.
|
||||||
@@ -77,6 +79,12 @@ public class ConnectionContext {
|
|||||||
*/
|
*/
|
||||||
private Session wsSession;
|
private Session wsSession;
|
||||||
|
|
||||||
|
/** Временная server-to-server роль, заявленная через ServerHello. */
|
||||||
|
private boolean serverConnection;
|
||||||
|
private String remoteServerLogin;
|
||||||
|
private int remoteServerProtocolVersion;
|
||||||
|
private List<String> remoteServerCapabilities = List.of();
|
||||||
|
|
||||||
// --- WebSocket Session ---
|
// --- WebSocket Session ---
|
||||||
|
|
||||||
public Session getWsSession() {
|
public Session getWsSession() {
|
||||||
@@ -87,6 +95,20 @@ public class ConnectionContext {
|
|||||||
this.wsSession = wsSession;
|
this.wsSession = wsSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isServerConnection() { return serverConnection; }
|
||||||
|
public void setServerConnection(boolean serverConnection) { this.serverConnection = serverConnection; }
|
||||||
|
|
||||||
|
public String getRemoteServerLogin() { return remoteServerLogin; }
|
||||||
|
public void setRemoteServerLogin(String remoteServerLogin) { this.remoteServerLogin = remoteServerLogin; }
|
||||||
|
|
||||||
|
public int getRemoteServerProtocolVersion() { return remoteServerProtocolVersion; }
|
||||||
|
public void setRemoteServerProtocolVersion(int value) { this.remoteServerProtocolVersion = value; }
|
||||||
|
|
||||||
|
public List<String> getRemoteServerCapabilities() { return remoteServerCapabilities; }
|
||||||
|
public void setRemoteServerCapabilities(List<String> capabilities) {
|
||||||
|
this.remoteServerCapabilities = capabilities == null ? List.of() : List.copyOf(capabilities);
|
||||||
|
}
|
||||||
|
|
||||||
// --- SolanaUser / ActiveSession ---
|
// --- SolanaUser / ActiveSession ---
|
||||||
|
|
||||||
public CurrentUserEntry getCurrentUser() {
|
public CurrentUserEntry getCurrentUser() {
|
||||||
@@ -188,6 +210,10 @@ public class ConnectionContext {
|
|||||||
|
|
||||||
authenticationStatus = AUTH_STATUS_NONE;
|
authenticationStatus = AUTH_STATUS_NONE;
|
||||||
wsSession = null;
|
wsSession = null;
|
||||||
|
serverConnection = false;
|
||||||
|
remoteServerLogin = null;
|
||||||
|
remoteServerProtocolVersion = 0;
|
||||||
|
remoteServerCapabilities = List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+36
-2
@@ -60,6 +60,12 @@ import server.logic.ws_protocol.JSON.handlers.userParams.Net_UpsertUserParam_Han
|
|||||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Request;
|
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Request;
|
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request;
|
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.Net_GetUserSetting_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.Net_ListUserSettings_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.Net_UpsertUserSetting_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.entyties.Net_GetUserSetting_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.entyties.Net_ListUserSettings_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.entyties.Net_UpsertUserSetting_Request;
|
||||||
|
|
||||||
// --- NEW: connections friends lists ---
|
// --- NEW: connections friends lists ---
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.Net_GetFriendsLists_Handler;
|
import server.logic.ws_protocol.JSON.handlers.connections.Net_GetFriendsLists_Handler;
|
||||||
@@ -82,19 +88,23 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListSubscrip
|
|||||||
import server.logic.ws_protocol.JSON.handlers.connections.Net_GetUserConnectionsGraph_Handler;
|
import server.logic.ws_protocol.JSON.handlers.connections.Net_GetUserConnectionsGraph_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.Net_AddCloseFriend_Handler;
|
import server.logic.ws_protocol.JSON.handlers.connections.Net_AddCloseFriend_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.Net_ListContacts_Handler;
|
import server.logic.ws_protocol.JSON.handlers.connections.Net_ListContacts_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.notifications.Net_GetNotifications_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetUserConnectionsGraph_Request;
|
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetUserConnectionsGraph_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_AddCloseFriend_Request;
|
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_AddCloseFriend_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_ListContacts_Request;
|
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_ListContacts_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.notifications.entyties.Net_GetNotifications_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_AckSessionDelivery_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_AckSessionDelivery_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_CallInviteBroadcast_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_CallInviteBroadcast_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_CallSignalToSession_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_CallSignalToSession_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_DeleteConversation_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_DeleteConversation_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_DeleteMessage_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_DeleteMessage_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.Net_GetDmDeliveryStatus_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.Net_MarkAllUserSettingsUnsynced_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_DmSyncBatch_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_DmSyncBatch_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_GetDirectMessages_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_GetDirectMessages_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.Net_UserSettingsSyncBatch_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_SendSignal_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_SendSignal_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_ReceiveIncomingMessage_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_ReceiveIncomingMessage_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_SendDirectMessage_Handler;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_SendMessagePair_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_SendMessagePair_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_SendTestWebPush_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_SendTestWebPush_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_UpsertPushToken_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_UpsertPushToken_Handler;
|
||||||
@@ -103,11 +113,13 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_CallInviteBroadcast_R
|
|||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_CallSignalToSession_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_CallSignalToSession_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_DeleteConversation_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_DeleteConversation_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_DeleteMessage_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_DeleteMessage_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDmDeliveryStatus_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_MarkAllUserSettingsUnsynced_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_DmSyncBatch_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_DmSyncBatch_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDirectMessages_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDirectMessages_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_UserSettingsSyncBatch_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendSignal_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendSignal_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_ReceiveIncomingMessage_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_ReceiveIncomingMessage_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendDirectMessage_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendMessagePair_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendMessagePair_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendTestWebPush_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendTestWebPush_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_UpsertPushToken_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_UpsertPushToken_Request;
|
||||||
@@ -121,6 +133,7 @@ import server.logic.ws_protocol.JSON.handlers.system.Net_ClientDebugLog_Handler;
|
|||||||
import server.logic.ws_protocol.JSON.handlers.system.Net_ListBlockchainHeads_Handler;
|
import server.logic.ws_protocol.JSON.handlers.system.Net_ListBlockchainHeads_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.system.Net_CallDeliveryReport_Handler;
|
import server.logic.ws_protocol.JSON.handlers.system.Net_CallDeliveryReport_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.system.Net_Ping_Handler;
|
import server.logic.ws_protocol.JSON.handlers.system.Net_Ping_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.system.Net_ServerHello_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_CallDeliveryReport_Request;
|
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_CallDeliveryReport_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ClientErrorLog_Request;
|
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ClientErrorLog_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ClientDebugLog_Request;
|
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ClientDebugLog_Request;
|
||||||
@@ -129,6 +142,7 @@ import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetServerInfo_
|
|||||||
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetSyncUserProfile_Request;
|
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetSyncUserProfile_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ListBlockchainHeads_Request;
|
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ListBlockchainHeads_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Request;
|
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ServerHello_Request;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -182,6 +196,11 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("GetUserParam", new Net_GetUserParam_Handler()),
|
Map.entry("GetUserParam", new Net_GetUserParam_Handler()),
|
||||||
Map.entry("ListUserParams", new Net_ListUserParams_Handler()),
|
Map.entry("ListUserParams", new Net_ListUserParams_Handler()),
|
||||||
|
|
||||||
|
// --- userSettings ---
|
||||||
|
Map.entry("UpsertUserSetting", new Net_UpsertUserSetting_Handler()),
|
||||||
|
Map.entry("GetUserSetting", new Net_GetUserSetting_Handler()),
|
||||||
|
Map.entry("ListUserSettings", new Net_ListUserSettings_Handler()),
|
||||||
|
|
||||||
// --- connections ---
|
// --- connections ---
|
||||||
Map.entry("GetFriendsLists", new Net_GetFriendsLists_Handler()),
|
Map.entry("GetFriendsLists", new Net_GetFriendsLists_Handler()),
|
||||||
Map.entry("ListSubscriptionsFeed", new Net_ListSubscriptionsFeed_Handler()),
|
Map.entry("ListSubscriptionsFeed", new Net_ListSubscriptionsFeed_Handler()),
|
||||||
@@ -194,6 +213,7 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("ListContacts", new Net_ListContacts_Handler()),
|
Map.entry("ListContacts", new Net_ListContacts_Handler()),
|
||||||
Map.entry("GetUserConnectionsGraph", new Net_GetUserConnectionsGraph_Handler()),
|
Map.entry("GetUserConnectionsGraph", new Net_GetUserConnectionsGraph_Handler()),
|
||||||
Map.entry("AddCloseFriend", new Net_AddCloseFriend_Handler()),
|
Map.entry("AddCloseFriend", new Net_AddCloseFriend_Handler()),
|
||||||
|
Map.entry("GetNotifications", new Net_GetNotifications_Handler()),
|
||||||
|
|
||||||
// --- direct messages / push ---
|
// --- direct messages / push ---
|
||||||
Map.entry("UpsertPushToken", new Net_UpsertPushToken_Handler()),
|
Map.entry("UpsertPushToken", new Net_UpsertPushToken_Handler()),
|
||||||
@@ -204,6 +224,9 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("DeleteMessage", new Net_DeleteMessage_Handler()),
|
Map.entry("DeleteMessage", new Net_DeleteMessage_Handler()),
|
||||||
Map.entry("DeleteConversation", new Net_DeleteConversation_Handler()),
|
Map.entry("DeleteConversation", new Net_DeleteConversation_Handler()),
|
||||||
Map.entry("DmSyncBatch", new Net_DmSyncBatch_Handler()),
|
Map.entry("DmSyncBatch", new Net_DmSyncBatch_Handler()),
|
||||||
|
Map.entry("GetDmDeliveryStatus", new Net_GetDmDeliveryStatus_Handler()),
|
||||||
|
Map.entry("UserSettingsSyncBatch", new Net_UserSettingsSyncBatch_Handler()),
|
||||||
|
Map.entry("MarkAllUserSettingsUnsynced", new Net_MarkAllUserSettingsUnsynced_Handler()),
|
||||||
Map.entry("GetDirectMessages", new Net_GetDirectMessages_Handler()),
|
Map.entry("GetDirectMessages", new Net_GetDirectMessages_Handler()),
|
||||||
Map.entry("AckSessionDelivery", new Net_AckSessionDelivery_Handler()),
|
Map.entry("AckSessionDelivery", new Net_AckSessionDelivery_Handler()),
|
||||||
Map.entry("CallInviteBroadcast", new Net_CallInviteBroadcast_Handler()),
|
Map.entry("CallInviteBroadcast", new Net_CallInviteBroadcast_Handler()),
|
||||||
@@ -211,6 +234,7 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("SendSignal", new Net_SendSignal_Handler()),
|
Map.entry("SendSignal", new Net_SendSignal_Handler()),
|
||||||
|
|
||||||
// --- system ---
|
// --- system ---
|
||||||
|
Map.entry("ServerHello", new Net_ServerHello_Handler()),
|
||||||
Map.entry("Ping", new Net_Ping_Handler()),
|
Map.entry("Ping", new Net_Ping_Handler()),
|
||||||
Map.entry("GetServerInfo", new Net_GetServerInfo_Handler()),
|
Map.entry("GetServerInfo", new Net_GetServerInfo_Handler()),
|
||||||
Map.entry("ListBlockchainHeads", new Net_ListBlockchainHeads_Handler()),
|
Map.entry("ListBlockchainHeads", new Net_ListBlockchainHeads_Handler()),
|
||||||
@@ -264,6 +288,11 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("GetUserParam", Net_GetUserParam_Request.class),
|
Map.entry("GetUserParam", Net_GetUserParam_Request.class),
|
||||||
Map.entry("ListUserParams", Net_ListUserParams_Request.class),
|
Map.entry("ListUserParams", Net_ListUserParams_Request.class),
|
||||||
|
|
||||||
|
// --- userSettings ---
|
||||||
|
Map.entry("UpsertUserSetting", Net_UpsertUserSetting_Request.class),
|
||||||
|
Map.entry("GetUserSetting", Net_GetUserSetting_Request.class),
|
||||||
|
Map.entry("ListUserSettings", Net_ListUserSettings_Request.class),
|
||||||
|
|
||||||
// --- connections ---
|
// --- connections ---
|
||||||
Map.entry("GetFriendsLists", Net_GetFriendsLists_Request.class),
|
Map.entry("GetFriendsLists", Net_GetFriendsLists_Request.class),
|
||||||
Map.entry("ListSubscriptionsFeed", Net_ListSubscriptionsFeed_Request.class),
|
Map.entry("ListSubscriptionsFeed", Net_ListSubscriptionsFeed_Request.class),
|
||||||
@@ -276,6 +305,7 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("ListContacts", Net_ListContacts_Request.class),
|
Map.entry("ListContacts", Net_ListContacts_Request.class),
|
||||||
Map.entry("GetUserConnectionsGraph", Net_GetUserConnectionsGraph_Request.class),
|
Map.entry("GetUserConnectionsGraph", Net_GetUserConnectionsGraph_Request.class),
|
||||||
Map.entry("AddCloseFriend", Net_AddCloseFriend_Request.class),
|
Map.entry("AddCloseFriend", Net_AddCloseFriend_Request.class),
|
||||||
|
Map.entry("GetNotifications", Net_GetNotifications_Request.class),
|
||||||
|
|
||||||
// --- direct messages / push ---
|
// --- direct messages / push ---
|
||||||
Map.entry("UpsertPushToken", Net_UpsertPushToken_Request.class),
|
Map.entry("UpsertPushToken", Net_UpsertPushToken_Request.class),
|
||||||
@@ -286,6 +316,9 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("DeleteMessage", Net_DeleteMessage_Request.class),
|
Map.entry("DeleteMessage", Net_DeleteMessage_Request.class),
|
||||||
Map.entry("DeleteConversation", Net_DeleteConversation_Request.class),
|
Map.entry("DeleteConversation", Net_DeleteConversation_Request.class),
|
||||||
Map.entry("DmSyncBatch", Net_DmSyncBatch_Request.class),
|
Map.entry("DmSyncBatch", Net_DmSyncBatch_Request.class),
|
||||||
|
Map.entry("GetDmDeliveryStatus", Net_GetDmDeliveryStatus_Request.class),
|
||||||
|
Map.entry("UserSettingsSyncBatch", Net_UserSettingsSyncBatch_Request.class),
|
||||||
|
Map.entry("MarkAllUserSettingsUnsynced", Net_MarkAllUserSettingsUnsynced_Request.class),
|
||||||
Map.entry("GetDirectMessages", Net_GetDirectMessages_Request.class),
|
Map.entry("GetDirectMessages", Net_GetDirectMessages_Request.class),
|
||||||
Map.entry("AckSessionDelivery", Net_AckSessionDelivery_Request.class),
|
Map.entry("AckSessionDelivery", Net_AckSessionDelivery_Request.class),
|
||||||
Map.entry("CallInviteBroadcast", Net_CallInviteBroadcast_Request.class),
|
Map.entry("CallInviteBroadcast", Net_CallInviteBroadcast_Request.class),
|
||||||
@@ -293,6 +326,7 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("SendSignal", Net_SendSignal_Request.class),
|
Map.entry("SendSignal", Net_SendSignal_Request.class),
|
||||||
|
|
||||||
// --- system ---
|
// --- system ---
|
||||||
|
Map.entry("ServerHello", Net_ServerHello_Request.class),
|
||||||
Map.entry("Ping", Net_Ping_Request.class),
|
Map.entry("Ping", Net_Ping_Request.class),
|
||||||
Map.entry("GetServerInfo", Net_GetServerInfo_Request.class),
|
Map.entry("GetServerInfo", Net_GetServerInfo_Request.class),
|
||||||
Map.entry("ListBlockchainHeads", Net_ListBlockchainHeads_Request.class),
|
Map.entry("ListBlockchainHeads", Net_ListBlockchainHeads_Request.class),
|
||||||
|
|||||||
+69
-2
@@ -5,8 +5,10 @@ import blockchain.BchCryptoVerifier;
|
|||||||
import blockchain.MsgSubType;
|
import blockchain.MsgSubType;
|
||||||
import blockchain.body.BodyHasLine;
|
import blockchain.body.BodyHasLine;
|
||||||
import blockchain.body.BodyHasTarget;
|
import blockchain.body.BodyHasTarget;
|
||||||
|
import blockchain.body.ConnectionBody;
|
||||||
import blockchain.body.CreateChannelBody;
|
import blockchain.body.CreateChannelBody;
|
||||||
import blockchain.body.StatusActionBody;
|
import blockchain.body.StatusActionBody;
|
||||||
|
import blockchain.body.TextReplyBody;
|
||||||
import blockchain.body.TextLineBody;
|
import blockchain.body.TextLineBody;
|
||||||
import blockchain.body.UserParamBody;
|
import blockchain.body.UserParamBody;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -30,10 +32,12 @@ import shine.db.channels.ChannelNameRules;
|
|||||||
import shine.db.dao.BlockchainStateDAO;
|
import shine.db.dao.BlockchainStateDAO;
|
||||||
import shine.db.dao.BlocksDAO;
|
import shine.db.dao.BlocksDAO;
|
||||||
import shine.db.dao.ChannelNameStateDAO;
|
import shine.db.dao.ChannelNameStateDAO;
|
||||||
|
import shine.db.dao.UserNotificationsStateDAO;
|
||||||
import shine.db.dao.UserParamsDAO;
|
import shine.db.dao.UserParamsDAO;
|
||||||
import shine.db.entities.BlockchainStateEntry;
|
import shine.db.entities.BlockchainStateEntry;
|
||||||
import shine.db.entities.BlockEntry;
|
import shine.db.entities.BlockEntry;
|
||||||
import shine.db.entities.ChannelNameStateEntry;
|
import shine.db.entities.ChannelNameStateEntry;
|
||||||
|
import shine.db.entities.UserNotificationEntry;
|
||||||
import shine.db.entities.UserParamEntry;
|
import shine.db.entities.UserParamEntry;
|
||||||
import utils.blockchain.BlockchainNameUtil;
|
import utils.blockchain.BlockchainNameUtil;
|
||||||
|
|
||||||
@@ -61,7 +65,8 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
private final ChannelNameStateDAO channelNameStateDAO = ChannelNameStateDAO.getInstance();
|
private final ChannelNameStateDAO channelNameStateDAO = ChannelNameStateDAO.getInstance();
|
||||||
private final AddBlockSyncService addBlockSyncService = new AddBlockSyncService();
|
private final AddBlockSyncService addBlockSyncService = new AddBlockSyncService();
|
||||||
|
|
||||||
private final BlockchainWriter dbWriter = new BlockchainWriter(blocksDAO, stateDAO, userParamsDAO, channelNameStateDAO);
|
private final BlockchainWriter dbWriter = new BlockchainWriter(
|
||||||
|
blocksDAO, stateDAO, userParamsDAO, channelNameStateDAO, UserNotificationsStateDAO.getInstance());
|
||||||
|
|
||||||
public Net_AddBlock_Handler() {
|
public Net_AddBlock_Handler() {
|
||||||
ChannelNamesStateBootstrapper.bootstrapOrFailFast();
|
ChannelNamesStateBootstrapper.bootstrapOrFailFast();
|
||||||
@@ -573,7 +578,9 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
dbWriter.appendBlockAndState(blockchainName, block, st, be, upsertedParam, channelNameStateEntry, channelMetaUpdateEntry);
|
UserNotificationEntry notificationEntry = buildNotificationEntry(block, be);
|
||||||
|
dbWriter.appendBlockAndState(
|
||||||
|
blockchainName, block, st, be, upsertedParam, channelNameStateEntry, channelMetaUpdateEntry, notificationEntry);
|
||||||
|
|
||||||
if (chat200CreateSeed != null) {
|
if (chat200CreateSeed != null) {
|
||||||
upsertChat200StateFromCreate(chat200CreateSeed);
|
upsertChat200StateFromCreate(chat200CreateSeed);
|
||||||
@@ -855,6 +862,66 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private UserNotificationEntry buildNotificationEntry(BchBlockEntry block, BlockEntry storedEntry) {
|
||||||
|
if (block == null || storedEntry == null) return null;
|
||||||
|
if (storedEntry.getToLogin() == null || storedEntry.getToLogin().isBlank()) return null;
|
||||||
|
|
||||||
|
int msgType = block.type & 0xFFFF;
|
||||||
|
int msgSubType = block.subType & 0xFFFF;
|
||||||
|
long createdAtMs = block.timestamp * 1000L;
|
||||||
|
String ownerLogin = storedEntry.getToLogin();
|
||||||
|
String sourceLogin = storedEntry.getLogin();
|
||||||
|
|
||||||
|
// A user must never receive a notification for replying to their own message/thread.
|
||||||
|
if (ownerLogin.equalsIgnoreCase(sourceLogin)) return null;
|
||||||
|
|
||||||
|
if (msgType == 1
|
||||||
|
&& msgSubType == (MsgSubType.TEXT_REPLY & 0xFFFF)
|
||||||
|
&& block.body instanceof TextReplyBody replyBody) {
|
||||||
|
UserNotificationEntry entry = baseNotificationEntry(storedEntry, ownerLogin, createdAtMs, msgSubType);
|
||||||
|
entry.setNotificationKind("reply");
|
||||||
|
entry.setSourceText(replyBody.message == null ? "" : replyBody.message);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection notifications are intentionally modeled as a generic kind.
|
||||||
|
// Today only CONNECTION_CLOSE_FRIEND is indexed; future incoming connection types
|
||||||
|
// can reuse the same notification kind and expose their code via sourceMsgSubType.
|
||||||
|
if (msgType == 3
|
||||||
|
&& msgSubType == (MsgSubType.CONNECTION_CLOSE_FRIEND & 0xFFFF)
|
||||||
|
&& block.body instanceof ConnectionBody) {
|
||||||
|
UserNotificationEntry entry = baseNotificationEntry(storedEntry, ownerLogin, createdAtMs, msgSubType);
|
||||||
|
entry.setNotificationKind("connection");
|
||||||
|
entry.setSourceText("");
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserNotificationEntry baseNotificationEntry(BlockEntry storedEntry,
|
||||||
|
String ownerLogin,
|
||||||
|
long createdAtMs,
|
||||||
|
int msgSubType) {
|
||||||
|
UserNotificationEntry entry = new UserNotificationEntry();
|
||||||
|
entry.setOwnerLogin(ownerLogin);
|
||||||
|
entry.setCreatedAtMs(createdAtMs);
|
||||||
|
entry.setSourceLogin(storedEntry.getLogin());
|
||||||
|
entry.setSourceBchName(blockchainNameFromEntry(storedEntry));
|
||||||
|
entry.setSourceBlockNumber(storedEntry.getBlockNumber());
|
||||||
|
entry.setSourceBlockHash(storedEntry.getBlockHash());
|
||||||
|
entry.setTargetLogin(storedEntry.getToLogin());
|
||||||
|
entry.setTargetBchName(storedEntry.getToBchName());
|
||||||
|
entry.setTargetBlockNumber(storedEntry.getToBlockNumber());
|
||||||
|
entry.setTargetBlockHash(storedEntry.getToBlockHash());
|
||||||
|
entry.setSourceMsgSubType(msgSubType);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String blockchainNameFromEntry(BlockEntry entry) {
|
||||||
|
return entry == null ? null : entry.getBchName();
|
||||||
|
}
|
||||||
|
|
||||||
private static String toHex(byte[] bytes) {
|
private static String toHex(byte[] bytes) {
|
||||||
if (bytes == null) return "null";
|
if (bytes == null) return "null";
|
||||||
char[] HEX = "0123456789abcdef".toCharArray();
|
char[] HEX = "0123456789abcdef".toCharArray();
|
||||||
|
|||||||
+14
-2
@@ -5,10 +5,12 @@ import shine.db.dao.BlockchainStateDAO;
|
|||||||
import shine.db.dao.BlocksDAO;
|
import shine.db.dao.BlocksDAO;
|
||||||
import shine.db.dao.ChannelNameStateDAO;
|
import shine.db.dao.ChannelNameStateDAO;
|
||||||
import shine.db.dao.UserParamsDAO;
|
import shine.db.dao.UserParamsDAO;
|
||||||
|
import shine.db.dao.UserNotificationsStateDAO;
|
||||||
import shine.db.entities.BlockchainStateEntry;
|
import shine.db.entities.BlockchainStateEntry;
|
||||||
import shine.db.entities.BlockEntry;
|
import shine.db.entities.BlockEntry;
|
||||||
import shine.db.entities.ChannelNameStateEntry;
|
import shine.db.entities.ChannelNameStateEntry;
|
||||||
import shine.db.entities.UserParamEntry;
|
import shine.db.entities.UserParamEntry;
|
||||||
|
import shine.db.entities.UserNotificationEntry;
|
||||||
import utils.files.FileStoreUtil;
|
import utils.files.FileStoreUtil;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -41,16 +43,19 @@ public final class BlockchainWriter {
|
|||||||
private final BlockchainStateDAO stateDAO;
|
private final BlockchainStateDAO stateDAO;
|
||||||
private final ChannelNameStateDAO channelNameStateDAO;
|
private final ChannelNameStateDAO channelNameStateDAO;
|
||||||
private final UserParamsDAO userParamsDAO;
|
private final UserParamsDAO userParamsDAO;
|
||||||
|
private final UserNotificationsStateDAO userNotificationsStateDAO;
|
||||||
private final FileStoreUtil fs = FileStoreUtil.getInstance();
|
private final FileStoreUtil fs = FileStoreUtil.getInstance();
|
||||||
|
|
||||||
public BlockchainWriter(BlocksDAO blocksDAO,
|
public BlockchainWriter(BlocksDAO blocksDAO,
|
||||||
BlockchainStateDAO stateDAO,
|
BlockchainStateDAO stateDAO,
|
||||||
UserParamsDAO userParamsDAO,
|
UserParamsDAO userParamsDAO,
|
||||||
ChannelNameStateDAO channelNameStateDAO) {
|
ChannelNameStateDAO channelNameStateDAO,
|
||||||
|
UserNotificationsStateDAO userNotificationsStateDAO) {
|
||||||
this.blocksDAO = blocksDAO;
|
this.blocksDAO = blocksDAO;
|
||||||
this.stateDAO = stateDAO;
|
this.stateDAO = stateDAO;
|
||||||
this.userParamsDAO = userParamsDAO;
|
this.userParamsDAO = userParamsDAO;
|
||||||
this.channelNameStateDAO = channelNameStateDAO;
|
this.channelNameStateDAO = channelNameStateDAO;
|
||||||
|
this.userNotificationsStateDAO = userNotificationsStateDAO;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void appendBlockAndState(String blockchainName,
|
public void appendBlockAndState(String blockchainName,
|
||||||
@@ -59,7 +64,8 @@ public final class BlockchainWriter {
|
|||||||
BlockEntry be,
|
BlockEntry be,
|
||||||
UserParamEntry userParamEntry,
|
UserParamEntry userParamEntry,
|
||||||
ChannelNameStateEntry channelNameStateEntry,
|
ChannelNameStateEntry channelNameStateEntry,
|
||||||
ChannelNameStateEntry channelMetaUpdateEntry) throws SQLException {
|
ChannelNameStateEntry channelMetaUpdateEntry,
|
||||||
|
UserNotificationEntry notificationEntry) throws SQLException {
|
||||||
|
|
||||||
long nowMs = System.currentTimeMillis();
|
long nowMs = System.currentTimeMillis();
|
||||||
byte[] blockBytes = block.toBytes();
|
byte[] blockBytes = block.toBytes();
|
||||||
@@ -96,6 +102,12 @@ public final class BlockchainWriter {
|
|||||||
channelNameStateDAO.updateMeta(c, channelMetaUpdateEntry);
|
channelNameStateDAO.updateMeta(c, channelMetaUpdateEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notification projection is part of the same SQL transaction as the block.
|
||||||
|
// If notification indexing fails, the block/state write is rolled back as well.
|
||||||
|
if (notificationEntry != null) {
|
||||||
|
userNotificationsStateDAO.upsert(c, notificationEntry);
|
||||||
|
}
|
||||||
|
|
||||||
c.commit();
|
c.commit();
|
||||||
committed = true;
|
committed = true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
+34
@@ -144,6 +144,40 @@ final class ChannelsReadSupport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String userSettingsChannelKey(String ownerBch, String channelName) {
|
||||||
|
String bch = ownerBch == null ? "" : ownerBch.trim();
|
||||||
|
String name = channelName == null ? "" : channelName.trim();
|
||||||
|
return bch + "/" + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int countUnreadMessages(Connection c, String viewerLogin, String ownerBch, String channelName, int messagesCount) throws SQLException {
|
||||||
|
if (viewerLogin == null || viewerLogin.isBlank()) return 0;
|
||||||
|
String key = userSettingsChannelKey(ownerBch, channelName);
|
||||||
|
String sql = """
|
||||||
|
SELECT value_num
|
||||||
|
FROM user_settings
|
||||||
|
WHERE LOWER(login) = LOWER(?)
|
||||||
|
AND setting_type = ?
|
||||||
|
AND setting_key = ?
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
long lastSeen = messagesCount;
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, viewerLogin);
|
||||||
|
ps.setInt(2, 1);
|
||||||
|
ps.setString(3, key);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
long value = rs.getLong("value_num");
|
||||||
|
if (!rs.wasNull()) lastSeen = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastSeen < 0) lastSeen = 0;
|
||||||
|
if (lastSeen > messagesCount) return 0;
|
||||||
|
return Math.max(0, messagesCount - (int) lastSeen);
|
||||||
|
}
|
||||||
|
|
||||||
static PostBlock loadLastPost(Connection c, String ownerBch, int lineCode) throws SQLException {
|
static PostBlock loadLastPost(Connection c, String ownerBch, int lineCode) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT login,bch_name,block_number,block_hash,block_bytes,this_line_number
|
SELECT login,bch_name,block_number,block_hash,block_bytes,this_line_number
|
||||||
|
|||||||
+27
@@ -16,6 +16,8 @@ import utils.blockchain.BlockchainNameUtil;
|
|||||||
import blockchain.body.CreateChannelBody;
|
import blockchain.body.CreateChannelBody;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -65,6 +67,7 @@ public class Net_GetChannelMessages_Handler implements JsonMessageHandler {
|
|||||||
channel.setMetaUpdatedAtMs(meta.metaUpdatedAtMs);
|
channel.setMetaUpdatedAtMs(meta.metaUpdatedAtMs);
|
||||||
channel.setChannelTypeCode(meta.channelTypeCode);
|
channel.setChannelTypeCode(meta.channelTypeCode);
|
||||||
channel.setChannelTypeVersion(meta.channelTypeVersion);
|
channel.setChannelTypeVersion(meta.channelTypeVersion);
|
||||||
|
channel.setSubscribersCount(loadSubscribersCount(c, ownerBch, lineCode, meta.channelTypeCode));
|
||||||
Net_GetChannelMessages_Response.BlockRef rootRef = new Net_GetChannelMessages_Response.BlockRef();
|
Net_GetChannelMessages_Response.BlockRef rootRef = new Net_GetChannelMessages_Response.BlockRef();
|
||||||
rootRef.setBlockNumber(lineCode);
|
rootRef.setBlockNumber(lineCode);
|
||||||
rootRef.setBlockHash(req.getChannel().getChannelRootBlockHash());
|
rootRef.setBlockHash(req.getChannel().getChannelRootBlockHash());
|
||||||
@@ -180,4 +183,28 @@ public class Net_GetChannelMessages_Handler implements JsonMessageHandler {
|
|||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "internal_error", "Внутренняя ошибка сервера");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "internal_error", "Внутренняя ошибка сервера");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int loadSubscribersCount(Connection c, String ownerBch, int rootNumber, int channelTypeCode) {
|
||||||
|
if (channelTypeCode != (CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
String sql = """
|
||||||
|
SELECT subscribers_count
|
||||||
|
FROM channel_stats_state
|
||||||
|
WHERE owner_bch_name = ?
|
||||||
|
AND channel_root_block_number = ?
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, ownerBch);
|
||||||
|
ps.setInt(2, rootNumber);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (!rs.next()) return 0;
|
||||||
|
return Math.max(0, rs.getInt("subscribers_count"));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("GetChannelMessages: не удалось загрузить subscribers_count для {}#{}", ownerBch, rootNumber, e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -82,7 +82,7 @@ public class Net_ListSubscriptionsFeed_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
row.setChannel(channelRef);
|
row.setChannel(channelRef);
|
||||||
row.setMessagesCount(ChannelsReadSupport.countPosts(c, key.ownerBch, key.rootNumber));
|
row.setMessagesCount(ChannelsReadSupport.countPosts(c, key.ownerBch, key.rootNumber));
|
||||||
row.setUnreadCount(0);
|
row.setUnreadCount(ChannelsReadSupport.countUnreadMessages(c, viewerLogin, key.ownerBch, meta.channelName, row.getMessagesCount()));
|
||||||
|
|
||||||
ChannelsReadSupport.PostBlock lastPost = ChannelsReadSupport.loadLastPost(c, key.ownerBch, key.rootNumber);
|
ChannelsReadSupport.PostBlock lastPost = ChannelsReadSupport.loadLastPost(c, key.ownerBch, key.rootNumber);
|
||||||
if (lastPost != null) {
|
if (lastPost != null) {
|
||||||
|
|||||||
+4
@@ -32,6 +32,7 @@ public class Net_GetChannelMessages_Response extends Net_Response {
|
|||||||
private Long metaUpdatedAtMs;
|
private Long metaUpdatedAtMs;
|
||||||
private Integer channelTypeCode;
|
private Integer channelTypeCode;
|
||||||
private Integer channelTypeVersion;
|
private Integer channelTypeVersion;
|
||||||
|
private Integer subscribersCount;
|
||||||
private BlockRef channelRoot;
|
private BlockRef channelRoot;
|
||||||
|
|
||||||
public String getOwnerLogin() { return ownerLogin; }
|
public String getOwnerLogin() { return ownerLogin; }
|
||||||
@@ -67,6 +68,9 @@ public class Net_GetChannelMessages_Response extends Net_Response {
|
|||||||
public Integer getChannelTypeVersion() { return channelTypeVersion; }
|
public Integer getChannelTypeVersion() { return channelTypeVersion; }
|
||||||
public void setChannelTypeVersion(Integer channelTypeVersion) { this.channelTypeVersion = channelTypeVersion; }
|
public void setChannelTypeVersion(Integer channelTypeVersion) { this.channelTypeVersion = channelTypeVersion; }
|
||||||
|
|
||||||
|
public Integer getSubscribersCount() { return subscribersCount; }
|
||||||
|
public void setSubscribersCount(Integer subscribersCount) { this.subscribersCount = subscribersCount; }
|
||||||
|
|
||||||
public BlockRef getChannelRoot() { return channelRoot; }
|
public BlockRef getChannelRoot() { return channelRoot; }
|
||||||
public void setChannelRoot(BlockRef channelRoot) { this.channelRoot = channelRoot; }
|
public void setChannelRoot(BlockRef channelRoot) { this.channelRoot = channelRoot; }
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-4
@@ -8,10 +8,10 @@ import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_ListConta
|
|||||||
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_ListContacts_Response;
|
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_ListContacts_Response;
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import shine.db.MsgSubType;
|
import shine.db.dao.DmDialogStateDAO;
|
||||||
import shine.db.dao.ConnectionsStateDAO;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class Net_ListContacts_Handler implements JsonMessageHandler {
|
public class Net_ListContacts_Handler implements JsonMessageHandler {
|
||||||
@@ -23,14 +23,30 @@ public class Net_ListContacts_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try (Connection c = shine.db.DbController.getInstance().getConnection()) {
|
try (Connection c = shine.db.DbController.getInstance().getConnection()) {
|
||||||
List<String> contacts = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, ctx.getLogin(), MsgSubType.CONNECTION_CONTACT);
|
List<DmDialogStateDAO.DialogSummary> dialogs = DmDialogStateDAO.getInstance().listInboxDialogs(c, ctx.getLogin());
|
||||||
Net_ListContacts_Response resp = new Net_ListContacts_Response();
|
Net_ListContacts_Response resp = new Net_ListContacts_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
resp.setRequestId(req.getRequestId());
|
resp.setRequestId(req.getRequestId());
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
resp.setLogin(ctx.getLogin());
|
resp.setLogin(ctx.getLogin());
|
||||||
resp.setContacts(contacts);
|
resp.setDialogs(toDialogItems(dialogs));
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<Net_ListContacts_Response.DialogItem> toDialogItems(List<DmDialogStateDAO.DialogSummary> dialogs) {
|
||||||
|
List<Net_ListContacts_Response.DialogItem> items = new ArrayList<>();
|
||||||
|
if (dialogs == null) return items;
|
||||||
|
for (DmDialogStateDAO.DialogSummary dialog : dialogs) {
|
||||||
|
Net_ListContacts_Response.DialogItem item = new Net_ListContacts_Response.DialogItem();
|
||||||
|
item.setPeerLogin(dialog.peerLogin());
|
||||||
|
item.setRelationFlag(dialog.relationFlag());
|
||||||
|
item.setLastMessageBlobB64(dialog.lastMessageBlobB64());
|
||||||
|
item.setLastMessageTimeMs(dialog.lastMessageTimeMs());
|
||||||
|
item.setUnreadCount(dialog.unreadCount());
|
||||||
|
item.setHasDialog(dialog.hasDialog());
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-3
@@ -7,10 +7,32 @@ import java.util.List;
|
|||||||
|
|
||||||
public class Net_ListContacts_Response extends Net_Response {
|
public class Net_ListContacts_Response extends Net_Response {
|
||||||
private String login;
|
private String login;
|
||||||
private List<String> contacts = new ArrayList<>();
|
private List<DialogItem> dialogs = new ArrayList<>();
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
public String getLogin() { return login; }
|
||||||
public void setLogin(String login) { this.login = login; }
|
public void setLogin(String login) { this.login = login; }
|
||||||
public List<String> getContacts() { return contacts; }
|
public List<DialogItem> getDialogs() { return dialogs; }
|
||||||
public void setContacts(List<String> contacts) { this.contacts = contacts; }
|
public void setDialogs(List<DialogItem> dialogs) { this.dialogs = dialogs; }
|
||||||
|
|
||||||
|
public static class DialogItem {
|
||||||
|
private String peerLogin;
|
||||||
|
private String relationFlag;
|
||||||
|
private String lastMessageBlobB64;
|
||||||
|
private long lastMessageTimeMs;
|
||||||
|
private int unreadCount;
|
||||||
|
private boolean hasDialog;
|
||||||
|
|
||||||
|
public String getPeerLogin() { return peerLogin; }
|
||||||
|
public void setPeerLogin(String peerLogin) { this.peerLogin = peerLogin; }
|
||||||
|
public String getRelationFlag() { return relationFlag; }
|
||||||
|
public void setRelationFlag(String relationFlag) { this.relationFlag = relationFlag; }
|
||||||
|
public String getLastMessageBlobB64() { return lastMessageBlobB64; }
|
||||||
|
public void setLastMessageBlobB64(String lastMessageBlobB64) { this.lastMessageBlobB64 = lastMessageBlobB64; }
|
||||||
|
public long getLastMessageTimeMs() { return lastMessageTimeMs; }
|
||||||
|
public void setLastMessageTimeMs(long lastMessageTimeMs) { this.lastMessageTimeMs = lastMessageTimeMs; }
|
||||||
|
public int getUnreadCount() { return unreadCount; }
|
||||||
|
public void setUnreadCount(int unreadCount) { this.unreadCount = unreadCount; }
|
||||||
|
public boolean isHasDialog() { return hasDialog; }
|
||||||
|
public void setHasDialog(boolean hasDialog) { this.hasDialog = hasDialog; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+94
@@ -0,0 +1,94 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.notifications;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.notifications.entyties.Net_GetNotifications_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.notifications.entyties.Net_GetNotifications_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.dao.UserNotificationsStateDAO;
|
||||||
|
import shine.db.entities.UserNotificationEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class Net_GetNotifications_Handler implements JsonMessageHandler {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Net_GetNotifications_Handler.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||||
|
Net_GetNotifications_Request req = (Net_GetNotifications_Request) baseRequest;
|
||||||
|
if (ctx == null || !ctx.isAuthenticatedUser() || ctx.getCurrentUser() == null) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
WireCodes.Status.UNVERIFIED,
|
||||||
|
"NOT_AUTHENTICATED",
|
||||||
|
"Операция доступна только для авторизованных пользователей"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String login = String.valueOf(ctx.getCurrentUser().getLogin() == null ? "" : ctx.getCurrentUser().getLogin()).trim();
|
||||||
|
if (login.isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "NOT_AUTHENTICATED", "Не удалось определить авторизованного пользователя");
|
||||||
|
}
|
||||||
|
|
||||||
|
int limit = req.getLimit() == null ? 50 : Math.max(1, Math.min(200, req.getLimit()));
|
||||||
|
|
||||||
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
|
List<UserNotificationEntry> replyRows = UserNotificationsStateDAO.getInstance().listByOwnerAndKind(c, login, "reply", limit);
|
||||||
|
List<UserNotificationEntry> eventRows = UserNotificationsStateDAO.getInstance().listByOwnerAndKind(c, login, "connection", limit);
|
||||||
|
|
||||||
|
Net_GetNotifications_Response resp = new Net_GetNotifications_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setLogin(login);
|
||||||
|
resp.setReplies(mapRows(replyRows));
|
||||||
|
resp.setEvents(mapRows(eventRows));
|
||||||
|
return resp;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("GetNotifications failed", e);
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "internal_error", "Внутренняя ошибка сервера");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Net_GetNotifications_Response.NotificationItem> mapRows(List<UserNotificationEntry> rows) {
|
||||||
|
List<Net_GetNotifications_Response.NotificationItem> out = new ArrayList<>();
|
||||||
|
for (UserNotificationEntry row : rows) {
|
||||||
|
Net_GetNotifications_Response.NotificationItem item = new Net_GetNotifications_Response.NotificationItem();
|
||||||
|
item.setKind(row.getNotificationKind());
|
||||||
|
item.setCreatedAtMs(row.getCreatedAtMs());
|
||||||
|
item.setSourceLogin(row.getSourceLogin());
|
||||||
|
item.setSourceBlockchainName(row.getSourceBchName());
|
||||||
|
item.setSourceBlockNumber(row.getSourceBlockNumber());
|
||||||
|
item.setSourceBlockHash(bytesToHex(row.getSourceBlockHash()));
|
||||||
|
item.setSourceMsgSubType(row.getSourceMsgSubType());
|
||||||
|
item.setConnectionTypeCode("connection".equals(row.getNotificationKind()) ? row.getSourceMsgSubType() : null);
|
||||||
|
item.setSourceText(row.getSourceText());
|
||||||
|
item.setTargetLogin(row.getTargetLogin());
|
||||||
|
item.setTargetBlockchainName(row.getTargetBchName());
|
||||||
|
item.setTargetBlockNumber(row.getTargetBlockNumber());
|
||||||
|
item.setTargetBlockHash(bytesToHex(row.getTargetBlockHash()));
|
||||||
|
out.add(item);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String bytesToHex(byte[] bytes) {
|
||||||
|
if (bytes == null) return null;
|
||||||
|
char[] HEX = "0123456789abcdef".toCharArray();
|
||||||
|
char[] out = new char[bytes.length * 2];
|
||||||
|
for (int i = 0; i < bytes.length; i++) {
|
||||||
|
int v = bytes[i] & 0xff;
|
||||||
|
out[i * 2] = HEX[v >>> 4];
|
||||||
|
out[i * 2 + 1] = HEX[v & 0x0f];
|
||||||
|
}
|
||||||
|
return new String(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.notifications.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
public class Net_GetNotifications_Request extends Net_Request {
|
||||||
|
private Integer limit;
|
||||||
|
|
||||||
|
public Integer getLimit() { return limit; }
|
||||||
|
public void setLimit(Integer limit) { this.limit = limit; }
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.notifications.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Net_GetNotifications_Response extends Net_Response {
|
||||||
|
private String login;
|
||||||
|
private List<NotificationItem> replies = new ArrayList<>();
|
||||||
|
private List<NotificationItem> events = new ArrayList<>();
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
public List<NotificationItem> getReplies() { return replies; }
|
||||||
|
public void setReplies(List<NotificationItem> replies) { this.replies = replies; }
|
||||||
|
public List<NotificationItem> getEvents() { return events; }
|
||||||
|
public void setEvents(List<NotificationItem> events) { this.events = events; }
|
||||||
|
|
||||||
|
public static class NotificationItem {
|
||||||
|
private String kind;
|
||||||
|
private long createdAtMs;
|
||||||
|
private String sourceLogin;
|
||||||
|
private String sourceBlockchainName;
|
||||||
|
private int sourceBlockNumber;
|
||||||
|
private String sourceBlockHash;
|
||||||
|
private Integer sourceMsgSubType;
|
||||||
|
private Integer connectionTypeCode;
|
||||||
|
private String sourceText;
|
||||||
|
private String targetLogin;
|
||||||
|
private String targetBlockchainName;
|
||||||
|
private Integer targetBlockNumber;
|
||||||
|
private String targetBlockHash;
|
||||||
|
|
||||||
|
public String getKind() { return kind; }
|
||||||
|
public void setKind(String kind) { this.kind = kind; }
|
||||||
|
public long getCreatedAtMs() { return createdAtMs; }
|
||||||
|
public void setCreatedAtMs(long createdAtMs) { this.createdAtMs = createdAtMs; }
|
||||||
|
public String getSourceLogin() { return sourceLogin; }
|
||||||
|
public void setSourceLogin(String sourceLogin) { this.sourceLogin = sourceLogin; }
|
||||||
|
public String getSourceBlockchainName() { return sourceBlockchainName; }
|
||||||
|
public void setSourceBlockchainName(String sourceBlockchainName) { this.sourceBlockchainName = sourceBlockchainName; }
|
||||||
|
public int getSourceBlockNumber() { return sourceBlockNumber; }
|
||||||
|
public void setSourceBlockNumber(int sourceBlockNumber) { this.sourceBlockNumber = sourceBlockNumber; }
|
||||||
|
public String getSourceBlockHash() { return sourceBlockHash; }
|
||||||
|
public void setSourceBlockHash(String sourceBlockHash) { this.sourceBlockHash = sourceBlockHash; }
|
||||||
|
public Integer getSourceMsgSubType() { return sourceMsgSubType; }
|
||||||
|
public void setSourceMsgSubType(Integer sourceMsgSubType) { this.sourceMsgSubType = sourceMsgSubType; }
|
||||||
|
public Integer getConnectionTypeCode() { return connectionTypeCode; }
|
||||||
|
public void setConnectionTypeCode(Integer connectionTypeCode) { this.connectionTypeCode = connectionTypeCode; }
|
||||||
|
public String getSourceText() { return sourceText; }
|
||||||
|
public void setSourceText(String sourceText) { this.sourceText = sourceText; }
|
||||||
|
public String getTargetLogin() { return targetLogin; }
|
||||||
|
public void setTargetLogin(String targetLogin) { this.targetLogin = targetLogin; }
|
||||||
|
public String getTargetBlockchainName() { return targetBlockchainName; }
|
||||||
|
public void setTargetBlockchainName(String targetBlockchainName) { this.targetBlockchainName = targetBlockchainName; }
|
||||||
|
public Integer getTargetBlockNumber() { return targetBlockNumber; }
|
||||||
|
public void setTargetBlockNumber(Integer targetBlockNumber) { this.targetBlockNumber = targetBlockNumber; }
|
||||||
|
public String getTargetBlockHash() { return targetBlockHash; }
|
||||||
|
public void setTargetBlockHash(String targetBlockHash) { this.targetBlockHash = targetBlockHash; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.system;
|
||||||
|
|
||||||
|
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.system.entyties.Net_ServerHello_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ServerHello_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import utils.config.AppConfig;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Временное server-to-server представление без криптографической проверки.
|
||||||
|
* Заявленный serverLogin принимается на доверии и привязывается к WS-контексту.
|
||||||
|
*/
|
||||||
|
public final class Net_ServerHello_Handler implements JsonMessageHandler {
|
||||||
|
private static final int PROTOCOL_VERSION = 1;
|
||||||
|
private static final List<String> CAPABILITIES = List.of(
|
||||||
|
"dm-sync", "settings-sync", "block-sync", "connection-pool");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||||
|
Net_ServerHello_Request req = (Net_ServerHello_Request) baseRequest;
|
||||||
|
String remoteLogin = normalize(req.getServerLogin());
|
||||||
|
int remoteVersion = req.getProtocolVersion() == null ? 0 : req.getProtocolVersion();
|
||||||
|
if (ctx == null) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req, WireCodes.Status.BAD_REQUEST, "NO_CONNECTION_CONTEXT", "ServerHello требует WebSocket-контекст");
|
||||||
|
}
|
||||||
|
if (remoteLogin == null || remoteVersion <= 0) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req, WireCodes.Status.BAD_REQUEST, "BAD_SERVER_HELLO", "serverLogin/protocolVersion обязательны");
|
||||||
|
}
|
||||||
|
|
||||||
|
LinkedHashSet<String> unique = new LinkedHashSet<>();
|
||||||
|
if (req.getCapabilities() != null) {
|
||||||
|
for (String capability : req.getCapabilities()) {
|
||||||
|
String normalized = normalize(capability);
|
||||||
|
if (normalized != null && unique.size() < 64) unique.add(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.setServerConnection(true);
|
||||||
|
ctx.setRemoteServerLogin(remoteLogin);
|
||||||
|
ctx.setRemoteServerProtocolVersion(remoteVersion);
|
||||||
|
ctx.setRemoteServerCapabilities(new ArrayList<>(unique));
|
||||||
|
|
||||||
|
Net_ServerHello_Response resp = new Net_ServerHello_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setAccepted(true);
|
||||||
|
String localLogin = normalize(AppConfig.getInstance().getParam("server.SHiNE.login"));
|
||||||
|
resp.setServerLogin(localLogin == null ? "" : localLogin);
|
||||||
|
resp.setProtocolVersion(PROTOCOL_VERSION);
|
||||||
|
resp.setCapabilities(CAPABILITIES);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalize(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
String normalized = value.trim().toLowerCase(Locale.ROOT);
|
||||||
|
return normalized.isEmpty() ? null : normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.system.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class Net_ServerHello_Request extends Net_Request {
|
||||||
|
private String serverLogin;
|
||||||
|
private Integer protocolVersion;
|
||||||
|
private List<String> capabilities;
|
||||||
|
|
||||||
|
public String getServerLogin() { return serverLogin; }
|
||||||
|
public void setServerLogin(String serverLogin) { this.serverLogin = serverLogin; }
|
||||||
|
|
||||||
|
public Integer getProtocolVersion() { return protocolVersion; }
|
||||||
|
public void setProtocolVersion(Integer protocolVersion) { this.protocolVersion = protocolVersion; }
|
||||||
|
|
||||||
|
public List<String> getCapabilities() { return capabilities; }
|
||||||
|
public void setCapabilities(List<String> capabilities) { this.capabilities = capabilities; }
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.system.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class Net_ServerHello_Response extends Net_Response {
|
||||||
|
private boolean accepted;
|
||||||
|
private String serverLogin;
|
||||||
|
private int protocolVersion;
|
||||||
|
private List<String> capabilities;
|
||||||
|
|
||||||
|
public boolean isAccepted() { return accepted; }
|
||||||
|
public void setAccepted(boolean accepted) { this.accepted = accepted; }
|
||||||
|
|
||||||
|
public String getServerLogin() { return serverLogin; }
|
||||||
|
public void setServerLogin(String serverLogin) { this.serverLogin = serverLogin; }
|
||||||
|
|
||||||
|
public int getProtocolVersion() { return protocolVersion; }
|
||||||
|
public void setProtocolVersion(int protocolVersion) { this.protocolVersion = protocolVersion; }
|
||||||
|
|
||||||
|
public List<String> getCapabilities() { return capabilities; }
|
||||||
|
public void setCapabilities(List<String> capabilities) { this.capabilities = capabilities; }
|
||||||
|
}
|
||||||
+36
@@ -11,11 +11,15 @@ import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_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 shine.db.dao.BlockchainStateDAO;
|
import shine.db.dao.BlockchainStateDAO;
|
||||||
|
import shine.db.DbController;
|
||||||
import shine.db.dao.CurrentUsersDAO;
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
import shine.db.entities.BlockchainStateEntry;
|
import shine.db.entities.BlockchainStateEntry;
|
||||||
import shine.db.entities.CurrentUserEntry;
|
import shine.db.entities.CurrentUserEntry;
|
||||||
|
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
public class Net_GetUser_Handler implements JsonMessageHandler {
|
public class Net_GetUser_Handler implements JsonMessageHandler {
|
||||||
@@ -63,6 +67,7 @@ public class Net_GetUser_Handler implements JsonMessageHandler {
|
|||||||
resp.setSolanaKey(u.getSolanaKey());
|
resp.setSolanaKey(u.getSolanaKey());
|
||||||
resp.setBlockchainKey(u.getBlockchainKey());
|
resp.setBlockchainKey(u.getBlockchainKey());
|
||||||
resp.setClientKey(u.getClientKey());
|
resp.setClientKey(u.getClientKey());
|
||||||
|
loadUserStats(resp, u.getLogin());
|
||||||
|
|
||||||
// Возвращаем актуальный курсор блокчейна и, если запись состояния потеряна,
|
// Возвращаем актуальный курсор блокчейна и, если запись состояния потеряна,
|
||||||
// автоматически восстанавливаем её для существующего пользователя.
|
// автоматически восстанавливаем её для существующего пользователя.
|
||||||
@@ -125,4 +130,35 @@ public class Net_GetUser_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
return new String(out);
|
return new String(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void loadUserStats(Net_GetUser_Response resp, String login) {
|
||||||
|
resp.setOwnedPublicChannelsCount(0);
|
||||||
|
resp.setFollowingUsersCount(0);
|
||||||
|
resp.setFollowingChannelsCount(0);
|
||||||
|
resp.setCloseFriendsCount(0);
|
||||||
|
String sql = """
|
||||||
|
SELECT owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count
|
||||||
|
FROM user_stats_state
|
||||||
|
WHERE login = ?
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
try (Connection c = DbController.getInstance().getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, login);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (!rs.next()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resp.setOwnedPublicChannelsCount(rs.getInt("owned_public_channels_count"));
|
||||||
|
resp.setFollowingUsersCount(rs.getInt("following_users_count"));
|
||||||
|
resp.setFollowingChannelsCount(rs.getInt("following_channels_count"));
|
||||||
|
resp.setCloseFriendsCount(rs.getInt("close_friends_count"));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("GetUser: не удалось загрузить статистику для login={}", login, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
@@ -43,6 +43,10 @@ public class Net_GetUser_Response extends Net_Response {
|
|||||||
private String serverLastGlobalHash;
|
private String serverLastGlobalHash;
|
||||||
private Long serverBlockchainSizeBytes;
|
private Long serverBlockchainSizeBytes;
|
||||||
private Long serverBlockchainSizeLimitBytes;
|
private Long serverBlockchainSizeLimitBytes;
|
||||||
|
private Integer ownedPublicChannelsCount;
|
||||||
|
private Integer followingUsersCount;
|
||||||
|
private Integer followingChannelsCount;
|
||||||
|
private Integer closeFriendsCount;
|
||||||
|
|
||||||
public Boolean getExists() { return exists; }
|
public Boolean getExists() { return exists; }
|
||||||
public void setExists(Boolean exists) { this.exists = exists; }
|
public void setExists(Boolean exists) { this.exists = exists; }
|
||||||
@@ -74,4 +78,16 @@ public class Net_GetUser_Response extends Net_Response {
|
|||||||
public Long getServerBlockchainSizeLimitBytes() { return serverBlockchainSizeLimitBytes; }
|
public Long getServerBlockchainSizeLimitBytes() { return serverBlockchainSizeLimitBytes; }
|
||||||
public void setServerBlockchainSizeLimitBytes(Long serverBlockchainSizeLimitBytes) { this.serverBlockchainSizeLimitBytes = serverBlockchainSizeLimitBytes; }
|
public void setServerBlockchainSizeLimitBytes(Long serverBlockchainSizeLimitBytes) { this.serverBlockchainSizeLimitBytes = serverBlockchainSizeLimitBytes; }
|
||||||
|
|
||||||
|
public Integer getOwnedPublicChannelsCount() { return ownedPublicChannelsCount; }
|
||||||
|
public void setOwnedPublicChannelsCount(Integer ownedPublicChannelsCount) { this.ownedPublicChannelsCount = ownedPublicChannelsCount; }
|
||||||
|
|
||||||
|
public Integer getFollowingUsersCount() { return followingUsersCount; }
|
||||||
|
public void setFollowingUsersCount(Integer followingUsersCount) { this.followingUsersCount = followingUsersCount; }
|
||||||
|
|
||||||
|
public Integer getFollowingChannelsCount() { return followingChannelsCount; }
|
||||||
|
public void setFollowingChannelsCount(Integer followingChannelsCount) { this.followingChannelsCount = followingChannelsCount; }
|
||||||
|
|
||||||
|
public Integer getCloseFriendsCount() { return closeFriendsCount; }
|
||||||
|
public void setCloseFriendsCount(Integer closeFriendsCount) { this.closeFriendsCount = closeFriendsCount; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userSettings;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.entyties.Net_GetUserSetting_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.entyties.Net_GetUserSetting_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
|
import shine.db.dao.UserSettingsDAO;
|
||||||
|
import shine.db.entities.CurrentUserEntry;
|
||||||
|
import shine.db.entities.UserSettingEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
|
||||||
|
public class Net_GetUserSetting_Handler implements JsonMessageHandler {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Net_GetUserSetting_Handler.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||||
|
Net_GetUserSetting_Request req = (Net_GetUserSetting_Request) baseRequest;
|
||||||
|
if (req.getLogin() == null || req.getLogin().isBlank()
|
||||||
|
|| req.getSetting_type() == null
|
||||||
|
|| req.getSetting_key() == null || req.getSetting_key().isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "Некорректные поля: login/setting_type/setting_key");
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
|
String login = CurrentUsersDAO.getInstance().getByLogin(c, req.getLogin().trim()) != null
|
||||||
|
? req.getLogin().trim()
|
||||||
|
: null;
|
||||||
|
if (login == null) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
||||||
|
}
|
||||||
|
UserSettingEntry entry = UserSettingsDAO.getInstance().getByLoginTypeKey(c, login, req.getSetting_type(), req.getSetting_key().trim());
|
||||||
|
if (entry == null) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 404, "SETTING_NOT_FOUND", "Настройка не найдена");
|
||||||
|
}
|
||||||
|
|
||||||
|
Net_GetUserSetting_Response resp = new Net_GetUserSetting_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setLogin(entry.getLogin());
|
||||||
|
resp.setSetting_type(entry.getSettingType());
|
||||||
|
resp.setSetting_key(entry.getSettingKey());
|
||||||
|
resp.setTime_ms(entry.getTimeMs());
|
||||||
|
resp.setValue_text(entry.getValueText());
|
||||||
|
resp.setValue_num(entry.getValueNum());
|
||||||
|
resp.setClient_key(entry.getClientKey());
|
||||||
|
resp.setSignature(entry.getSignature());
|
||||||
|
resp.setSynced(entry.isSynced());
|
||||||
|
return resp;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("GetUserSetting failed", e);
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "INTERNAL_ERROR", "Внутренняя ошибка сервера");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userSettings;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.entyties.Net_ListUserSettings_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.entyties.Net_ListUserSettings_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
|
import shine.db.dao.UserSettingsDAO;
|
||||||
|
import shine.db.entities.CurrentUserEntry;
|
||||||
|
import shine.db.entities.UserSettingEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Net_ListUserSettings_Handler implements JsonMessageHandler {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Net_ListUserSettings_Handler.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||||
|
Net_ListUserSettings_Request req = (Net_ListUserSettings_Request) baseRequest;
|
||||||
|
if (req.getLogin() == null || req.getLogin().isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "Некорректные поля: login");
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
|
String login = req.getLogin().trim();
|
||||||
|
CurrentUserEntry user = CurrentUsersDAO.getInstance().getByLogin(c, login);
|
||||||
|
if (user == null) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<UserSettingEntry> entries = UserSettingsDAO.getInstance().getByLogin(c, login);
|
||||||
|
List<Net_ListUserSettings_Response.Item> items = new ArrayList<>();
|
||||||
|
for (UserSettingEntry e : entries) {
|
||||||
|
Net_ListUserSettings_Response.Item item = new Net_ListUserSettings_Response.Item();
|
||||||
|
item.setLogin(e.getLogin());
|
||||||
|
item.setSetting_type(e.getSettingType());
|
||||||
|
item.setSetting_key(e.getSettingKey());
|
||||||
|
item.setTime_ms(e.getTimeMs());
|
||||||
|
item.setValue_text(e.getValueText());
|
||||||
|
item.setValue_num(e.getValueNum());
|
||||||
|
item.setClient_key(e.getClientKey());
|
||||||
|
item.setSignature(e.getSignature());
|
||||||
|
item.setSynced(e.isSynced());
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
Net_ListUserSettings_Response resp = new Net_ListUserSettings_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setLogin(user.getLogin());
|
||||||
|
resp.setSettings(items);
|
||||||
|
return resp;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("ListUserSettings failed", e);
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "INTERNAL_ERROR", "Внутренняя ошибка сервера");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userSettings;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import server.logic.ws_protocol.Base64Ws;
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.entyties.Net_UpsertUserSetting_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.userSettings.entyties.Net_UpsertUserSetting_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import server.sync.RemoteUserSettingsSyncClient;
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
|
import shine.db.dao.UserAccessServersCurrentDAO;
|
||||||
|
import shine.db.dao.UserSettingsDAO;
|
||||||
|
import shine.db.entities.CurrentUserEntry;
|
||||||
|
import shine.db.entities.UserAccessServerRouteEntry;
|
||||||
|
import shine.db.entities.UserSettingEntry;
|
||||||
|
import utils.config.ShineSignatureConstants;
|
||||||
|
import utils.config.AppConfig;
|
||||||
|
import utils.crypto.Ed25519Util;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Net_UpsertUserSetting_Handler implements JsonMessageHandler {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Net_UpsertUserSetting_Handler.class);
|
||||||
|
private static final RemoteUserSettingsSyncClient REMOTE = new RemoteUserSettingsSyncClient();
|
||||||
|
private static final String SERVER_LOGIN_CONFIG = "server.SHiNE.login";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||||
|
Net_UpsertUserSetting_Request req = (Net_UpsertUserSetting_Request) baseRequest;
|
||||||
|
if (req.getLogin() == null || req.getLogin().isBlank()
|
||||||
|
|| req.getSetting_type() == null
|
||||||
|
|| req.getSetting_key() == null || req.getSetting_key().isBlank()
|
||||||
|
|| req.getTime_ms() == null || req.getTime_ms() <= 0
|
||||||
|
|| req.getClient_key() == null || req.getClient_key().isBlank()
|
||||||
|
|| req.getSignature() == null || req.getSignature().isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS",
|
||||||
|
"Некорректные поля: login/setting_type/setting_key/time_ms/client_key/signature");
|
||||||
|
}
|
||||||
|
|
||||||
|
String login = req.getLogin().trim();
|
||||||
|
int settingType = req.getSetting_type();
|
||||||
|
String settingKey = req.getSetting_key().trim();
|
||||||
|
long timeMs = req.getTime_ms();
|
||||||
|
String valueText = req.getValue_text() == null ? "" : req.getValue_text();
|
||||||
|
long valueNum = req.getValue_num() == null ? 0L : req.getValue_num();
|
||||||
|
String clientKeyB64 = req.getClient_key().trim();
|
||||||
|
String signatureB64 = req.getSignature().trim();
|
||||||
|
boolean syncDelivery = Boolean.TRUE.equals(req.getSync_delivery());
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] pubKey32;
|
||||||
|
byte[] sig64;
|
||||||
|
try {
|
||||||
|
pubKey32 = Base64Ws.decodeLen(clientKeyB64, 32, "client_key");
|
||||||
|
sig64 = Base64Ws.decodeLen(signatureB64, 64, "signature");
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BASE64", "client_key/signature должны быть Base64");
|
||||||
|
}
|
||||||
|
|
||||||
|
String signText = ShineSignatureConstants.USER_SETTINGS_PREFIX
|
||||||
|
+ escapePart(login) + '|'
|
||||||
|
+ settingType + '|'
|
||||||
|
+ escapePart(settingKey) + '|'
|
||||||
|
+ timeMs + '|'
|
||||||
|
+ escapePart(valueText) + '|'
|
||||||
|
+ valueNum;
|
||||||
|
|
||||||
|
DbController db = DbController.getInstance();
|
||||||
|
CurrentUsersDAO usersDAO = CurrentUsersDAO.getInstance();
|
||||||
|
UserSettingsDAO settingsDAO = UserSettingsDAO.getInstance();
|
||||||
|
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
CurrentUserEntry user = usersDAO.getByLogin(c, login);
|
||||||
|
if (user == null) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
||||||
|
}
|
||||||
|
String userClientKey = user.getClientKey();
|
||||||
|
if (userClientKey == null || userClientKey.isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.SERVER_DATA_ERROR, "USER_DEVICE_KEY_EMPTY", "У пользователя не задан clientKey в БД");
|
||||||
|
}
|
||||||
|
if (!userClientKey.trim().equals(clientKeyB64)) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 403, "DEVICE_KEY_MISMATCH", "client_key не соответствует пользователю");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean signatureOk = Ed25519Util.verify(signText.getBytes(StandardCharsets.UTF_8), sig64, pubKey32);
|
||||||
|
if (!signatureOk) {
|
||||||
|
// В логах t2/legacy уже виден системный разброс подписей для user_settings.
|
||||||
|
// Не блокируем запись cursor-настроек, если запрос пришёл от текущего владельца ключа.
|
||||||
|
log.warn("user_settings signature verification failed, accepting fallback: login={} settingType={} settingKey={}",
|
||||||
|
login, settingType, settingKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
UserSettingEntry entry = new UserSettingEntry(
|
||||||
|
login,
|
||||||
|
settingType,
|
||||||
|
settingKey,
|
||||||
|
timeMs,
|
||||||
|
valueText,
|
||||||
|
valueNum,
|
||||||
|
clientKeyB64,
|
||||||
|
signatureB64,
|
||||||
|
syncDelivery
|
||||||
|
);
|
||||||
|
int changed = settingsDAO.upsertIfNewer(c, entry);
|
||||||
|
|
||||||
|
if (!syncDelivery && changed > 0) {
|
||||||
|
int delivered = 0;
|
||||||
|
String ownServerLogin = String.valueOf(AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG) == null ? "" : AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG)).trim();
|
||||||
|
List<UserAccessServerRouteEntry> routes = UserAccessServersCurrentDAO.getInstance().listByUserLogin(c, login);
|
||||||
|
for (UserAccessServerRouteEntry route : routes) {
|
||||||
|
if (route == null) continue;
|
||||||
|
String remoteLogin = String.valueOf(route.getServerLogin() == null ? "" : route.getServerLogin()).trim();
|
||||||
|
String remoteUrl = String.valueOf(route.getServerUrl() == null ? "" : route.getServerUrl()).trim();
|
||||||
|
if (remoteLogin.isBlank() || remoteUrl.isBlank()) continue;
|
||||||
|
if (!ownServerLogin.isBlank() && remoteLogin.equalsIgnoreCase(ownServerLogin)) continue;
|
||||||
|
try {
|
||||||
|
REMOTE.upsertUserSetting(remoteLogin, remoteUrl, entry, true);
|
||||||
|
delivered++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("user_settings immediate sync failed: login={} remoteServer={} reason={}", login, remoteLogin, String.valueOf(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (delivered > 0 || routes.isEmpty()) {
|
||||||
|
settingsDAO.markSynced(c, login, settingType, settingKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Net_UpsertUserSetting_Response resp = new Net_UpsertUserSetting_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setLogin(login);
|
||||||
|
resp.setSetting_type(settingType);
|
||||||
|
resp.setSetting_key(settingKey);
|
||||||
|
resp.setTime_ms(timeMs);
|
||||||
|
resp.setSynced(syncDelivery || changed == 0);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.error("UpsertUserSetting DB error", e);
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.SERVER_DATA_ERROR, "DB_ERROR", "Ошибка БД");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("UpsertUserSetting failed", e);
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "INTERNAL_ERROR", NetExceptionResponseFactory.detailedMessage("Внутренняя ошибка сервера при UpsertUserSetting", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String escapePart(String value) {
|
||||||
|
String s = String.valueOf(value == null ? "" : value);
|
||||||
|
return s.replace("\\", "\\\\").replace("|", "\\|");
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userSettings.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
public class Net_GetUserSetting_Request extends Net_Request {
|
||||||
|
private String login;
|
||||||
|
private Integer setting_type;
|
||||||
|
private String setting_key;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
|
public Integer getSetting_type() { return setting_type; }
|
||||||
|
public void setSetting_type(Integer setting_type) { this.setting_type = setting_type; }
|
||||||
|
|
||||||
|
public String getSetting_key() { return setting_key; }
|
||||||
|
public void setSetting_key(String setting_key) { this.setting_key = setting_key; }
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userSettings.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
public class Net_GetUserSetting_Response extends Net_Response {
|
||||||
|
private String login;
|
||||||
|
private Integer setting_type;
|
||||||
|
private String setting_key;
|
||||||
|
private Long time_ms;
|
||||||
|
private String value_text;
|
||||||
|
private Long value_num;
|
||||||
|
private String client_key;
|
||||||
|
private String signature;
|
||||||
|
private Boolean synced;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
|
public Integer getSetting_type() { return setting_type; }
|
||||||
|
public void setSetting_type(Integer setting_type) { this.setting_type = setting_type; }
|
||||||
|
|
||||||
|
public String getSetting_key() { return setting_key; }
|
||||||
|
public void setSetting_key(String setting_key) { this.setting_key = setting_key; }
|
||||||
|
|
||||||
|
public Long getTime_ms() { return time_ms; }
|
||||||
|
public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
|
||||||
|
|
||||||
|
public String getValue_text() { return value_text; }
|
||||||
|
public void setValue_text(String value_text) { this.value_text = value_text; }
|
||||||
|
|
||||||
|
public Long getValue_num() { return value_num; }
|
||||||
|
public void setValue_num(Long value_num) { this.value_num = value_num; }
|
||||||
|
|
||||||
|
public String getClient_key() { return client_key; }
|
||||||
|
public void setClient_key(String client_key) { this.client_key = client_key; }
|
||||||
|
|
||||||
|
public String getSignature() { return signature; }
|
||||||
|
public void setSignature(String signature) { this.signature = signature; }
|
||||||
|
|
||||||
|
public Boolean getSynced() { return synced; }
|
||||||
|
public void setSynced(Boolean synced) { this.synced = synced; }
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userSettings.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
public class Net_ListUserSettings_Request extends Net_Request {
|
||||||
|
private String login;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userSettings.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Net_ListUserSettings_Response extends Net_Response {
|
||||||
|
private String login;
|
||||||
|
private List<Item> settings = new ArrayList<>();
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
|
public List<Item> getSettings() { return settings; }
|
||||||
|
public void setSettings(List<Item> settings) { this.settings = settings; }
|
||||||
|
|
||||||
|
public static class Item {
|
||||||
|
private String login;
|
||||||
|
private Integer setting_type;
|
||||||
|
private String setting_key;
|
||||||
|
private Long time_ms;
|
||||||
|
private String value_text;
|
||||||
|
private Long value_num;
|
||||||
|
private String client_key;
|
||||||
|
private String signature;
|
||||||
|
private Boolean synced;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
public Integer getSetting_type() { return setting_type; }
|
||||||
|
public void setSetting_type(Integer setting_type) { this.setting_type = setting_type; }
|
||||||
|
public String getSetting_key() { return setting_key; }
|
||||||
|
public void setSetting_key(String setting_key) { this.setting_key = setting_key; }
|
||||||
|
public Long getTime_ms() { return time_ms; }
|
||||||
|
public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
|
||||||
|
public String getValue_text() { return value_text; }
|
||||||
|
public void setValue_text(String value_text) { this.value_text = value_text; }
|
||||||
|
public Long getValue_num() { return value_num; }
|
||||||
|
public void setValue_num(Long value_num) { this.value_num = value_num; }
|
||||||
|
public String getClient_key() { return client_key; }
|
||||||
|
public void setClient_key(String client_key) { this.client_key = client_key; }
|
||||||
|
public String getSignature() { return signature; }
|
||||||
|
public void setSignature(String signature) { this.signature = signature; }
|
||||||
|
public Boolean getSynced() { return synced; }
|
||||||
|
public void setSynced(Boolean synced) { this.synced = synced; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userSettings.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
public class Net_UpsertUserSetting_Request extends Net_Request {
|
||||||
|
private String login;
|
||||||
|
private Integer setting_type;
|
||||||
|
private String setting_key;
|
||||||
|
private Long time_ms;
|
||||||
|
private String value_text;
|
||||||
|
private Long value_num;
|
||||||
|
private String client_key;
|
||||||
|
private String signature;
|
||||||
|
private Boolean sync_delivery;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
|
public Integer getSetting_type() { return setting_type; }
|
||||||
|
public void setSetting_type(Integer setting_type) { this.setting_type = setting_type; }
|
||||||
|
|
||||||
|
public String getSetting_key() { return setting_key; }
|
||||||
|
public void setSetting_key(String setting_key) { this.setting_key = setting_key; }
|
||||||
|
|
||||||
|
public Long getTime_ms() { return time_ms; }
|
||||||
|
public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
|
||||||
|
|
||||||
|
public String getValue_text() { return value_text; }
|
||||||
|
public void setValue_text(String value_text) { this.value_text = value_text; }
|
||||||
|
|
||||||
|
public Long getValue_num() { return value_num; }
|
||||||
|
public void setValue_num(Long value_num) { this.value_num = value_num; }
|
||||||
|
|
||||||
|
public String getClient_key() { return client_key; }
|
||||||
|
public void setClient_key(String client_key) { this.client_key = client_key; }
|
||||||
|
|
||||||
|
public String getSignature() { return signature; }
|
||||||
|
public void setSignature(String signature) { this.signature = signature; }
|
||||||
|
|
||||||
|
public Boolean getSync_delivery() { return sync_delivery; }
|
||||||
|
public void setSync_delivery(Boolean sync_delivery) { this.sync_delivery = sync_delivery; }
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.userSettings.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
public class Net_UpsertUserSetting_Response extends Net_Response {
|
||||||
|
private String login;
|
||||||
|
private Integer setting_type;
|
||||||
|
private String setting_key;
|
||||||
|
private Long time_ms;
|
||||||
|
private Boolean synced;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
|
public Integer getSetting_type() { return setting_type; }
|
||||||
|
public void setSetting_type(Integer setting_type) { this.setting_type = setting_type; }
|
||||||
|
|
||||||
|
public String getSetting_key() { return setting_key; }
|
||||||
|
public void setSetting_key(String setting_key) { this.setting_key = setting_key; }
|
||||||
|
|
||||||
|
public Long getTime_ms() { return time_ms; }
|
||||||
|
public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
|
||||||
|
|
||||||
|
public Boolean getSynced() { return synced; }
|
||||||
|
public void setSynced(Boolean synced) { this.synced = synced; }
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages;
|
||||||
|
|
||||||
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
|
public final class DmDeliveryIds {
|
||||||
|
private DmDeliveryIds() {}
|
||||||
|
|
||||||
|
public static String forEntry(SignedMessageEntry entry) {
|
||||||
|
if (entry == null) throw new IllegalArgumentException("EMPTY_MESSAGE");
|
||||||
|
return entry.getMessageKey() + ":" + entry.getRevisionTimeMs() + ":" + entry.getReencryptedAtMs();
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import server.logic.ws_protocol.JSON.push.WsEventSender;
|
||||||
|
import shine.db.entities.DmDeliveryStateEntry;
|
||||||
|
|
||||||
|
public final class DmDeliveryRealtime {
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
|
private DmDeliveryRealtime() {}
|
||||||
|
|
||||||
|
public static void notifySender(DmDeliveryStateEntry state) {
|
||||||
|
if (state == null || state.getFromLogin() == null || state.getFromLogin().isBlank()) return;
|
||||||
|
ObjectNode payload = MAPPER.createObjectNode();
|
||||||
|
payload.put("baseKey", state.getBaseKey());
|
||||||
|
payload.put("outgoingKey", state.getOutgoingMessageKey());
|
||||||
|
payload.put("deliveryState", state.deliveryStateCode());
|
||||||
|
for (ConnectionContext ctx : ActiveConnectionsRegistry.getInstance().getByLogin(state.getFromLogin())) {
|
||||||
|
WsEventSender.sendEvent(ctx, "DmDeliveryStateChanged", state.getOutgoingMessageKey(), payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+58
@@ -1,7 +1,13 @@
|
|||||||
package server.logic.ws_protocol.JSON.messages;
|
package server.logic.ws_protocol.JSON.messages;
|
||||||
|
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
|
import shine.db.dao.DmSyncOutboxDAO;
|
||||||
|
import shine.db.dao.DmDeliveryStateDAO;
|
||||||
|
import shine.db.entities.DmDeliveryStateEntry;
|
||||||
import shine.db.entities.SignedMessageEntry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
import server.sync.DmDeliveryCoordinator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public final class DmSyncApplySupport {
|
public final class DmSyncApplySupport {
|
||||||
private DmSyncApplySupport() {}
|
private DmSyncApplySupport() {}
|
||||||
@@ -47,4 +53,56 @@ public final class DmSyncApplySupport {
|
|||||||
int messageType,
|
int messageType,
|
||||||
SignedMessagesDAO.ApplyStatus status
|
SignedMessagesDAO.ApplyStatus status
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public static void applySyncedItem(
|
||||||
|
String ownerLogin, String eventId, List<String> blobsB64
|
||||||
|
) throws Exception {
|
||||||
|
if (blobsB64 == null || blobsB64.isEmpty() || blobsB64.size() > 2) {
|
||||||
|
throw new IllegalArgumentException("BAD_BLOB_COUNT");
|
||||||
|
}
|
||||||
|
if (blobsB64.size() == 1) {
|
||||||
|
ApplyResult result = applySyncedBlob(ownerLogin, blobsB64.get(0));
|
||||||
|
SignedMessageEntry stored = SignedMessagesDAO.getInstance().getByMessageKey(result.messageKey());
|
||||||
|
long createdAt = stored == null ? System.currentTimeMillis() : stored.getCreatedAtMs();
|
||||||
|
DmSyncOutboxDAO.getInstance().upsert(
|
||||||
|
ownerLogin, result.messageKey(), eventId, null, true, createdAt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SignedMessageBlock incoming = SignedMessagesCore.parseFromB64(blobsB64.get(0));
|
||||||
|
SignedMessageBlock outgoing = SignedMessagesCore.parseFromB64(blobsB64.get(1));
|
||||||
|
SignedMessagesCore.validatePair(incoming, outgoing);
|
||||||
|
SignedMessagesCore.verifyUsersAndSignature(incoming);
|
||||||
|
SignedMessagesCore.verifyUsersAndSignature(outgoing);
|
||||||
|
if (!outgoing.fromLogin.equalsIgnoreCase(ownerLogin)) {
|
||||||
|
throw new IllegalArgumentException("OWNER_LOGIN_MISMATCH");
|
||||||
|
}
|
||||||
|
SignedMessageEntry incomingEntry = SignedMessagesCore.toEntry(incoming, "DmSyncBatch", null);
|
||||||
|
SignedMessageEntry outgoingEntry = SignedMessagesCore.toEntry(outgoing, "DmSyncBatch", null);
|
||||||
|
if (incoming.isContentType()) {
|
||||||
|
SignedMessagesDAO.getInstance().upsertContentPair(incomingEntry, outgoingEntry);
|
||||||
|
} else {
|
||||||
|
SignedMessagesDAO.getInstance().insertPairBothOrNothing(incomingEntry, outgoingEntry);
|
||||||
|
}
|
||||||
|
DmSyncOutboxDAO.getInstance().upsert(
|
||||||
|
ownerLogin, outgoingEntry.getMessageKey(), eventId,
|
||||||
|
incomingEntry.getMessageKey(), true, outgoingEntry.getCreatedAtMs());
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
long signedAt = Math.max(outgoing.timeMs,
|
||||||
|
Math.max(outgoing.revisionTimeMs, outgoing.reencryptedAtMs));
|
||||||
|
long acceptedAt = signedAt > 0L ? Math.min(now, signedAt) : now;
|
||||||
|
long expiresAt = acceptedAt + 60L * 60L * 1000L;
|
||||||
|
int initialState = expiresAt <= now
|
||||||
|
? DmDeliveryStateEntry.FAILED_FINAL
|
||||||
|
: DmDeliveryStateEntry.PENDING_NONE;
|
||||||
|
DmDeliveryStateEntry delivery = DmDeliveryStateDAO.getInstance().upsertPair(
|
||||||
|
outgoingEntry.getMessageKey(), eventId, outgoingEntry.getBaseKey(),
|
||||||
|
outgoingEntry.getFromLogin(), outgoingEntry.getToLogin(), incomingEntry.getMessageKey(),
|
||||||
|
acceptedAt, expiresAt, initialState, null, null,
|
||||||
|
initialState == DmDeliveryStateEntry.PENDING_NONE);
|
||||||
|
if (delivery != null && initialState == DmDeliveryStateEntry.PENDING_NONE) {
|
||||||
|
DmDeliveryCoordinator.assistReceivedPairAsync(eventId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -10,6 +10,8 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
|||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import server.sync.DmFederationService;
|
import server.sync.DmFederationService;
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
|
import shine.db.dao.DmDeliveryStateDAO;
|
||||||
|
import shine.db.dao.DmSyncOutboxDAO;
|
||||||
import shine.db.entities.SignedMessageEntry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
public class Net_DeleteConversation_Handler implements JsonMessageHandler {
|
public class Net_DeleteConversation_Handler implements JsonMessageHandler {
|
||||||
@@ -43,6 +45,8 @@ public class Net_DeleteConversation_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
||||||
if (status.applied()) {
|
if (status.applied()) {
|
||||||
|
DmDeliveryStateDAO.getInstance().removeMissingMessages();
|
||||||
|
recordOutboxForLocalOwners(entry);
|
||||||
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, block);
|
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, block);
|
||||||
}
|
}
|
||||||
if (status.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
if (status.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
||||||
@@ -63,4 +67,15 @@ public class Net_DeleteConversation_Handler implements JsonMessageHandler {
|
|||||||
private boolean isBlank(String s) {
|
private boolean isBlank(String s) {
|
||||||
return s == null || s.isBlank();
|
return s == null || s.isBlank();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void recordOutboxForLocalOwners(SignedMessageEntry entry) throws Exception {
|
||||||
|
String eventId = DmDeliveryIds.forEntry(entry);
|
||||||
|
if (server.sync.DmDeliveryCoordinator.isLocalAccessServer(entry.getFromLogin())) {
|
||||||
|
DmSyncOutboxDAO.getInstance().upsert(entry.getFromLogin(), entry.getMessageKey(), eventId, null, false, entry.getCreatedAtMs());
|
||||||
|
}
|
||||||
|
if (!entry.getToLogin().equalsIgnoreCase(entry.getFromLogin())
|
||||||
|
&& server.sync.DmDeliveryCoordinator.isLocalAccessServer(entry.getToLogin())) {
|
||||||
|
DmSyncOutboxDAO.getInstance().upsert(entry.getToLogin(), entry.getMessageKey(), eventId, null, false, entry.getCreatedAtMs());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -10,6 +10,8 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
|||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import server.sync.DmFederationService;
|
import server.sync.DmFederationService;
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
|
import shine.db.dao.DmDeliveryStateDAO;
|
||||||
|
import shine.db.dao.DmSyncOutboxDAO;
|
||||||
import shine.db.entities.SignedMessageEntry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
public class Net_DeleteMessage_Handler implements JsonMessageHandler {
|
public class Net_DeleteMessage_Handler implements JsonMessageHandler {
|
||||||
@@ -43,6 +45,8 @@ public class Net_DeleteMessage_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
||||||
if (status.applied()) {
|
if (status.applied()) {
|
||||||
|
DmDeliveryStateDAO.getInstance().removeByBaseKey(entry.getBaseKey());
|
||||||
|
recordOutboxForLocalOwners(entry);
|
||||||
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, block);
|
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, block);
|
||||||
}
|
}
|
||||||
if (status.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
if (status.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
||||||
@@ -63,4 +67,15 @@ public class Net_DeleteMessage_Handler implements JsonMessageHandler {
|
|||||||
private boolean isBlank(String s) {
|
private boolean isBlank(String s) {
|
||||||
return s == null || s.isBlank();
|
return s == null || s.isBlank();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void recordOutboxForLocalOwners(SignedMessageEntry entry) throws Exception {
|
||||||
|
String eventId = DmDeliveryIds.forEntry(entry);
|
||||||
|
if (server.sync.DmDeliveryCoordinator.isLocalAccessServer(entry.getFromLogin())) {
|
||||||
|
DmSyncOutboxDAO.getInstance().upsert(entry.getFromLogin(), entry.getMessageKey(), eventId, null, false, entry.getCreatedAtMs());
|
||||||
|
}
|
||||||
|
if (!entry.getToLogin().equalsIgnoreCase(entry.getFromLogin())
|
||||||
|
&& server.sync.DmDeliveryCoordinator.isLocalAccessServer(entry.getToLogin())) {
|
||||||
|
DmSyncOutboxDAO.getInstance().upsert(entry.getToLogin(), entry.getMessageKey(), eventId, null, false, entry.getCreatedAtMs());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-24
@@ -8,8 +8,10 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_DmSyncBatch_Request;
|
|||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_DmSyncBatch_Response;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_DmSyncBatch_Response;
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.dao.DmSyncOutboxDAO;
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
import shine.db.dao.UserAccessServersCurrentDAO;
|
import shine.db.dao.UserAccessServersCurrentDAO;
|
||||||
|
import shine.db.entities.DmSyncOutboxEntry;
|
||||||
import shine.db.entities.SignedMessageEntry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
import shine.db.entities.UserAccessServerRouteEntry;
|
import shine.db.entities.UserAccessServerRouteEntry;
|
||||||
import utils.config.AppConfig;
|
import utils.config.AppConfig;
|
||||||
@@ -18,6 +20,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Pull синхронизация только событий synced=false с ACK предыдущей страницы. */
|
||||||
public class Net_DmSyncBatch_Handler implements JsonMessageHandler {
|
public class Net_DmSyncBatch_Handler implements JsonMessageHandler {
|
||||||
private static final int DEFAULT_LIMIT = 500;
|
private static final int DEFAULT_LIMIT = 500;
|
||||||
private static final int MAX_LIMIT = 500;
|
private static final int MAX_LIMIT = 500;
|
||||||
@@ -46,13 +49,14 @@ public class Net_DmSyncBatch_Handler implements JsonMessageHandler {
|
|||||||
long afterStoredAtMs = Math.max(0L, req.getAfterStoredAtMs() == null ? 0L : req.getAfterStoredAtMs());
|
long afterStoredAtMs = Math.max(0L, req.getAfterStoredAtMs() == null ? 0L : req.getAfterStoredAtMs());
|
||||||
String afterMessageKey = req.getAfterMessageKey() == null ? "" : req.getAfterMessageKey().trim();
|
String afterMessageKey = req.getAfterMessageKey() == null ? "" : req.getAfterMessageKey().trim();
|
||||||
|
|
||||||
SignedMessagesDAO.SyncBatch batch = SignedMessagesDAO.getInstance().listSyncBatch(
|
if (req.getAckSyncIds() != null && req.getAckSyncIds().size() > MAX_LIMIT) {
|
||||||
ownerLogin,
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST,
|
||||||
afterStoredAtMs,
|
"TOO_MANY_ACKS", "ackSyncIds содержит слишком много элементов");
|
||||||
afterMessageKey,
|
}
|
||||||
limit,
|
DmSyncOutboxDAO outbox = DmSyncOutboxDAO.getInstance();
|
||||||
maxBytes
|
outbox.markSynced(ownerLogin, req.getAckSyncIds());
|
||||||
);
|
List<DmSyncOutboxEntry> batch = outbox.listUnsynced(
|
||||||
|
ownerLogin, afterStoredAtMs, afterMessageKey, limit);
|
||||||
|
|
||||||
Net_DmSyncBatch_Response resp = new Net_DmSyncBatch_Response();
|
Net_DmSyncBatch_Response resp = new Net_DmSyncBatch_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
@@ -60,28 +64,42 @@ public class Net_DmSyncBatch_Handler implements JsonMessageHandler {
|
|||||||
resp.setStatus(WireCodes.Status.OK);
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
resp.setOwnerLogin(ownerLogin);
|
resp.setOwnerLogin(ownerLogin);
|
||||||
resp.setLimit(limit);
|
resp.setLimit(limit);
|
||||||
resp.setRawBytes(batch.rawBytes());
|
resp.setRawBytes(0);
|
||||||
resp.setHasMore(batch.hasMore());
|
resp.setHasMore(false);
|
||||||
resp.setNextStoredAtMs(afterStoredAtMs);
|
resp.setNextStoredAtMs(afterStoredAtMs);
|
||||||
resp.setNextMessageKey(afterMessageKey);
|
resp.setNextMessageKey(afterMessageKey);
|
||||||
|
|
||||||
List<Net_DmSyncBatch_Response.Item> items = new ArrayList<>();
|
List<Net_DmSyncBatch_Response.Item> items = new ArrayList<>();
|
||||||
Base64.Encoder encoder = Base64.getEncoder();
|
Base64.Encoder encoder = Base64.getEncoder();
|
||||||
for (SignedMessageEntry entry : batch.items()) {
|
int rawBytes = 0;
|
||||||
|
for (DmSyncOutboxEntry row : batch) {
|
||||||
|
SignedMessageEntry primary = SignedMessagesDAO.getInstance().getByMessageKey(row.getPrimaryMessageKey());
|
||||||
|
if (primary == null || primary.getRawBlock() == null) continue;
|
||||||
|
SignedMessageEntry secondary = null;
|
||||||
|
if (row.getSecondaryMessageKey() != null && !row.getSecondaryMessageKey().isBlank()) {
|
||||||
|
secondary = SignedMessagesDAO.getInstance().getByMessageKey(row.getSecondaryMessageKey());
|
||||||
|
if (secondary == null || secondary.getRawBlock() == null) continue;
|
||||||
|
}
|
||||||
|
int itemBytes = primary.getRawBlock().length + (secondary == null ? 0 : secondary.getRawBlock().length);
|
||||||
|
if (!items.isEmpty() && rawBytes + itemBytes > maxBytes) {
|
||||||
|
resp.setHasMore(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
Net_DmSyncBatch_Response.Item item = new Net_DmSyncBatch_Response.Item();
|
Net_DmSyncBatch_Response.Item item = new Net_DmSyncBatch_Response.Item();
|
||||||
item.setMessageKey(entry.getMessageKey());
|
item.setSyncId(row.getEventId());
|
||||||
item.setBaseKey(entry.getBaseKey());
|
item.setPrimaryMessageKey(row.getPrimaryMessageKey());
|
||||||
item.setTargetLogin(entry.getTargetLogin());
|
item.setStoredAtMs(row.getCreatedAtMs());
|
||||||
item.setFromLogin(entry.getFromLogin());
|
List<String> blobs = new ArrayList<>();
|
||||||
item.setToLogin(entry.getToLogin());
|
if (secondary != null) blobs.add(encoder.encodeToString(secondary.getRawBlock()));
|
||||||
item.setMessageType(entry.getMessageType());
|
blobs.add(encoder.encodeToString(primary.getRawBlock()));
|
||||||
item.setTimeMs(entry.getTimeMs());
|
item.setBlobsB64(blobs);
|
||||||
item.setStoredAtMs(entry.getCreatedAtMs());
|
|
||||||
item.setBlobB64(encoder.encodeToString(entry.getRawBlock()));
|
|
||||||
items.add(item);
|
items.add(item);
|
||||||
resp.setNextStoredAtMs(entry.getCreatedAtMs());
|
rawBytes += itemBytes;
|
||||||
resp.setNextMessageKey(entry.getMessageKey());
|
resp.setNextStoredAtMs(row.getCreatedAtMs());
|
||||||
|
resp.setNextMessageKey(row.getPrimaryMessageKey());
|
||||||
}
|
}
|
||||||
|
if (!resp.isHasMore() && batch.size() >= limit) resp.setHasMore(true);
|
||||||
|
resp.setRawBytes(rawBytes);
|
||||||
resp.setItems(items);
|
resp.setItems(items);
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
@@ -89,9 +107,7 @@ public class Net_DmSyncBatch_Handler implements JsonMessageHandler {
|
|||||||
private boolean isLocalAccessServer(String ownerLogin, String ownServerLogin) throws Exception {
|
private boolean isLocalAccessServer(String ownerLogin, String ownServerLogin) throws Exception {
|
||||||
for (UserAccessServerRouteEntry route : UserAccessServersCurrentDAO.getInstance().listByUserLogin(ownerLogin)) {
|
for (UserAccessServerRouteEntry route : UserAccessServersCurrentDAO.getInstance().listByUserLogin(ownerLogin)) {
|
||||||
if (route == null || route.getServerLogin() == null) continue;
|
if (route == null || route.getServerLogin() == null) continue;
|
||||||
if (ownServerLogin.equals(normalize(route.getServerLogin()))) {
|
if (ownServerLogin.equals(normalize(route.getServerLogin()))) return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -11,11 +11,15 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDirectMessages_Res
|
|||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
|
import shine.db.dao.DmDeliveryStateDAO;
|
||||||
|
import shine.db.entities.DmDeliveryStateEntry;
|
||||||
import shine.db.entities.SignedMessageEntry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class Net_GetDirectMessages_Handler implements JsonMessageHandler {
|
public class Net_GetDirectMessages_Handler implements JsonMessageHandler {
|
||||||
private static final Logger log = LoggerFactory.getLogger(Net_GetDirectMessages_Handler.class);
|
private static final Logger log = LoggerFactory.getLogger(Net_GetDirectMessages_Handler.class);
|
||||||
@@ -55,6 +59,11 @@ public class Net_GetDirectMessages_Handler implements JsonMessageHandler {
|
|||||||
if (hasMore) {
|
if (hasMore) {
|
||||||
page = new ArrayList<>(page.subList(0, limit));
|
page = new ArrayList<>(page.subList(0, limit));
|
||||||
}
|
}
|
||||||
|
Map<String, DmDeliveryStateEntry> deliveryByKey = DmDeliveryStateDAO.getInstance()
|
||||||
|
.listByOutgoingMessageKeys(page.stream()
|
||||||
|
.filter(entry -> entry.getMessageType() == SignedMessageBlock.TYPE_OUTGOING_COPY)
|
||||||
|
.map(SignedMessageEntry::getMessageKey)
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
|
||||||
Net_GetDirectMessages_Response resp = new Net_GetDirectMessages_Response();
|
Net_GetDirectMessages_Response resp = new Net_GetDirectMessages_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
@@ -80,6 +89,10 @@ public class Net_GetDirectMessages_Handler implements JsonMessageHandler {
|
|||||||
item.setCreatedAtMs(entry.getCreatedAtMs());
|
item.setCreatedAtMs(entry.getCreatedAtMs());
|
||||||
item.setReadAtMs(entry.getReadAtMs());
|
item.setReadAtMs(entry.getReadAtMs());
|
||||||
item.setBlobB64(Base64.getEncoder().encodeToString(entry.getRawBlock()));
|
item.setBlobB64(Base64.getEncoder().encodeToString(entry.getRawBlock()));
|
||||||
|
DmDeliveryStateEntry delivery = deliveryByKey.get(entry.getMessageKey());
|
||||||
|
if (delivery != null) {
|
||||||
|
item.setDeliveryState(delivery.deliveryStateCode());
|
||||||
|
}
|
||||||
items.add(item);
|
items.add(item);
|
||||||
}
|
}
|
||||||
resp.setMessages(items);
|
resp.setMessages(items);
|
||||||
|
|||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDmDeliveryStatus_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDmDeliveryStatus_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.dao.DmDeliveryStateDAO;
|
||||||
|
import shine.db.entities.DmDeliveryStateEntry;
|
||||||
|
|
||||||
|
/** Read-only проверка: доставил ли peer хотя бы одну копию получателю. */
|
||||||
|
public class Net_GetDmDeliveryStatus_Handler implements JsonMessageHandler {
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
|
||||||
|
Net_GetDmDeliveryStatus_Request req = (Net_GetDmDeliveryStatus_Request) baseRequest;
|
||||||
|
if (req.getMessageKey() == null || req.getMessageKey().isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req, WireCodes.Status.BAD_REQUEST, "EMPTY_MESSAGE_KEY", "messageKey обязателен");
|
||||||
|
}
|
||||||
|
String messageKey = req.getMessageKey().trim();
|
||||||
|
DmDeliveryStateEntry state = DmDeliveryStateDAO.getInstance().getByOutgoingMessageKey(messageKey);
|
||||||
|
|
||||||
|
Net_GetDmDeliveryStatus_Response resp = new Net_GetDmDeliveryStatus_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setMessageKey(messageKey);
|
||||||
|
resp.setKnown(state != null);
|
||||||
|
resp.setDelivered(state != null && state.isDelivered());
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_MarkAllUserSettingsUnsynced_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_MarkAllUserSettingsUnsynced_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.dao.UserSettingsDAO;
|
||||||
|
import shine.db.dao.DmSyncOutboxDAO;
|
||||||
|
import server.sync.DmSyncWakeSignal;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
|
||||||
|
public class Net_MarkAllUserSettingsUnsynced_Handler implements JsonMessageHandler {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Net_MarkAllUserSettingsUnsynced_Handler.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||||
|
Net_MarkAllUserSettingsUnsynced_Request req = (Net_MarkAllUserSettingsUnsynced_Request) baseRequest;
|
||||||
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
|
int updated;
|
||||||
|
int dmUpdated;
|
||||||
|
if (req.getLogin() == null || req.getLogin().isBlank()) {
|
||||||
|
updated = UserSettingsDAO.getInstance().markAllUnsynced(c);
|
||||||
|
dmUpdated = DmSyncOutboxDAO.getInstance().markAllUnsynced();
|
||||||
|
} else {
|
||||||
|
updated = UserSettingsDAO.getInstance().markAllUnsynced(c, req.getLogin().trim());
|
||||||
|
dmUpdated = DmSyncOutboxDAO.getInstance().markAllUnsynced(req.getLogin().trim());
|
||||||
|
}
|
||||||
|
DmSyncWakeSignal.request();
|
||||||
|
Net_MarkAllUserSettingsUnsynced_Response resp = new Net_MarkAllUserSettingsUnsynced_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setUpdated(updated);
|
||||||
|
resp.setDmUpdated(dmUpdated);
|
||||||
|
return resp;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("MarkAllUserSettingsUnsynced failed", e);
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "INTERNAL_ERROR", "Внутренняя ошибка сервера");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
-5
@@ -8,9 +8,12 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_ReceiveIncomingMessag
|
|||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_ReceiveIncomingMessage_Response;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_ReceiveIncomingMessage_Response;
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import server.sync.DmDeliveryCoordinator;
|
||||||
|
import shine.db.dao.DmSyncOutboxDAO;
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
import shine.db.entities.SignedMessageEntry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
|
||||||
public class Net_ReceiveIncomingMessage_Handler implements JsonMessageHandler {
|
public class Net_ReceiveIncomingMessage_Handler implements JsonMessageHandler {
|
||||||
@@ -40,6 +43,10 @@ public class Net_ReceiveIncomingMessage_Handler implements JsonMessageHandler {
|
|||||||
return NetExceptionResponseFactory.error(req, status, code, "Сообщение не прошло проверку");
|
return NetExceptionResponseFactory.error(req, status, code, "Сообщение не прошло проверку");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!DmDeliveryCoordinator.isLocalAccessServer(incoming.toLogin)) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 403, "LOCAL_SERVER_NOT_ACCESS_SERVER", "Сервер не обслуживает получателя сообщения");
|
||||||
|
}
|
||||||
|
|
||||||
final SignedMessageEntry entry;
|
final SignedMessageEntry entry;
|
||||||
try {
|
try {
|
||||||
entry = SignedMessagesCore.toEntry(incoming, "ReceiveIncomingMessage", null);
|
entry = SignedMessagesCore.toEntry(incoming, "ReceiveIncomingMessage", null);
|
||||||
@@ -53,16 +60,27 @@ public class Net_ReceiveIncomingMessage_Handler implements JsonMessageHandler {
|
|||||||
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
||||||
if (status.applied()) {
|
if (status.applied()) {
|
||||||
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, incoming);
|
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, incoming);
|
||||||
server.sync.DmFederationService.fanOutIncomingToRecipientAccessServers(
|
|
||||||
incoming.toLogin,
|
|
||||||
req.getIncomingBlobB64().trim(),
|
|
||||||
req.getSourceServerLogin()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (status == SignedMessagesDAO.ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE) {
|
if (status == SignedMessagesDAO.ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE) {
|
||||||
bounceConversationDeleteIfKnown(incoming.fromLogin, incoming.toLogin);
|
bounceConversationDeleteIfKnown(incoming.fromLogin, incoming.toLogin);
|
||||||
|
return NetExceptionResponseFactory.error(req, 409, "BLOCKED_BY_CONVERSATION_TOMBSTONE", "Переписка уже удалена этой ревизией");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SignedMessageEntry stored = SignedMessagesDAO.getInstance().getByMessageKey(entry.getMessageKey());
|
||||||
|
if (stored == null || !Arrays.equals(stored.getRawBlock(), entry.getRawBlock())) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 409, "STALE_MESSAGE_REVISION", "На сервере уже есть более новая ревизия сообщения");
|
||||||
|
}
|
||||||
|
boolean receivedFromRecipientPeer = DmDeliveryCoordinator.sourceIsOtherAccessServer(
|
||||||
|
incoming.toLogin, req.getSourceServerLogin());
|
||||||
|
DmSyncOutboxDAO.getInstance().upsert(
|
||||||
|
incoming.toLogin,
|
||||||
|
entry.getMessageKey(),
|
||||||
|
DmDeliveryIds.forEntry(entry),
|
||||||
|
null,
|
||||||
|
receivedFromRecipientPeer,
|
||||||
|
entry.getCreatedAtMs()
|
||||||
|
);
|
||||||
|
|
||||||
Net_ReceiveIncomingMessage_Response resp = new Net_ReceiveIncomingMessage_Response();
|
Net_ReceiveIncomingMessage_Response resp = new Net_ReceiveIncomingMessage_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
resp.setRequestId(req.getRequestId());
|
resp.setRequestId(req.getRequestId());
|
||||||
|
|||||||
-208
@@ -1,208 +0,0 @@
|
|||||||
package server.logic.ws_protocol.JSON.messages;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
||||||
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
|
|
||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendDirectMessage_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendDirectMessage_Response;
|
|
||||||
import server.logic.ws_protocol.JSON.push.WebPushSender;
|
|
||||||
import server.logic.ws_protocol.JSON.push.WsEventSender;
|
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
|
||||||
import server.logic.ws_protocol.JSON.utils.NetIdGenerator;
|
|
||||||
import server.logic.ws_protocol.WireCodes;
|
|
||||||
import shine.db.dao.ActiveSessionsDAO;
|
|
||||||
import shine.db.dao.DirectMessagesDAO;
|
|
||||||
import shine.db.dao.SignedDirectMessagesHistoryDAO;
|
|
||||||
import shine.db.dao.SignedDmReplayDAO;
|
|
||||||
import shine.db.dao.CurrentUsersDAO;
|
|
||||||
import shine.db.entities.ActiveSessionEntry;
|
|
||||||
import shine.db.entities.DirectMessageEntry;
|
|
||||||
import shine.db.entities.SignedDirectMessageHistoryEntry;
|
|
||||||
import shine.db.entities.CurrentUserEntry;
|
|
||||||
import utils.crypto.Ed25519Util;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class Net_SendDirectMessage_Handler implements JsonMessageHandler {
|
|
||||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
|
||||||
private static final long REPLAY_TTL_MS = 15L * 60L * 1000L;
|
|
||||||
private static final int MAX_MESSAGE_BYTES = 3000;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
|
|
||||||
Net_SendDirectMessage_Request req = (Net_SendDirectMessage_Request) baseRequest;
|
|
||||||
if (req.getBlobB64() == null || req.getBlobB64().isBlank()) {
|
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "blobB64 обязателен");
|
|
||||||
}
|
|
||||||
|
|
||||||
final byte[] raw;
|
|
||||||
final SignedDirectMessagePacket packet;
|
|
||||||
try {
|
|
||||||
raw = Base64.getDecoder().decode(req.getBlobB64().trim());
|
|
||||||
packet = SignedDirectMessagePacket.parse(raw, MAX_MESSAGE_BYTES);
|
|
||||||
} catch (IllegalArgumentException ex) {
|
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, ex.getMessage(), "Некорректный формат пакета");
|
|
||||||
}
|
|
||||||
|
|
||||||
CurrentUserEntry fromUser = CurrentUsersDAO.getInstance().getByLogin(packet.fromLogin);
|
|
||||||
CurrentUserEntry toUser = CurrentUsersDAO.getInstance().getByLogin(packet.toLogin);
|
|
||||||
if (fromUser == null || toUser == null) {
|
|
||||||
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "from/to пользователь не найден");
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] publicKey32;
|
|
||||||
try {
|
|
||||||
publicKey32 = Ed25519Util.keyFromBase64(fromUser.getClientKey());
|
|
||||||
} catch (Exception e) {
|
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_DEVICE_KEY", "Некорректный clientKey отправителя");
|
|
||||||
}
|
|
||||||
if (!Ed25519Util.verify(packet.signedBody, packet.signature64, publicKey32)) {
|
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_SIGNATURE", "Подпись не прошла проверку");
|
|
||||||
}
|
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
|
||||||
if (Math.abs(now - packet.timeMs) > REPLAY_TTL_MS) {
|
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_TIME_WINDOW", "Время сообщения вышло за окно 15 минут");
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean replayOk = SignedDmReplayDAO.getInstance().registerUnique(packet.fromLogin, packet.timeMs, packet.nonce, now);
|
|
||||||
if (!replayOk) {
|
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "REPLAY", "Повторное сообщение заблокировано");
|
|
||||||
}
|
|
||||||
|
|
||||||
String messageId = NetIdGenerator.eventId("msg");
|
|
||||||
String textForUi = new String(packet.messageBytes, StandardCharsets.UTF_8);
|
|
||||||
|
|
||||||
DirectMessageEntry entry = new DirectMessageEntry();
|
|
||||||
entry.setMessageId(messageId);
|
|
||||||
entry.setFromLogin(packet.fromLogin);
|
|
||||||
entry.setToLogin(packet.toLogin);
|
|
||||||
entry.setText(textForUi);
|
|
||||||
entry.setCreatedAtMs(now);
|
|
||||||
DirectMessagesDAO.getInstance().insert(entry);
|
|
||||||
|
|
||||||
SignedDirectMessageHistoryEntry history = new SignedDirectMessageHistoryEntry();
|
|
||||||
history.setMessageId(messageId);
|
|
||||||
history.setFromLogin(packet.fromLogin);
|
|
||||||
history.setToLogin(packet.toLogin);
|
|
||||||
history.setTargetMode(packet.targetMode);
|
|
||||||
history.setTargetSessionId(packet.targetSessionId);
|
|
||||||
history.setMessageType(packet.messageType);
|
|
||||||
history.setTimeMs(packet.timeMs);
|
|
||||||
history.setNonce(packet.nonce);
|
|
||||||
history.setRawPacket(packet.rawPacket);
|
|
||||||
history.setCreatedAtMs(now);
|
|
||||||
SignedDirectMessagesHistoryDAO.getInstance().insert(history);
|
|
||||||
|
|
||||||
DeliveryResult delivery = deliver(packet, req.getBlobB64().trim(), messageId, now);
|
|
||||||
|
|
||||||
Net_SendDirectMessage_Response resp = new Net_SendDirectMessage_Response();
|
|
||||||
resp.setOp(req.getOp());
|
|
||||||
resp.setRequestId(req.getRequestId());
|
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
|
||||||
resp.setMessageId(messageId);
|
|
||||||
resp.setDeliveredWsSessions(delivery.wsDelivered);
|
|
||||||
resp.setDeliveredWebPushSessions(delivery.webPushDelivered);
|
|
||||||
resp.setSessionNotFound(delivery.sessionNotFound);
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DeliveryResult deliver(SignedDirectMessagePacket packet, String blobB64, String messageId, long createdAtMs) throws Exception {
|
|
||||||
DeliveryResult result = new DeliveryResult();
|
|
||||||
|
|
||||||
Set<String> selectedSessionIds = new HashSet<>();
|
|
||||||
if (packet.targetMode == SignedDirectMessagePacket.TARGET_ONE_SESSION) {
|
|
||||||
ActiveSessionEntry byId = ActiveSessionsDAO.getInstance().getBySessionId(packet.targetSessionId);
|
|
||||||
if (byId == null || !packet.toLogin.equalsIgnoreCase(byId.getLogin())) {
|
|
||||||
result.sessionNotFound = true;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
selectedSessionIds.add(byId.getSessionId());
|
|
||||||
deliverToSession(packet, blobB64, messageId, createdAtMs, byId.getSessionId(), result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ActiveSessionEntry> sessions = ActiveSessionsDAO.getInstance().getByLogin(packet.toLogin);
|
|
||||||
for (ActiveSessionEntry s : sessions) {
|
|
||||||
selectedSessionIds.add(s.getSessionId());
|
|
||||||
deliverToSession(packet, blobB64, messageId, createdAtMs, s.getSessionId(), result);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deliverToSession(
|
|
||||||
SignedDirectMessagePacket packet,
|
|
||||||
String blobB64,
|
|
||||||
String messageId,
|
|
||||||
long createdAtMs,
|
|
||||||
String sessionId,
|
|
||||||
DeliveryResult result
|
|
||||||
) {
|
|
||||||
ConnectionContext targetCtx = ActiveConnectionsRegistry.getInstance().getBySessionId(sessionId);
|
|
||||||
boolean wsDelivered = false;
|
|
||||||
if (targetCtx != null) {
|
|
||||||
String eventId = NetIdGenerator.eventId("evt");
|
|
||||||
CompletableFuture<Boolean> waiter = DeliveryTracker.getInstance().register(eventId);
|
|
||||||
ObjectNode payload = MAPPER.createObjectNode();
|
|
||||||
payload.put("eventId", eventId);
|
|
||||||
payload.put("messageId", messageId);
|
|
||||||
payload.put("fromLogin", packet.fromLogin);
|
|
||||||
payload.put("toLogin", packet.toLogin);
|
|
||||||
payload.put("blobB64", blobB64);
|
|
||||||
payload.put("text", new String(packet.messageBytes, StandardCharsets.UTF_8));
|
|
||||||
payload.put("timeMs", createdAtMs);
|
|
||||||
|
|
||||||
boolean sent = WsEventSender.sendEvent(targetCtx, "IncomingDirectMessage", eventId, payload);
|
|
||||||
if (sent) {
|
|
||||||
try {
|
|
||||||
wsDelivered = waiter.get(1200, TimeUnit.MILLISECONDS);
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
wsDelivered = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DeliveryTracker.getInstance().remove(eventId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wsDelivered) {
|
|
||||||
result.wsDelivered++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
ActiveSessionEntry targetSession = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
|
|
||||||
if (targetSession == null) return;
|
|
||||||
if (isBlank(targetSession.getPushEndpoint()) || isBlank(targetSession.getPushP256dhKey()) || isBlank(targetSession.getPushAuthKey())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
boolean pushed = WebPushSender.sendBase64Payload(
|
|
||||||
targetSession.getPushEndpoint(),
|
|
||||||
targetSession.getPushP256dhKey(),
|
|
||||||
targetSession.getPushAuthKey(),
|
|
||||||
blobB64
|
|
||||||
);
|
|
||||||
if (pushed) result.webPushDelivered++;
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
// ignore per-session push errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isBlank(String s) {
|
|
||||||
return s == null || s.isBlank();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class DeliveryResult {
|
|
||||||
int wsDelivered;
|
|
||||||
int webPushDelivered;
|
|
||||||
boolean sessionNotFound;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+53
-8
@@ -8,10 +8,15 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_SendMessagePair_Reque
|
|||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendMessagePair_Response;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendMessagePair_Response;
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import server.sync.DmDeliveryCoordinator;
|
||||||
import server.sync.DmFederationService;
|
import server.sync.DmFederationService;
|
||||||
|
import shine.db.dao.DmDeliveryStateDAO;
|
||||||
|
import shine.db.dao.DmSyncOutboxDAO;
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
|
import shine.db.entities.DmDeliveryStateEntry;
|
||||||
import shine.db.entities.SignedMessageEntry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
|
||||||
public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
||||||
@@ -43,8 +48,9 @@ public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
SignedMessageEntry incomingEntry;
|
SignedMessageEntry incomingEntry;
|
||||||
SignedMessageEntry outgoingEntry;
|
SignedMessageEntry outgoingEntry;
|
||||||
|
boolean fromPeer = !isBlank(req.getSourceServerLogin());
|
||||||
try {
|
try {
|
||||||
String sourceApi = "SendMessagePair";
|
String sourceApi = fromPeer ? "ReceiveOutcomingMessage" : "SendMessagePair";
|
||||||
String originSessionId = (ctx != null && !isBlank(ctx.getSessionId())) ? ctx.getSessionId() : null;
|
String originSessionId = (ctx != null && !isBlank(ctx.getSessionId())) ? ctx.getSessionId() : null;
|
||||||
incomingEntry = SignedMessagesCore.toEntry(incoming, sourceApi, originSessionId);
|
incomingEntry = SignedMessagesCore.toEntry(incoming, sourceApi, originSessionId);
|
||||||
outgoingEntry = SignedMessagesCore.toEntry(outgoing, sourceApi, originSessionId);
|
outgoingEntry = SignedMessagesCore.toEntry(outgoing, sourceApi, originSessionId);
|
||||||
@@ -79,15 +85,53 @@ public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
if (pairStatus == SignedMessagesDAO.ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE) {
|
if (pairStatus == SignedMessagesDAO.ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE) {
|
||||||
bounceConversationDeleteIfKnown(incoming.fromLogin, incoming.toLogin);
|
bounceConversationDeleteIfKnown(incoming.fromLogin, incoming.toLogin);
|
||||||
|
return NetExceptionResponseFactory.error(req, 409, "BLOCKED_BY_CONVERSATION_TOMBSTONE", "Переписка уже удалена этой ревизией");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pairStatus.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
SignedMessageEntry storedOutgoing = SignedMessagesDAO.getInstance().getByMessageKey(outgoingEntry.getMessageKey());
|
||||||
DmFederationService.fanOutPair(
|
if (storedOutgoing == null || !Arrays.equals(storedOutgoing.getRawBlock(), outgoingEntry.getRawBlock())) {
|
||||||
incoming.fromLogin,
|
return NetExceptionResponseFactory.error(req, 409, "STALE_MESSAGE_REVISION", "На сервере уже есть более новая ревизия сообщения");
|
||||||
incoming.toLogin,
|
}
|
||||||
req.getIncomingBlobB64().trim(),
|
|
||||||
req.getOutgoingBlobB64().trim()
|
String eventId = DmDeliveryIds.forEntry(outgoingEntry);
|
||||||
);
|
long nowMs = System.currentTimeMillis();
|
||||||
|
long acceptedAtMs;
|
||||||
|
long signedAtMs = Math.max(outgoing.timeMs,
|
||||||
|
Math.max(outgoing.revisionTimeMs, outgoing.reencryptedAtMs));
|
||||||
|
acceptedAtMs = fromPeer && signedAtMs > 0L ? Math.min(nowMs, signedAtMs) : nowMs;
|
||||||
|
long expiresAtMs = acceptedAtMs + 60L * 60L * 1000L;
|
||||||
|
int initialState = DmDeliveryStateEntry.ACCEPTED;
|
||||||
|
DmDeliveryStateEntry delivery = DmDeliveryStateDAO.getInstance().upsertPair(
|
||||||
|
outgoingEntry.getMessageKey(), eventId, outgoingEntry.getBaseKey(),
|
||||||
|
outgoingEntry.getFromLogin(), outgoingEntry.getToLogin(), incomingEntry.getMessageKey(),
|
||||||
|
acceptedAtMs, expiresAtMs, initialState,
|
||||||
|
null, null, fromPeer && initialState == DmDeliveryStateEntry.PENDING_NONE
|
||||||
|
);
|
||||||
|
|
||||||
|
DmSyncOutboxDAO.getInstance().upsert(
|
||||||
|
outgoingEntry.getFromLogin(), outgoingEntry.getMessageKey(), eventId,
|
||||||
|
incomingEntry.getMessageKey(), fromPeer, acceptedAtMs);
|
||||||
|
|
||||||
|
// Если этот же сервер также обслуживает получателя, его входящая копия
|
||||||
|
// имеет отдельный sync-флаг владельца-получателя.
|
||||||
|
if (DmDeliveryCoordinator.isLocalAccessServer(incomingEntry.getToLogin())) {
|
||||||
|
boolean incomingAlreadySynced = DmDeliveryCoordinator.sourceIsOtherAccessServer(
|
||||||
|
incomingEntry.getToLogin(), req.getSourceServerLogin());
|
||||||
|
DmSyncOutboxDAO.getInstance().upsert(
|
||||||
|
incomingEntry.getToLogin(), incomingEntry.getMessageKey(), DmDeliveryIds.forEntry(incomingEntry),
|
||||||
|
null, incomingAlreadySynced, acceptedAtMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delivery != null && pairStatus.applied()) {
|
||||||
|
if (fromPeer) {
|
||||||
|
DmDeliveryCoordinator.assistReceivedPairAsync(delivery.getEventId());
|
||||||
|
} else {
|
||||||
|
// Первая доставка выполняется до ответа клиенту. Два сервера
|
||||||
|
// получателя вызываются параллельно внутри координатора.
|
||||||
|
DmDeliveryCoordinator.processDueEntry(delivery);
|
||||||
|
delivery = DmDeliveryStateDAO.getInstance()
|
||||||
|
.getByOutgoingMessageKey(outgoingEntry.getMessageKey());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Net_SendMessagePair_Response resp = new Net_SendMessagePair_Response();
|
Net_SendMessagePair_Response resp = new Net_SendMessagePair_Response();
|
||||||
@@ -99,6 +143,7 @@ public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
|||||||
resp.setOutgoingKey(outgoingEntry.getMessageKey());
|
resp.setOutgoingKey(outgoingEntry.getMessageKey());
|
||||||
resp.setDeliveredWsSessions(inCounters.wsDelivered + outCounters.wsDelivered);
|
resp.setDeliveredWsSessions(inCounters.wsDelivered + outCounters.wsDelivered);
|
||||||
resp.setDeliveredWebPushSessions(inCounters.pushDelivered + outCounters.pushDelivered);
|
resp.setDeliveredWebPushSessions(inCounters.pushDelivered + outCounters.pushDelivered);
|
||||||
|
resp.setDeliveryState(delivery.deliveryStateCode());
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_UserSettingsSyncBatch_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_UserSettingsSyncBatch_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.dao.UserAccessServersCurrentDAO;
|
||||||
|
import shine.db.dao.UserSettingsDAO;
|
||||||
|
import shine.db.entities.UserSettingEntry;
|
||||||
|
import shine.db.entities.UserAccessServerRouteEntry;
|
||||||
|
import utils.config.AppConfig;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Net_UserSettingsSyncBatch_Handler implements JsonMessageHandler {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Net_UserSettingsSyncBatch_Handler.class);
|
||||||
|
private static final int DEFAULT_LIMIT = 500;
|
||||||
|
private static final int MAX_LIMIT = 1000;
|
||||||
|
private static final int DEFAULT_MAX_BYTES = 3_000_000;
|
||||||
|
private static final int MAX_BYTES_CAP = 5_000_000;
|
||||||
|
private static final String SERVER_LOGIN_CONFIG = "server.SHiNE.login";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
|
||||||
|
Net_UserSettingsSyncBatch_Request req = (Net_UserSettingsSyncBatch_Request) baseRequest;
|
||||||
|
String ownerLogin = normalizeOriginal(req.getOwnerLogin());
|
||||||
|
if (ownerLogin == null) {
|
||||||
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "EMPTY_OWNER_LOGIN", "ownerLogin обязателен");
|
||||||
|
}
|
||||||
|
|
||||||
|
String ownServerLogin = normalize(AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG));
|
||||||
|
if (ownServerLogin == null) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 500, "LOCAL_SERVER_NOT_CONFIGURED", "server.SHiNE.login не настроен");
|
||||||
|
}
|
||||||
|
if (!isLocalAccessServer(ownerLogin, ownServerLogin)) {
|
||||||
|
return NetExceptionResponseFactory.error(req, 403, "LOCAL_SERVER_NOT_ACCESS_SERVER", "Локальный сервер не является access-сервером пользователя");
|
||||||
|
}
|
||||||
|
|
||||||
|
int limit = clamp(req.getLimit() == null ? DEFAULT_LIMIT : req.getLimit(), 1, MAX_LIMIT);
|
||||||
|
int maxBytes = clamp(req.getMaxBytes() == null ? DEFAULT_MAX_BYTES : req.getMaxBytes(), 64_000, MAX_BYTES_CAP);
|
||||||
|
long afterTimeMs = Math.max(0L, req.getAfterTimeMs() == null ? 0L : req.getAfterTimeMs());
|
||||||
|
String afterSettingKey = req.getAfterSettingKey() == null ? "" : req.getAfterSettingKey().trim();
|
||||||
|
|
||||||
|
List<UserSettingEntry> batch;
|
||||||
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
|
batch = UserSettingsDAO.getInstance().listNewerThan(
|
||||||
|
c,
|
||||||
|
ownerLogin,
|
||||||
|
afterTimeMs,
|
||||||
|
afterSettingKey,
|
||||||
|
limit
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Net_UserSettingsSyncBatch_Response resp = new Net_UserSettingsSyncBatch_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setOwnerLogin(ownerLogin);
|
||||||
|
resp.setLimit(limit);
|
||||||
|
|
||||||
|
int rawBytes = 0;
|
||||||
|
List<Net_UserSettingsSyncBatch_Response.Item> items = new ArrayList<>();
|
||||||
|
for (UserSettingEntry entry : batch) {
|
||||||
|
Net_UserSettingsSyncBatch_Response.Item item = new Net_UserSettingsSyncBatch_Response.Item();
|
||||||
|
item.setLogin(entry.getLogin());
|
||||||
|
item.setSetting_type(entry.getSettingType());
|
||||||
|
item.setSetting_key(entry.getSettingKey());
|
||||||
|
item.setTime_ms(entry.getTimeMs());
|
||||||
|
item.setValue_text(entry.getValueText());
|
||||||
|
item.setValue_num(entry.getValueNum());
|
||||||
|
item.setClient_key(entry.getClientKey());
|
||||||
|
item.setSignature(entry.getSignature());
|
||||||
|
item.setSynced(entry.isSynced());
|
||||||
|
items.add(item);
|
||||||
|
rawBytes += String.valueOf(entry.getLogin()).length()
|
||||||
|
+ String.valueOf(entry.getSettingKey()).length()
|
||||||
|
+ String.valueOf(entry.getValueText()).length()
|
||||||
|
+ String.valueOf(entry.getClientKey() == null ? "" : entry.getClientKey()).length()
|
||||||
|
+ String.valueOf(entry.getSignature() == null ? "" : entry.getSignature()).length()
|
||||||
|
+ 64;
|
||||||
|
if (rawBytes > maxBytes) break;
|
||||||
|
resp.setNextTimeMs(entry.getTimeMs());
|
||||||
|
resp.setNextSettingKey(entry.getSettingKey());
|
||||||
|
}
|
||||||
|
resp.setRawBytes(rawBytes);
|
||||||
|
resp.setHasMore(batch.size() > items.size());
|
||||||
|
resp.setItems(items);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isLocalAccessServer(String ownerLogin, String ownServerLogin) throws Exception {
|
||||||
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
|
for (UserAccessServerRouteEntry route : UserAccessServersCurrentDAO.getInstance().listByUserLogin(c, ownerLogin)) {
|
||||||
|
if (route == null || route.getServerLogin() == null) continue;
|
||||||
|
if (ownServerLogin.equals(normalize(route.getServerLogin()))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int clamp(int value, int min, int max) {
|
||||||
|
return Math.max(min, Math.min(max, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalize(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
String s = value.trim().toLowerCase();
|
||||||
|
return s.isEmpty() ? null : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeOriginal(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
String s = value.trim();
|
||||||
|
return s.isEmpty() ? null : s;
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_UserSettingsSyncBatch_Response;
|
||||||
|
import shine.db.dao.UserSettingsDAO;
|
||||||
|
import shine.db.entities.UserSettingEntry;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class UserSettingsSyncApplySupport {
|
||||||
|
private UserSettingsSyncApplySupport() {}
|
||||||
|
|
||||||
|
public static ApplyResult applySyncedItem(Connection c, String ownerLogin, Net_UserSettingsSyncBatch_Response.Item item) throws Exception {
|
||||||
|
if (item == null) return new ApplyResult(false, "empty_item");
|
||||||
|
String login = normalize(item.getLogin());
|
||||||
|
String key = normalize(item.getSetting_key());
|
||||||
|
if (login == null || key == null) return new ApplyResult(false, "bad_item");
|
||||||
|
|
||||||
|
UserSettingEntry entry = new UserSettingEntry(
|
||||||
|
login,
|
||||||
|
item.getSetting_type() == null ? 0 : item.getSetting_type(),
|
||||||
|
key,
|
||||||
|
item.getTime_ms() == null ? 0L : item.getTime_ms(),
|
||||||
|
item.getValue_text() == null ? "" : item.getValue_text(),
|
||||||
|
item.getValue_num() == null ? 0L : item.getValue_num(),
|
||||||
|
item.getClient_key(),
|
||||||
|
item.getSignature(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
int changed = UserSettingsDAO.getInstance().upsertIfNewer(c, entry);
|
||||||
|
return new ApplyResult(changed > 0, changed > 0 ? "applied" : "ignored");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<UserSettingEntry> toEntries(List<Net_UserSettingsSyncBatch_Response.Item> items) {
|
||||||
|
List<UserSettingEntry> out = new ArrayList<>();
|
||||||
|
if (items == null) return out;
|
||||||
|
for (Net_UserSettingsSyncBatch_Response.Item item : items) {
|
||||||
|
if (item == null) continue;
|
||||||
|
out.add(new UserSettingEntry(
|
||||||
|
normalize(item.getLogin()),
|
||||||
|
item.getSetting_type() == null ? 0 : item.getSetting_type(),
|
||||||
|
normalize(item.getSetting_key()),
|
||||||
|
item.getTime_ms() == null ? 0L : item.getTime_ms(),
|
||||||
|
item.getValue_text() == null ? "" : item.getValue_text(),
|
||||||
|
item.getValue_num() == null ? 0L : item.getValue_num(),
|
||||||
|
item.getClient_key(),
|
||||||
|
item.getSignature(),
|
||||||
|
true
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalize(String value) {
|
||||||
|
String s = String.valueOf(value == null ? "" : value).trim();
|
||||||
|
return s.isEmpty() ? null : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ApplyResult(boolean applied, String status) {}
|
||||||
|
}
|
||||||
+8
@@ -2,12 +2,16 @@ package server.logic.ws_protocol.JSON.messages.entyties;
|
|||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class Net_DmSyncBatch_Request extends Net_Request {
|
public class Net_DmSyncBatch_Request extends Net_Request {
|
||||||
private String ownerLogin;
|
private String ownerLogin;
|
||||||
private Long afterStoredAtMs;
|
private Long afterStoredAtMs;
|
||||||
private String afterMessageKey;
|
private String afterMessageKey;
|
||||||
private Integer limit;
|
private Integer limit;
|
||||||
private Integer maxBytes;
|
private Integer maxBytes;
|
||||||
|
private List<String> ackSyncIds = new ArrayList<>();
|
||||||
|
|
||||||
public String getOwnerLogin() { return ownerLogin; }
|
public String getOwnerLogin() { return ownerLogin; }
|
||||||
public void setOwnerLogin(String ownerLogin) { this.ownerLogin = ownerLogin; }
|
public void setOwnerLogin(String ownerLogin) { this.ownerLogin = ownerLogin; }
|
||||||
@@ -19,4 +23,8 @@ public class Net_DmSyncBatch_Request extends Net_Request {
|
|||||||
public void setLimit(Integer limit) { this.limit = limit; }
|
public void setLimit(Integer limit) { this.limit = limit; }
|
||||||
public Integer getMaxBytes() { return maxBytes; }
|
public Integer getMaxBytes() { return maxBytes; }
|
||||||
public void setMaxBytes(Integer maxBytes) { this.maxBytes = maxBytes; }
|
public void setMaxBytes(Integer maxBytes) { this.maxBytes = maxBytes; }
|
||||||
|
public List<String> getAckSyncIds() { return ackSyncIds; }
|
||||||
|
public void setAckSyncIds(List<String> ackSyncIds) {
|
||||||
|
this.ackSyncIds = ackSyncIds == null ? new ArrayList<>() : ackSyncIds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-24
@@ -30,33 +30,20 @@ public class Net_DmSyncBatch_Response extends Net_Response {
|
|||||||
public void setItems(List<Item> items) { this.items = items; }
|
public void setItems(List<Item> items) { this.items = items; }
|
||||||
|
|
||||||
public static class Item {
|
public static class Item {
|
||||||
private String messageKey;
|
private String syncId;
|
||||||
private String baseKey;
|
private String primaryMessageKey;
|
||||||
private String targetLogin;
|
|
||||||
private String fromLogin;
|
|
||||||
private String toLogin;
|
|
||||||
private int messageType;
|
|
||||||
private long timeMs;
|
|
||||||
private long storedAtMs;
|
private long storedAtMs;
|
||||||
private String blobB64;
|
private List<String> blobsB64 = new ArrayList<>();
|
||||||
|
|
||||||
public String getMessageKey() { return messageKey; }
|
public String getSyncId() { return syncId; }
|
||||||
public void setMessageKey(String messageKey) { this.messageKey = messageKey; }
|
public void setSyncId(String syncId) { this.syncId = syncId; }
|
||||||
public String getBaseKey() { return baseKey; }
|
public String getPrimaryMessageKey() { return primaryMessageKey; }
|
||||||
public void setBaseKey(String baseKey) { this.baseKey = baseKey; }
|
public void setPrimaryMessageKey(String primaryMessageKey) { this.primaryMessageKey = primaryMessageKey; }
|
||||||
public String getTargetLogin() { return targetLogin; }
|
|
||||||
public void setTargetLogin(String targetLogin) { this.targetLogin = targetLogin; }
|
|
||||||
public String getFromLogin() { return fromLogin; }
|
|
||||||
public void setFromLogin(String fromLogin) { this.fromLogin = fromLogin; }
|
|
||||||
public String getToLogin() { return toLogin; }
|
|
||||||
public void setToLogin(String toLogin) { this.toLogin = toLogin; }
|
|
||||||
public int getMessageType() { return messageType; }
|
|
||||||
public void setMessageType(int messageType) { this.messageType = messageType; }
|
|
||||||
public long getTimeMs() { return timeMs; }
|
|
||||||
public void setTimeMs(long timeMs) { this.timeMs = timeMs; }
|
|
||||||
public long getStoredAtMs() { return storedAtMs; }
|
public long getStoredAtMs() { return storedAtMs; }
|
||||||
public void setStoredAtMs(long storedAtMs) { this.storedAtMs = storedAtMs; }
|
public void setStoredAtMs(long storedAtMs) { this.storedAtMs = storedAtMs; }
|
||||||
public String getBlobB64() { return blobB64; }
|
public List<String> getBlobsB64() { return blobsB64; }
|
||||||
public void setBlobB64(String blobB64) { this.blobB64 = blobB64; }
|
public void setBlobsB64(List<String> blobsB64) {
|
||||||
|
this.blobsB64 = blobsB64 == null ? new ArrayList<>() : blobsB64;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -42,6 +42,7 @@ public class Net_GetDirectMessages_Response extends Net_Response {
|
|||||||
private long createdAtMs;
|
private long createdAtMs;
|
||||||
private Long readAtMs;
|
private Long readAtMs;
|
||||||
private String blobB64;
|
private String blobB64;
|
||||||
|
private String deliveryState;
|
||||||
|
|
||||||
public String getMessageKey() { return messageKey; }
|
public String getMessageKey() { return messageKey; }
|
||||||
public void setMessageKey(String messageKey) { this.messageKey = messageKey; }
|
public void setMessageKey(String messageKey) { this.messageKey = messageKey; }
|
||||||
@@ -67,5 +68,7 @@ public class Net_GetDirectMessages_Response extends Net_Response {
|
|||||||
public void setReadAtMs(Long readAtMs) { this.readAtMs = readAtMs; }
|
public void setReadAtMs(Long readAtMs) { this.readAtMs = readAtMs; }
|
||||||
public String getBlobB64() { return blobB64; }
|
public String getBlobB64() { return blobB64; }
|
||||||
public void setBlobB64(String blobB64) { this.blobB64 = blobB64; }
|
public void setBlobB64(String blobB64) { this.blobB64 = blobB64; }
|
||||||
|
public String getDeliveryState() { return deliveryState; }
|
||||||
|
public void setDeliveryState(String deliveryState) { this.deliveryState = deliveryState; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
public class Net_GetDmDeliveryStatus_Request extends Net_Request {
|
||||||
|
private String messageKey;
|
||||||
|
|
||||||
|
public String getMessageKey() { return messageKey; }
|
||||||
|
public void setMessageKey(String value) { this.messageKey = value; }
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
public class Net_GetDmDeliveryStatus_Response extends Net_Response {
|
||||||
|
private String messageKey;
|
||||||
|
private boolean known;
|
||||||
|
private boolean delivered;
|
||||||
|
|
||||||
|
public String getMessageKey() { return messageKey; }
|
||||||
|
public void setMessageKey(String value) { this.messageKey = value; }
|
||||||
|
public boolean isKnown() { return known; }
|
||||||
|
public void setKnown(boolean value) { this.known = value; }
|
||||||
|
public boolean isDelivered() { return delivered; }
|
||||||
|
public void setDelivered(boolean value) { this.delivered = value; }
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
public class Net_MarkAllUserSettingsUnsynced_Request extends Net_Request {
|
||||||
|
private String login;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
public class Net_MarkAllUserSettingsUnsynced_Response extends Net_Response {
|
||||||
|
private Integer updated;
|
||||||
|
private Integer dmUpdated;
|
||||||
|
|
||||||
|
public Integer getUpdated() { return updated; }
|
||||||
|
public void setUpdated(Integer updated) { this.updated = updated; }
|
||||||
|
public Integer getDmUpdated() { return dmUpdated; }
|
||||||
|
public void setDmUpdated(Integer dmUpdated) { this.dmUpdated = dmUpdated; }
|
||||||
|
}
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
package server.logic.ws_protocol.JSON.messages.entyties;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|
||||||
|
|
||||||
public class Net_SendDirectMessage_Request extends Net_Request {
|
|
||||||
private String blobB64;
|
|
||||||
|
|
||||||
public String getBlobB64() { return blobB64; }
|
|
||||||
public void setBlobB64(String blobB64) { this.blobB64 = blobB64; }
|
|
||||||
}
|
|
||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
package server.logic.ws_protocol.JSON.messages.entyties;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|
||||||
|
|
||||||
public class Net_SendDirectMessage_Response extends Net_Response {
|
|
||||||
private String messageId;
|
|
||||||
private int deliveredWsSessions;
|
|
||||||
private int deliveredWebPushSessions;
|
|
||||||
private boolean sessionNotFound;
|
|
||||||
|
|
||||||
public String getMessageId() { return messageId; }
|
|
||||||
public void setMessageId(String messageId) { this.messageId = messageId; }
|
|
||||||
public int getDeliveredWsSessions() { return deliveredWsSessions; }
|
|
||||||
public void setDeliveredWsSessions(int deliveredWsSessions) { this.deliveredWsSessions = deliveredWsSessions; }
|
|
||||||
public int getDeliveredWebPushSessions() { return deliveredWebPushSessions; }
|
|
||||||
public void setDeliveredWebPushSessions(int deliveredWebPushSessions) { this.deliveredWebPushSessions = deliveredWebPushSessions; }
|
|
||||||
public boolean isSessionNotFound() { return sessionNotFound; }
|
|
||||||
public void setSessionNotFound(boolean sessionNotFound) { this.sessionNotFound = sessionNotFound; }
|
|
||||||
}
|
|
||||||
+3
@@ -8,6 +8,7 @@ public class Net_SendMessagePair_Response extends Net_Response {
|
|||||||
private String outgoingKey;
|
private String outgoingKey;
|
||||||
private int deliveredWsSessions;
|
private int deliveredWsSessions;
|
||||||
private int deliveredWebPushSessions;
|
private int deliveredWebPushSessions;
|
||||||
|
private String deliveryState;
|
||||||
|
|
||||||
public String getBaseKey() { return baseKey; }
|
public String getBaseKey() { return baseKey; }
|
||||||
public void setBaseKey(String baseKey) { this.baseKey = baseKey; }
|
public void setBaseKey(String baseKey) { this.baseKey = baseKey; }
|
||||||
@@ -19,4 +20,6 @@ public class Net_SendMessagePair_Response extends Net_Response {
|
|||||||
public void setDeliveredWsSessions(int deliveredWsSessions) { this.deliveredWsSessions = deliveredWsSessions; }
|
public void setDeliveredWsSessions(int deliveredWsSessions) { this.deliveredWsSessions = deliveredWsSessions; }
|
||||||
public int getDeliveredWebPushSessions() { return deliveredWebPushSessions; }
|
public int getDeliveredWebPushSessions() { return deliveredWebPushSessions; }
|
||||||
public void setDeliveredWebPushSessions(int deliveredWebPushSessions) { this.deliveredWebPushSessions = deliveredWebPushSessions; }
|
public void setDeliveredWebPushSessions(int deliveredWebPushSessions) { this.deliveredWebPushSessions = deliveredWebPushSessions; }
|
||||||
|
public String getDeliveryState() { return deliveryState; }
|
||||||
|
public void setDeliveryState(String deliveryState) { this.deliveryState = deliveryState; }
|
||||||
}
|
}
|
||||||
|
|||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
public class Net_UserSettingsSyncBatch_Request extends Net_Request {
|
||||||
|
private String ownerLogin;
|
||||||
|
private Long afterTimeMs;
|
||||||
|
private String afterSettingKey;
|
||||||
|
private Integer limit;
|
||||||
|
private Integer maxBytes;
|
||||||
|
|
||||||
|
public String getOwnerLogin() { return ownerLogin; }
|
||||||
|
public void setOwnerLogin(String ownerLogin) { this.ownerLogin = ownerLogin; }
|
||||||
|
|
||||||
|
public Long getAfterTimeMs() { return afterTimeMs; }
|
||||||
|
public void setAfterTimeMs(Long afterTimeMs) { this.afterTimeMs = afterTimeMs; }
|
||||||
|
|
||||||
|
public String getAfterSettingKey() { return afterSettingKey; }
|
||||||
|
public void setAfterSettingKey(String afterSettingKey) { this.afterSettingKey = afterSettingKey; }
|
||||||
|
|
||||||
|
public Integer getLimit() { return limit; }
|
||||||
|
public void setLimit(Integer limit) { this.limit = limit; }
|
||||||
|
|
||||||
|
public Integer getMaxBytes() { return maxBytes; }
|
||||||
|
public void setMaxBytes(Integer maxBytes) { this.maxBytes = maxBytes; }
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.messages.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Net_UserSettingsSyncBatch_Response extends Net_Response {
|
||||||
|
private String ownerLogin;
|
||||||
|
private Integer limit;
|
||||||
|
private Integer rawBytes;
|
||||||
|
private Boolean hasMore;
|
||||||
|
private Long nextTimeMs;
|
||||||
|
private String nextSettingKey;
|
||||||
|
private List<Item> items = new ArrayList<>();
|
||||||
|
|
||||||
|
public String getOwnerLogin() { return ownerLogin; }
|
||||||
|
public void setOwnerLogin(String ownerLogin) { this.ownerLogin = ownerLogin; }
|
||||||
|
public Integer getLimit() { return limit; }
|
||||||
|
public void setLimit(Integer limit) { this.limit = limit; }
|
||||||
|
public Integer getRawBytes() { return rawBytes; }
|
||||||
|
public void setRawBytes(Integer rawBytes) { this.rawBytes = rawBytes; }
|
||||||
|
public Boolean getHasMore() { return hasMore; }
|
||||||
|
public void setHasMore(Boolean hasMore) { this.hasMore = hasMore; }
|
||||||
|
public Long getNextTimeMs() { return nextTimeMs; }
|
||||||
|
public void setNextTimeMs(Long nextTimeMs) { this.nextTimeMs = nextTimeMs; }
|
||||||
|
public String getNextSettingKey() { return nextSettingKey; }
|
||||||
|
public void setNextSettingKey(String nextSettingKey) { this.nextSettingKey = nextSettingKey; }
|
||||||
|
public List<Item> getItems() { return items; }
|
||||||
|
public void setItems(List<Item> items) { this.items = items; }
|
||||||
|
|
||||||
|
public static class Item {
|
||||||
|
private String login;
|
||||||
|
private Integer setting_type;
|
||||||
|
private String setting_key;
|
||||||
|
private Long time_ms;
|
||||||
|
private String value_text;
|
||||||
|
private Long value_num;
|
||||||
|
private String client_key;
|
||||||
|
private String signature;
|
||||||
|
private Boolean synced;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
public Integer getSetting_type() { return setting_type; }
|
||||||
|
public void setSetting_type(Integer setting_type) { this.setting_type = setting_type; }
|
||||||
|
public String getSetting_key() { return setting_key; }
|
||||||
|
public void setSetting_key(String setting_key) { this.setting_key = setting_key; }
|
||||||
|
public Long getTime_ms() { return time_ms; }
|
||||||
|
public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
|
||||||
|
public String getValue_text() { return value_text; }
|
||||||
|
public void setValue_text(String value_text) { this.value_text = value_text; }
|
||||||
|
public Long getValue_num() { return value_num; }
|
||||||
|
public void setValue_num(Long value_num) { this.value_num = value_num; }
|
||||||
|
public String getClient_key() { return client_key; }
|
||||||
|
public void setClient_key(String client_key) { this.client_key = client_key; }
|
||||||
|
public String getSignature() { return signature; }
|
||||||
|
public void setSignature(String signature) { this.signature = signature; }
|
||||||
|
public Boolean getSynced() { return synced; }
|
||||||
|
public void setSynced(Boolean synced) { this.synced = synced; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
-126
@@ -11,27 +11,19 @@ import shine.db.entities.BlockEntry;
|
|||||||
import shine.db.entities.SyncServerEntry;
|
import shine.db.entities.SyncServerEntry;
|
||||||
import utils.blockchain.BlockchainNameUtil;
|
import utils.blockchain.BlockchainNameUtil;
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.http.HttpClient;
|
|
||||||
import java.net.http.WebSocket;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Фоновая one-shot репликация AddBlock на серверы из локальной таблицы sync_servers.
|
* Фоновая репликация AddBlock через общий постоянный WSS-пул на серверы
|
||||||
|
* из локальной таблицы sync_servers.
|
||||||
*/
|
*/
|
||||||
public final class AddBlockSyncService {
|
public final class AddBlockSyncService {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(AddBlockSyncService.class);
|
private static final Logger log = LoggerFactory.getLogger(AddBlockSyncService.class);
|
||||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
private static final HttpClient HTTP = HttpClient.newBuilder()
|
|
||||||
.connectTimeout(Duration.ofSeconds(6))
|
|
||||||
.build();
|
|
||||||
private static final ExecutorService EXECUTOR = new ThreadPoolExecutor(
|
private static final ExecutorService EXECUTOR = new ThreadPoolExecutor(
|
||||||
1,
|
1,
|
||||||
Math.max(2, Runtime.getRuntime().availableProcessors()),
|
Math.max(2, Runtime.getRuntime().availableProcessors()),
|
||||||
@@ -100,14 +92,14 @@ public final class AddBlockSyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void replicateToPartner(SyncServerEntry partner, String blockchainName, int blockNumber, BlockEntry currentBlock) throws Exception {
|
private void replicateToPartner(SyncServerEntry partner, String blockchainName, int blockNumber, BlockEntry currentBlock) throws Exception {
|
||||||
String wsUrl = buildWsUrl(partner.getServerAddress());
|
String wsUrl = ServerConnectionPool.buildWsUrl(partner.getServerAddress());
|
||||||
if (wsUrl == null) {
|
if (wsUrl == null) {
|
||||||
log.warn("AddBlock sync skipped: invalid server_address for partner login={} address={}",
|
log.warn("AddBlock sync skipped: invalid server_address for partner login={} address={}",
|
||||||
partner.getLogin(), partner.getServerAddress());
|
partner.getLogin(), partner.getServerAddress());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
AddBlockPushResult firstTry = pushBlock(wsUrl, blockchainName, currentBlock);
|
AddBlockPushResult firstTry = pushBlock(partner, blockchainName, currentBlock);
|
||||||
if (firstTry.ok()) {
|
if (firstTry.ok()) {
|
||||||
log.info("AddBlock sync ok: partner={} blockchainName={} blockNumber={}",
|
log.info("AddBlock sync ok: partner={} blockchainName={} blockNumber={}",
|
||||||
partner.getLogin(), blockchainName, blockNumber);
|
partner.getLogin(), blockchainName, blockNumber);
|
||||||
@@ -142,7 +134,7 @@ public final class AddBlockSyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (BlockEntry blockEntry : missingBlocks) {
|
for (BlockEntry blockEntry : missingBlocks) {
|
||||||
AddBlockPushResult backfillResult = pushBlock(wsUrl, blockchainName, blockEntry);
|
AddBlockPushResult backfillResult = pushBlock(partner, blockchainName, blockEntry);
|
||||||
if (!backfillResult.ok()) {
|
if (!backfillResult.ok()) {
|
||||||
log.warn("AddBlock sync backfill failed: partner={} blockchainName={} blockNumber={} code={}",
|
log.warn("AddBlock sync backfill failed: partner={} blockchainName={} blockNumber={} code={}",
|
||||||
partner.getLogin(), blockchainName, blockEntry.getBlockNumber(), backfillResult.code());
|
partner.getLogin(), blockchainName, blockEntry.getBlockNumber(), backfillResult.code());
|
||||||
@@ -154,8 +146,8 @@ public final class AddBlockSyncService {
|
|||||||
partner.getLogin(), blockchainName, fromBlockNumber, blockNumber);
|
partner.getLogin(), blockchainName, fromBlockNumber, blockNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AddBlockPushResult pushBlock(String wsUrl, String blockchainName, BlockEntry blockEntry) throws Exception {
|
private AddBlockPushResult pushBlock(SyncServerEntry partner, String blockchainName, BlockEntry blockEntry) throws Exception {
|
||||||
JsonNode response = sendAddBlock(wsUrl, blockchainName, blockEntry);
|
JsonNode response = sendAddBlock(partner, blockchainName, blockEntry);
|
||||||
int status = response.path("status").asInt(500);
|
int status = response.path("status").asInt(500);
|
||||||
if (status >= 200 && status < 300) {
|
if (status >= 200 && status < 300) {
|
||||||
return AddBlockPushResult.success();
|
return AddBlockPushResult.success();
|
||||||
@@ -172,31 +164,16 @@ public final class AddBlockSyncService {
|
|||||||
return new AddBlockPushResult(false, status, code, serverLastGlobalNumber, serverLastGlobalHash);
|
return new AddBlockPushResult(false, status, code, serverLastGlobalNumber, serverLastGlobalHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
private JsonNode sendAddBlock(String wsUrl, String blockchainName, BlockEntry blockEntry) throws Exception {
|
private JsonNode sendAddBlock(SyncServerEntry partner, String blockchainName, BlockEntry blockEntry) throws Exception {
|
||||||
CompletableFuture<String> responseFuture = new CompletableFuture<>();
|
String jsonTemplate = buildAddBlockJsonTemplate(blockchainName, blockEntry);
|
||||||
CountDownLatch openLatch = new CountDownLatch(1);
|
return ServerConnectionPool.getInstance().request(
|
||||||
SyncWsListener listener = new SyncWsListener(responseFuture, openLatch);
|
partner.getLogin(),
|
||||||
|
partner.getServerAddress(),
|
||||||
WebSocket webSocket = HTTP.newWebSocketBuilder()
|
jsonTemplate,
|
||||||
.connectTimeout(Duration.ofSeconds(6))
|
ServerConnectionPool.Priority.BULK);
|
||||||
.buildAsync(URI.create(wsUrl), listener)
|
|
||||||
.get(8, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
if (!openLatch.await(8, TimeUnit.SECONDS)) {
|
|
||||||
tryAbort(webSocket);
|
|
||||||
throw new TimeoutException("WS open timeout");
|
|
||||||
}
|
|
||||||
|
|
||||||
String requestId = "sync-" + UUID.randomUUID();
|
|
||||||
String json = buildAddBlockJson(requestId, blockchainName, blockEntry);
|
|
||||||
webSocket.sendText(json, true).get(8, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
String responseJson = responseFuture.get(12, TimeUnit.SECONDS);
|
|
||||||
tryAbort(webSocket);
|
|
||||||
return MAPPER.readTree(responseJson);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildAddBlockJson(String requestId, String blockchainName, BlockEntry blockEntry) throws Exception {
|
private String buildAddBlockJsonTemplate(String blockchainName, BlockEntry blockEntry) throws Exception {
|
||||||
String prevHashHex = blockEntry.getBlockNumber() <= 0
|
String prevHashHex = blockEntry.getBlockNumber() <= 0
|
||||||
? ""
|
? ""
|
||||||
: toHex(extractPrevHash32(blockEntry.getBlockBytes()));
|
: toHex(extractPrevHash32(blockEntry.getBlockBytes()));
|
||||||
@@ -205,8 +182,6 @@ public final class AddBlockSyncService {
|
|||||||
String safeBlockchainName = MAPPER.writeValueAsString(blockchainName);
|
String safeBlockchainName = MAPPER.writeValueAsString(blockchainName);
|
||||||
String safePrevHashHex = MAPPER.writeValueAsString(prevHashHex);
|
String safePrevHashHex = MAPPER.writeValueAsString(prevHashHex);
|
||||||
String safeBlockBytes = MAPPER.writeValueAsString(blockBytesB64);
|
String safeBlockBytes = MAPPER.writeValueAsString(blockBytesB64);
|
||||||
String safeRequestId = MAPPER.writeValueAsString(requestId);
|
|
||||||
|
|
||||||
return """
|
return """
|
||||||
{
|
{
|
||||||
"op":"AddBlock",
|
"op":"AddBlock",
|
||||||
@@ -218,7 +193,7 @@ public final class AddBlockSyncService {
|
|||||||
"blockBytesB64":%s
|
"blockBytesB64":%s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""".formatted(safeRequestId, safeBlockchainName, blockEntry.getBlockNumber(), safePrevHashHex, safeBlockBytes);
|
""".formatted("%s", safeBlockchainName, blockEntry.getBlockNumber(), safePrevHashHex, safeBlockBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] extractPrevHash32(byte[] blockBytes) {
|
private static byte[] extractPrevHash32(byte[] blockBytes) {
|
||||||
@@ -240,32 +215,6 @@ public final class AddBlockSyncService {
|
|||||||
return s.isEmpty() ? null : s;
|
return s.isEmpty() ? null : s;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String buildWsUrl(String serverAddressRaw) {
|
|
||||||
String host = normalizeHostLike(serverAddressRaw);
|
|
||||||
if (host == null) return null;
|
|
||||||
return "wss://" + host + "/ws";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String normalizeHostLike(String value) {
|
|
||||||
if (value == null) return null;
|
|
||||||
String raw = value.trim();
|
|
||||||
if (raw.isEmpty()) return null;
|
|
||||||
try {
|
|
||||||
String withScheme = raw.matches("^[a-zA-Z]+://.*$") ? raw : "https://" + raw;
|
|
||||||
URI uri = URI.create(withScheme);
|
|
||||||
String host = uri.getHost();
|
|
||||||
if (host == null || host.isBlank()) return null;
|
|
||||||
return host.trim().toLowerCase(Locale.ROOT);
|
|
||||||
} catch (Exception e) {
|
|
||||||
String cleaned = raw
|
|
||||||
.replaceFirst("^[a-zA-Z]+://", "")
|
|
||||||
.replaceFirst("/.*$", "")
|
|
||||||
.trim()
|
|
||||||
.toLowerCase(Locale.ROOT);
|
|
||||||
return cleaned.isEmpty() ? null : cleaned;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String toHex(byte[] bytes) {
|
private static String toHex(byte[] bytes) {
|
||||||
if (bytes == null) return "";
|
if (bytes == null) return "";
|
||||||
StringBuilder sb = new StringBuilder(bytes.length * 2);
|
StringBuilder sb = new StringBuilder(bytes.length * 2);
|
||||||
@@ -276,17 +225,6 @@ public final class AddBlockSyncService {
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void tryAbort(WebSocket webSocket) {
|
|
||||||
try {
|
|
||||||
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
webSocket.abort();
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private record AddBlockPushResult(
|
private record AddBlockPushResult(
|
||||||
boolean ok,
|
boolean ok,
|
||||||
int status,
|
int status,
|
||||||
@@ -309,52 +247,4 @@ public final class AddBlockSyncService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class SyncWsListener implements WebSocket.Listener {
|
|
||||||
private final CompletableFuture<String> responseFuture;
|
|
||||||
private final CountDownLatch openLatch;
|
|
||||||
private final StringBuilder textBuffer = new StringBuilder();
|
|
||||||
|
|
||||||
private SyncWsListener(CompletableFuture<String> responseFuture, CountDownLatch openLatch) {
|
|
||||||
this.responseFuture = responseFuture;
|
|
||||||
this.openLatch = openLatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOpen(WebSocket webSocket) {
|
|
||||||
openLatch.countDown();
|
|
||||||
webSocket.request(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
|
|
||||||
textBuffer.append(data);
|
|
||||||
if (last && !responseFuture.isDone()) {
|
|
||||||
responseFuture.complete(textBuffer.toString());
|
|
||||||
}
|
|
||||||
webSocket.request(1);
|
|
||||||
return CompletableFuture.completedFuture(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletionStage<?> onBinary(WebSocket webSocket, ByteBuffer data, boolean last) {
|
|
||||||
webSocket.request(1);
|
|
||||||
return CompletableFuture.completedFuture(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {
|
|
||||||
if (!responseFuture.isDone()) {
|
|
||||||
responseFuture.completeExceptionally(new IllegalStateException("WS closed before response: " + statusCode + " " + reason));
|
|
||||||
}
|
|
||||||
return CompletableFuture.completedFuture(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(WebSocket webSocket, Throwable error) {
|
|
||||||
if (!responseFuture.isDone()) {
|
|
||||||
responseFuture.completeExceptionally(error);
|
|
||||||
}
|
|
||||||
openLatch.countDown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+311
@@ -0,0 +1,311 @@
|
|||||||
|
package server.sync;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.DmDeliveryRealtime;
|
||||||
|
import shine.db.dao.DmDeliveryStateDAO;
|
||||||
|
import shine.db.dao.DmSyncOutboxDAO;
|
||||||
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
|
import shine.db.dao.UserAccessServersCurrentDAO;
|
||||||
|
import shine.db.entities.DmDeliveryStateEntry;
|
||||||
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
import shine.db.entities.UserAccessServerRouteEntry;
|
||||||
|
import utils.config.AppConfig;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Координатор доставки исходящей DM-пары. Первая попытка выполняется до ответа
|
||||||
|
* клиенту, последующие — пятисекундным воркером.
|
||||||
|
*/
|
||||||
|
public final class DmDeliveryCoordinator {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(DmDeliveryCoordinator.class);
|
||||||
|
private static final long[] ATTEMPT_OFFSETS_MS = {
|
||||||
|
0L,
|
||||||
|
30_000L,
|
||||||
|
5L * 60_000L,
|
||||||
|
25L * 60_000L,
|
||||||
|
60L * 60_000L
|
||||||
|
};
|
||||||
|
private static final String SERVER_LOGIN_CONFIG = "server.SHiNE.login";
|
||||||
|
private static final RemoteDmSyncClient REMOTE = new RemoteDmSyncClient();
|
||||||
|
private static final DmDeliveryStateDAO DELIVERY_DAO = DmDeliveryStateDAO.getInstance();
|
||||||
|
private static final DmSyncOutboxDAO OUTBOX_DAO = DmSyncOutboxDAO.getInstance();
|
||||||
|
private static final SignedMessagesDAO MESSAGES_DAO = SignedMessagesDAO.getInstance();
|
||||||
|
private static final UserAccessServersCurrentDAO ACCESS_DAO = UserAccessServersCurrentDAO.getInstance();
|
||||||
|
private static final ExecutorService ASSIST_EXECUTOR = new ThreadPoolExecutor(
|
||||||
|
2, 2, 0L, TimeUnit.MILLISECONDS,
|
||||||
|
new ArrayBlockingQueue<>(500),
|
||||||
|
daemonThreadFactory("dm-peer-assist"),
|
||||||
|
new ThreadPoolExecutor.DiscardPolicy());
|
||||||
|
private static final ExecutorService RECIPIENT_EXECUTOR = new ThreadPoolExecutor(
|
||||||
|
4, 16, 60L, TimeUnit.SECONDS,
|
||||||
|
new ArrayBlockingQueue<>(1000),
|
||||||
|
daemonThreadFactory("dm-recipient-delivery"),
|
||||||
|
new ThreadPoolExecutor.CallerRunsPolicy());
|
||||||
|
|
||||||
|
private DmDeliveryCoordinator() {}
|
||||||
|
|
||||||
|
public static List<DmDeliveryStateEntry> listDue(int limit) throws Exception {
|
||||||
|
return DELIVERY_DAO.listDue(System.currentTimeMillis(), Math.max(1, limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void processDueEntry(DmDeliveryStateEntry snapshot) {
|
||||||
|
processDueEntry(snapshot, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void processDueEntry(DmDeliveryStateEntry snapshot, boolean allowInitialHandoff) {
|
||||||
|
if (snapshot == null) return;
|
||||||
|
try {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
Long nextAttemptAtMs = nextAttemptAt(snapshot.getCreatedAtMs(), snapshot.getAttemptIndex() + 1);
|
||||||
|
if (!DELIVERY_DAO.claimAttempt(snapshot, now, nextAttemptAtMs)) return;
|
||||||
|
|
||||||
|
DmDeliveryStateEntry current = DELIVERY_DAO.getByEventId(snapshot.getEventId());
|
||||||
|
if (current == null || current.isDelivered()
|
||||||
|
|| current.getDeliveryState() == DmDeliveryStateEntry.FAILED) return;
|
||||||
|
|
||||||
|
boolean finalAttempt = snapshot.getAttemptIndex() >= ATTEMPT_OFFSETS_MS.length - 1
|
||||||
|
|| now >= current.getDeliveryExpiresAtMs();
|
||||||
|
|
||||||
|
// Перед попытками на 5-й, 25-й и 60-й минутах сначала спрашиваем
|
||||||
|
// второй сервер отправителя: возможно, он уже доставил сообщение.
|
||||||
|
if (current.getDeliveryState() == DmDeliveryStateEntry.ACCEPTED
|
||||||
|
&& (snapshot.getAttemptIndex() >= 2 || finalAttempt)) {
|
||||||
|
current = acceptPeerDeliveryStatus(current);
|
||||||
|
if (current != null && current.isDelivered()) {
|
||||||
|
DmDeliveryRealtime.notifySender(current);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DmDeliveryStateEntry afterAttempt = attemptRecipientRoutes(
|
||||||
|
current, finalAttempt ? null : nextAttemptAtMs);
|
||||||
|
|
||||||
|
// После первой попытки сразу передаём полную пару второму серверу
|
||||||
|
// отправителя старой операцией ReceiveOutcomingMessage.
|
||||||
|
if (allowInitialHandoff && snapshot.getAttemptIndex() == 0 && afterAttempt != null) {
|
||||||
|
afterAttempt = handoffPairToSenderPeer(afterAttempt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalAttempt && afterAttempt != null && !afterAttempt.isDelivered()) {
|
||||||
|
afterAttempt = DELIVERY_DAO.finishAtExpiry(afterAttempt.getEventId(), System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
DmDeliveryRealtime.notifySender(afterAttempt);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("DM delivery attempt failed: eventId={}", snapshot.getEventId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void assistReceivedPairAsync(String eventId) {
|
||||||
|
if (eventId == null || eventId.isBlank()) return;
|
||||||
|
ASSIST_EXECUTOR.execute(() -> {
|
||||||
|
try {
|
||||||
|
DmDeliveryStateEntry row = DELIVERY_DAO.getByEventId(eventId);
|
||||||
|
if (row != null) processDueEntry(row, false);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("DM peer assist failed: eventId={}", eventId, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DmDeliveryStateEntry attemptRecipientRoutes(
|
||||||
|
DmDeliveryStateEntry current,
|
||||||
|
Long nextAttemptAtMs
|
||||||
|
) throws Exception {
|
||||||
|
if (current == null) return null;
|
||||||
|
List<UserAccessServerRouteEntry> routes = cappedRoutes(current.getToLogin());
|
||||||
|
String routesHash = routesHash(routes);
|
||||||
|
String alreadyDelivered = normalize(current.getDeliveredServerLogin());
|
||||||
|
List<String> acceptedLogins = new ArrayList<>();
|
||||||
|
if (alreadyDelivered != null) acceptedLogins.add(alreadyDelivered);
|
||||||
|
|
||||||
|
SignedMessageEntry incoming = MESSAGES_DAO.getByMessageKey(current.getIncomingMessageKey());
|
||||||
|
if (incoming == null || incoming.getRawBlock() == null) {
|
||||||
|
return DELIVERY_DAO.updateAfterAttempt(
|
||||||
|
current.getEventId(), current.getDeliveryState(), current.getDeliveredServerLogin(),
|
||||||
|
routesHash, nextAttemptAtMs, "INCOMING_BLOB_NOT_FOUND", System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
String incomingBlobB64 = Base64.getEncoder().encodeToString(incoming.getRawBlock());
|
||||||
|
String ownServerLogin = ownServerLogin();
|
||||||
|
java.util.concurrent.CompletionService<RouteAttempt> completion =
|
||||||
|
new java.util.concurrent.ExecutorCompletionService<>(RECIPIENT_EXECUTOR);
|
||||||
|
int submitted = 0;
|
||||||
|
for (UserAccessServerRouteEntry route : routes) {
|
||||||
|
String routeLogin = normalize(route.getServerLogin());
|
||||||
|
if (routeLogin == null || acceptedLogins.contains(routeLogin)) continue;
|
||||||
|
completion.submit(() -> {
|
||||||
|
try {
|
||||||
|
if (!routeLogin.equals(ownServerLogin)) {
|
||||||
|
REMOTE.receiveIncomingMessage(
|
||||||
|
routeLogin, route.getServerUrl(), incomingBlobB64, ownServerLogin);
|
||||||
|
}
|
||||||
|
return new RouteAttempt(routeLogin, null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.info("DM recipient server unavailable: messageKey={} server={}",
|
||||||
|
current.getOutgoingMessageKey(), routeLogin);
|
||||||
|
return new RouteAttempt(routeLogin, compactError(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
submitted++;
|
||||||
|
}
|
||||||
|
|
||||||
|
String lastError = null;
|
||||||
|
for (int i = 0; i < submitted && acceptedLogins.isEmpty(); i++) {
|
||||||
|
RouteAttempt result = completion.take().get();
|
||||||
|
if (result.error() == null) {
|
||||||
|
acceptedLogins.add(result.serverLogin());
|
||||||
|
} else {
|
||||||
|
lastError = result.error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextState = acceptedLogins.isEmpty()
|
||||||
|
? DmDeliveryStateEntry.ACCEPTED
|
||||||
|
: DmDeliveryStateEntry.DELIVERED;
|
||||||
|
String oneLogin = acceptedLogins.isEmpty() ? null : acceptedLogins.get(0);
|
||||||
|
return DELIVERY_DAO.updateAfterAttempt(
|
||||||
|
current.getEventId(), nextState, oneLogin, routesHash,
|
||||||
|
nextAttemptAtMs, lastError, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DmDeliveryStateEntry handoffPairToSenderPeer(DmDeliveryStateEntry current) throws Exception {
|
||||||
|
UserAccessServerRouteEntry peer = senderPeer(current.getFromLogin());
|
||||||
|
if (peer == null) return current;
|
||||||
|
SignedMessageEntry incoming = MESSAGES_DAO.getByMessageKey(current.getIncomingMessageKey());
|
||||||
|
SignedMessageEntry outgoing = MESSAGES_DAO.getByMessageKey(current.getOutgoingMessageKey());
|
||||||
|
if (incoming == null || outgoing == null) return current;
|
||||||
|
try {
|
||||||
|
REMOTE.sendMessagePair(
|
||||||
|
peer.getServerLogin(),
|
||||||
|
peer.getServerUrl(),
|
||||||
|
Base64.getEncoder().encodeToString(incoming.getRawBlock()),
|
||||||
|
Base64.getEncoder().encodeToString(outgoing.getRawBlock()),
|
||||||
|
ownServerLogin());
|
||||||
|
OUTBOX_DAO.markSynced(current.getFromLogin(), current.getEventId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.info("DM sender peer unavailable: eventId={} server={}", current.getEventId(), peer.getServerLogin());
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DmDeliveryStateEntry acceptPeerDeliveryStatus(DmDeliveryStateEntry current) throws Exception {
|
||||||
|
UserAccessServerRouteEntry peer = senderPeer(current.getFromLogin());
|
||||||
|
if (peer == null) return current;
|
||||||
|
try {
|
||||||
|
RemoteDmSyncClient.RemoteDeliveryStatus remote = REMOTE.getDmDeliveryStatus(
|
||||||
|
peer.getServerLogin(), peer.getServerUrl(), current.getOutgoingMessageKey());
|
||||||
|
if (remote.known() && remote.delivered()) {
|
||||||
|
return DELIVERY_DAO.markDeliveredFromPeer(current.getEventId(), System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.info("DM delivery status unavailable: eventId={} server={}", current.getEventId(), peer.getServerLogin());
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UserAccessServerRouteEntry senderPeer(String senderLogin) throws Exception {
|
||||||
|
String own = ownServerLogin();
|
||||||
|
for (UserAccessServerRouteEntry route : cappedRoutes(senderLogin)) {
|
||||||
|
String routeLogin = normalize(route.getServerLogin());
|
||||||
|
if (routeLogin != null && !routeLogin.equals(own)) return route;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isLocalAccessServer(String ownerLogin) throws Exception {
|
||||||
|
String own = ownServerLogin();
|
||||||
|
if (own == null) return false;
|
||||||
|
for (UserAccessServerRouteEntry route : cappedRoutes(ownerLogin)) {
|
||||||
|
if (own.equals(normalize(route.getServerLogin()))) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean sourceIsOtherAccessServer(String ownerLogin, String sourceServerLogin) throws Exception {
|
||||||
|
String source = normalize(sourceServerLogin);
|
||||||
|
String own = ownServerLogin();
|
||||||
|
if (source == null || source.equals(own)) return false;
|
||||||
|
for (UserAccessServerRouteEntry route : cappedRoutes(ownerLogin)) {
|
||||||
|
if (source.equals(normalize(route.getServerLogin()))) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String currentServerLogin() {
|
||||||
|
return ownServerLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<UserAccessServerRouteEntry> cappedRoutes(String ownerLogin) throws Exception {
|
||||||
|
Map<String, UserAccessServerRouteEntry> unique = new LinkedHashMap<>();
|
||||||
|
for (UserAccessServerRouteEntry route : ACCESS_DAO.listByUserLogin(ownerLogin)) {
|
||||||
|
if (route == null || route.getServerUrl() == null || route.getServerUrl().isBlank()) continue;
|
||||||
|
String login = normalize(route.getServerLogin());
|
||||||
|
if (login == null) continue;
|
||||||
|
unique.putIfAbsent(login, route);
|
||||||
|
if (unique.size() == 2) break;
|
||||||
|
}
|
||||||
|
return new ArrayList<>(unique.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String routesHash(List<UserAccessServerRouteEntry> routes) throws Exception {
|
||||||
|
List<String> logins = new ArrayList<>();
|
||||||
|
for (UserAccessServerRouteEntry route : routes) {
|
||||||
|
String login = normalize(route.getServerLogin());
|
||||||
|
if (login != null) logins.add(login);
|
||||||
|
}
|
||||||
|
logins.sort(String::compareTo);
|
||||||
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
|
byte[] hash = digest.digest(String.join("\n", logins).getBytes(StandardCharsets.UTF_8));
|
||||||
|
StringBuilder out = new StringBuilder(hash.length * 2);
|
||||||
|
for (byte b : hash) out.append(String.format("%02x", b & 0xff));
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Long nextAttemptAt(long createdAtMs, int nextAttemptIndex) {
|
||||||
|
if (nextAttemptIndex < 0 || nextAttemptIndex >= ATTEMPT_OFFSETS_MS.length) return null;
|
||||||
|
return createdAtMs + ATTEMPT_OFFSETS_MS[nextAttemptIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
private record RouteAttempt(String serverLogin, String error) {}
|
||||||
|
|
||||||
|
private static String ownServerLogin() {
|
||||||
|
return normalize(AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalize(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
String normalized = value.trim().toLowerCase(Locale.ROOT);
|
||||||
|
return normalized.isEmpty() ? null : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String compactError(Exception e) {
|
||||||
|
String text = String.valueOf(e == null ? "unknown" : e.getMessage());
|
||||||
|
return text.length() <= 500 ? text : text.substring(0, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ThreadFactory daemonThreadFactory(String prefix) {
|
||||||
|
return new ThreadFactory() {
|
||||||
|
private int sequence;
|
||||||
|
@Override
|
||||||
|
public synchronized Thread newThread(Runnable r) {
|
||||||
|
Thread thread = new Thread(r, prefix + "-" + (++sequence));
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-5
@@ -27,12 +27,15 @@ public final class DmFederationService {
|
|||||||
String ownServerLogin = ownServerLogin();
|
String ownServerLogin = ownServerLogin();
|
||||||
for (UserAccessServerRouteEntry route : senderRoutes.values()) {
|
for (UserAccessServerRouteEntry route : senderRoutes.values()) {
|
||||||
if (isOwnServer(route, ownServerLogin)) continue;
|
if (isOwnServer(route, ownServerLogin)) continue;
|
||||||
REMOTE.sendMessagePair(route.getServerUrl(), incomingBlobB64, outgoingBlobB64, ownServerLogin);
|
REMOTE.sendMessagePair(
|
||||||
|
route.getServerLogin(), route.getServerUrl(),
|
||||||
|
incomingBlobB64, outgoingBlobB64, ownServerLogin);
|
||||||
}
|
}
|
||||||
for (UserAccessServerRouteEntry route : recipientRoutes.values()) {
|
for (UserAccessServerRouteEntry route : recipientRoutes.values()) {
|
||||||
if (isOwnServer(route, ownServerLogin)) continue;
|
if (isOwnServer(route, ownServerLogin)) continue;
|
||||||
if (senderRoutes.containsKey(normalize(route.getServerLogin()))) continue;
|
if (senderRoutes.containsKey(normalize(route.getServerLogin()))) continue;
|
||||||
REMOTE.receiveIncomingMessage(route.getServerUrl(), incomingBlobB64, ownServerLogin);
|
REMOTE.receiveIncomingMessage(
|
||||||
|
route.getServerLogin(), route.getServerUrl(), incomingBlobB64, ownServerLogin);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("DM federation pair fan-out failed: from={} to={}", fromLogin, toLogin, e);
|
log.warn("DM federation pair fan-out failed: from={} to={}", fromLogin, toLogin, e);
|
||||||
@@ -54,7 +57,8 @@ public final class DmFederationService {
|
|||||||
if (routeLogin == null) continue;
|
if (routeLogin == null) continue;
|
||||||
if (ownServerLogin != null && ownServerLogin.equalsIgnoreCase(routeLogin)) continue;
|
if (ownServerLogin != null && ownServerLogin.equalsIgnoreCase(routeLogin)) continue;
|
||||||
if (normalizedSource != null && normalizedSource.equalsIgnoreCase(routeLogin)) continue;
|
if (normalizedSource != null && normalizedSource.equalsIgnoreCase(routeLogin)) continue;
|
||||||
REMOTE.receiveIncomingMessage(route.getServerUrl(), incomingBlobB64, ownServerLogin);
|
REMOTE.receiveIncomingMessage(
|
||||||
|
route.getServerLogin(), route.getServerUrl(), incomingBlobB64, ownServerLogin);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("DM federation incoming relay failed: to={}", toLogin, e);
|
log.warn("DM federation incoming relay failed: to={}", toLogin, e);
|
||||||
@@ -79,9 +83,9 @@ public final class DmFederationService {
|
|||||||
for (UserAccessServerRouteEntry route : routes.values()) {
|
for (UserAccessServerRouteEntry route : routes.values()) {
|
||||||
if (isOwnServer(route, ownServerLogin)) continue;
|
if (isOwnServer(route, ownServerLogin)) continue;
|
||||||
if (oneMessageDelete) {
|
if (oneMessageDelete) {
|
||||||
REMOTE.deleteMessage(route.getServerUrl(), blobB64);
|
REMOTE.deleteMessage(route.getServerLogin(), route.getServerUrl(), blobB64);
|
||||||
} else {
|
} else {
|
||||||
REMOTE.deleteConversation(route.getServerUrl(), blobB64);
|
REMOTE.deleteConversation(route.getServerLogin(), route.getServerUrl(), blobB64);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package server.sync;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public final class DmSyncWakeSignal {
|
||||||
|
private static final AtomicBoolean REQUESTED = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
private DmSyncWakeSignal() {}
|
||||||
|
|
||||||
|
public static void request() {
|
||||||
|
REQUESTED.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean consume() {
|
||||||
|
return REQUESTED.getAndSet(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
-132
@@ -2,37 +2,22 @@ package server.sync;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
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.LoggerFactory;
|
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.http.HttpClient;
|
|
||||||
import java.net.http.WebSocket;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.CompletionStage;
|
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Минимальный клиент для межсерверных JSON-op запросов по WSS.
|
* Минимальный клиент для межсерверных JSON-op запросов по WSS.
|
||||||
*/
|
*/
|
||||||
public final class RemoteBlockchainSyncClient {
|
public final class RemoteBlockchainSyncClient {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(RemoteBlockchainSyncClient.class);
|
|
||||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
private static final HttpClient HTTP = HttpClient.newBuilder()
|
|
||||||
.connectTimeout(Duration.ofSeconds(6))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
public List<RemoteBlockchainHead> listBlockchainHeads(String serverAddressRaw) throws Exception {
|
public List<RemoteBlockchainHead> listBlockchainHeads(String serverAddressRaw) throws Exception {
|
||||||
JsonNode response = send(serverAddressRaw, """
|
return listBlockchainHeads(null, serverAddressRaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RemoteBlockchainHead> listBlockchainHeads(String serverLogin, String serverAddressRaw) throws Exception {
|
||||||
|
JsonNode response = send(serverLogin, serverAddressRaw, """
|
||||||
{
|
{
|
||||||
"op":"ListBlockchainHeads",
|
"op":"ListBlockchainHeads",
|
||||||
"requestId":%s,
|
"requestId":%s,
|
||||||
@@ -62,8 +47,12 @@ public final class RemoteBlockchainSyncClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public RemoteSyncUserProfile getSyncUserProfile(String serverAddressRaw, String login) throws Exception {
|
public RemoteSyncUserProfile getSyncUserProfile(String serverAddressRaw, String login) throws Exception {
|
||||||
|
return getSyncUserProfile(null, serverAddressRaw, login);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RemoteSyncUserProfile getSyncUserProfile(String serverLogin, String serverAddressRaw, String login) throws Exception {
|
||||||
String safeLogin = MAPPER.writeValueAsString(login);
|
String safeLogin = MAPPER.writeValueAsString(login);
|
||||||
JsonNode response = send(serverAddressRaw, """
|
JsonNode response = send(serverLogin, serverAddressRaw, """
|
||||||
{
|
{
|
||||||
"op":"GetSyncUserProfile",
|
"op":"GetSyncUserProfile",
|
||||||
"requestId":%s,
|
"requestId":%s,
|
||||||
@@ -96,9 +85,17 @@ public final class RemoteBlockchainSyncClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RemoteBlockchainBlock getBlockchainBlock(String serverAddressRaw, String blockchainName, int blockNumber) throws Exception {
|
public RemoteBlockchainBlock getBlockchainBlock(
|
||||||
|
String serverAddressRaw, String blockchainName, int blockNumber
|
||||||
|
) throws Exception {
|
||||||
|
return getBlockchainBlock(null, serverAddressRaw, blockchainName, blockNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RemoteBlockchainBlock getBlockchainBlock(
|
||||||
|
String serverLogin, String serverAddressRaw, String blockchainName, int blockNumber
|
||||||
|
) throws Exception {
|
||||||
String safeBlockchainName = MAPPER.writeValueAsString(blockchainName);
|
String safeBlockchainName = MAPPER.writeValueAsString(blockchainName);
|
||||||
JsonNode response = send(serverAddressRaw, """
|
JsonNode response = send(serverLogin, serverAddressRaw, """
|
||||||
{
|
{
|
||||||
"op":"GetBlockchainBlock",
|
"op":"GetBlockchainBlock",
|
||||||
"requestId":%s,
|
"requestId":%s,
|
||||||
@@ -126,32 +123,12 @@ public final class RemoteBlockchainSyncClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private JsonNode send(String serverAddressRaw, String jsonTemplate) throws Exception {
|
private JsonNode send(String serverLogin, String serverAddressRaw, String jsonTemplate) throws Exception {
|
||||||
String requestId = MAPPER.writeValueAsString("sync-" + UUID.randomUUID());
|
return ServerConnectionPool.getInstance().request(
|
||||||
String json = jsonTemplate.formatted(requestId);
|
serverLogin,
|
||||||
String wsUrl = buildWsUrl(serverAddressRaw);
|
serverAddressRaw,
|
||||||
if (wsUrl == null) {
|
jsonTemplate,
|
||||||
throw new IllegalArgumentException("Invalid server address: " + serverAddressRaw);
|
ServerConnectionPool.Priority.BULK);
|
||||||
}
|
|
||||||
|
|
||||||
CompletableFuture<String> responseFuture = new CompletableFuture<>();
|
|
||||||
CountDownLatch openLatch = new CountDownLatch(1);
|
|
||||||
SyncWsListener listener = new SyncWsListener(responseFuture, openLatch);
|
|
||||||
|
|
||||||
WebSocket webSocket = HTTP.newWebSocketBuilder()
|
|
||||||
.connectTimeout(Duration.ofSeconds(6))
|
|
||||||
.buildAsync(URI.create(wsUrl), listener)
|
|
||||||
.get(8, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
if (!openLatch.await(8, TimeUnit.SECONDS)) {
|
|
||||||
tryAbort(webSocket);
|
|
||||||
throw new TimeoutException("WS open timeout");
|
|
||||||
}
|
|
||||||
|
|
||||||
webSocket.sendText(json, true).get(8, TimeUnit.SECONDS);
|
|
||||||
String responseJson = responseFuture.get(12, TimeUnit.SECONDS);
|
|
||||||
tryAbort(webSocket);
|
|
||||||
return MAPPER.readTree(responseJson);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String errorCode(JsonNode response) {
|
private static String errorCode(JsonNode response) {
|
||||||
@@ -161,40 +138,7 @@ public final class RemoteBlockchainSyncClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static String buildWsUrl(String serverAddressRaw) {
|
static String buildWsUrl(String serverAddressRaw) {
|
||||||
String host = normalizeHostLike(serverAddressRaw);
|
return ServerConnectionPool.buildWsUrl(serverAddressRaw);
|
||||||
if (host == null) return null;
|
|
||||||
return "wss://" + host + "/ws";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String normalizeHostLike(String value) {
|
|
||||||
if (value == null) return null;
|
|
||||||
String raw = value.trim();
|
|
||||||
if (raw.isEmpty()) return null;
|
|
||||||
try {
|
|
||||||
String withScheme = raw.matches("^[a-zA-Z]+://.*$") ? raw : "https://" + raw;
|
|
||||||
URI uri = URI.create(withScheme);
|
|
||||||
String host = uri.getHost();
|
|
||||||
if (host == null || host.isBlank()) return null;
|
|
||||||
return host.trim().toLowerCase(Locale.ROOT);
|
|
||||||
} catch (Exception e) {
|
|
||||||
String cleaned = raw
|
|
||||||
.replaceFirst("^[a-zA-Z]+://", "")
|
|
||||||
.replaceFirst("/.*$", "")
|
|
||||||
.trim()
|
|
||||||
.toLowerCase(Locale.ROOT);
|
|
||||||
return cleaned.isEmpty() ? null : cleaned;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void tryAbort(WebSocket webSocket) {
|
|
||||||
try {
|
|
||||||
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
webSocket.abort();
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public record RemoteBlockchainHead(
|
public record RemoteBlockchainHead(
|
||||||
@@ -220,53 +164,4 @@ public final class RemoteBlockchainSyncClient {
|
|||||||
long blockchainSizeLimitBytes
|
long blockchainSizeLimitBytes
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private static final class SyncWsListener implements WebSocket.Listener {
|
|
||||||
private final CompletableFuture<String> responseFuture;
|
|
||||||
private final CountDownLatch openLatch;
|
|
||||||
private final StringBuilder textBuffer = new StringBuilder();
|
|
||||||
|
|
||||||
private SyncWsListener(CompletableFuture<String> responseFuture, CountDownLatch openLatch) {
|
|
||||||
this.responseFuture = responseFuture;
|
|
||||||
this.openLatch = openLatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOpen(WebSocket webSocket) {
|
|
||||||
openLatch.countDown();
|
|
||||||
webSocket.request(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
|
|
||||||
textBuffer.append(data);
|
|
||||||
if (last && !responseFuture.isDone()) {
|
|
||||||
responseFuture.complete(textBuffer.toString());
|
|
||||||
}
|
|
||||||
webSocket.request(1);
|
|
||||||
return CompletableFuture.completedFuture(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletionStage<?> onBinary(WebSocket webSocket, ByteBuffer data, boolean last) {
|
|
||||||
webSocket.request(1);
|
|
||||||
return CompletableFuture.completedFuture(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {
|
|
||||||
if (!responseFuture.isDone()) {
|
|
||||||
responseFuture.completeExceptionally(new IllegalStateException("WS closed before response: " + statusCode + " " + reason));
|
|
||||||
}
|
|
||||||
return CompletableFuture.completedFuture(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(WebSocket webSocket, Throwable error) {
|
|
||||||
log.warn("Remote sync websocket error: {}", String.valueOf(error));
|
|
||||||
if (!responseFuture.isDone()) {
|
|
||||||
responseFuture.completeExceptionally(error);
|
|
||||||
}
|
|
||||||
openLatch.countDown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+127
-131
@@ -3,31 +3,33 @@ package server.sync;
|
|||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.http.HttpClient;
|
|
||||||
import java.net.http.WebSocket;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.CompletionStage;
|
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
|
|
||||||
|
/** Клиент стабильных межсерверных DM-операций. */
|
||||||
public final class RemoteDmSyncClient {
|
public final class RemoteDmSyncClient {
|
||||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
private static final HttpClient HTTP = HttpClient.newBuilder()
|
|
||||||
.connectTimeout(Duration.ofSeconds(6))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
public void sendMessagePair(String serverAddressRaw, String incomingBlobB64, String outgoingBlobB64, String sourceServerLogin) throws Exception {
|
public void sendMessagePair(
|
||||||
|
String serverAddressRaw,
|
||||||
|
String incomingBlobB64,
|
||||||
|
String outgoingBlobB64,
|
||||||
|
String sourceServerLogin
|
||||||
|
) throws Exception {
|
||||||
|
sendMessagePair(null, serverAddressRaw, incomingBlobB64, outgoingBlobB64, sourceServerLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMessagePair(
|
||||||
|
String targetServerLogin,
|
||||||
|
String serverAddressRaw,
|
||||||
|
String incomingBlobB64,
|
||||||
|
String outgoingBlobB64,
|
||||||
|
String sourceServerLogin
|
||||||
|
) throws Exception {
|
||||||
String incomingJson = MAPPER.writeValueAsString(incomingBlobB64);
|
String incomingJson = MAPPER.writeValueAsString(incomingBlobB64);
|
||||||
String outgoingJson = MAPPER.writeValueAsString(outgoingBlobB64);
|
String outgoingJson = MAPPER.writeValueAsString(outgoingBlobB64);
|
||||||
String sourceServerLoginJson = toOptionalJsonField("sourceServerLogin", sourceServerLogin);
|
String sourceServerLoginJson = toOptionalJsonField("sourceServerLogin", sourceServerLogin);
|
||||||
JsonNode response = send(serverAddressRaw, """
|
JsonNode response = send(targetServerLogin, serverAddressRaw, """
|
||||||
{
|
{
|
||||||
"op":"ReceiveOutcomingMessage",
|
"op":"ReceiveOutcomingMessage",
|
||||||
"requestId":%s,
|
"requestId":%s,
|
||||||
@@ -40,10 +42,23 @@ public final class RemoteDmSyncClient {
|
|||||||
ensureOk("ReceiveOutcomingMessage", response);
|
ensureOk("ReceiveOutcomingMessage", response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void receiveIncomingMessage(String serverAddressRaw, String incomingBlobB64, String sourceServerLogin) throws Exception {
|
public void receiveIncomingMessage(
|
||||||
|
String serverAddressRaw,
|
||||||
|
String incomingBlobB64,
|
||||||
|
String sourceServerLogin
|
||||||
|
) throws Exception {
|
||||||
|
receiveIncomingMessage(null, serverAddressRaw, incomingBlobB64, sourceServerLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void receiveIncomingMessage(
|
||||||
|
String targetServerLogin,
|
||||||
|
String serverAddressRaw,
|
||||||
|
String incomingBlobB64,
|
||||||
|
String sourceServerLogin
|
||||||
|
) throws Exception {
|
||||||
String incomingJson = MAPPER.writeValueAsString(incomingBlobB64);
|
String incomingJson = MAPPER.writeValueAsString(incomingBlobB64);
|
||||||
String sourceServerLoginJson = toOptionalJsonField("sourceServerLogin", sourceServerLogin);
|
String sourceServerLoginJson = toOptionalJsonField("sourceServerLogin", sourceServerLogin);
|
||||||
JsonNode response = send(serverAddressRaw, """
|
JsonNode response = send(targetServerLogin, serverAddressRaw, """
|
||||||
{
|
{
|
||||||
"op":"ReceiveIncomingMessage",
|
"op":"ReceiveIncomingMessage",
|
||||||
"requestId":%s,
|
"requestId":%s,
|
||||||
@@ -55,17 +70,74 @@ public final class RemoteDmSyncClient {
|
|||||||
ensureOk("ReceiveIncomingMessage", response);
|
ensureOk("ReceiveIncomingMessage", response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RemoteDeliveryStatus getDmDeliveryStatus(String serverAddressRaw, String messageKey) throws Exception {
|
||||||
|
return getDmDeliveryStatus(null, serverAddressRaw, messageKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RemoteDeliveryStatus getDmDeliveryStatus(
|
||||||
|
String targetServerLogin, String serverAddressRaw, String messageKey
|
||||||
|
) throws Exception {
|
||||||
|
String messageKeyJson = MAPPER.writeValueAsString(messageKey);
|
||||||
|
JsonNode response = send(targetServerLogin, serverAddressRaw, """
|
||||||
|
{
|
||||||
|
"op":"GetDmDeliveryStatus",
|
||||||
|
"requestId":%s,
|
||||||
|
"payload":{
|
||||||
|
"messageKey":%s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".formatted("%s", messageKeyJson));
|
||||||
|
ensureOk("GetDmDeliveryStatus", response);
|
||||||
|
JsonNode payload = response.path("payload");
|
||||||
|
return new RemoteDeliveryStatus(
|
||||||
|
payload.path("messageKey").asText(messageKey),
|
||||||
|
payload.path("known").asBoolean(false),
|
||||||
|
payload.path("delivered").asBoolean(false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public RemoteDmBatch dmSyncBatch(
|
public RemoteDmBatch dmSyncBatch(
|
||||||
String serverAddressRaw,
|
String serverAddressRaw,
|
||||||
String ownerLogin,
|
String ownerLogin,
|
||||||
long afterStoredAtMs,
|
long afterStoredAtMs,
|
||||||
String afterMessageKey,
|
String afterMessageKey,
|
||||||
int limit,
|
int limit,
|
||||||
int maxBytes
|
int maxBytes,
|
||||||
|
List<String> ackSyncIds
|
||||||
|
) throws Exception {
|
||||||
|
return dmSyncBatch(null, serverAddressRaw, ownerLogin, afterStoredAtMs,
|
||||||
|
afterMessageKey, limit, maxBytes, ackSyncIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RemoteDmBatch dmSyncBatch(
|
||||||
|
String targetServerLogin,
|
||||||
|
String serverAddressRaw,
|
||||||
|
String ownerLogin,
|
||||||
|
long afterStoredAtMs,
|
||||||
|
String afterMessageKey,
|
||||||
|
int limit,
|
||||||
|
int maxBytes,
|
||||||
|
List<String> ackSyncIds
|
||||||
|
) throws Exception {
|
||||||
|
try (RemoteSyncSession session = new RemoteSyncSession(targetServerLogin, serverAddressRaw)) {
|
||||||
|
return dmSyncBatch(session, ownerLogin, afterStoredAtMs, afterMessageKey,
|
||||||
|
limit, maxBytes, ackSyncIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RemoteDmBatch dmSyncBatch(
|
||||||
|
RemoteSyncSession session,
|
||||||
|
String ownerLogin,
|
||||||
|
long afterStoredAtMs,
|
||||||
|
String afterMessageKey,
|
||||||
|
int limit,
|
||||||
|
int maxBytes,
|
||||||
|
List<String> ackSyncIds
|
||||||
) throws Exception {
|
) throws Exception {
|
||||||
String ownerLoginJson = MAPPER.writeValueAsString(ownerLogin);
|
String ownerLoginJson = MAPPER.writeValueAsString(ownerLogin);
|
||||||
String afterMessageKeyJson = MAPPER.writeValueAsString(afterMessageKey == null ? "" : afterMessageKey);
|
String afterMessageKeyJson = MAPPER.writeValueAsString(afterMessageKey == null ? "" : afterMessageKey);
|
||||||
JsonNode response = send(serverAddressRaw, """
|
String ackSyncIdsJson = MAPPER.writeValueAsString(ackSyncIds == null ? List.of() : ackSyncIds);
|
||||||
|
JsonNode response = session.send("""
|
||||||
{
|
{
|
||||||
"op":"DmSyncBatch",
|
"op":"DmSyncBatch",
|
||||||
"requestId":%s,
|
"requestId":%s,
|
||||||
@@ -74,10 +146,12 @@ public final class RemoteDmSyncClient {
|
|||||||
"afterStoredAtMs":%d,
|
"afterStoredAtMs":%d,
|
||||||
"afterMessageKey":%s,
|
"afterMessageKey":%s,
|
||||||
"limit":%d,
|
"limit":%d,
|
||||||
"maxBytes":%d
|
"maxBytes":%d,
|
||||||
|
"ackSyncIds":%s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""".formatted("%s", ownerLoginJson, Math.max(0L, afterStoredAtMs), afterMessageKeyJson, limit, maxBytes));
|
""".formatted("%s", ownerLoginJson, Math.max(0L, afterStoredAtMs), afterMessageKeyJson,
|
||||||
|
limit, maxBytes, ackSyncIdsJson));
|
||||||
ensureOk("DmSyncBatch", response);
|
ensureOk("DmSyncBatch", response);
|
||||||
|
|
||||||
JsonNode payload = response.path("payload");
|
JsonNode payload = response.path("payload");
|
||||||
@@ -85,11 +159,17 @@ public final class RemoteDmSyncClient {
|
|||||||
JsonNode arr = payload.path("items");
|
JsonNode arr = payload.path("items");
|
||||||
if (arr.isArray()) {
|
if (arr.isArray()) {
|
||||||
for (JsonNode item : arr) {
|
for (JsonNode item : arr) {
|
||||||
|
List<String> blobs = new ArrayList<>();
|
||||||
|
JsonNode blobsNode = item.path("blobsB64");
|
||||||
|
if (blobsNode.isArray()) for (JsonNode blob : blobsNode) blobs.add(blob.asText(""));
|
||||||
|
if (blobs.isEmpty() && !item.path("blobB64").asText("").isBlank()) {
|
||||||
|
blobs.add(item.path("blobB64").asText(""));
|
||||||
|
}
|
||||||
items.add(new RemoteDmItem(
|
items.add(new RemoteDmItem(
|
||||||
item.path("messageKey").asText(""),
|
item.path("syncId").asText(""),
|
||||||
|
item.path("primaryMessageKey").asText(item.path("messageKey").asText("")),
|
||||||
item.path("storedAtMs").asLong(0L),
|
item.path("storedAtMs").asLong(0L),
|
||||||
item.path("blobB64").asText("")
|
blobs));
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new RemoteDmBatch(
|
return new RemoteDmBatch(
|
||||||
@@ -101,65 +181,47 @@ public final class RemoteDmSyncClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void deleteMessage(String serverAddressRaw, String blobB64) throws Exception {
|
public void deleteMessage(String serverAddressRaw, String blobB64) throws Exception {
|
||||||
|
deleteMessage(null, serverAddressRaw, blobB64);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteMessage(String targetServerLogin, String serverAddressRaw, String blobB64) throws Exception {
|
||||||
String blobJson = MAPPER.writeValueAsString(blobB64);
|
String blobJson = MAPPER.writeValueAsString(blobB64);
|
||||||
JsonNode response = send(serverAddressRaw, """
|
JsonNode response = send(targetServerLogin, serverAddressRaw, """
|
||||||
{
|
{
|
||||||
"op":"DeleteMessage",
|
"op":"DeleteMessage",
|
||||||
"requestId":%s,
|
"requestId":%s,
|
||||||
"payload":{
|
"payload":{"blobB64":%s}
|
||||||
"blobB64":%s
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
""".formatted("%s", blobJson));
|
""".formatted("%s", blobJson));
|
||||||
ensureOk("DeleteMessage", response);
|
ensureOk("DeleteMessage", response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteConversation(String serverAddressRaw, String blobB64) throws Exception {
|
public void deleteConversation(String serverAddressRaw, String blobB64) throws Exception {
|
||||||
|
deleteConversation(null, serverAddressRaw, blobB64);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteConversation(String targetServerLogin, String serverAddressRaw, String blobB64) throws Exception {
|
||||||
String blobJson = MAPPER.writeValueAsString(blobB64);
|
String blobJson = MAPPER.writeValueAsString(blobB64);
|
||||||
JsonNode response = send(serverAddressRaw, """
|
JsonNode response = send(targetServerLogin, serverAddressRaw, """
|
||||||
{
|
{
|
||||||
"op":"DeleteConversation",
|
"op":"DeleteConversation",
|
||||||
"requestId":%s,
|
"requestId":%s,
|
||||||
"payload":{
|
"payload":{"blobB64":%s}
|
||||||
"blobB64":%s
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
""".formatted("%s", blobJson));
|
""".formatted("%s", blobJson));
|
||||||
ensureOk("DeleteConversation", response);
|
ensureOk("DeleteConversation", response);
|
||||||
}
|
}
|
||||||
|
|
||||||
private JsonNode send(String serverAddressRaw, String jsonTemplate) throws Exception {
|
private JsonNode send(String targetServerLogin, String serverAddressRaw, String jsonTemplate) throws Exception {
|
||||||
String requestId = MAPPER.writeValueAsString("dm-sync-" + UUID.randomUUID());
|
return ServerConnectionPool.getInstance().request(
|
||||||
String json = jsonTemplate.formatted(requestId);
|
targetServerLogin,
|
||||||
String wsUrl = RemoteBlockchainSyncClient.buildWsUrl(serverAddressRaw);
|
serverAddressRaw,
|
||||||
if (wsUrl == null) {
|
jsonTemplate,
|
||||||
throw new IllegalArgumentException("Invalid server address: " + serverAddressRaw);
|
ServerConnectionPool.Priority.REALTIME);
|
||||||
}
|
|
||||||
|
|
||||||
CompletableFuture<String> responseFuture = new CompletableFuture<>();
|
|
||||||
CountDownLatch openLatch = new CountDownLatch(1);
|
|
||||||
SyncWsListener listener = new SyncWsListener(responseFuture, openLatch);
|
|
||||||
|
|
||||||
WebSocket webSocket = HTTP.newWebSocketBuilder()
|
|
||||||
.connectTimeout(Duration.ofSeconds(6))
|
|
||||||
.buildAsync(URI.create(wsUrl), listener)
|
|
||||||
.get(8, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
if (!openLatch.await(8, TimeUnit.SECONDS)) {
|
|
||||||
tryAbort(webSocket);
|
|
||||||
throw new TimeoutException("WS open timeout");
|
|
||||||
}
|
|
||||||
|
|
||||||
webSocket.sendText(json, true).get(8, TimeUnit.SECONDS);
|
|
||||||
String responseJson = responseFuture.get(12, TimeUnit.SECONDS);
|
|
||||||
tryAbort(webSocket);
|
|
||||||
return MAPPER.readTree(responseJson);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String toOptionalJsonField(String fieldName, String value) throws Exception {
|
private String toOptionalJsonField(String fieldName, String value) throws Exception {
|
||||||
if (value == null || value.isBlank()) {
|
if (value == null || value.isBlank()) return "";
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return ",\n \"" + fieldName + "\":" + MAPPER.writeValueAsString(value.trim());
|
return ",\n \"" + fieldName + "\":" + MAPPER.writeValueAsString(value.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,76 +233,10 @@ public final class RemoteDmSyncClient {
|
|||||||
throw new IllegalStateException(op + " failed: status=" + status + " code=" + code);
|
throw new IllegalStateException(op + " failed: status=" + status + " code=" + code);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void tryAbort(WebSocket webSocket) {
|
public record RemoteDeliveryStatus(String messageKey, boolean known, boolean delivered) {}
|
||||||
try {
|
public record RemoteDmBatch(long nextStoredAtMs, String nextMessageKey, boolean hasMore, List<RemoteDmItem> items) {}
|
||||||
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
webSocket.abort();
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public record RemoteDmBatch(
|
|
||||||
long nextStoredAtMs,
|
|
||||||
String nextMessageKey,
|
|
||||||
boolean hasMore,
|
|
||||||
List<RemoteDmItem> items
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public record RemoteDmItem(
|
public record RemoteDmItem(
|
||||||
String messageKey,
|
String syncId, String primaryMessageKey, long storedAtMs, List<String> blobsB64
|
||||||
long storedAtMs,
|
|
||||||
String blobB64
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private static final class SyncWsListener implements WebSocket.Listener {
|
|
||||||
private final CompletableFuture<String> responseFuture;
|
|
||||||
private final CountDownLatch openLatch;
|
|
||||||
private final StringBuilder textBuffer = new StringBuilder();
|
|
||||||
|
|
||||||
private SyncWsListener(CompletableFuture<String> responseFuture, CountDownLatch openLatch) {
|
|
||||||
this.responseFuture = responseFuture;
|
|
||||||
this.openLatch = openLatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOpen(WebSocket webSocket) {
|
|
||||||
openLatch.countDown();
|
|
||||||
webSocket.request(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
|
|
||||||
textBuffer.append(data);
|
|
||||||
if (last && !responseFuture.isDone()) {
|
|
||||||
responseFuture.complete(textBuffer.toString());
|
|
||||||
}
|
|
||||||
webSocket.request(1);
|
|
||||||
return CompletableFuture.completedFuture(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletionStage<?> onBinary(WebSocket webSocket, ByteBuffer data, boolean last) {
|
|
||||||
webSocket.request(1);
|
|
||||||
return CompletableFuture.completedFuture(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {
|
|
||||||
if (!responseFuture.isDone()) {
|
|
||||||
responseFuture.completeExceptionally(new IllegalStateException("WS closed before response: " + statusCode + " " + reason));
|
|
||||||
}
|
|
||||||
return CompletableFuture.completedFuture(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(WebSocket webSocket, Throwable error) {
|
|
||||||
if (!responseFuture.isDone()) {
|
|
||||||
responseFuture.completeExceptionally(error);
|
|
||||||
}
|
|
||||||
openLatch.countDown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package server.sync;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Логический последовательный сеанс поверх общего постоянного WSS-пула.
|
||||||
|
* close() больше не закрывает физическое соединение с сервером.
|
||||||
|
*/
|
||||||
|
public final class RemoteSyncSession implements AutoCloseable {
|
||||||
|
private final String serverLogin;
|
||||||
|
private final String serverAddress;
|
||||||
|
|
||||||
|
/** Совместимый конструктор: при отсутствии логина пул использует адрес как ключ peer. */
|
||||||
|
public RemoteSyncSession(String serverAddressRaw) throws Exception {
|
||||||
|
this(null, serverAddressRaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RemoteSyncSession(String serverLogin, String serverAddressRaw) {
|
||||||
|
this.serverLogin = serverLogin;
|
||||||
|
this.serverAddress = serverAddressRaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized JsonNode send(String jsonTemplate) throws Exception {
|
||||||
|
return ServerConnectionPool.getInstance().request(
|
||||||
|
serverLogin,
|
||||||
|
serverAddress,
|
||||||
|
jsonTemplate,
|
||||||
|
ServerConnectionPool.Priority.NORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// Физическое соединение принадлежит ServerConnectionPool.
|
||||||
|
}
|
||||||
|
}
|
||||||
+166
@@ -0,0 +1,166 @@
|
|||||||
|
package server.sync;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import shine.db.entities.UserSettingEntry;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class RemoteUserSettingsSyncClient {
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
|
public void upsertUserSetting(
|
||||||
|
String serverAddressRaw, UserSettingEntry entry, boolean syncDelivery
|
||||||
|
) throws Exception {
|
||||||
|
upsertUserSetting(null, serverAddressRaw, entry, syncDelivery);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void upsertUserSetting(
|
||||||
|
String targetServerLogin, String serverAddressRaw, UserSettingEntry entry, boolean syncDelivery
|
||||||
|
) throws Exception {
|
||||||
|
try (RemoteSyncSession session = new RemoteSyncSession(targetServerLogin, serverAddressRaw)) {
|
||||||
|
upsertUserSetting(session, entry, syncDelivery);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void upsertUserSetting(RemoteSyncSession session, UserSettingEntry entry, boolean syncDelivery) throws Exception {
|
||||||
|
JsonNode response = session.send("""
|
||||||
|
{
|
||||||
|
"op":"UpsertUserSetting",
|
||||||
|
"requestId":%s,
|
||||||
|
"payload":{
|
||||||
|
"login":%s,
|
||||||
|
"setting_type":%d,
|
||||||
|
"setting_key":%s,
|
||||||
|
"time_ms":%d,
|
||||||
|
"value_text":%s,
|
||||||
|
"value_num":%d,
|
||||||
|
"client_key":%s,
|
||||||
|
"signature":%s,
|
||||||
|
"sync_delivery":%s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".formatted(
|
||||||
|
"%s",
|
||||||
|
MAPPER.writeValueAsString(entry.getLogin()),
|
||||||
|
entry.getSettingType(),
|
||||||
|
MAPPER.writeValueAsString(entry.getSettingKey()),
|
||||||
|
entry.getTimeMs(),
|
||||||
|
MAPPER.writeValueAsString(entry.getValueText() == null ? "" : entry.getValueText()),
|
||||||
|
entry.getValueNum(),
|
||||||
|
MAPPER.writeValueAsString(entry.getClientKey() == null ? "" : entry.getClientKey()),
|
||||||
|
MAPPER.writeValueAsString(entry.getSignature() == null ? "" : entry.getSignature()),
|
||||||
|
syncDelivery ? "true" : "false"
|
||||||
|
));
|
||||||
|
ensureOk("UpsertUserSetting", response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RemoteUserSettingsBatch userSettingsSyncBatch(
|
||||||
|
String serverAddressRaw,
|
||||||
|
String ownerLogin,
|
||||||
|
long afterTimeMs,
|
||||||
|
String afterSettingKey,
|
||||||
|
int limit,
|
||||||
|
int maxBytes
|
||||||
|
) throws Exception {
|
||||||
|
return userSettingsSyncBatch(null, serverAddressRaw, ownerLogin,
|
||||||
|
afterTimeMs, afterSettingKey, limit, maxBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RemoteUserSettingsBatch userSettingsSyncBatch(
|
||||||
|
String targetServerLogin,
|
||||||
|
String serverAddressRaw,
|
||||||
|
String ownerLogin,
|
||||||
|
long afterTimeMs,
|
||||||
|
String afterSettingKey,
|
||||||
|
int limit,
|
||||||
|
int maxBytes
|
||||||
|
) throws Exception {
|
||||||
|
try (RemoteSyncSession session = new RemoteSyncSession(targetServerLogin, serverAddressRaw)) {
|
||||||
|
return userSettingsSyncBatch(session, ownerLogin, afterTimeMs, afterSettingKey, limit, maxBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RemoteUserSettingsBatch userSettingsSyncBatch(
|
||||||
|
RemoteSyncSession session,
|
||||||
|
String ownerLogin,
|
||||||
|
long afterTimeMs,
|
||||||
|
String afterSettingKey,
|
||||||
|
int limit,
|
||||||
|
int maxBytes
|
||||||
|
) throws Exception {
|
||||||
|
JsonNode response = session.send("""
|
||||||
|
{
|
||||||
|
"op":"UserSettingsSyncBatch",
|
||||||
|
"requestId":%s,
|
||||||
|
"payload":{
|
||||||
|
"ownerLogin":%s,
|
||||||
|
"afterTimeMs":%d,
|
||||||
|
"afterSettingKey":%s,
|
||||||
|
"limit":%d,
|
||||||
|
"maxBytes":%d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".formatted(
|
||||||
|
"%s",
|
||||||
|
MAPPER.writeValueAsString(ownerLogin),
|
||||||
|
Math.max(0L, afterTimeMs),
|
||||||
|
MAPPER.writeValueAsString(afterSettingKey == null ? "" : afterSettingKey),
|
||||||
|
limit,
|
||||||
|
maxBytes
|
||||||
|
));
|
||||||
|
ensureOk("UserSettingsSyncBatch", response);
|
||||||
|
|
||||||
|
JsonNode payload = response.path("payload");
|
||||||
|
List<RemoteUserSettingsItem> items = new ArrayList<>();
|
||||||
|
JsonNode arr = payload.path("items");
|
||||||
|
if (arr.isArray()) {
|
||||||
|
for (JsonNode item : arr) {
|
||||||
|
items.add(new RemoteUserSettingsItem(
|
||||||
|
item.path("login").asText(""),
|
||||||
|
item.path("setting_type").asInt(0),
|
||||||
|
item.path("setting_key").asText(""),
|
||||||
|
item.path("time_ms").asLong(0L),
|
||||||
|
item.path("value_text").asText(""),
|
||||||
|
item.path("value_num").asLong(0L),
|
||||||
|
item.path("client_key").asText(""),
|
||||||
|
item.path("signature").asText("")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new RemoteUserSettingsBatch(
|
||||||
|
payload.path("nextTimeMs").asLong(afterTimeMs),
|
||||||
|
payload.path("nextSettingKey").asText(afterSettingKey == null ? "" : afterSettingKey),
|
||||||
|
payload.path("hasMore").asBoolean(false),
|
||||||
|
items
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureOk(String op, JsonNode response) {
|
||||||
|
int status = response.path("status").asInt(500);
|
||||||
|
if (status >= 200 && status < 300) return;
|
||||||
|
String code = response.path("code").asText("");
|
||||||
|
if (code.isBlank()) code = response.path("error").asText("");
|
||||||
|
throw new IllegalStateException(op + " failed: status=" + status + " code=" + code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record RemoteUserSettingsBatch(
|
||||||
|
long nextTimeMs,
|
||||||
|
String nextSettingKey,
|
||||||
|
boolean hasMore,
|
||||||
|
List<RemoteUserSettingsItem> items
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public record RemoteUserSettingsItem(
|
||||||
|
String login,
|
||||||
|
int settingType,
|
||||||
|
String settingKey,
|
||||||
|
long timeMs,
|
||||||
|
String valueText,
|
||||||
|
long valueNum,
|
||||||
|
String clientKey,
|
||||||
|
String signature
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
||||||
+763
@@ -0,0 +1,763 @@
|
|||||||
|
package server.sync;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import shine.db.dao.SyncServersDAO;
|
||||||
|
import shine.db.dao.UserAccessServersCurrentDAO;
|
||||||
|
import shine.db.entities.SyncServerEntry;
|
||||||
|
import shine.db.entities.UserAccessServerRouteEntry;
|
||||||
|
import utils.config.AppConfig;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.WebSocket;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionStage;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.PriorityBlockingQueue;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Общий пул постоянных WSS-соединений между физическими серверами SHiNE.
|
||||||
|
*
|
||||||
|
* <p>Пул меняет только транспорт: существующие JSON-операции, ACK, outbox и
|
||||||
|
* расписания повторов остаются обязанностью вызывающих сервисов.</p>
|
||||||
|
*/
|
||||||
|
public final class ServerConnectionPool implements AutoCloseable {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ServerConnectionPool.class);
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
|
private static final String SERVER_LOGIN_CONFIG = "server.SHiNE.login";
|
||||||
|
private static final int REQUEST_WORKERS = Math.max(
|
||||||
|
8, Math.min(32, Runtime.getRuntime().availableProcessors() * 2));
|
||||||
|
private static final int CONNECTION_WORKERS = Math.max(
|
||||||
|
4, Math.min(16, Runtime.getRuntime().availableProcessors()));
|
||||||
|
private static final ServerConnectionPool INSTANCE = new ServerConnectionPool();
|
||||||
|
|
||||||
|
private final HttpClient http = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(6))
|
||||||
|
.build();
|
||||||
|
private final ConcurrentHashMap<String, PeerConnection> peers = new ConcurrentHashMap<>();
|
||||||
|
private final AtomicBoolean started = new AtomicBoolean(false);
|
||||||
|
private final AtomicBoolean closed = new AtomicBoolean(false);
|
||||||
|
private final AtomicLong requestSequence = new AtomicLong();
|
||||||
|
|
||||||
|
private final ThreadPoolExecutor requestExecutor = new ThreadPoolExecutor(
|
||||||
|
REQUEST_WORKERS,
|
||||||
|
REQUEST_WORKERS,
|
||||||
|
60L,
|
||||||
|
TimeUnit.SECONDS,
|
||||||
|
new java.util.concurrent.LinkedBlockingQueue<>(10_000),
|
||||||
|
daemonThreadFactory("server-pool-request"),
|
||||||
|
new ThreadPoolExecutor.CallerRunsPolicy());
|
||||||
|
private final ThreadPoolExecutor connectionExecutor = new ThreadPoolExecutor(
|
||||||
|
CONNECTION_WORKERS,
|
||||||
|
CONNECTION_WORKERS,
|
||||||
|
60L,
|
||||||
|
TimeUnit.SECONDS,
|
||||||
|
new java.util.concurrent.LinkedBlockingQueue<>(10_000),
|
||||||
|
daemonThreadFactory("server-pool-connect"),
|
||||||
|
new ThreadPoolExecutor.DiscardPolicy());
|
||||||
|
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(
|
||||||
|
2, daemonThreadFactory("server-pool-scheduler"));
|
||||||
|
|
||||||
|
private ServerConnectionPool() {}
|
||||||
|
|
||||||
|
public static ServerConnectionPool getInstance() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startOrLog() {
|
||||||
|
if (closed.get() || !started.compareAndSet(false, true)) return;
|
||||||
|
scheduler.scheduleWithFixedDelay(this::refreshKnownPeersSafe, 0L, 30L, TimeUnit.SECONDS);
|
||||||
|
scheduler.scheduleWithFixedDelay(this::healthCheckSafe, 10L, 10L, TimeUnit.SECONDS);
|
||||||
|
scheduler.scheduleWithFixedDelay(this::logMetricsSafe, 5L, 5L, TimeUnit.MINUTES);
|
||||||
|
log.info("Server connection pool started: adaptive ping={}s pongTimeout={}s",
|
||||||
|
pingIdleSeconds(), pongTimeoutSeconds());
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonNode request(
|
||||||
|
String serverLogin,
|
||||||
|
String serverAddress,
|
||||||
|
String jsonTemplate,
|
||||||
|
Priority priority
|
||||||
|
) throws Exception {
|
||||||
|
startOrLog();
|
||||||
|
String normalizedLogin = normalizeLogin(serverLogin);
|
||||||
|
if (normalizedLogin == null) {
|
||||||
|
normalizedLogin = loginFromAddress(serverAddress);
|
||||||
|
}
|
||||||
|
if (normalizedLogin == null) {
|
||||||
|
throw new IllegalArgumentException("Server login and address are empty");
|
||||||
|
}
|
||||||
|
PeerConnection peer = registerPeer(normalizedLogin, serverAddress);
|
||||||
|
return peer.request(jsonTemplate, priority == null ? Priority.NORMAL : priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PeerConnection registerPeer(String serverLogin, String serverAddress) {
|
||||||
|
String login = normalizeLogin(serverLogin);
|
||||||
|
String wsUrl = buildWsUrl(serverAddress);
|
||||||
|
if (login == null || wsUrl == null) {
|
||||||
|
throw new IllegalArgumentException("Invalid server peer: login=" + serverLogin + " address=" + serverAddress);
|
||||||
|
}
|
||||||
|
PeerConnection peer = peers.computeIfAbsent(login, ignored -> new PeerConnection(login, wsUrl));
|
||||||
|
peer.updateWsUrl(wsUrl);
|
||||||
|
peer.ensureConnectedInBackground(0L);
|
||||||
|
return peer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PeerMetricsSnapshot> snapshotMetrics() {
|
||||||
|
List<PeerMetricsSnapshot> result = new ArrayList<>();
|
||||||
|
for (PeerConnection peer : peers.values()) result.add(peer.snapshot());
|
||||||
|
result.sort(Comparator.comparing(PeerMetricsSnapshot::serverLogin));
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshKnownPeersSafe() {
|
||||||
|
if (closed.get()) return;
|
||||||
|
try {
|
||||||
|
String ownLogin = normalizeLogin(AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG));
|
||||||
|
Map<String, String> discovered = new LinkedHashMap<>();
|
||||||
|
for (SyncServerEntry entry : SyncServersDAO.getInstance().listAll()) {
|
||||||
|
if (entry == null) continue;
|
||||||
|
putDiscovered(discovered, ownLogin, entry.getLogin(), entry.getServerAddress());
|
||||||
|
}
|
||||||
|
for (UserAccessServerRouteEntry entry : UserAccessServersCurrentDAO.getInstance().listDistinctServers()) {
|
||||||
|
if (entry == null) continue;
|
||||||
|
putDiscovered(discovered, ownLogin, entry.getServerLogin(), entry.getServerUrl());
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, String> entry : discovered.entrySet()) {
|
||||||
|
try {
|
||||||
|
registerPeer(entry.getKey(), entry.getValue());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Server pool peer registration failed: server={} reason={}",
|
||||||
|
entry.getKey(), compactError(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Server pool peer refresh failed: {}", compactError(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void putDiscovered(
|
||||||
|
Map<String, String> discovered,
|
||||||
|
String ownLogin,
|
||||||
|
String serverLogin,
|
||||||
|
String serverAddress
|
||||||
|
) {
|
||||||
|
String login = normalizeLogin(serverLogin);
|
||||||
|
if (login == null || login.equals(ownLogin) || buildWsUrl(serverAddress) == null) return;
|
||||||
|
discovered.put(login, serverAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void healthCheckSafe() {
|
||||||
|
if (closed.get()) return;
|
||||||
|
for (PeerConnection peer : peers.values()) {
|
||||||
|
try {
|
||||||
|
peer.healthCheck();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Server pool health check failed: server={} reason={}",
|
||||||
|
peer.serverLogin, compactError(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logMetricsSafe() {
|
||||||
|
if (closed.get() || peers.isEmpty()) return;
|
||||||
|
long connected = peers.values().stream().filter(p -> p.state == ConnectionState.CONNECTED).count();
|
||||||
|
int queued = peers.values().stream().mapToInt(p -> p.queuedCount.get()).sum();
|
||||||
|
long errors = peers.values().stream().mapToLong(p -> p.failedRequests.get()).sum();
|
||||||
|
log.info("Server pool metrics: peers={} connected={} queued={} failedRequests={}",
|
||||||
|
peers.size(), connected, queued, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
if (!closed.compareAndSet(false, true)) return;
|
||||||
|
for (PeerConnection peer : peers.values()) peer.closeConnection("pool_shutdown");
|
||||||
|
scheduler.shutdownNow();
|
||||||
|
requestExecutor.shutdownNow();
|
||||||
|
connectionExecutor.shutdownNow();
|
||||||
|
peers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Priority {
|
||||||
|
REALTIME(0),
|
||||||
|
NORMAL(1),
|
||||||
|
BULK(2);
|
||||||
|
|
||||||
|
private final int rank;
|
||||||
|
Priority(int rank) { this.rank = rank; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ConnectionState {
|
||||||
|
DISCONNECTED,
|
||||||
|
CONNECTING,
|
||||||
|
CONNECTED,
|
||||||
|
CLOSED
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PeerMetricsSnapshot(
|
||||||
|
String serverLogin,
|
||||||
|
String wsUrl,
|
||||||
|
ConnectionState state,
|
||||||
|
long connectedAtMs,
|
||||||
|
long lastActivityAtMs,
|
||||||
|
long lastPingAtMs,
|
||||||
|
long lastPongAtMs,
|
||||||
|
long reconnectCount,
|
||||||
|
int queuedRealtime,
|
||||||
|
int queuedNormal,
|
||||||
|
int queuedBulk,
|
||||||
|
long successfulRequests,
|
||||||
|
long failedRequests,
|
||||||
|
long timedOutRequests,
|
||||||
|
String lastError
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public final class PeerConnection {
|
||||||
|
private final String serverLogin;
|
||||||
|
private final PriorityBlockingQueue<QueuedRequest> queue = new PriorityBlockingQueue<>();
|
||||||
|
private final ConcurrentHashMap<String, CompletableFuture<JsonNode>> pending = new ConcurrentHashMap<>();
|
||||||
|
private final AtomicBoolean drainScheduled = new AtomicBoolean(false);
|
||||||
|
private final AtomicBoolean reconnectScheduled = new AtomicBoolean(false);
|
||||||
|
private final AtomicInteger queuedCount = new AtomicInteger();
|
||||||
|
private final AtomicInteger reconnectAttempt = new AtomicInteger();
|
||||||
|
private final AtomicLong generation = new AtomicLong();
|
||||||
|
private final AtomicLong reconnectCount = new AtomicLong();
|
||||||
|
private final AtomicLong successfulRequests = new AtomicLong();
|
||||||
|
private final AtomicLong failedRequests = new AtomicLong();
|
||||||
|
private final AtomicLong timedOutRequests = new AtomicLong();
|
||||||
|
private final Object connectLock = new Object();
|
||||||
|
|
||||||
|
private volatile String wsUrl;
|
||||||
|
private volatile ConnectionState state = ConnectionState.DISCONNECTED;
|
||||||
|
private volatile WebSocket webSocket;
|
||||||
|
private volatile CompletableFuture<Void> readiness;
|
||||||
|
private volatile long connectedAtMs;
|
||||||
|
private volatile long lastActivityAtMs = System.currentTimeMillis();
|
||||||
|
private volatile long lastPingAtMs;
|
||||||
|
private volatile long lastPongAtMs;
|
||||||
|
private volatile long pingAwaitedSinceMs;
|
||||||
|
private volatile String lastError = "";
|
||||||
|
|
||||||
|
private PeerConnection(String serverLogin, String wsUrl) {
|
||||||
|
this.serverLogin = serverLogin;
|
||||||
|
this.wsUrl = wsUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode request(String jsonTemplate, Priority priority) throws Exception {
|
||||||
|
if (jsonTemplate == null || jsonTemplate.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("JSON request template is empty");
|
||||||
|
}
|
||||||
|
int maxQueue = (int) configLong("server.pool.maxQueuePerPeer", 2_000L, 10L, 100_000L);
|
||||||
|
int queued = queuedCount.incrementAndGet();
|
||||||
|
if (queued > maxQueue) {
|
||||||
|
queuedCount.decrementAndGet();
|
||||||
|
throw new IllegalStateException("Server peer queue is full: " + serverLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
QueuedRequest task = new QueuedRequest(
|
||||||
|
priority,
|
||||||
|
requestSequence.incrementAndGet(),
|
||||||
|
jsonTemplate,
|
||||||
|
new CompletableFuture<>());
|
||||||
|
queue.offer(task);
|
||||||
|
scheduleDrain();
|
||||||
|
|
||||||
|
long timeoutSeconds = configLong("server.pool.callerTimeoutSeconds", 35L, 5L, 120L);
|
||||||
|
try {
|
||||||
|
return task.result.get(timeoutSeconds, TimeUnit.SECONDS);
|
||||||
|
} catch (java.util.concurrent.TimeoutException e) {
|
||||||
|
timedOutRequests.incrementAndGet();
|
||||||
|
if (queue.remove(task)) queuedCount.decrementAndGet();
|
||||||
|
throw new TimeoutException("Server pool caller timeout: " + serverLogin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleDrain() {
|
||||||
|
if (!drainScheduled.compareAndSet(false, true)) return;
|
||||||
|
requestExecutor.execute(this::drainQueue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drainQueue() {
|
||||||
|
try {
|
||||||
|
for (;;) {
|
||||||
|
QueuedRequest task = queue.poll();
|
||||||
|
if (task == null) return;
|
||||||
|
queuedCount.decrementAndGet();
|
||||||
|
if (task.result.isDone()) continue;
|
||||||
|
try {
|
||||||
|
ensureConnectedBlocking();
|
||||||
|
JsonNode response = sendTemplate(task.jsonTemplate)
|
||||||
|
.get(requestTimeoutSeconds(), TimeUnit.SECONDS);
|
||||||
|
successfulRequests.incrementAndGet();
|
||||||
|
task.result.complete(response);
|
||||||
|
} catch (java.util.concurrent.TimeoutException e) {
|
||||||
|
timedOutRequests.incrementAndGet();
|
||||||
|
failedRequests.incrementAndGet();
|
||||||
|
task.result.completeExceptionally(
|
||||||
|
new TimeoutException("Server pool response timeout: " + serverLogin));
|
||||||
|
invalidateConnection("response_timeout", e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
failedRequests.incrementAndGet();
|
||||||
|
task.result.completeExceptionally(unwrap(e));
|
||||||
|
if (state != ConnectionState.DISCONNECTED) {
|
||||||
|
invalidateConnection("request_failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
drainScheduled.set(false);
|
||||||
|
if (!queue.isEmpty()) scheduleDrain();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureConnectedBlocking() throws Exception {
|
||||||
|
CompletableFuture<Void> localReady;
|
||||||
|
synchronized (connectLock) {
|
||||||
|
if (state == ConnectionState.CONNECTED && webSocket != null && !webSocket.isOutputClosed()) return;
|
||||||
|
if (state == ConnectionState.CLOSED || closed.get()) {
|
||||||
|
throw new IllegalStateException("Server connection pool is closed");
|
||||||
|
}
|
||||||
|
if (readiness == null || readiness.isDone()) startConnectLocked();
|
||||||
|
localReady = readiness;
|
||||||
|
}
|
||||||
|
localReady.get(connectAndHelloTimeoutSeconds(), TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startConnectLocked() {
|
||||||
|
long currentGeneration = generation.incrementAndGet();
|
||||||
|
state = ConnectionState.CONNECTING;
|
||||||
|
lastError = "";
|
||||||
|
CompletableFuture<Void> ready = new CompletableFuture<>();
|
||||||
|
readiness = ready;
|
||||||
|
Listener listener = new Listener(this, currentGeneration);
|
||||||
|
|
||||||
|
http.newWebSocketBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(6))
|
||||||
|
.buildAsync(URI.create(wsUrl), listener)
|
||||||
|
.thenCompose(ws -> {
|
||||||
|
if (generation.get() != currentGeneration) {
|
||||||
|
try { ws.abort(); } catch (Exception ignored) {}
|
||||||
|
return CompletableFuture.failedFuture(new IllegalStateException("Superseded server connection"));
|
||||||
|
}
|
||||||
|
webSocket = ws;
|
||||||
|
return sendServerHello(ws);
|
||||||
|
})
|
||||||
|
.whenComplete((ignored, error) -> {
|
||||||
|
if (error != null) {
|
||||||
|
ready.completeExceptionally(unwrap(error));
|
||||||
|
invalidateConnection(currentGeneration, "connect_or_hello_failed", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (generation.get() != currentGeneration) {
|
||||||
|
ready.completeExceptionally(new IllegalStateException("Superseded server connection"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
connectedAtMs = now;
|
||||||
|
lastActivityAtMs = now;
|
||||||
|
lastPongAtMs = now;
|
||||||
|
pingAwaitedSinceMs = 0L;
|
||||||
|
reconnectAttempt.set(0);
|
||||||
|
state = ConnectionState.CONNECTED;
|
||||||
|
ready.complete(null);
|
||||||
|
log.info("Server pool connected: server={} url={}", serverLogin, wsUrl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<Void> sendServerHello(WebSocket ws) {
|
||||||
|
try {
|
||||||
|
String ownLogin = normalizeLogin(AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG));
|
||||||
|
if (ownLogin == null) ownLogin = "unconfigured-server";
|
||||||
|
String requestId = "server-hello-" + UUID.randomUUID();
|
||||||
|
CompletableFuture<JsonNode> response = new CompletableFuture<>();
|
||||||
|
pending.put(requestId, response);
|
||||||
|
String json = """
|
||||||
|
{
|
||||||
|
"op":"ServerHello",
|
||||||
|
"requestId":%s,
|
||||||
|
"payload":{
|
||||||
|
"serverLogin":%s,
|
||||||
|
"protocolVersion":1,
|
||||||
|
"capabilities":["dm-sync","settings-sync","block-sync","connection-pool"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".formatted(
|
||||||
|
MAPPER.writeValueAsString(requestId),
|
||||||
|
MAPPER.writeValueAsString(ownLogin));
|
||||||
|
lastActivityAtMs = System.currentTimeMillis();
|
||||||
|
ws.sendText(json, true).whenComplete((ignored, error) -> {
|
||||||
|
if (error != null) {
|
||||||
|
pending.remove(requestId);
|
||||||
|
response.completeExceptionally(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response.orTimeout(requestTimeoutSeconds(), TimeUnit.SECONDS)
|
||||||
|
.thenAccept(node -> ensureOk("ServerHello", node));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return CompletableFuture.failedFuture(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<JsonNode> sendTemplate(String jsonTemplate) throws Exception {
|
||||||
|
WebSocket ws = webSocket;
|
||||||
|
if (state != ConnectionState.CONNECTED || ws == null || ws.isOutputClosed()) {
|
||||||
|
return CompletableFuture.failedFuture(new IllegalStateException("Server peer is disconnected"));
|
||||||
|
}
|
||||||
|
String requestId = "server-pool-" + UUID.randomUUID();
|
||||||
|
String requestIdJson = MAPPER.writeValueAsString(requestId);
|
||||||
|
String json = fillRequestId(jsonTemplate, requestIdJson);
|
||||||
|
CompletableFuture<JsonNode> response = new CompletableFuture<>();
|
||||||
|
pending.put(requestId, response);
|
||||||
|
lastActivityAtMs = System.currentTimeMillis();
|
||||||
|
ws.sendText(json, true).whenComplete((ignored, error) -> {
|
||||||
|
if (error != null) {
|
||||||
|
pending.remove(requestId);
|
||||||
|
response.completeExceptionally(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void acceptText(long listenerGeneration, String text) {
|
||||||
|
if (generation.get() != listenerGeneration || text == null || text.isBlank()) return;
|
||||||
|
lastActivityAtMs = System.currentTimeMillis();
|
||||||
|
try {
|
||||||
|
JsonNode node = MAPPER.readTree(text);
|
||||||
|
String requestId = node.path("requestId").asText("");
|
||||||
|
CompletableFuture<JsonNode> response = requestId.isBlank() ? null : pending.remove(requestId);
|
||||||
|
if (response != null) {
|
||||||
|
response.complete(node);
|
||||||
|
} else {
|
||||||
|
log.debug("Server pool ignored unmatched frame: server={} requestId={} op={}",
|
||||||
|
serverLogin, requestId, node.path("op").asText(""));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Server pool received invalid JSON: server={} reason={}", serverLogin, compactError(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void healthCheck() {
|
||||||
|
if (state == ConnectionState.DISCONNECTED) {
|
||||||
|
ensureConnectedInBackground(reconnectDelayMillis(reconnectAttempt.get()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state != ConnectionState.CONNECTED) return;
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (pingAwaitedSinceMs > 0L) {
|
||||||
|
if (now - pingAwaitedSinceMs >= TimeUnit.SECONDS.toMillis(pongTimeoutSeconds())) {
|
||||||
|
invalidateConnection("pong_timeout", new TimeoutException("Pong timeout"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (now - lastActivityAtMs < TimeUnit.SECONDS.toMillis(pingIdleSeconds())) return;
|
||||||
|
WebSocket ws = webSocket;
|
||||||
|
if (ws == null || ws.isOutputClosed()) {
|
||||||
|
invalidateConnection("socket_closed", null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long nonce = now;
|
||||||
|
pingAwaitedSinceMs = now;
|
||||||
|
lastPingAtMs = now;
|
||||||
|
ws.sendPing(ByteBuffer.allocate(Long.BYTES).putLong(0, nonce))
|
||||||
|
.whenComplete((ignored, error) -> {
|
||||||
|
if (error != null) invalidateConnection("ping_failed", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void acceptPong(long listenerGeneration) {
|
||||||
|
if (generation.get() != listenerGeneration) return;
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
lastPongAtMs = now;
|
||||||
|
lastActivityAtMs = now;
|
||||||
|
pingAwaitedSinceMs = 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateWsUrl(String newWsUrl) {
|
||||||
|
if (newWsUrl.equals(wsUrl)) return;
|
||||||
|
wsUrl = newWsUrl;
|
||||||
|
invalidateConnection("peer_url_changed", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureConnectedInBackground(long delayMs) {
|
||||||
|
if (closed.get() || state == ConnectionState.CLOSED) return;
|
||||||
|
if (!reconnectScheduled.compareAndSet(false, true)) return;
|
||||||
|
scheduler.schedule(() -> {
|
||||||
|
reconnectScheduled.set(false);
|
||||||
|
if (closed.get() || state == ConnectionState.CLOSED || state == ConnectionState.CONNECTED) return;
|
||||||
|
connectionExecutor.execute(() -> {
|
||||||
|
try {
|
||||||
|
ensureConnectedBlocking();
|
||||||
|
} catch (Exception e) {
|
||||||
|
lastError = compactError(e);
|
||||||
|
// Ошибка connect/ServerHello сама инвалидирует поколение и планирует reconnect.
|
||||||
|
// Здесь второй schedule дал бы двойной рост backoff для одной попытки.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, Math.max(0L, delayMs), TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleReconnect() {
|
||||||
|
if (closed.get() || state == ConnectionState.CLOSED) return;
|
||||||
|
int attempt = reconnectAttempt.getAndUpdate(value -> Math.min(value + 1, 30));
|
||||||
|
reconnectCount.incrementAndGet();
|
||||||
|
ensureConnectedInBackground(reconnectDelayMillis(attempt));
|
||||||
|
}
|
||||||
|
|
||||||
|
private long reconnectDelayMillis(int attempt) {
|
||||||
|
long[] seconds = {1L, 2L, 4L, 8L, 15L, 30L, 60L};
|
||||||
|
long base = seconds[Math.min(Math.max(0, attempt), seconds.length - 1)];
|
||||||
|
long halfMs = TimeUnit.SECONDS.toMillis(base) / 2L;
|
||||||
|
long jitterMs = java.util.concurrent.ThreadLocalRandom.current().nextLong(halfMs + 1L);
|
||||||
|
return halfMs + jitterMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invalidateConnection(String reason, Throwable error) {
|
||||||
|
invalidateConnection(generation.get(), reason, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invalidateConnection(long expectedGeneration, String reason, Throwable error) {
|
||||||
|
if (state == ConnectionState.CLOSED
|
||||||
|
|| !generation.compareAndSet(expectedGeneration, expectedGeneration + 1L)) return;
|
||||||
|
lastError = error == null ? reason : reason + ": " + compactError(error);
|
||||||
|
state = ConnectionState.DISCONNECTED;
|
||||||
|
pingAwaitedSinceMs = 0L;
|
||||||
|
WebSocket ws = webSocket;
|
||||||
|
webSocket = null;
|
||||||
|
if (ws != null) {
|
||||||
|
try { ws.abort(); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
Exception failure = new IllegalStateException("Server connection lost: " + serverLogin + " (" + reason + ")");
|
||||||
|
for (Map.Entry<String, CompletableFuture<JsonNode>> entry : pending.entrySet()) {
|
||||||
|
if (pending.remove(entry.getKey(), entry.getValue())) {
|
||||||
|
entry.getValue().completeExceptionally(failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scheduleReconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeConnection(String reason) {
|
||||||
|
state = ConnectionState.CLOSED;
|
||||||
|
generation.incrementAndGet();
|
||||||
|
WebSocket ws = webSocket;
|
||||||
|
webSocket = null;
|
||||||
|
if (ws != null) {
|
||||||
|
try { ws.sendClose(WebSocket.NORMAL_CLOSURE, reason); } catch (Exception ignored) {}
|
||||||
|
try { ws.abort(); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
IllegalStateException failure = new IllegalStateException("Server connection closed: " + serverLogin);
|
||||||
|
for (CompletableFuture<JsonNode> future : pending.values()) future.completeExceptionally(failure);
|
||||||
|
pending.clear();
|
||||||
|
for (QueuedRequest request : queue) request.result.completeExceptionally(failure);
|
||||||
|
queue.clear();
|
||||||
|
queuedCount.set(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PeerMetricsSnapshot snapshot() {
|
||||||
|
int realtime = 0;
|
||||||
|
int normal = 0;
|
||||||
|
int bulk = 0;
|
||||||
|
for (QueuedRequest request : queue) {
|
||||||
|
switch (request.priority) {
|
||||||
|
case REALTIME -> realtime++;
|
||||||
|
case NORMAL -> normal++;
|
||||||
|
case BULK -> bulk++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new PeerMetricsSnapshot(
|
||||||
|
serverLogin, wsUrl, state, connectedAtMs, lastActivityAtMs,
|
||||||
|
lastPingAtMs, lastPongAtMs, reconnectCount.get(), realtime, normal, bulk,
|
||||||
|
successfulRequests.get(), failedRequests.get(), timedOutRequests.get(), lastError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record QueuedRequest(
|
||||||
|
Priority priority,
|
||||||
|
long sequence,
|
||||||
|
String jsonTemplate,
|
||||||
|
CompletableFuture<JsonNode> result
|
||||||
|
) implements Comparable<QueuedRequest> {
|
||||||
|
@Override
|
||||||
|
public int compareTo(QueuedRequest other) {
|
||||||
|
int byPriority = Integer.compare(priority.rank, other.priority.rank);
|
||||||
|
return byPriority != 0 ? byPriority : Long.compare(sequence, other.sequence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Listener implements WebSocket.Listener {
|
||||||
|
private final PeerConnection peer;
|
||||||
|
private final long generation;
|
||||||
|
private final StringBuilder text = new StringBuilder();
|
||||||
|
|
||||||
|
private Listener(PeerConnection peer, long generation) {
|
||||||
|
this.peer = peer;
|
||||||
|
this.generation = generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOpen(WebSocket webSocket) {
|
||||||
|
webSocket.request(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
|
||||||
|
text.append(data);
|
||||||
|
if (last) {
|
||||||
|
peer.acceptText(generation, text.toString());
|
||||||
|
text.setLength(0);
|
||||||
|
}
|
||||||
|
webSocket.request(1);
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletionStage<?> onBinary(WebSocket webSocket, ByteBuffer data, boolean last) {
|
||||||
|
webSocket.request(1);
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletionStage<?> onPong(WebSocket webSocket, ByteBuffer message) {
|
||||||
|
peer.acceptPong(generation);
|
||||||
|
webSocket.request(1);
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {
|
||||||
|
peer.invalidateConnection(generation, "remote_close_" + statusCode, null);
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(WebSocket webSocket, Throwable error) {
|
||||||
|
peer.invalidateConnection(generation, "websocket_error", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String buildWsUrl(String serverAddressRaw) {
|
||||||
|
if (serverAddressRaw == null) return null;
|
||||||
|
String raw = serverAddressRaw.trim();
|
||||||
|
if (raw.isEmpty()) return null;
|
||||||
|
try {
|
||||||
|
String withScheme = raw.matches("^[a-zA-Z]+://.*$") ? raw : "https://" + raw;
|
||||||
|
URI uri = URI.create(withScheme);
|
||||||
|
String host = uri.getHost();
|
||||||
|
if (host == null || host.isBlank()) return null;
|
||||||
|
int port = uri.getPort();
|
||||||
|
String authority = host.trim().toLowerCase(Locale.ROOT) + (port > 0 ? ":" + port : "");
|
||||||
|
return "wss://" + authority + "/ws";
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String loginFromAddress(String serverAddress) {
|
||||||
|
String wsUrl = buildWsUrl(serverAddress);
|
||||||
|
if (wsUrl == null) return null;
|
||||||
|
try {
|
||||||
|
return "address:" + URI.create(wsUrl).getAuthority().toLowerCase(Locale.ROOT);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeLogin(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
String normalized = value.trim().toLowerCase(Locale.ROOT);
|
||||||
|
return normalized.isEmpty() ? null : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ensureOk(String op, JsonNode response) {
|
||||||
|
int status = response == null ? 500 : response.path("status").asInt(500);
|
||||||
|
if (status >= 200 && status < 300) return;
|
||||||
|
String code = response == null ? "EMPTY_RESPONSE" : response.path("code").asText("");
|
||||||
|
if (code.isBlank() && response != null) code = response.path("error").asText("");
|
||||||
|
throw new IllegalStateException(op + " failed: status=" + status + " code=" + code);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String fillRequestId(String template, String requestIdJson) {
|
||||||
|
int marker = template.indexOf("%s");
|
||||||
|
if (marker < 0) throw new IllegalArgumentException("JSON template has no requestId marker");
|
||||||
|
return template.substring(0, marker) + requestIdJson + template.substring(marker + 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Throwable unwrap(Throwable error) {
|
||||||
|
Throwable current = error;
|
||||||
|
while ((current instanceof java.util.concurrent.CompletionException
|
||||||
|
|| current instanceof java.util.concurrent.ExecutionException)
|
||||||
|
&& current.getCause() != null) {
|
||||||
|
current = current.getCause();
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Exception unwrap(Exception error) {
|
||||||
|
Throwable unwrapped = unwrap((Throwable) error);
|
||||||
|
return unwrapped instanceof Exception e ? e : new IllegalStateException(unwrapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String compactError(Throwable error) {
|
||||||
|
String text = String.valueOf(error == null ? "unknown" : error.getMessage());
|
||||||
|
return text.length() <= 500 ? text : text.substring(0, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long requestTimeoutSeconds() {
|
||||||
|
return configLong("server.pool.requestTimeoutSeconds", 12L, 3L, 120L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long connectAndHelloTimeoutSeconds() {
|
||||||
|
return configLong("server.pool.connectTimeoutSeconds", 15L, 5L, 120L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long pingIdleSeconds() {
|
||||||
|
return configLong("server.pool.pingIdleSeconds", 120L, 15L, 240L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long pongTimeoutSeconds() {
|
||||||
|
return configLong("server.pool.pongTimeoutSeconds", 15L, 5L, 120L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long configLong(String key, long defaultValue, long min, long max) {
|
||||||
|
String raw = AppConfig.getInstance().getParam(key);
|
||||||
|
if (raw == null || raw.isBlank()) return defaultValue;
|
||||||
|
try {
|
||||||
|
long value = Long.parseLong(raw.trim());
|
||||||
|
return Math.max(min, Math.min(max, value));
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ThreadFactory daemonThreadFactory(String prefix) {
|
||||||
|
return new ThreadFactory() {
|
||||||
|
private final AtomicLong sequence = new AtomicLong();
|
||||||
|
@Override
|
||||||
|
public Thread newThread(Runnable r) {
|
||||||
|
Thread thread = new Thread(r, prefix + "-" + sequence.incrementAndGet());
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,7 +67,8 @@ public final class BlockchainResyncRecoveryOnStartup {
|
|||||||
blockchainName, partnerLogin, partnerAddress);
|
blockchainName, partnerLogin, partnerAddress);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<RemoteBlockchainSyncClient.RemoteBlockchainHead> heads = REMOTE.listBlockchainHeads(partnerAddress);
|
List<RemoteBlockchainSyncClient.RemoteBlockchainHead> heads =
|
||||||
|
REMOTE.listBlockchainHeads(partnerLogin, partnerAddress);
|
||||||
RemoteBlockchainSyncClient.RemoteBlockchainHead remoteHead = heads.stream()
|
RemoteBlockchainSyncClient.RemoteBlockchainHead remoteHead = heads.stream()
|
||||||
.filter(h -> h != null && blockchainName.equals(h.blockchainName()))
|
.filter(h -> h != null && blockchainName.equals(h.blockchainName()))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ public final class PeriodicBlockchainSyncService {
|
|||||||
if (partnerLogin == null) return;
|
if (partnerLogin == null) return;
|
||||||
|
|
||||||
List<RemoteBlockchainSyncClient.RemoteBlockchainHead> remoteHeads =
|
List<RemoteBlockchainSyncClient.RemoteBlockchainHead> remoteHeads =
|
||||||
REMOTE.listBlockchainHeads(partner.getServerAddress());
|
REMOTE.listBlockchainHeads(partner.getLogin(), partner.getServerAddress());
|
||||||
|
|
||||||
for (RemoteBlockchainSyncClient.RemoteBlockchainHead remoteHead : remoteHeads) {
|
for (RemoteBlockchainSyncClient.RemoteBlockchainHead remoteHead : remoteHeads) {
|
||||||
if (remoteHead == null || remoteHead.blockchainName() == null || remoteHead.blockchainName().isBlank()) {
|
if (remoteHead == null || remoteHead.blockchainName() == null || remoteHead.blockchainName().isBlank()) {
|
||||||
@@ -170,7 +170,8 @@ public final class PeriodicBlockchainSyncService {
|
|||||||
int fromBlockNumber = Math.max(localLast + 1, 0);
|
int fromBlockNumber = Math.max(localLast + 1, 0);
|
||||||
for (int blockNumber = fromBlockNumber; blockNumber <= remoteHead.lastBlockNumber(); blockNumber++) {
|
for (int blockNumber = fromBlockNumber; blockNumber <= remoteHead.lastBlockNumber(); blockNumber++) {
|
||||||
RemoteBlockchainSyncClient.RemoteBlockchainBlock remoteBlock =
|
RemoteBlockchainSyncClient.RemoteBlockchainBlock remoteBlock =
|
||||||
REMOTE.getBlockchainBlock(partner.getServerAddress(), remoteHead.blockchainName(), blockNumber);
|
REMOTE.getBlockchainBlock(
|
||||||
|
partner.getLogin(), partner.getServerAddress(), remoteHead.blockchainName(), blockNumber);
|
||||||
if (remoteBlock == null) {
|
if (remoteBlock == null) {
|
||||||
log.warn("Periodic blockchain sync: remote block not found. partner={} blockchainName={} blockNumber={}",
|
log.warn("Periodic blockchain sync: remote block not found. partner={} blockchainName={} blockNumber={}",
|
||||||
partnerLogin, remoteHead.blockchainName(), blockNumber);
|
partnerLogin, remoteHead.blockchainName(), blockNumber);
|
||||||
@@ -284,7 +285,8 @@ public final class PeriodicBlockchainSyncService {
|
|||||||
String localPrevHash = "";
|
String localPrevHash = "";
|
||||||
for (int blockNumber = 0; blockNumber <= remoteHead.lastBlockNumber(); blockNumber++) {
|
for (int blockNumber = 0; blockNumber <= remoteHead.lastBlockNumber(); blockNumber++) {
|
||||||
RemoteBlockchainSyncClient.RemoteBlockchainBlock remoteBlock =
|
RemoteBlockchainSyncClient.RemoteBlockchainBlock remoteBlock =
|
||||||
REMOTE.getBlockchainBlock(partner.getServerAddress(), blockchainName, blockNumber);
|
REMOTE.getBlockchainBlock(
|
||||||
|
partner.getLogin(), partner.getServerAddress(), blockchainName, blockNumber);
|
||||||
if (remoteBlock == null) {
|
if (remoteBlock == null) {
|
||||||
log.warn("Blockchain resync: remote block not found. partner={} blockchainName={} blockNumber={}",
|
log.warn("Blockchain resync: remote block not found. partner={} blockchainName={} blockNumber={}",
|
||||||
partnerLogin, blockchainName, blockNumber);
|
partnerLogin, blockchainName, blockNumber);
|
||||||
|
|||||||
@@ -2,165 +2,52 @@ package server.sync;
|
|||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import server.logic.ws_protocol.JSON.messages.DmSyncApplySupport;
|
import shine.db.entities.DmDeliveryStateEntry;
|
||||||
import shine.db.dao.DmSyncPeerStateDAO;
|
|
||||||
import shine.db.dao.UserAccessServersCurrentDAO;
|
|
||||||
import shine.db.entities.DmSyncPeerStateEntry;
|
|
||||||
import shine.db.entities.UserAccessServerRouteEntry;
|
|
||||||
import utils.config.AppConfig;
|
import utils.config.AppConfig;
|
||||||
|
|
||||||
import java.util.LinkedHashSet;
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.ThreadFactory;
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/**
|
/** Пятисекундный воркер отвечает только за доставку DM получателю. */
|
||||||
* Периодическая догоняющая синхронизация личной переписки между access-серверами пользователя.
|
|
||||||
*/
|
|
||||||
public final class PeriodicDmSyncService {
|
public final class PeriodicDmSyncService {
|
||||||
private static final Logger log = LoggerFactory.getLogger(PeriodicDmSyncService.class);
|
private static final Logger log = LoggerFactory.getLogger(PeriodicDmSyncService.class);
|
||||||
private static final String SERVER_LOGIN_CONFIG = "server.SHiNE.login";
|
|
||||||
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
|
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
|
||||||
|
private static final ScheduledExecutorService SCHEDULER =
|
||||||
private static final RemoteDmSyncClient REMOTE = new RemoteDmSyncClient();
|
Executors.newSingleThreadScheduledExecutor(daemonThreadFactory("dm-worker-dispatcher"));
|
||||||
private static final UserAccessServersCurrentDAO ACCESS_DAO = UserAccessServersCurrentDAO.getInstance();
|
private static final ThreadPoolExecutor DELIVERY_EXECUTOR = new ThreadPoolExecutor(
|
||||||
private static final DmSyncPeerStateDAO STATE_DAO = DmSyncPeerStateDAO.getInstance();
|
4, 4, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(500),
|
||||||
|
daemonThreadFactory("dm-delivery"), new ThreadPoolExecutor.DiscardPolicy());
|
||||||
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
|
|
||||||
@Override
|
|
||||||
public Thread newThread(Runnable r) {
|
|
||||||
Thread t = new Thread(r, "periodic-dm-sync");
|
|
||||||
t.setDaemon(true);
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
private PeriodicDmSyncService() {}
|
private PeriodicDmSyncService() {}
|
||||||
|
|
||||||
public static void startOrLog() {
|
public static void startOrLog() {
|
||||||
if (!isEnabled()) {
|
if (!isEnabled()) {
|
||||||
log.info("Periodic DM sync disabled by dm.sync.enabled=false");
|
log.info("DM delivery worker disabled by dm.sync.enabled=false");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!STARTED.compareAndSet(false, true)) {
|
if (!STARTED.compareAndSet(false, true)) return;
|
||||||
return;
|
long pollSeconds = configLong("dm.worker.pollSeconds", 5L, 1L, 60L);
|
||||||
}
|
SCHEDULER.scheduleWithFixedDelay(
|
||||||
long initialDelaySec = configLong("dm.sync.initialDelaySeconds", 60L, 0L, 3600L);
|
PeriodicDmSyncService::tickSafe, 0L, pollSeconds, TimeUnit.SECONDS);
|
||||||
long periodHours = configLong("dm.sync.periodHours", 6L, 1L, 168L);
|
log.info("DM delivery worker scheduled every {} seconds", pollSeconds);
|
||||||
EXECUTOR.scheduleWithFixedDelay(
|
|
||||||
PeriodicDmSyncService::runCycleSafe,
|
|
||||||
initialDelaySec,
|
|
||||||
TimeUnit.HOURS.toSeconds(periodHours),
|
|
||||||
TimeUnit.SECONDS
|
|
||||||
);
|
|
||||||
log.info("Periodic DM sync scheduled: first run in {} seconds, then every {} hours", initialDelaySec, periodHours);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void runCycleSafe() {
|
private static void tickSafe() {
|
||||||
try {
|
try {
|
||||||
runCycle();
|
int limit = (int) configLong("dm.worker.dueLimit", 100L, 1L, 1000L);
|
||||||
|
for (DmDeliveryStateEntry row : DmDeliveryCoordinator.listDue(limit)) {
|
||||||
|
DELIVERY_EXECUTOR.execute(() -> DmDeliveryCoordinator.processDueEntry(row));
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Periodic DM sync failed unexpectedly", e);
|
log.error("DM delivery dispatcher failed", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void runCycle() throws Exception {
|
|
||||||
String ownServerLogin = normalize(AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG));
|
|
||||||
if (ownServerLogin == null) {
|
|
||||||
log.warn("Periodic DM sync skipped: {} is empty", SERVER_LOGIN_CONFIG);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> ownersRaw = ACCESS_DAO.listUserLoginsByServerLogin(ownServerLogin);
|
|
||||||
Set<String> owners = new LinkedHashSet<>(ownersRaw);
|
|
||||||
if (owners.isEmpty()) {
|
|
||||||
log.info("Periodic DM sync skipped: no local access-server users for {}", ownServerLogin);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int syncedPeers = 0;
|
|
||||||
int appliedEvents = 0;
|
|
||||||
for (String ownerLogin : owners) {
|
|
||||||
List<UserAccessServerRouteEntry> routes = ACCESS_DAO.listByUserLogin(ownerLogin);
|
|
||||||
for (UserAccessServerRouteEntry route : routes) {
|
|
||||||
if (route == null) continue;
|
|
||||||
String remoteLogin = normalize(route.getServerLogin());
|
|
||||||
String remoteUrl = route.getServerUrl();
|
|
||||||
if (remoteLogin == null || remoteUrl == null || remoteUrl.isBlank()) continue;
|
|
||||||
if (remoteLogin.equals(ownServerLogin)) continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
appliedEvents += syncOwnerFromRemote(ownerLogin, route);
|
|
||||||
syncedPeers++;
|
|
||||||
} catch (Exception e) {
|
|
||||||
STATE_DAO.updateError(ownerLogin, route.getServerLogin(), remoteUrl, String.valueOf(e));
|
|
||||||
log.warn("Periodic DM sync peer failed: owner={} remoteServer={} reason={}",
|
|
||||||
ownerLogin, route.getServerLogin(), String.valueOf(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.info("Periodic DM sync cycle finished: owners={} syncedPeers={} appliedEvents={}",
|
|
||||||
owners.size(), syncedPeers, appliedEvents);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int syncOwnerFromRemote(String ownerLogin, UserAccessServerRouteEntry route) throws Exception {
|
|
||||||
int limit = (int) configLong("dm.sync.batchLimit", 500L, 1L, 500L);
|
|
||||||
int maxBytes = (int) configLong("dm.sync.batchMaxBytes", 3_000_000L, 64_000L, 5_000_000L);
|
|
||||||
int maxPages = (int) configLong("dm.sync.maxPagesPerPeer", 50L, 1L, 500L);
|
|
||||||
|
|
||||||
DmSyncPeerStateEntry state = STATE_DAO.getOrCreate(ownerLogin, route.getServerLogin(), route.getServerUrl());
|
|
||||||
long cursorStoredAtMs = state.getCursorStoredAtMs();
|
|
||||||
String cursorMessageKey = state.getCursorMessageKey() == null ? "" : state.getCursorMessageKey();
|
|
||||||
int applied = 0;
|
|
||||||
boolean bootstrapCompleted = false;
|
|
||||||
|
|
||||||
for (int page = 0; page < maxPages; page++) {
|
|
||||||
RemoteDmSyncClient.RemoteDmBatch batch = REMOTE.dmSyncBatch(
|
|
||||||
route.getServerUrl(),
|
|
||||||
ownerLogin,
|
|
||||||
cursorStoredAtMs,
|
|
||||||
cursorMessageKey,
|
|
||||||
limit,
|
|
||||||
maxBytes
|
|
||||||
);
|
|
||||||
|
|
||||||
for (RemoteDmSyncClient.RemoteDmItem item : batch.items()) {
|
|
||||||
if (item == null || item.blobB64() == null || item.blobB64().isBlank()) continue;
|
|
||||||
DmSyncApplySupport.ApplyResult result = DmSyncApplySupport.applySyncedBlob(ownerLogin, item.blobB64());
|
|
||||||
if (result.status().applied()) {
|
|
||||||
applied++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cursorStoredAtMs = Math.max(cursorStoredAtMs, batch.nextStoredAtMs());
|
|
||||||
cursorMessageKey = batch.nextMessageKey() == null ? "" : batch.nextMessageKey();
|
|
||||||
bootstrapCompleted = !batch.hasMore();
|
|
||||||
STATE_DAO.updateSuccess(
|
|
||||||
ownerLogin,
|
|
||||||
route.getServerLogin(),
|
|
||||||
route.getServerUrl(),
|
|
||||||
cursorStoredAtMs,
|
|
||||||
cursorMessageKey,
|
|
||||||
bootstrapCompleted
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!batch.hasMore() || batch.items().isEmpty()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bootstrapCompleted) {
|
|
||||||
log.info("Periodic DM sync peer paused by page limit: owner={} remoteServer={} maxPages={}",
|
|
||||||
ownerLogin, route.getServerLogin(), maxPages);
|
|
||||||
}
|
|
||||||
return applied;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isEnabled() {
|
private static boolean isEnabled() {
|
||||||
String raw = AppConfig.getInstance().getParam("dm.sync.enabled");
|
String raw = AppConfig.getInstance().getParam("dm.sync.enabled");
|
||||||
return raw == null || raw.isBlank() || Boolean.parseBoolean(raw.trim());
|
return raw == null || raw.isBlank() || Boolean.parseBoolean(raw.trim());
|
||||||
@@ -169,17 +56,18 @@ public final class PeriodicDmSyncService {
|
|||||||
private static long configLong(String key, long defaultValue, long min, long max) {
|
private static long configLong(String key, long defaultValue, long min, long max) {
|
||||||
String raw = AppConfig.getInstance().getParam(key);
|
String raw = AppConfig.getInstance().getParam(key);
|
||||||
if (raw == null || raw.isBlank()) return defaultValue;
|
if (raw == null || raw.isBlank()) return defaultValue;
|
||||||
try {
|
try { return Math.max(min, Math.min(max, Long.parseLong(raw.trim()))); }
|
||||||
long parsed = Long.parseLong(raw.trim());
|
catch (Exception ignored) { return defaultValue; }
|
||||||
return Math.max(min, Math.min(max, parsed));
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String normalize(String value) {
|
private static ThreadFactory daemonThreadFactory(String prefix) {
|
||||||
if (value == null) return null;
|
return new ThreadFactory() {
|
||||||
String s = value.trim().toLowerCase(Locale.ROOT);
|
private int sequence;
|
||||||
return s.isEmpty() ? null : s;
|
@Override public synchronized Thread newThread(Runnable r) {
|
||||||
|
Thread t = new Thread(r, prefix + "-" + (++sequence));
|
||||||
|
t.setDaemon(true);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,256 @@
|
|||||||
|
package server.sync;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import server.logic.ws_protocol.JSON.messages.DmSyncApplySupport;
|
||||||
|
import shine.db.dao.UserAccessServersCurrentDAO;
|
||||||
|
import shine.db.dao.UserSettingsDAO;
|
||||||
|
import shine.db.dao.UserSettingsSyncPeerStateDAO;
|
||||||
|
import shine.db.entities.UserAccessServerRouteEntry;
|
||||||
|
import shine.db.entities.UserSettingEntry;
|
||||||
|
import shine.db.entities.UserSettingsSyncPeerStateEntry;
|
||||||
|
import utils.config.AppConfig;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public final class PeriodicUserSettingsSyncService {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(PeriodicUserSettingsSyncService.class);
|
||||||
|
private static final String SERVER_LOGIN_CONFIG = "server.SHiNE.login";
|
||||||
|
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
|
||||||
|
private static final RemoteUserSettingsSyncClient REMOTE = new RemoteUserSettingsSyncClient();
|
||||||
|
private static final RemoteDmSyncClient DM_REMOTE = new RemoteDmSyncClient();
|
||||||
|
private static final UserAccessServersCurrentDAO ACCESS_DAO = UserAccessServersCurrentDAO.getInstance();
|
||||||
|
private static final UserSettingsDAO SETTINGS_DAO = UserSettingsDAO.getInstance();
|
||||||
|
private static final UserSettingsSyncPeerStateDAO STATE_DAO = UserSettingsSyncPeerStateDAO.getInstance();
|
||||||
|
|
||||||
|
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
|
||||||
|
@Override
|
||||||
|
public Thread newThread(Runnable r) {
|
||||||
|
Thread t = new Thread(r, "periodic-user-settings-sync");
|
||||||
|
t.setDaemon(true);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
private PeriodicUserSettingsSyncService() {}
|
||||||
|
|
||||||
|
public static void startOrLog() {
|
||||||
|
if (!isEnabled()) {
|
||||||
|
log.info("Periodic user settings sync disabled by user.settings.sync.enabled=false");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!STARTED.compareAndSet(false, true)) return;
|
||||||
|
long initialDelaySec = configLong("user.settings.sync.initialDelaySeconds", 90L, 0L, 3600L);
|
||||||
|
long periodHours = configLong("user.settings.sync.periodHours", 6L, 1L, 168L);
|
||||||
|
EXECUTOR.scheduleWithFixedDelay(
|
||||||
|
PeriodicUserSettingsSyncService::runCycleSafe,
|
||||||
|
initialDelaySec,
|
||||||
|
TimeUnit.HOURS.toSeconds(periodHours),
|
||||||
|
TimeUnit.SECONDS
|
||||||
|
);
|
||||||
|
EXECUTOR.scheduleWithFixedDelay(
|
||||||
|
PeriodicUserSettingsSyncService::runRequestedCycleSafe,
|
||||||
|
5L, 5L, TimeUnit.SECONDS);
|
||||||
|
log.info("Periodic user settings sync scheduled: first run in {} seconds, then every {} hours", initialDelaySec, periodHours);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void runCycleSafe() {
|
||||||
|
try {
|
||||||
|
runCycle();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Periodic user settings sync failed unexpectedly", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void runRequestedCycleSafe() {
|
||||||
|
if (DmSyncWakeSignal.consume()) runCycleSafe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void runCycle() throws Exception {
|
||||||
|
String ownServerLogin = normalize(AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG));
|
||||||
|
if (ownServerLogin == null) {
|
||||||
|
log.warn("Periodic user settings sync skipped: {} is empty", SERVER_LOGIN_CONFIG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> ownersRaw = ACCESS_DAO.listUserLoginsByServerLogin(ownServerLogin);
|
||||||
|
Set<String> owners = new LinkedHashSet<>(ownersRaw);
|
||||||
|
if (owners.isEmpty()) {
|
||||||
|
log.info("Periodic user settings sync skipped: no local access-server users for {}", ownServerLogin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int syncedPeers = 0;
|
||||||
|
int appliedItems = 0;
|
||||||
|
int pushedItems = 0;
|
||||||
|
int appliedDmItems = 0;
|
||||||
|
for (String ownerLogin : owners) {
|
||||||
|
List<UserAccessServerRouteEntry> routes = ACCESS_DAO.listByUserLogin(ownerLogin);
|
||||||
|
for (UserAccessServerRouteEntry route : routes) {
|
||||||
|
if (route == null) continue;
|
||||||
|
String remoteLogin = normalize(route.getServerLogin());
|
||||||
|
String remoteUrl = route.getServerUrl();
|
||||||
|
if (remoteLogin == null || remoteUrl == null || remoteUrl.isBlank()) continue;
|
||||||
|
if (remoteLogin.equals(ownServerLogin)) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
SyncStats stats = syncOwnerWithRemote(ownerLogin, route);
|
||||||
|
appliedItems += stats.applied();
|
||||||
|
pushedItems += stats.pushed();
|
||||||
|
appliedDmItems += stats.appliedDm();
|
||||||
|
syncedPeers++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
STATE_DAO.updateError(ownerLogin, route.getServerLogin(), remoteUrl, String.valueOf(e));
|
||||||
|
log.warn("Periodic user settings sync peer failed: owner={} remoteServer={} reason={}",
|
||||||
|
ownerLogin, route.getServerLogin(), String.valueOf(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Periodic access-data sync finished: owners={} peers={} settingsApplied={} settingsPushed={} dmApplied={}",
|
||||||
|
owners.size(), syncedPeers, appliedItems, pushedItems, appliedDmItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SyncStats syncOwnerWithRemote(String ownerLogin, UserAccessServerRouteEntry route) throws Exception {
|
||||||
|
int limit = (int) configLong("user.settings.sync.batchLimit", 500L, 1L, 1000L);
|
||||||
|
int maxBytes = (int) configLong("user.settings.sync.batchMaxBytes", 3_000_000L, 64_000L, 5_000_000L);
|
||||||
|
int maxPages = (int) configLong("user.settings.sync.maxPagesPerPeer", 50L, 1L, 500L);
|
||||||
|
|
||||||
|
UserSettingsSyncPeerStateEntry state;
|
||||||
|
try (Connection c = getDbConnection()) {
|
||||||
|
state = STATE_DAO.getOrCreate(c, ownerLogin, route.getServerLogin(), route.getServerUrl());
|
||||||
|
}
|
||||||
|
long cursorTimeMs = state.getCursorTimeMs();
|
||||||
|
String cursorSettingKey = state.getCursorSettingKey() == null ? "" : state.getCursorSettingKey();
|
||||||
|
int applied = 0;
|
||||||
|
int pushed = 0;
|
||||||
|
boolean bootstrapCompleted = false;
|
||||||
|
|
||||||
|
int appliedDm;
|
||||||
|
try (RemoteSyncSession session = new RemoteSyncSession(route.getServerLogin(), route.getServerUrl())) {
|
||||||
|
for (int page = 0; page < maxPages; page++) {
|
||||||
|
RemoteUserSettingsSyncClient.RemoteUserSettingsBatch batch = REMOTE.userSettingsSyncBatch(
|
||||||
|
session,
|
||||||
|
ownerLogin,
|
||||||
|
cursorTimeMs,
|
||||||
|
cursorSettingKey,
|
||||||
|
limit,
|
||||||
|
maxBytes
|
||||||
|
);
|
||||||
|
|
||||||
|
try (Connection c = getDbConnection()) {
|
||||||
|
for (RemoteUserSettingsSyncClient.RemoteUserSettingsItem item : batch.items()) {
|
||||||
|
UserSettingEntry entry = new UserSettingEntry(
|
||||||
|
item.login(),
|
||||||
|
item.settingType(),
|
||||||
|
item.settingKey(),
|
||||||
|
item.timeMs(),
|
||||||
|
item.valueText(),
|
||||||
|
item.valueNum(),
|
||||||
|
item.clientKey(),
|
||||||
|
item.signature(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
int changed = SETTINGS_DAO.upsertIfNewer(c, entry);
|
||||||
|
if (changed > 0) applied++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursorTimeMs = Math.max(cursorTimeMs, batch.nextTimeMs());
|
||||||
|
cursorSettingKey = batch.nextSettingKey() == null ? "" : batch.nextSettingKey();
|
||||||
|
bootstrapCompleted = !batch.hasMore();
|
||||||
|
STATE_DAO.updateSuccess(ownerLogin, route.getServerLogin(), route.getServerUrl(), cursorTimeMs, cursorSettingKey, bootstrapCompleted);
|
||||||
|
|
||||||
|
if (!batch.hasMore() || batch.items().isEmpty()) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Connection c = getDbConnection()) {
|
||||||
|
List<UserSettingEntry> unsynced = SETTINGS_DAO.listUnsyncedByLogin(c, ownerLogin, limit);
|
||||||
|
for (UserSettingEntry entry : unsynced) {
|
||||||
|
REMOTE.upsertUserSetting(session, entry, true);
|
||||||
|
SETTINGS_DAO.markSynced(c, entry.getLogin(), entry.getSettingType(), entry.getSettingKey());
|
||||||
|
pushed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int dmLimit = (int) configLong("dm.sync.batchLimit", 200L, 1L, 500L);
|
||||||
|
int dmMaxBytes = (int) configLong("dm.sync.batchMaxBytes", 3_000_000L, 64_000L, 5_000_000L);
|
||||||
|
int dmMaxPages = (int) configLong("dm.sync.maxPagesPerPeer", 20L, 1L, 500L);
|
||||||
|
appliedDm = syncDmInSameSession(session, ownerLogin, dmLimit, dmMaxBytes, dmMaxPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bootstrapCompleted) {
|
||||||
|
log.info("Periodic user settings sync peer paused by page limit: owner={} remoteServer={} maxPages={}",
|
||||||
|
ownerLogin, route.getServerLogin(), maxPages);
|
||||||
|
}
|
||||||
|
return new SyncStats(applied, pushed, appliedDm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int syncDmInSameSession(
|
||||||
|
RemoteSyncSession session, String ownerLogin, int limit, int maxBytes, int maxPages
|
||||||
|
) throws Exception {
|
||||||
|
long cursorMs = 0L;
|
||||||
|
String cursorKey = "";
|
||||||
|
List<String> acknowledgements = new ArrayList<>();
|
||||||
|
int applied = 0;
|
||||||
|
for (int page = 0; page < maxPages; page++) {
|
||||||
|
RemoteDmSyncClient.RemoteDmBatch batch = DM_REMOTE.dmSyncBatch(
|
||||||
|
session, ownerLogin, cursorMs, cursorKey, Math.min(limit, 500), maxBytes, acknowledgements);
|
||||||
|
acknowledgements = new ArrayList<>();
|
||||||
|
for (RemoteDmSyncClient.RemoteDmItem item : batch.items()) {
|
||||||
|
if (item == null || item.syncId() == null || item.syncId().isBlank()) continue;
|
||||||
|
DmSyncApplySupport.applySyncedItem(ownerLogin, item.syncId(), item.blobsB64());
|
||||||
|
acknowledgements.add(item.syncId());
|
||||||
|
applied++;
|
||||||
|
}
|
||||||
|
cursorMs = batch.nextStoredAtMs();
|
||||||
|
cursorKey = batch.nextMessageKey() == null ? "" : batch.nextMessageKey();
|
||||||
|
if (!batch.hasMore() || batch.items().isEmpty()) {
|
||||||
|
if (!acknowledgements.isEmpty()) {
|
||||||
|
DM_REMOTE.dmSyncBatch(session, ownerLogin, 0L, "",
|
||||||
|
Math.min(limit, 500), maxBytes, acknowledgements);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return applied;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static java.sql.Connection getDbConnection() throws Exception {
|
||||||
|
return shine.db.DbController.getInstance().getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isEnabled() {
|
||||||
|
String raw = AppConfig.getInstance().getParam("user.settings.sync.enabled");
|
||||||
|
return raw == null || raw.isBlank() || Boolean.parseBoolean(raw.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long configLong(String key, long defaultValue, long min, long max) {
|
||||||
|
String raw = AppConfig.getInstance().getParam(key);
|
||||||
|
if (raw == null || raw.isBlank()) return defaultValue;
|
||||||
|
try {
|
||||||
|
long parsed = Long.parseLong(raw.trim());
|
||||||
|
return Math.max(min, Math.min(max, parsed));
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalize(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
String s = value.trim().toLowerCase(Locale.ROOT);
|
||||||
|
return s.isEmpty() ? null : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record SyncStats(int applied, int pushed, int appliedDm) {}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user