SHA256
Compare commits
30
Commits
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
578f5cad4a | ||
|
|
d2ff277b16 | ||
|
|
df4f9b3f8e | ||
|
|
be7f53a9ae | ||
|
|
6ed91a105e | ||
|
|
0db3c3af5a | ||
|
|
23748504e6 | ||
|
|
f74fecddd8 | ||
|
|
618a30c2ab | ||
|
|
b5116474c7 | ||
|
|
91e7239866 | ||
|
|
1dddb5fb3c | ||
|
|
8ddf592ffc | ||
|
|
59a1117ed3 | ||
|
|
3bb6fa1e59 | ||
|
|
5af98ac911 | ||
|
|
f197196c21 | ||
|
|
ce5595bc16 | ||
|
|
1406111f22 | ||
|
|
bfdada6792 | ||
|
|
5326ad85d8 | ||
|
|
f9dd245481 | ||
|
|
20677a9090 | ||
|
|
d54cb7507a | ||
|
|
d6b313b582 | ||
|
|
f31cdb413b | ||
|
|
69faf55b2b | ||
|
|
4d9e42205f | ||
|
|
5df69d73c7 | ||
|
|
4704f8485b |
@@ -45,6 +45,11 @@
|
||||
- `docs/Solana_Architecture/README.md`
|
||||
- Документ формата пользовательской PDA-записи `shine_users` находится в:
|
||||
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`
|
||||
- Актуальная документация по серверному модулю синхронизации Solana users находится в:
|
||||
- `docs/Solana/SOLANA_USERS_SYNC_MODULE_DESIGN.md`
|
||||
- При любом изменении логики серверной синхронизации `shine_users`, её таблиц PostgreSQL, checkpoint-механизма, startup/lifecycle или deploy-настроек обязательно обновлять:
|
||||
- `docs/Solana/SOLANA_USERS_SYNC_MODULE_DESIGN.md`
|
||||
- `deploy/SOLANA_USERS_SYNC_SERVER_SETUP.md`
|
||||
|
||||
## Документация блокчейна
|
||||
- Актуальная документация по форматам блокчейна находится в `docs/Blockchain/README.md`.
|
||||
|
||||
@@ -9,7 +9,7 @@ SHiNE-server — серверная часть мессенджера SHiNE: Web
|
||||
|
||||
- `shine-server-net-server/` — точка входа, запуск HTTP/WS сервера
|
||||
- `shine-server-net-protocol/` — обработчики операций (RPC и события WS)
|
||||
- `shine-server-db/` — DAO, SQL-схема, SQLite
|
||||
- `shine-server-db/` — DAO, SQL-схема, PostgreSQL runtime
|
||||
- `shine-server-blockchain/` — логика хранения и проверки блоков блокчейна
|
||||
- `shine-server-crypto/` — криптографические утилиты
|
||||
- `shine-server-config/` — конфигурация сервера
|
||||
|
||||
@@ -59,6 +59,8 @@ public final class AppConfig {
|
||||
public String getParam(String name) {
|
||||
String fromSystem = System.getProperty(name);
|
||||
if (fromSystem != null) return fromSystem;
|
||||
String fromEnv = System.getenv(toEnvName(name));
|
||||
if (fromEnv != null && !fromEnv.isBlank()) return fromEnv.trim();
|
||||
return properties.getProperty(name);
|
||||
}
|
||||
|
||||
@@ -78,4 +80,11 @@ public final class AppConfig {
|
||||
String v = properties.getProperty(name);
|
||||
return v == null ? defaultValue : Boolean.parseBoolean(v);
|
||||
}
|
||||
|
||||
private static String toEnvName(String name) {
|
||||
return name
|
||||
.replace('.', '_')
|
||||
.replace('-', '_')
|
||||
.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.xerial:sqlite-jdbc:3.47.0.0' // sqlite
|
||||
implementation 'org.postgresql:postgresql:42.7.7'
|
||||
|
||||
implementation "org.slf4j:slf4j-api:2.0.16" // вызов логгера
|
||||
|
||||
|
||||
@@ -1,49 +1,38 @@
|
||||
package shine.db;
|
||||
|
||||
import utils.config.AppConfig;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.*;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* DatabaseInitializer — создание новой SQLite-БД по схеме SHiNE.
|
||||
*
|
||||
* В этой версии:
|
||||
* - создаём ТОЛЬКО таблицы/индексы
|
||||
* - в конце вызываем DatabaseTriggersInstaller.createAllTriggers(st)
|
||||
*
|
||||
* v2 (sessions):
|
||||
* - active_sessions.session_pwd удалён
|
||||
* - active_sessions.session_key хранит публичный ключ сессии целиком одной строкой
|
||||
* PostgreSQL runtime schema bootstrapper for SHiNE server.
|
||||
*/
|
||||
public final class DatabaseInitializer {
|
||||
|
||||
public static final String DB_SCHEMA_VERSION_TABLE = "db_schema_version";
|
||||
public static final int SCHEMA_VERSION_1 = 1;
|
||||
public static final int SCHEMA_VERSION_2 = 2;
|
||||
public static final String POSTGRES_SCHEMA_RESOURCE = "postgres/schema_v1.sql";
|
||||
public static final String POSTGRES_MIGRATION_V2_RESOURCE = "postgres/migration_v2.sql";
|
||||
|
||||
private DatabaseInitializer() {}
|
||||
|
||||
/* ===================== TEXT (msg_type=1) ===================== */
|
||||
|
||||
public static final short TEXT_POST = 10;
|
||||
public static final short TEXT_EDIT_POST = 11;
|
||||
public static final short TEXT_REPLY = 20;
|
||||
public static final short TEXT_EDIT_REPLY = 21;
|
||||
public static final short TEXT_REPOST = 30;
|
||||
|
||||
/* ===================== REACTION (msg_type=2) ===================== */
|
||||
|
||||
public static final short REACTION_LIKE = 1;
|
||||
public static final short REACTION_UNLIKE = 2;
|
||||
|
||||
/* ===================== CONNECTION (msg_type=3) ===================== */
|
||||
// Близкий друг (close friend). Исторически в коде использовалось имя FRIEND.
|
||||
public static final short CONNECTION_FRIEND = 10;
|
||||
public static final short CONNECTION_UNFRIEND = 11;
|
||||
public static final short CONNECTION_CLOSE_FRIEND = CONNECTION_FRIEND;
|
||||
@@ -76,636 +65,217 @@ public final class DatabaseInitializer {
|
||||
public static final short CONNECTION_SHINE_SEEN = 74;
|
||||
public static final short CONNECTION_SHINE_UNSEEN = 75;
|
||||
|
||||
public static void createNewDB(String[] args) {
|
||||
AppConfig config = AppConfig.getInstance();
|
||||
String dbPath = config.getParam("db.path");
|
||||
|
||||
if (dbPath == null || dbPath.isBlank()) {
|
||||
System.err.println("Параметр db.path не задан в application.properties");
|
||||
return;
|
||||
}
|
||||
|
||||
Path dbFile = Paths.get(dbPath);
|
||||
public static void ensurePostgresSchemaInitialized(String jdbcUrl,
|
||||
String user,
|
||||
String password) throws SQLException {
|
||||
try {
|
||||
Path parent = dbFile.getParent();
|
||||
if (parent != null && !Files.exists(parent)) {
|
||||
Files.createDirectories(parent);
|
||||
}
|
||||
|
||||
if (Files.exists(dbFile)) {
|
||||
System.out.println("Файл базы данных уже существует: " + dbFile.toAbsolutePath());
|
||||
System.out.print("Пересоздать БД (СТАРАЯ БУДЕТ УДАЛЕНА)? [y/N]: ");
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
|
||||
String answer = reader.readLine();
|
||||
if (!"y".equalsIgnoreCase(answer) && !"yes".equalsIgnoreCase(answer)) {
|
||||
System.out.println("Операция отменена. БД не изменена.");
|
||||
return;
|
||||
}
|
||||
|
||||
Files.delete(dbFile);
|
||||
System.out.println("Старый файл БД удалён.");
|
||||
}
|
||||
|
||||
createSchema("jdbc:sqlite:" + dbPath);
|
||||
System.out.println("Новая БД успешно создана по пути: " + dbFile.toAbsolutePath());
|
||||
|
||||
} catch (IOException e) {
|
||||
System.err.println("Ошибка работы с файлом БД: " + e.getMessage());
|
||||
} catch (SQLException e) {
|
||||
System.err.println("Ошибка создания схемы БД: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void ensureSchemaV1Structure(String jdbcUrl) throws SQLException {
|
||||
createSchema(jdbcUrl, false);
|
||||
}
|
||||
|
||||
private static void createSchema(String jdbcUrl) throws SQLException {
|
||||
createSchema(jdbcUrl, true);
|
||||
}
|
||||
|
||||
private static void createSchema(String jdbcUrl, boolean initializeVersionRow) throws SQLException {
|
||||
try {
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
Class.forName("org.postgresql.Driver");
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException("SQLite JDBC driver not found", e);
|
||||
throw new RuntimeException("PostgreSQL JDBC driver not found", e);
|
||||
}
|
||||
|
||||
try (Connection conn = DriverManager.getConnection(jdbcUrl);
|
||||
Statement st = conn.createStatement()) {
|
||||
|
||||
st.execute("PRAGMA foreign_keys = ON");
|
||||
|
||||
// 1. solana_users
|
||||
// ВАЖНО:
|
||||
// - Все требуемые поля теперь лежат в solana_users:
|
||||
// login, blockchain_name, solana_key, blockchain_key, client_key
|
||||
// - Поиск по login в DAO сделан case-insensitive.
|
||||
// - Для защиты от дублей "Anya" и "anya" добавляем COLLATE NOCASE на PRIMARY KEY.
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS solana_users (
|
||||
login TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,
|
||||
blockchain_name TEXT NOT NULL,
|
||||
solana_key TEXT NOT NULL,
|
||||
blockchain_key TEXT NOT NULL,
|
||||
client_key TEXT NOT NULL
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_solana_users_blockchain_name
|
||||
ON solana_users (blockchain_name);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_solana_users_login
|
||||
ON solana_users (login);
|
||||
""");
|
||||
|
||||
// 2. active_sessions (v2)
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS active_sessions (
|
||||
session_id TEXT NOT NULL PRIMARY KEY,
|
||||
login TEXT NOT NULL,
|
||||
session_key TEXT NOT NULL,
|
||||
storage_pwd TEXT NOT NULL,
|
||||
session_created_at_ms INTEGER NOT NULL,
|
||||
last_authirificated_at_ms INTEGER NOT NULL,
|
||||
push_endpoint TEXT,
|
||||
push_p256dh_key TEXT,
|
||||
push_auth_key TEXT,
|
||||
client_ip TEXT,
|
||||
client_info_from_client TEXT,
|
||||
client_info_from_request TEXT,
|
||||
session_type INTEGER NOT NULL DEFAULT 1,
|
||||
client_platform TEXT NOT NULL DEFAULT '',
|
||||
user_language TEXT,
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_active_sessions_login
|
||||
ON active_sessions (login);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS esp_pairing_settings (
|
||||
login TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,
|
||||
enabled INTEGER NOT NULL DEFAULT 0,
|
||||
password_hash TEXT NOT NULL DEFAULT '',
|
||||
ttl_seconds INTEGER NOT NULL DEFAULT 300,
|
||||
failed_attempts INTEGER NOT NULL DEFAULT 0,
|
||||
first_failed_at_ms INTEGER NOT NULL DEFAULT 0,
|
||||
blocked_until_ms INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS esp_pairing_requests (
|
||||
pairing_id TEXT NOT NULL PRIMARY KEY,
|
||||
login TEXT NOT NULL,
|
||||
requester_session_key TEXT NOT NULL,
|
||||
requester_session_type INTEGER NOT NULL DEFAULT 1,
|
||||
requester_client_platform TEXT NOT NULL DEFAULT '',
|
||||
payload_type INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
short_code TEXT NOT NULL,
|
||||
fingerprint_b58 TEXT NOT NULL,
|
||||
encrypted_payload TEXT,
|
||||
reject_reason TEXT,
|
||||
approved_by_session_id TEXT,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
expires_at_ms INTEGER NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
delivered_to_homeserver INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_esp_pairing_requests_login_status
|
||||
ON esp_pairing_requests (login, status, expires_at_ms);
|
||||
""");
|
||||
|
||||
// 3. users_params
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS users_params (
|
||||
login TEXT NOT NULL,
|
||||
param TEXT NOT NULL,
|
||||
time_ms INTEGER NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
client_key TEXT,
|
||||
signature TEXT,
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login),
|
||||
UNIQUE (login, param)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_users_params_login
|
||||
ON users_params (login);
|
||||
""");
|
||||
|
||||
// 4. ip_geo_cache
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS ip_geo_cache (
|
||||
ip TEXT NOT NULL PRIMARY KEY,
|
||||
geo TEXT,
|
||||
updated_at_ms INTEGER NOT NULL
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_ip_geo_cache_updated_at
|
||||
ON ip_geo_cache (updated_at_ms);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS test_free_avatar_uploads (
|
||||
login TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,
|
||||
used_count INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
last_tx_id TEXT NOT NULL DEFAULT '',
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_test_free_avatar_uploads_updated
|
||||
ON test_free_avatar_uploads (updated_at_ms);
|
||||
""");
|
||||
|
||||
// 5. blockchain_state
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS blockchain_state (
|
||||
blockchain_name TEXT NOT NULL PRIMARY KEY,
|
||||
login TEXT NOT NULL,
|
||||
blockchain_key TEXT NOT NULL,
|
||||
|
||||
size_limit INTEGER NOT NULL,
|
||||
file_size_bytes INTEGER NOT NULL,
|
||||
|
||||
last_block_number INTEGER NOT NULL,
|
||||
last_block_hash BLOB,
|
||||
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_blockchain_state_login
|
||||
ON blockchain_state (login);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_blockchain_state_updated_at
|
||||
ON blockchain_state (updated_at_ms);
|
||||
""");
|
||||
|
||||
// 6. blocks (+ line_code)
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS blocks (
|
||||
login TEXT NOT NULL,
|
||||
bch_name TEXT NOT NULL,
|
||||
block_number INTEGER NOT NULL CHECK(block_number >= 0),
|
||||
|
||||
msg_type INTEGER NOT NULL,
|
||||
msg_sub_type INTEGER NOT NULL,
|
||||
|
||||
block_bytes BLOB NOT NULL,
|
||||
|
||||
-- target (reply/like/edit и т.д.)
|
||||
to_login TEXT,
|
||||
to_bch_name TEXT,
|
||||
to_block_number INTEGER CHECK(to_block_number IS NULL OR to_block_number >= 0),
|
||||
to_block_hash BLOB,
|
||||
|
||||
-- собственные данные
|
||||
block_hash BLOB NOT NULL,
|
||||
block_signature BLOB NOT NULL,
|
||||
|
||||
-- если этот блок был изменён последним edit'ом
|
||||
edited_by_block_number INTEGER CHECK(edited_by_block_number IS NULL OR edited_by_block_number >= 0),
|
||||
|
||||
-- линейность (опционально)
|
||||
line_code INTEGER CHECK(line_code IS NULL OR line_code >= 0),
|
||||
prev_line_number INTEGER CHECK(prev_line_number IS NULL OR prev_line_number >= 0),
|
||||
prev_line_hash BLOB,
|
||||
this_line_number INTEGER CHECK(this_line_number IS NULL OR this_line_number >= 0),
|
||||
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login),
|
||||
FOREIGN KEY (bch_name) REFERENCES blockchain_state(blockchain_name),
|
||||
|
||||
UNIQUE (bch_name, block_number)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_blocks_by_chain_number
|
||||
ON blocks (bch_name, block_number);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_blocks_to_target
|
||||
ON blocks (to_login, to_bch_name, to_block_number);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_blocks_by_line
|
||||
ON blocks (bch_name, line_code, this_line_number);
|
||||
""");
|
||||
|
||||
// 7) connections_state
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS connections_state (
|
||||
login TEXT NOT NULL,
|
||||
rel_type INTEGER NOT NULL,
|
||||
to_login TEXT NOT NULL,
|
||||
to_bch_name TEXT NOT NULL,
|
||||
to_block_number INTEGER NOT NULL,
|
||||
to_block_hash BLOB NOT NULL,
|
||||
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login),
|
||||
|
||||
UNIQUE (login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_connections_state_login
|
||||
ON connections_state (login);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_connections_state_to_login
|
||||
ON connections_state (to_login);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_connections_state_pair
|
||||
ON connections_state (login, to_login);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_connections_state_target
|
||||
ON connections_state (login, rel_type, to_bch_name, to_block_number);
|
||||
""");
|
||||
|
||||
// 8) message_stats
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS message_stats (
|
||||
to_login TEXT NOT NULL,
|
||||
to_bch_name TEXT NOT NULL,
|
||||
to_block_number INTEGER NOT NULL,
|
||||
to_block_hash BLOB NOT NULL,
|
||||
|
||||
likes_count INTEGER NOT NULL DEFAULT 0,
|
||||
replies_count INTEGER NOT NULL DEFAULT 0,
|
||||
edits_count INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
UNIQUE (
|
||||
to_login,
|
||||
to_bch_name,
|
||||
to_block_number,
|
||||
to_block_hash
|
||||
)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_message_stats_target
|
||||
ON message_stats (to_bch_name, to_block_number, to_block_hash);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_message_stats_login
|
||||
ON message_stats (to_login);
|
||||
""");
|
||||
|
||||
// 8.0) reactions_state (идемпотентный LIKE/UNLIKE per actor/target)
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS reactions_state (
|
||||
from_login TEXT NOT NULL,
|
||||
from_bch_name TEXT NOT NULL,
|
||||
reaction_type INTEGER NOT NULL,
|
||||
to_login TEXT NOT NULL,
|
||||
to_bch_name TEXT NOT NULL,
|
||||
to_block_number INTEGER NOT NULL,
|
||||
to_block_hash BLOB NOT NULL,
|
||||
last_sub_type INTEGER NOT NULL,
|
||||
|
||||
UNIQUE (
|
||||
from_login,
|
||||
from_bch_name,
|
||||
reaction_type,
|
||||
to_login,
|
||||
to_bch_name,
|
||||
to_block_number,
|
||||
to_block_hash
|
||||
)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_reactions_state_target
|
||||
ON reactions_state (to_bch_name, to_block_number, to_block_hash);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_reactions_state_actor
|
||||
ON reactions_state (from_login, from_bch_name, reaction_type);
|
||||
""");
|
||||
|
||||
// 9) channel_names_state (global normalized channel names)
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS channel_names_state (
|
||||
slug TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
channel_description TEXT NOT NULL DEFAULT '',
|
||||
owner_login TEXT NOT NULL,
|
||||
owner_bch_name TEXT NOT NULL,
|
||||
channel_type_code INTEGER NOT NULL DEFAULT 1,
|
||||
channel_type_version INTEGER NOT NULL DEFAULT 1,
|
||||
channel_root_block_number INTEGER NOT NULL,
|
||||
channel_root_block_hash BLOB NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_names_state_owner_type_slug
|
||||
ON channel_names_state (owner_bch_name, channel_type_code, slug);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_names_state_target
|
||||
ON channel_names_state (owner_bch_name, channel_root_block_number, channel_root_block_hash);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_names_state_owner
|
||||
ON channel_names_state (owner_login, owner_bch_name);
|
||||
""");
|
||||
|
||||
// 9.1) chat200_state
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS chat200_state (
|
||||
owner_login TEXT NOT NULL,
|
||||
owner_bch_name TEXT NOT NULL,
|
||||
channel_root_block_number INTEGER NOT NULL,
|
||||
channel_root_block_hash BLOB NOT NULL,
|
||||
channel_name TEXT NOT NULL,
|
||||
channel_type_version INTEGER NOT NULL,
|
||||
chat_title TEXT NOT NULL DEFAULT '',
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
PRIMARY KEY (owner_bch_name, channel_root_block_number)
|
||||
);
|
||||
""");
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_chat200_state_owner
|
||||
ON chat200_state (owner_login, owner_bch_name);
|
||||
""");
|
||||
|
||||
// 9.2) chat200_members_state
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS chat200_members_state (
|
||||
owner_bch_name TEXT NOT NULL,
|
||||
channel_root_block_number INTEGER NOT NULL,
|
||||
member_login TEXT NOT NULL,
|
||||
member_channel_name TEXT NOT NULL,
|
||||
is_active INTEGER NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
updated_by_block_number INTEGER NOT NULL,
|
||||
PRIMARY KEY (
|
||||
owner_bch_name,
|
||||
channel_root_block_number,
|
||||
member_login,
|
||||
member_channel_name
|
||||
)
|
||||
);
|
||||
""");
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_chat200_members_owner
|
||||
ON chat200_members_state (owner_bch_name, channel_root_block_number, is_active);
|
||||
""");
|
||||
|
||||
// 10) direct_messages
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS direct_messages (
|
||||
message_id TEXT NOT NULL PRIMARY KEY,
|
||||
from_login TEXT NOT NULL,
|
||||
to_login TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
FOREIGN KEY (from_login) REFERENCES solana_users(login),
|
||||
FOREIGN KEY (to_login) REFERENCES solana_users(login)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_direct_messages_to_login
|
||||
ON direct_messages (to_login, created_at_ms);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_direct_messages_from_login
|
||||
ON direct_messages (from_login, created_at_ms);
|
||||
""");
|
||||
|
||||
// 11) user_push_tokens
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS user_push_tokens (
|
||||
token_id TEXT NOT NULL PRIMARY KEY,
|
||||
login TEXT NOT NULL,
|
||||
session_id TEXT NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
platform TEXT,
|
||||
user_agent TEXT,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_user_push_tokens_login
|
||||
ON user_push_tokens (login);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_user_push_tokens_login_session
|
||||
ON user_push_tokens (login, session_id);
|
||||
""");
|
||||
|
||||
// 11) signed_direct_message_replay (anti-replay window)
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS signed_direct_message_replay (
|
||||
from_login TEXT NOT NULL,
|
||||
time_ms INTEGER NOT NULL,
|
||||
nonce INTEGER NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
UNIQUE (from_login, time_ms, nonce)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_signed_dm_replay_created
|
||||
ON signed_direct_message_replay (created_at_ms);
|
||||
""");
|
||||
|
||||
// 12) signed_direct_messages_history (сырой бинарный пакет + мета)
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS signed_direct_messages_history (
|
||||
message_id TEXT NOT NULL PRIMARY KEY,
|
||||
from_login TEXT NOT NULL,
|
||||
to_login TEXT NOT NULL,
|
||||
target_mode INTEGER NOT NULL,
|
||||
target_session_id TEXT,
|
||||
message_type INTEGER NOT NULL,
|
||||
time_ms INTEGER NOT NULL,
|
||||
nonce INTEGER NOT NULL,
|
||||
raw_packet BLOB NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
FOREIGN KEY (from_login) REFERENCES solana_users(login),
|
||||
FOREIGN KEY (to_login) REFERENCES solana_users(login)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_signed_dm_history_to
|
||||
ON signed_direct_messages_history (to_login, created_at_ms);
|
||||
""");
|
||||
|
||||
// 13) signed_messages_v2 (универсальное хранилище блоков типов 1..8)
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS signed_messages_v2 (
|
||||
message_key TEXT NOT NULL PRIMARY KEY,
|
||||
base_key TEXT NOT NULL,
|
||||
target_login TEXT NOT NULL,
|
||||
from_login TEXT NOT NULL,
|
||||
to_login TEXT NOT NULL,
|
||||
time_ms INTEGER NOT NULL,
|
||||
nonce INTEGER NOT NULL,
|
||||
message_type INTEGER NOT NULL,
|
||||
revision_time_ms INTEGER NOT NULL DEFAULT 0,
|
||||
reencrypted_at_ms INTEGER NOT NULL DEFAULT 0,
|
||||
raw_block BLOB NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
source_api TEXT NOT NULL,
|
||||
origin_session_id TEXT,
|
||||
receipt_ref_base_key TEXT,
|
||||
receipt_ref_type INTEGER,
|
||||
read_at_ms INTEGER,
|
||||
FOREIGN KEY (from_login) REFERENCES solana_users(login),
|
||||
FOREIGN KEY (to_login) REFERENCES solana_users(login)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_signed_messages_v2_target
|
||||
ON signed_messages_v2 (target_login, time_ms, created_at_ms);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_signed_messages_v2_base
|
||||
ON signed_messages_v2 (base_key, message_type);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_signed_messages_v2_receipt_incoming
|
||||
ON signed_messages_v2 (target_login, receipt_ref_base_key)
|
||||
WHERE message_type = 3 AND receipt_ref_base_key IS NOT NULL;
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_signed_messages_v2_receipt_outgoing
|
||||
ON signed_messages_v2 (target_login, receipt_ref_base_key)
|
||||
WHERE message_type = 4 AND receipt_ref_base_key IS NOT NULL;
|
||||
""");
|
||||
|
||||
// 14) signed_message_session_delivery (доставка по сессиям)
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS signed_message_session_delivery (
|
||||
message_key TEXT NOT NULL,
|
||||
session_id TEXT NOT NULL,
|
||||
delivered INTEGER NOT NULL DEFAULT 0,
|
||||
delivered_at_ms INTEGER,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
PRIMARY KEY (message_key, session_id),
|
||||
FOREIGN KEY (message_key) REFERENCES signed_messages_v2(message_key)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_signed_message_delivery_session
|
||||
ON signed_message_session_delivery (session_id, delivered);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS db_schema_version (
|
||||
id INTEGER NOT NULL PRIMARY KEY CHECK (id = 1),
|
||||
schema_version INTEGER NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL
|
||||
);
|
||||
""");
|
||||
|
||||
if (initializeVersionRow) {
|
||||
st.executeUpdate("""
|
||||
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||
VALUES (1, 1, CAST(strftime('%s','now') AS INTEGER) * 1000)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
schema_version = excluded.schema_version,
|
||||
updated_at_ms = excluded.updated_at_ms;
|
||||
""");
|
||||
try (Connection conn = openConnection(jdbcUrl, user, password)) {
|
||||
if (!postgresSchemaVersionTableExists(conn)) {
|
||||
runSqlScript(conn, POSTGRES_SCHEMA_RESOURCE);
|
||||
return;
|
||||
}
|
||||
int currentVersion = readCurrentSchemaVersion(conn);
|
||||
if (currentVersion < SCHEMA_VERSION_2) {
|
||||
runSqlScript(conn, POSTGRES_MIGRATION_V2_RESOURCE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Connection openConnection(String jdbcUrl, String user, String password) throws SQLException {
|
||||
if (user == null || user.isBlank()) {
|
||||
return DriverManager.getConnection(jdbcUrl);
|
||||
}
|
||||
return DriverManager.getConnection(jdbcUrl, user, password == null ? "" : password);
|
||||
}
|
||||
|
||||
private static boolean postgresSchemaVersionTableExists(Connection conn) throws SQLException {
|
||||
String sql = """
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = ?
|
||||
)
|
||||
""";
|
||||
try (var ps = conn.prepareStatement(sql)) {
|
||||
ps.setString(1, DB_SCHEMA_VERSION_TABLE);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
return rs.next() && rs.getBoolean(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int readCurrentSchemaVersion(Connection conn) throws SQLException {
|
||||
String sql = """
|
||||
SELECT schema_version
|
||||
FROM db_schema_version
|
||||
WHERE id = 1
|
||||
""";
|
||||
try (Statement st = conn.createStatement();
|
||||
ResultSet rs = st.executeQuery(sql)) {
|
||||
if (!rs.next()) {
|
||||
return SCHEMA_VERSION_1;
|
||||
}
|
||||
return rs.getInt(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void runSqlScript(Connection conn, String resourcePath) throws SQLException {
|
||||
String sqlScript = loadClasspathResource(resourcePath);
|
||||
List<String> statements = splitSqlStatements(sqlScript);
|
||||
|
||||
boolean previousAutoCommit = conn.getAutoCommit();
|
||||
conn.setAutoCommit(true);
|
||||
try (Statement st = conn.createStatement()) {
|
||||
for (String statement : statements) {
|
||||
String trimmed = statement.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
st.execute(trimmed);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
conn.setAutoCommit(previousAutoCommit);
|
||||
}
|
||||
}
|
||||
|
||||
private static String loadClasspathResource(String resourcePath) {
|
||||
try (InputStream in = DatabaseInitializer.class.getClassLoader().getResourceAsStream(resourcePath)) {
|
||||
if (in == null) {
|
||||
throw new RuntimeException("Resource not found: " + resourcePath);
|
||||
}
|
||||
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to read resource: " + resourcePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
static List<String> splitSqlStatements(String sqlScript) {
|
||||
List<String> statements = new ArrayList<>();
|
||||
StringBuilder current = new StringBuilder();
|
||||
boolean inSingleQuote = false;
|
||||
boolean inLineComment = false;
|
||||
boolean inBlockComment = false;
|
||||
String dollarQuoteTag = null;
|
||||
|
||||
for (int i = 0; i < sqlScript.length(); i++) {
|
||||
char ch = sqlScript.charAt(i);
|
||||
|
||||
if (inLineComment) {
|
||||
current.append(ch);
|
||||
if (ch == '\n') {
|
||||
inLineComment = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
DatabaseTriggersInstaller.createAllTriggers(st);
|
||||
if (inBlockComment) {
|
||||
current.append(ch);
|
||||
if (ch == '*' && i + 1 < sqlScript.length() && sqlScript.charAt(i + 1) == '/') {
|
||||
current.append('/');
|
||||
i += 1;
|
||||
inBlockComment = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dollarQuoteTag != null) {
|
||||
if (startsWithAt(sqlScript, i, dollarQuoteTag)) {
|
||||
current.append(dollarQuoteTag);
|
||||
i += dollarQuoteTag.length() - 1;
|
||||
dollarQuoteTag = null;
|
||||
} else {
|
||||
current.append(ch);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inSingleQuote) {
|
||||
current.append(ch);
|
||||
if (ch == '\'') {
|
||||
if (i + 1 < sqlScript.length() && sqlScript.charAt(i + 1) == '\'') {
|
||||
current.append('\'');
|
||||
i += 1;
|
||||
} else {
|
||||
inSingleQuote = false;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == '-' && i + 1 < sqlScript.length() && sqlScript.charAt(i + 1) == '-') {
|
||||
current.append("--");
|
||||
i += 1;
|
||||
inLineComment = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == '/' && i + 1 < sqlScript.length() && sqlScript.charAt(i + 1) == '*') {
|
||||
current.append("/*");
|
||||
i += 1;
|
||||
inBlockComment = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == '\'') {
|
||||
inSingleQuote = true;
|
||||
current.append(ch);
|
||||
continue;
|
||||
}
|
||||
|
||||
String tag = readDollarQuoteTag(sqlScript, i);
|
||||
if (tag != null) {
|
||||
current.append(tag);
|
||||
i += tag.length() - 1;
|
||||
dollarQuoteTag = tag;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == ';') {
|
||||
String statement = current.toString().trim();
|
||||
if (!statement.isEmpty()) {
|
||||
statements.add(current.toString());
|
||||
}
|
||||
current.setLength(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
current.append(ch);
|
||||
}
|
||||
|
||||
if (!current.isEmpty()) {
|
||||
String statement = current.toString().trim();
|
||||
if (!statement.isEmpty()) {
|
||||
statements.add(current.toString());
|
||||
}
|
||||
}
|
||||
return statements;
|
||||
}
|
||||
|
||||
private static boolean startsWithAt(String sqlScript, int index, String token) {
|
||||
return sqlScript.regionMatches(index, token, 0, token.length());
|
||||
}
|
||||
|
||||
private static String readDollarQuoteTag(String sqlScript, int index) {
|
||||
if (sqlScript.charAt(index) != '$') {
|
||||
return null;
|
||||
}
|
||||
|
||||
int end = index + 1;
|
||||
while (end < sqlScript.length()) {
|
||||
char current = sqlScript.charAt(end);
|
||||
if (current == '$') {
|
||||
return sqlScript.substring(index, end + 1);
|
||||
}
|
||||
if (!(Character.isLetterOrDigit(current) || current == '_')) {
|
||||
return null;
|
||||
}
|
||||
end++;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,569 +0,0 @@
|
||||
package shine.db;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* DatabaseTriggersInstaller — устанавливает триггеры, которые поддерживают бизнес-логику БД.
|
||||
*
|
||||
* Мы специально сделали триггеры максимально "совместимыми":
|
||||
* - НЕТ динамических сообщений в RAISE(...): только фиксированные строки.
|
||||
* (Некоторые SQLite-сборки / просмотрщики падают на "||" внутри RAISE.)
|
||||
* - НЕТ UPSERT "ON CONFLICT DO UPDATE" — вместо него:
|
||||
* INSERT OR IGNORE + UPDATE
|
||||
* (Старые SQLite не знают UPSERT.)
|
||||
*
|
||||
* =============================================================================
|
||||
* РћРџРРЎРђРќРР• РўР РГГЕРОВ
|
||||
* =============================================================================
|
||||
*
|
||||
* [1] trg_blocks_line_integrity_bi (BEFORE INSERT ON blocks)
|
||||
* Контроль целостности "линий" (line_code / prev_line_number / prev_line_hash / this_line_number).
|
||||
*
|
||||
* Зачем это нужно:
|
||||
* - В каналах/ветках/действиях ты хочешь иметь "линейную" последовательность,
|
||||
* где каждый следующий блок явно ссылается на предыдущий блок линии
|
||||
* и подтверждает, что ссылка не подменена.
|
||||
*
|
||||
* Когда срабатывает:
|
||||
* - ТОЛЬКО если при вставке передано ХОТЯ БЫ ОДНО из line-полей.
|
||||
* - Если line-поля не переданы — триггер вообще не работает (это важно).
|
||||
*
|
||||
* Что проверяет:
|
||||
* A) line-поля допускаются только для msg_type:
|
||||
* 0 (TECH), 1 (TEXT), 3 (CONNECTION), 4 (USER_PARAM)
|
||||
* B) Если пришло хоть одно line-поле — обязаны прийти ВСЕ 4 (никаких "частичных")
|
||||
* C) prev-блок линии существует в той же цепочке bch_name
|
||||
* D) prev_hash совпадает с block_hash найденного prev-блока
|
||||
* E) line_code корректный:
|
||||
* - либо первый шаг после root: prev_line_number == line_code
|
||||
* - либо prev уже принадлежит этой линии: p.line_code == NEW.line_code
|
||||
* F) this_line_number:
|
||||
* - первый шаг после root:
|
||||
* TEXT: this_line_number = 0
|
||||
* TECH/CONNECTION/USER_PARAM: this_line_number = 1
|
||||
* - обычный шаг:
|
||||
* TEXT: допускаем same или +1 (чтобы "edit" мог не двигать шаг)
|
||||
* TECH/CONNECTION/USER_PARAM: строго prev.this + 1
|
||||
*
|
||||
* Какие ошибки кидает:
|
||||
* - LINE_ERR_UNSUPPORTED_TYPE_WITH_LINE
|
||||
* - LINE_ERR_PARTIAL_FIELDS
|
||||
* - LINE_ERR_NO_PREV
|
||||
* - LINE_ERR_PREV_HASH_MISMATCH
|
||||
* - LINE_ERR_LINE_CODE_MISMATCH
|
||||
* - LINE_ERR_FIRST_STEP_BAD_THIS
|
||||
* - LINE_ERR_THIS_LINE_BAD_STEP
|
||||
*
|
||||
* [2] trg_blocks_connection_state_ai (AFTER INSERT ON blocks WHEN msg_type=3)
|
||||
* Поддерживает таблицу connections_state как "текущее состояние" отношений:
|
||||
* - FRIEND/CONTACT/FOLLOW -> добавить/обновить состояние
|
||||
* - UNFRIEND/UNCONTACT/UNFOLLOW -> удалить соответствующее "позитивное" состояние
|
||||
*
|
||||
* [3] trg_blocks_message_stats_like_ai (AFTER INSERT ON blocks WHEN msg_type=2 AND sub_type=LIKE)
|
||||
* Поддерживает likes_count в message_stats для цели (to_*).
|
||||
*
|
||||
* [4] trg_blocks_message_stats_reply_ai (AFTER INSERT ON blocks WHEN msg_type=1 AND sub_type=REPLY)
|
||||
* Поддерживает replies_count в message_stats.
|
||||
*
|
||||
* [5] trg_blocks_edit_apply_ai (AFTER INSERT ON blocks WHEN msg_type=1 AND sub_type=EDIT)
|
||||
* Логика edit:
|
||||
* - помечает исходный блок edited_by_block_number = NEW.block_number
|
||||
* - увеличивает edits_count в message_stats
|
||||
*/
|
||||
public final class DatabaseTriggersInstaller {
|
||||
|
||||
private DatabaseTriggersInstaller() {}
|
||||
|
||||
public static void createAllTriggers(Statement st) throws SQLException {
|
||||
dropTriggersByPrefix(st, "trg_blocks_");
|
||||
|
||||
// На всякий случай убираем старые "криво названные" триггеры,
|
||||
// если они когда-то попадали в БД.
|
||||
st.executeUpdate("DROP TRIGGER IF EXISTS trg_block_lini_integriti_by;");
|
||||
st.executeUpdate("DROP TRIGGER IF EXISTS trg_blocks_line_integrity_bi;");
|
||||
|
||||
st.executeUpdate("DROP TRIGGER IF EXISTS trg_blocks_connection_state_ai;");
|
||||
st.executeUpdate("DROP TRIGGER IF EXISTS trg_blocks_message_stats_like_ai;");
|
||||
st.executeUpdate("DROP TRIGGER IF EXISTS trg_blocks_message_stats_reply_ai;");
|
||||
st.executeUpdate("DROP TRIGGER IF EXISTS trg_blocks_edit_apply_ai;");
|
||||
|
||||
createLineIntegrityTrigger(st);
|
||||
createConnectionStateTrigger(st);
|
||||
createMessageStatsLikeTrigger(st);
|
||||
createMessageStatsReplyTrigger(st);
|
||||
createEditApplyTrigger(st);
|
||||
}
|
||||
|
||||
private static void dropTriggersByPrefix(Statement st, String prefix) throws SQLException {
|
||||
List<String> triggerNames = new ArrayList<>();
|
||||
String sql = "SELECT name FROM sqlite_master WHERE type='trigger' AND name LIKE '" + prefix + "%'";
|
||||
try (ResultSet rs = st.executeQuery(sql)) {
|
||||
while (rs.next()) {
|
||||
String name = rs.getString("name");
|
||||
if (name != null && !name.isBlank()) {
|
||||
triggerNames.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (String name : triggerNames) {
|
||||
String safeName = name.replace("\"", "\"\"");
|
||||
st.executeUpdate("DROP TRIGGER IF EXISTS \"" + safeName + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
private static void createLineIntegrityTrigger(Statement st) throws SQLException {
|
||||
st.executeUpdate("""
|
||||
CREATE TRIGGER IF NOT EXISTS trg_blocks_line_integrity_bi
|
||||
BEFORE INSERT ON blocks
|
||||
WHEN
|
||||
NEW.line_code IS NOT NULL
|
||||
OR NEW.prev_line_number IS NOT NULL
|
||||
OR NEW.prev_line_hash IS NOT NULL
|
||||
OR NEW.this_line_number IS NOT NULL
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'LINE_ERR_UNSUPPORTED_TYPE_WITH_LINE')
|
||||
WHERE NOT (NEW.msg_type IN (0, 1, 3, 4));
|
||||
|
||||
SELECT RAISE(ABORT, 'LINE_ERR_PARTIAL_FIELDS')
|
||||
WHERE NEW.line_code IS NULL
|
||||
OR NEW.prev_line_number IS NULL
|
||||
OR NEW.prev_line_hash IS NULL
|
||||
OR NEW.this_line_number IS NULL;
|
||||
|
||||
SELECT RAISE(ABORT, 'LINE_ERR_NO_PREV')
|
||||
WHERE NOT EXISTS(
|
||||
SELECT 1
|
||||
FROM blocks p
|
||||
WHERE p.bch_name = NEW.bch_name
|
||||
AND p.block_number = NEW.prev_line_number
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
SELECT RAISE(ABORT, 'LINE_ERR_PREV_HASH_MISMATCH')
|
||||
WHERE NOT EXISTS(
|
||||
SELECT 1
|
||||
FROM blocks p
|
||||
WHERE p.bch_name = NEW.bch_name
|
||||
AND p.block_number = NEW.prev_line_number
|
||||
AND p.block_hash = NEW.prev_line_hash
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
SELECT RAISE(ABORT, 'LINE_ERR_LINE_CODE_MISMATCH')
|
||||
WHERE NEW.prev_line_number <> NEW.line_code
|
||||
AND NOT EXISTS(
|
||||
SELECT 1
|
||||
FROM blocks p
|
||||
WHERE p.bch_name = NEW.bch_name
|
||||
AND p.block_number = NEW.prev_line_number
|
||||
AND p.line_code = NEW.line_code
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
SELECT RAISE(ABORT, 'LINE_ERR_FIRST_STEP_BAD_THIS')
|
||||
WHERE NEW.prev_line_number = NEW.line_code
|
||||
AND NEW.this_line_number <> (CASE WHEN NEW.msg_type = 1 THEN 0 ELSE 1 END);
|
||||
|
||||
SELECT RAISE(ABORT, 'LINE_ERR_THIS_LINE_BAD_STEP')
|
||||
WHERE NEW.prev_line_number <> NEW.line_code
|
||||
AND NOT EXISTS(
|
||||
SELECT 1
|
||||
FROM blocks p
|
||||
WHERE p.bch_name = NEW.bch_name
|
||||
AND p.block_number = NEW.prev_line_number
|
||||
AND p.this_line_number IS NOT NULL
|
||||
AND (
|
||||
(NEW.msg_type = 1 AND
|
||||
(NEW.this_line_number = p.this_line_number OR NEW.this_line_number = p.this_line_number + 1)
|
||||
)
|
||||
OR
|
||||
(NEW.msg_type IN (0,3,4) AND NEW.this_line_number = p.this_line_number + 1)
|
||||
)
|
||||
LIMIT 1
|
||||
);
|
||||
END;
|
||||
""");
|
||||
}
|
||||
|
||||
private static void createConnectionStateTrigger(Statement st) throws SQLException {
|
||||
int FRIEND = (int) DatabaseInitializer.CONNECTION_FRIEND;
|
||||
int CONTACT = (int) DatabaseInitializer.CONNECTION_CONTACT;
|
||||
int FOLLOW = (int) DatabaseInitializer.CONNECTION_FOLLOW;
|
||||
int SPOUSE = (int) DatabaseInitializer.CONNECTION_SPOUSE;
|
||||
int PARENT = (int) DatabaseInitializer.CONNECTION_PARENT;
|
||||
int CHILD = (int) DatabaseInitializer.CONNECTION_CHILD;
|
||||
int SIBLING = (int) DatabaseInitializer.CONNECTION_SIBLING;
|
||||
int KNOWN = (int) DatabaseInitializer.CONNECTION_KNOWN_PERSON;
|
||||
int SHINE_CONF = (int) DatabaseInitializer.CONNECTION_SHINE_CONFIRMED;
|
||||
int SHINE_SEEN = (int) DatabaseInitializer.CONNECTION_SHINE_SEEN;
|
||||
|
||||
int UNFRIEND = (int) DatabaseInitializer.CONNECTION_UNFRIEND;
|
||||
int UNCONTACT = (int) DatabaseInitializer.CONNECTION_UNCONTACT;
|
||||
int UNFOLLOW = (int) DatabaseInitializer.CONNECTION_UNFOLLOW;
|
||||
int UNSPOUSE = (int) DatabaseInitializer.CONNECTION_UNSPOUSE;
|
||||
int UNPARENT = (int) DatabaseInitializer.CONNECTION_UNPARENT;
|
||||
int UNCHILD = (int) DatabaseInitializer.CONNECTION_UNCHILD;
|
||||
int UNSIBLING = (int) DatabaseInitializer.CONNECTION_UNSIBLING;
|
||||
int UNKNOWN = (int) DatabaseInitializer.CONNECTION_UNKNOWN_PERSON;
|
||||
int SHINE_UNCONF = (int) DatabaseInitializer.CONNECTION_SHINE_UNCONFIRMED;
|
||||
int SHINE_UNSEEN = (int) DatabaseInitializer.CONNECTION_SHINE_UNSEEN;
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE TRIGGER IF NOT EXISTS trg_blocks_connection_state_ai
|
||||
AFTER INSERT ON blocks
|
||||
WHEN NEW.msg_type = 3
|
||||
BEGIN
|
||||
-- FRIEND/CONTACT/FOLLOW/SPOUSE/PARENT/CHILD/SIBLING/KNOWN_PERSON/SHINE_*:
|
||||
-- 1) если записи нет — создаём
|
||||
INSERT OR IGNORE INTO connections_state (
|
||||
login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash
|
||||
)
|
||||
SELECT
|
||||
NEW.login,
|
||||
NEW.msg_sub_type,
|
||||
COALESCE(
|
||||
NEW.to_login,
|
||||
(
|
||||
SELECT su.login
|
||||
FROM solana_users su
|
||||
WHERE su.blockchain_name = NEW.to_bch_name COLLATE NOCASE
|
||||
LIMIT 1
|
||||
),
|
||||
CASE
|
||||
WHEN NEW.to_bch_name IS NOT NULL
|
||||
AND length(NEW.to_bch_name) > 4
|
||||
AND substr(NEW.to_bch_name, length(NEW.to_bch_name) - 3, 1) = '-'
|
||||
THEN substr(NEW.to_bch_name, 1, length(NEW.to_bch_name) - 4)
|
||||
ELSE NULL
|
||||
END
|
||||
),
|
||||
NEW.to_bch_name,
|
||||
NEW.to_block_number,
|
||||
NEW.to_block_hash
|
||||
WHERE NEW.msg_sub_type IN (%d, %d, %d, %d, %d, %d, %d, %d, %d, %d)
|
||||
AND COALESCE(
|
||||
NEW.to_login,
|
||||
(
|
||||
SELECT su.login
|
||||
FROM solana_users su
|
||||
WHERE su.blockchain_name = NEW.to_bch_name COLLATE NOCASE
|
||||
LIMIT 1
|
||||
),
|
||||
CASE
|
||||
WHEN NEW.to_bch_name IS NOT NULL
|
||||
AND length(NEW.to_bch_name) > 4
|
||||
AND substr(NEW.to_bch_name, length(NEW.to_bch_name) - 3, 1) = '-'
|
||||
THEN substr(NEW.to_bch_name, 1, length(NEW.to_bch_name) - 4)
|
||||
ELSE NULL
|
||||
END
|
||||
) IS NOT NULL
|
||||
AND NEW.to_bch_name IS NOT NULL;
|
||||
|
||||
-- 2) если запись есть — обновляем актуальные to_*
|
||||
UPDATE connections_state
|
||||
SET
|
||||
to_bch_name = NEW.to_bch_name,
|
||||
to_block_number = NEW.to_block_number,
|
||||
to_block_hash = NEW.to_block_hash
|
||||
WHERE login = NEW.login
|
||||
AND rel_type = NEW.msg_sub_type
|
||||
AND to_login = COALESCE(
|
||||
NEW.to_login,
|
||||
(
|
||||
SELECT su.login
|
||||
FROM solana_users su
|
||||
WHERE su.blockchain_name = NEW.to_bch_name COLLATE NOCASE
|
||||
LIMIT 1
|
||||
),
|
||||
CASE
|
||||
WHEN NEW.to_bch_name IS NOT NULL
|
||||
AND length(NEW.to_bch_name) > 4
|
||||
AND substr(NEW.to_bch_name, length(NEW.to_bch_name) - 3, 1) = '-'
|
||||
THEN substr(NEW.to_bch_name, 1, length(NEW.to_bch_name) - 4)
|
||||
ELSE NULL
|
||||
END
|
||||
)
|
||||
AND NEW.msg_sub_type IN (%d, %d, %d, %d, %d, %d, %d, %d, %d, %d)
|
||||
AND COALESCE(
|
||||
NEW.to_login,
|
||||
(
|
||||
SELECT su.login
|
||||
FROM solana_users su
|
||||
WHERE su.blockchain_name = NEW.to_bch_name COLLATE NOCASE
|
||||
LIMIT 1
|
||||
),
|
||||
CASE
|
||||
WHEN NEW.to_bch_name IS NOT NULL
|
||||
AND length(NEW.to_bch_name) > 4
|
||||
AND substr(NEW.to_bch_name, length(NEW.to_bch_name) - 3, 1) = '-'
|
||||
THEN substr(NEW.to_bch_name, 1, length(NEW.to_bch_name) - 4)
|
||||
ELSE NULL
|
||||
END
|
||||
) IS NOT NULL
|
||||
AND NEW.to_bch_name IS NOT NULL;
|
||||
|
||||
-- UNFRIEND/UNCONTACT/UNFOLLOW/UNSPOUSE/UNPARENT/UNCHILD/UNSIBLING/UNKNOWN_PERSON/SHINE_UN*:
|
||||
-- удаляем соответствующее "позитивное" состояние
|
||||
DELETE FROM connections_state
|
||||
WHERE login = NEW.login
|
||||
AND to_login = COALESCE(
|
||||
NEW.to_login,
|
||||
(
|
||||
SELECT su.login
|
||||
FROM solana_users su
|
||||
WHERE su.blockchain_name = NEW.to_bch_name COLLATE NOCASE
|
||||
LIMIT 1
|
||||
),
|
||||
CASE
|
||||
WHEN NEW.to_bch_name IS NOT NULL
|
||||
AND length(NEW.to_bch_name) > 4
|
||||
AND substr(NEW.to_bch_name, length(NEW.to_bch_name) - 3, 1) = '-'
|
||||
THEN substr(NEW.to_bch_name, 1, length(NEW.to_bch_name) - 4)
|
||||
ELSE NULL
|
||||
END
|
||||
)
|
||||
AND rel_type = CASE NEW.msg_sub_type
|
||||
WHEN %d THEN %d
|
||||
WHEN %d THEN %d
|
||||
WHEN %d THEN %d
|
||||
WHEN %d THEN %d
|
||||
WHEN %d THEN %d
|
||||
WHEN %d THEN %d
|
||||
WHEN %d THEN %d
|
||||
WHEN %d THEN %d
|
||||
WHEN %d THEN %d
|
||||
WHEN %d THEN %d
|
||||
ELSE rel_type
|
||||
END
|
||||
AND COALESCE(
|
||||
NEW.to_login,
|
||||
(
|
||||
SELECT su.login
|
||||
FROM solana_users su
|
||||
WHERE su.blockchain_name = NEW.to_bch_name COLLATE NOCASE
|
||||
LIMIT 1
|
||||
),
|
||||
CASE
|
||||
WHEN NEW.to_bch_name IS NOT NULL
|
||||
AND length(NEW.to_bch_name) > 4
|
||||
AND substr(NEW.to_bch_name, length(NEW.to_bch_name) - 3, 1) = '-'
|
||||
THEN substr(NEW.to_bch_name, 1, length(NEW.to_bch_name) - 4)
|
||||
ELSE NULL
|
||||
END
|
||||
) IS NOT NULL
|
||||
AND NEW.msg_sub_type IN (%d, %d, %d, %d, %d, %d, %d, %d, %d, %d);
|
||||
END;
|
||||
""".formatted(
|
||||
FRIEND, CONTACT, FOLLOW, SPOUSE, PARENT, CHILD, SIBLING, KNOWN, SHINE_CONF, SHINE_SEEN,
|
||||
FRIEND, CONTACT, FOLLOW, SPOUSE, PARENT, CHILD, SIBLING, KNOWN, SHINE_CONF, SHINE_SEEN,
|
||||
|
||||
UNFRIEND, FRIEND,
|
||||
UNCONTACT, CONTACT,
|
||||
UNFOLLOW, FOLLOW,
|
||||
UNSPOUSE, SPOUSE,
|
||||
UNPARENT, PARENT,
|
||||
UNCHILD, CHILD,
|
||||
UNSIBLING, SIBLING,
|
||||
UNKNOWN, KNOWN,
|
||||
SHINE_UNCONF, SHINE_CONF,
|
||||
SHINE_UNSEEN, SHINE_SEEN,
|
||||
|
||||
UNFRIEND, UNCONTACT, UNFOLLOW, UNSPOUSE, UNPARENT, UNCHILD, UNSIBLING, UNKNOWN, SHINE_UNCONF, SHINE_UNSEEN
|
||||
));
|
||||
}
|
||||
|
||||
private static void createMessageStatsLikeTrigger(Statement st) throws SQLException {
|
||||
int LIKE = (int) DatabaseInitializer.REACTION_LIKE;
|
||||
int UNLIKE = (int) DatabaseInitializer.REACTION_UNLIKE;
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_like_ai
|
||||
AFTER INSERT ON blocks
|
||||
WHEN NEW.msg_type = 2 AND NEW.msg_sub_type IN (%d, %d)
|
||||
BEGIN
|
||||
-- ensure target stats row exists
|
||||
INSERT OR IGNORE INTO message_stats (
|
||||
to_login, to_bch_name, to_block_number, to_block_hash,
|
||||
likes_count, replies_count, edits_count
|
||||
)
|
||||
SELECT
|
||||
NEW.to_login, NEW.to_bch_name, NEW.to_block_number, NEW.to_block_hash,
|
||||
0, 0, 0
|
||||
WHERE NEW.to_login IS NOT NULL
|
||||
AND NEW.to_bch_name IS NOT NULL
|
||||
AND NEW.to_block_number IS NOT NULL
|
||||
AND NEW.to_block_hash IS NOT NULL;
|
||||
|
||||
-- apply delta by state transition (none/unlike->like = +1, like->unlike = -1)
|
||||
UPDATE message_stats
|
||||
SET likes_count = MAX(
|
||||
0,
|
||||
likes_count + (
|
||||
CASE
|
||||
WHEN NEW.msg_sub_type = %d
|
||||
AND COALESCE((
|
||||
SELECT b.msg_sub_type
|
||||
FROM blocks b
|
||||
WHERE b.login = NEW.login
|
||||
AND b.bch_name = NEW.bch_name
|
||||
AND b.msg_type = 2
|
||||
AND b.to_login = NEW.to_login
|
||||
AND b.to_bch_name = NEW.to_bch_name
|
||||
AND b.to_block_number = NEW.to_block_number
|
||||
AND b.to_block_hash = NEW.to_block_hash
|
||||
AND b.block_number < NEW.block_number
|
||||
ORDER BY b.block_number DESC
|
||||
LIMIT 1
|
||||
), -1) <> %d
|
||||
THEN 1
|
||||
WHEN NEW.msg_sub_type = %d
|
||||
AND COALESCE((
|
||||
SELECT b.msg_sub_type
|
||||
FROM blocks b
|
||||
WHERE b.login = NEW.login
|
||||
AND b.bch_name = NEW.bch_name
|
||||
AND b.msg_type = 2
|
||||
AND b.to_login = NEW.to_login
|
||||
AND b.to_bch_name = NEW.to_bch_name
|
||||
AND b.to_block_number = NEW.to_block_number
|
||||
AND b.to_block_hash = NEW.to_block_hash
|
||||
AND b.block_number < NEW.block_number
|
||||
ORDER BY b.block_number DESC
|
||||
LIMIT 1
|
||||
), -1) = %d
|
||||
THEN -1
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
)
|
||||
WHERE to_login = NEW.to_login
|
||||
AND to_bch_name = NEW.to_bch_name
|
||||
AND to_block_number = NEW.to_block_number
|
||||
AND to_block_hash = NEW.to_block_hash
|
||||
AND NEW.to_login IS NOT NULL
|
||||
AND NEW.to_bch_name IS NOT NULL
|
||||
AND NEW.to_block_number IS NOT NULL
|
||||
AND NEW.to_block_hash IS NOT NULL;
|
||||
|
||||
-- persist latest actor->target reaction state
|
||||
INSERT OR IGNORE INTO reactions_state (
|
||||
from_login, from_bch_name, reaction_type,
|
||||
to_login, to_bch_name, to_block_number, to_block_hash,
|
||||
last_sub_type
|
||||
)
|
||||
SELECT
|
||||
NEW.login, NEW.bch_name, %d,
|
||||
NEW.to_login, NEW.to_bch_name, NEW.to_block_number, NEW.to_block_hash,
|
||||
NEW.msg_sub_type
|
||||
WHERE NEW.to_login IS NOT NULL
|
||||
AND NEW.to_bch_name IS NOT NULL
|
||||
AND NEW.to_block_number IS NOT NULL
|
||||
AND NEW.to_block_hash IS NOT NULL;
|
||||
|
||||
UPDATE reactions_state
|
||||
SET last_sub_type = NEW.msg_sub_type
|
||||
WHERE from_login = NEW.login
|
||||
AND from_bch_name = NEW.bch_name
|
||||
AND reaction_type = %d
|
||||
AND to_login = NEW.to_login
|
||||
AND to_bch_name = NEW.to_bch_name
|
||||
AND to_block_number = NEW.to_block_number
|
||||
AND to_block_hash = NEW.to_block_hash
|
||||
AND NEW.to_login IS NOT NULL
|
||||
AND NEW.to_bch_name IS NOT NULL
|
||||
AND NEW.to_block_number IS NOT NULL
|
||||
AND NEW.to_block_hash IS NOT NULL;
|
||||
END;
|
||||
""".formatted(
|
||||
LIKE, UNLIKE,
|
||||
LIKE, LIKE,
|
||||
UNLIKE, LIKE,
|
||||
LIKE,
|
||||
LIKE
|
||||
));
|
||||
}
|
||||
|
||||
private static void createMessageStatsReplyTrigger(Statement st) throws SQLException {
|
||||
int REPLY = (int) DatabaseInitializer.TEXT_REPLY;
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_reply_ai
|
||||
AFTER INSERT ON blocks
|
||||
WHEN NEW.msg_type = 1 AND NEW.msg_sub_type = %d
|
||||
BEGIN
|
||||
INSERT OR IGNORE INTO message_stats (
|
||||
to_login, to_bch_name, to_block_number, to_block_hash,
|
||||
likes_count, replies_count, edits_count
|
||||
)
|
||||
SELECT
|
||||
NEW.to_login, NEW.to_bch_name, NEW.to_block_number, NEW.to_block_hash,
|
||||
0, 0, 0
|
||||
WHERE NEW.to_login IS NOT NULL
|
||||
AND NEW.to_bch_name IS NOT NULL
|
||||
AND NEW.to_block_number IS NOT NULL
|
||||
AND NEW.to_block_hash IS NOT NULL;
|
||||
|
||||
UPDATE message_stats
|
||||
SET replies_count = replies_count + 1
|
||||
WHERE to_login = NEW.to_login
|
||||
AND to_bch_name = NEW.to_bch_name
|
||||
AND to_block_number = NEW.to_block_number
|
||||
AND to_block_hash = NEW.to_block_hash
|
||||
AND NEW.to_login IS NOT NULL
|
||||
AND NEW.to_bch_name IS NOT NULL
|
||||
AND NEW.to_block_number IS NOT NULL
|
||||
AND NEW.to_block_hash IS NOT NULL;
|
||||
END;
|
||||
""".formatted(REPLY));
|
||||
}
|
||||
|
||||
private static void createEditApplyTrigger(Statement st) throws SQLException {
|
||||
int EDIT_POST = (int) DatabaseInitializer.TEXT_EDIT_POST;
|
||||
int EDIT_REPLY = (int) DatabaseInitializer.TEXT_EDIT_REPLY;
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE TRIGGER IF NOT EXISTS trg_blocks_edit_apply_ai
|
||||
AFTER INSERT ON blocks
|
||||
WHEN NEW.msg_type = 1 AND NEW.msg_sub_type IN (%d, %d)
|
||||
BEGIN
|
||||
-- 1) помечаем исходный блок, что его "перекрыл" этот edit
|
||||
UPDATE blocks
|
||||
SET edited_by_block_number = NEW.block_number
|
||||
WHERE login = NEW.login
|
||||
AND bch_name = NEW.bch_name
|
||||
AND block_number = NEW.to_block_number
|
||||
AND NEW.to_block_number IS NOT NULL;
|
||||
|
||||
-- 2) создаём stats-строку если её не было
|
||||
INSERT OR IGNORE INTO message_stats (
|
||||
to_login, to_bch_name, to_block_number, to_block_hash,
|
||||
likes_count, replies_count, edits_count
|
||||
)
|
||||
SELECT
|
||||
NEW.to_login, NEW.to_bch_name, NEW.to_block_number, NEW.to_block_hash,
|
||||
0, 0, 0
|
||||
WHERE NEW.to_login IS NOT NULL
|
||||
AND NEW.to_bch_name IS NOT NULL
|
||||
AND NEW.to_block_number IS NOT NULL
|
||||
AND NEW.to_block_hash IS NOT NULL;
|
||||
|
||||
-- 3) +1 edit
|
||||
UPDATE message_stats
|
||||
SET edits_count = edits_count + 1
|
||||
WHERE to_login = NEW.to_login
|
||||
AND to_bch_name = NEW.to_bch_name
|
||||
AND to_block_number = NEW.to_block_number
|
||||
AND to_block_hash = NEW.to_block_hash
|
||||
AND NEW.to_login IS NOT NULL
|
||||
AND NEW.to_bch_name IS NOT NULL
|
||||
AND NEW.to_block_number IS NOT NULL
|
||||
AND NEW.to_block_hash IS NOT NULL;
|
||||
END;
|
||||
""".formatted(EDIT_POST, EDIT_REPLY));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package shine.db;
|
||||
|
||||
import shine.db.connection.DbProvider;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Нейтральная точка входа в runtime БД сервера.
|
||||
*
|
||||
* Runtime-сервер теперь поддерживает только PostgreSQL.
|
||||
*/
|
||||
public final class DbController implements DbProvider {
|
||||
|
||||
private static volatile DbController instance;
|
||||
|
||||
private final PostgresDbController delegate;
|
||||
|
||||
private DbController() {
|
||||
this.delegate = PostgresDbController.getInstance();
|
||||
}
|
||||
|
||||
public static DbController getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (DbController.class) {
|
||||
if (instance == null) {
|
||||
instance = new DbController();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection getConnection() throws SQLException {
|
||||
return delegate.getConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
delegate.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package shine.db;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Нормализация ключей из разных storage-слоёв к каноническому Base64(32).
|
||||
*/
|
||||
public final class KeyEncodingUtil {
|
||||
|
||||
private static final String BASE58_ALPHABET =
|
||||
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
|
||||
private static final int[] BASE58_INDEXES = new int[128];
|
||||
|
||||
static {
|
||||
for (int i = 0; i < BASE58_INDEXES.length; i++) {
|
||||
BASE58_INDEXES[i] = -1;
|
||||
}
|
||||
for (int i = 0; i < BASE58_ALPHABET.length(); i++) {
|
||||
BASE58_INDEXES[BASE58_ALPHABET.charAt(i)] = i;
|
||||
}
|
||||
}
|
||||
|
||||
private KeyEncodingUtil() {}
|
||||
|
||||
public static String normalizeKeyToBase64_32(String rawKey) {
|
||||
if (rawKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String value = rawKey.trim();
|
||||
if (value.isEmpty()) {
|
||||
return value;
|
||||
}
|
||||
|
||||
byte[] asBase64 = tryDecodeBase64_32(value);
|
||||
if (asBase64 != null) {
|
||||
return Base64.getEncoder().encodeToString(asBase64);
|
||||
}
|
||||
|
||||
byte[] asBase58 = tryDecodeBase58_32(value);
|
||||
if (asBase58 != null) {
|
||||
return Base64.getEncoder().encodeToString(asBase58);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static byte[] tryDecodeBase64_32(String value) {
|
||||
try {
|
||||
byte[] decoded = Base64.getDecoder().decode(value);
|
||||
return decoded.length == 32 ? decoded : null;
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] tryDecodeBase58_32(String value) {
|
||||
byte[] decoded = decodeBase58(value);
|
||||
return decoded.length == 32 ? decoded : null;
|
||||
}
|
||||
|
||||
private static byte[] decodeBase58(String input) {
|
||||
if (input.isEmpty()) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
byte[] input58 = new byte[input.length()];
|
||||
for (int i = 0; i < input.length(); i++) {
|
||||
char c = input.charAt(i);
|
||||
if (c >= BASE58_INDEXES.length || BASE58_INDEXES[c] < 0) {
|
||||
return new byte[0];
|
||||
}
|
||||
input58[i] = (byte) BASE58_INDEXES[c];
|
||||
}
|
||||
|
||||
int zeros = 0;
|
||||
while (zeros < input58.length && input58[zeros] == 0) {
|
||||
zeros++;
|
||||
}
|
||||
|
||||
byte[] decoded = new byte[input.length()];
|
||||
int outputStart = decoded.length;
|
||||
int inputStart = zeros;
|
||||
while (inputStart < input58.length) {
|
||||
int mod = divmod256(input58, inputStart);
|
||||
if (input58[inputStart] == 0) {
|
||||
inputStart++;
|
||||
}
|
||||
decoded[--outputStart] = (byte) mod;
|
||||
}
|
||||
|
||||
while (outputStart < decoded.length && decoded[outputStart] == 0) {
|
||||
outputStart++;
|
||||
}
|
||||
|
||||
byte[] result = new byte[decoded.length - outputStart + zeros];
|
||||
for (int i = 0; i < zeros; i++) {
|
||||
result[i] = 0;
|
||||
}
|
||||
System.arraycopy(decoded, outputStart, result, zeros, decoded.length - outputStart);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int divmod256(byte[] number58, int startAt) {
|
||||
int remainder = 0;
|
||||
for (int i = startAt; i < number58.length; i++) {
|
||||
int digit58 = number58[i] & 0xFF;
|
||||
int temp = remainder * 58 + digit58;
|
||||
number58[i] = (byte) (temp / 256);
|
||||
remainder = temp % 256;
|
||||
}
|
||||
return remainder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package shine.db;
|
||||
|
||||
import shine.db.connection.DriverManagerDbProvider;
|
||||
import utils.config.AppConfig;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public final class PostgresDbController {
|
||||
|
||||
private static volatile PostgresDbController instance;
|
||||
|
||||
private final DriverManagerDbProvider delegate;
|
||||
|
||||
private PostgresDbController() {
|
||||
AppConfig config = AppConfig.getInstance();
|
||||
String jdbcUrl = trimToNull(config.getParam("db.url"));
|
||||
String dbUser = trimToNull(config.getParam("db.user"));
|
||||
String dbPassword = trimToNull(config.getParam("db.password"));
|
||||
|
||||
if (jdbcUrl == null) {
|
||||
throw new IllegalStateException("Config param 'db.url' is required and must point to PostgreSQL");
|
||||
}
|
||||
if (!jdbcUrl.startsWith("jdbc:postgresql:")) {
|
||||
throw new IllegalStateException("Only PostgreSQL runtime is supported. Unsupported db.url=" + jdbcUrl);
|
||||
}
|
||||
|
||||
try {
|
||||
DatabaseInitializer.ensurePostgresSchemaInitialized(jdbcUrl, dbUser, dbPassword);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("PostgreSQL schema auto-init failed", e);
|
||||
}
|
||||
|
||||
this.delegate = new DriverManagerDbProvider(jdbcUrl, dbUser, dbPassword, connection -> {
|
||||
connection.setAutoCommit(true);
|
||||
});
|
||||
}
|
||||
|
||||
public static PostgresDbController getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (PostgresDbController.class) {
|
||||
if (instance == null) {
|
||||
instance = new PostgresDbController();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public Connection getConnection() throws SQLException {
|
||||
return delegate.getConnection();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
private static String trimToNull(String value) {
|
||||
if (value == null) return null;
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
}
|
||||
@@ -1,846 +0,0 @@
|
||||
package shine.db;
|
||||
|
||||
import utils.config.AppConfig;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
|
||||
public final class SqliteDbController {
|
||||
|
||||
private static volatile SqliteDbController instance;
|
||||
private static final int LATEST_SCHEMA_VERSION = 12;
|
||||
|
||||
private final String jdbcUrl;
|
||||
|
||||
private SqliteDbController() {
|
||||
try {
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException("SQLite JDBC driver not found", e);
|
||||
}
|
||||
|
||||
String dbPath = AppConfig.getInstance().getParam("db.path");
|
||||
if (dbPath == null || dbPath.isBlank()) {
|
||||
throw new RuntimeException("Config param 'db.path' is not set in application.properties");
|
||||
}
|
||||
|
||||
Path dbFile = Paths.get(dbPath);
|
||||
|
||||
if (!Files.exists(dbFile)) {
|
||||
System.out.println("[DB] Файл БД не найден: " + dbFile.toAbsolutePath());
|
||||
System.out.println("[DB] Создаём новую БД с помощью DatabaseInitializer...");
|
||||
DatabaseInitializer.createNewDB(new String[0]);
|
||||
}
|
||||
|
||||
this.jdbcUrl = "jdbc:sqlite:" + dbPath;
|
||||
ensureSchemaMigrations();
|
||||
}
|
||||
|
||||
public static SqliteDbController getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (SqliteDbController.class) {
|
||||
if (instance == null) {
|
||||
instance = new SqliteDbController();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public Connection getConnection() throws SQLException {
|
||||
Connection conn = DriverManager.getConnection(jdbcUrl);
|
||||
conn.setAutoCommit(true);
|
||||
|
||||
try (Statement st = conn.createStatement()) {
|
||||
st.execute("PRAGMA foreign_keys = ON");
|
||||
st.execute("PRAGMA journal_mode = WAL");
|
||||
st.execute("PRAGMA synchronous = NORMAL");
|
||||
st.execute("PRAGMA busy_timeout = 5000");
|
||||
}
|
||||
|
||||
return conn;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
private void ensureSchemaMigrations() {
|
||||
int currentVersion = getCurrentSchemaVersion();
|
||||
|
||||
while (currentVersion < LATEST_SCHEMA_VERSION) {
|
||||
int nextVersion = currentVersion + 1;
|
||||
applyMigration(nextVersion);
|
||||
currentVersion = nextVersion;
|
||||
}
|
||||
}
|
||||
|
||||
private void applyMigration(int targetVersion) {
|
||||
switch (targetVersion) {
|
||||
case 1 -> migrateToV1();
|
||||
case 2 -> migrateToV2();
|
||||
case 3 -> migrateToV3();
|
||||
case 4 -> migrateToV4();
|
||||
case 5 -> migrateToV5();
|
||||
case 6 -> migrateToV6();
|
||||
case 7 -> migrateToV7();
|
||||
case 8 -> migrateToV8();
|
||||
case 9 -> migrateToV9();
|
||||
case 10 -> migrateToV10();
|
||||
case 11 -> migrateToV11();
|
||||
case 12 -> migrateToV12();
|
||||
default -> throw new RuntimeException("Unknown DB migration target version: " + targetVersion);
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateToV1() {
|
||||
try {
|
||||
DatabaseInitializer.ensureSchemaV1Structure(jdbcUrl);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("DB migration to v1 failed (base schema)", e);
|
||||
}
|
||||
|
||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
||||
Statement st = c.createStatement()) {
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
st.execute("PRAGMA foreign_keys = OFF");
|
||||
|
||||
if (tableExists(c, "connections_state") && needsConnectionsStateUpgrade(c)) {
|
||||
rebuildConnectionsStateTable(st);
|
||||
}
|
||||
|
||||
ensureChannelNamesDescriptionColumn(c, st);
|
||||
ensureChannelNamesTypeColumns(c, st);
|
||||
ensureSignedMessageReceiptUniq(c, st);
|
||||
DatabaseTriggersInstaller.createAllTriggers(st);
|
||||
setSchemaVersion(c, 1);
|
||||
|
||||
st.execute("PRAGMA foreign_keys = ON");
|
||||
c.commit();
|
||||
} catch (Exception e) {
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
throw new RuntimeException("DB migration to v1 failed", e);
|
||||
} finally {
|
||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("DB migration to v1 failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateToV2() {
|
||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
||||
Statement st = c.createStatement()) {
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
st.execute("PRAGMA foreign_keys = OFF");
|
||||
st.executeUpdate("DROP TABLE IF EXISTS message_views_state");
|
||||
st.executeUpdate("DROP INDEX IF EXISTS idx_message_views_state_target");
|
||||
st.executeUpdate("DROP INDEX IF EXISTS idx_message_views_state_viewer_channel");
|
||||
setSchemaVersion(c, 2);
|
||||
st.execute("PRAGMA foreign_keys = ON");
|
||||
c.commit();
|
||||
} catch (Exception e) {
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
throw new RuntimeException("DB migration to v2 failed", e);
|
||||
} finally {
|
||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("DB migration to v2 failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateToV3() {
|
||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
||||
Statement st = c.createStatement()) {
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
ensureChat200StateTables(st);
|
||||
setSchemaVersion(c, 3);
|
||||
c.commit();
|
||||
} catch (Exception e) {
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
throw new RuntimeException("DB migration to v3 failed", e);
|
||||
} finally {
|
||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("DB migration to v3 failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateToV4() {
|
||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
||||
Statement st = c.createStatement()) {
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
ensureActiveSessionsSessionTypeColumn(c, st);
|
||||
ensureActiveSessionsClientPlatformColumn(c, st);
|
||||
setSchemaVersion(c, 4);
|
||||
c.commit();
|
||||
} catch (Exception e) {
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
throw new RuntimeException("DB migration to v4 failed", e);
|
||||
} finally {
|
||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("DB migration to v4 failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateToV5() {
|
||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
||||
Statement st = c.createStatement()) {
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
ensureEspPairingTables(st);
|
||||
setSchemaVersion(c, 5);
|
||||
c.commit();
|
||||
} catch (Exception e) {
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
throw new RuntimeException("DB migration to v5 failed", e);
|
||||
} finally {
|
||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("DB migration to v5 failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateToV6() {
|
||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
||||
Statement st = c.createStatement()) {
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
ensureSignedMessagesRevisionColumn(c, st);
|
||||
setSchemaVersion(c, 6);
|
||||
c.commit();
|
||||
} catch (Exception e) {
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
throw new RuntimeException("DB migration to v6 failed", e);
|
||||
} finally {
|
||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("DB migration to v6 failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateToV7() {
|
||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
||||
Statement st = c.createStatement()) {
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
dropDmFileTables(st);
|
||||
setSchemaVersion(c, 7);
|
||||
c.commit();
|
||||
} catch (Exception e) {
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
throw new RuntimeException("DB migration to v7 failed", e);
|
||||
} finally {
|
||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("DB migration to v7 failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateToV8() {
|
||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
||||
Statement st = c.createStatement()) {
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
ensureTestFreeAvatarUploadsTable(st);
|
||||
setSchemaVersion(c, 8);
|
||||
c.commit();
|
||||
} catch (Exception e) {
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
throw new RuntimeException("DB migration to v8 failed", e);
|
||||
} finally {
|
||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("DB migration to v8 failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateToV9() {
|
||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
||||
Statement st = c.createStatement()) {
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
ensureSyncServersTable(st);
|
||||
setSchemaVersion(c, 9);
|
||||
c.commit();
|
||||
} catch (Exception e) {
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
throw new RuntimeException("DB migration to v9 failed", e);
|
||||
} finally {
|
||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("DB migration to v9 failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateToV10() {
|
||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
||||
Statement st = c.createStatement()) {
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
ensureSignedMessagesReencryptedColumn(c, st);
|
||||
setSchemaVersion(c, 10);
|
||||
c.commit();
|
||||
} catch (Exception e) {
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
throw new RuntimeException("DB migration to v10 failed", e);
|
||||
} finally {
|
||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("DB migration to v10 failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateToV11() {
|
||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
||||
Statement st = c.createStatement()) {
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
clearLegacySignedMessagesForDmV11(c, st);
|
||||
setSchemaVersion(c, 11);
|
||||
c.commit();
|
||||
} catch (Exception e) {
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
throw new RuntimeException("DB migration to v11 failed", e);
|
||||
} finally {
|
||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("DB migration to v11 failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateToV12() {
|
||||
try (Connection c = DriverManager.getConnection(jdbcUrl);
|
||||
Statement st = c.createStatement()) {
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
ensureSignedMessagesReadAtColumn(c, st);
|
||||
backfillSignedMessagesReadAt(st);
|
||||
setSchemaVersion(c, 12);
|
||||
c.commit();
|
||||
} catch (Exception e) {
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
throw new RuntimeException("DB migration to v12 failed", e);
|
||||
} finally {
|
||||
try { c.setAutoCommit(true); } catch (Exception ignored) {}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("DB migration to v12 failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureChat200StateTables(Statement st) throws SQLException {
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS chat200_state (
|
||||
owner_login TEXT NOT NULL,
|
||||
owner_bch_name TEXT NOT NULL,
|
||||
channel_root_block_number INTEGER NOT NULL,
|
||||
channel_root_block_hash BLOB NOT NULL,
|
||||
channel_name TEXT NOT NULL,
|
||||
channel_type_version INTEGER NOT NULL,
|
||||
chat_title TEXT NOT NULL DEFAULT '',
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
PRIMARY KEY (owner_bch_name, channel_root_block_number)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS chat200_members_state (
|
||||
owner_bch_name TEXT NOT NULL,
|
||||
channel_root_block_number INTEGER NOT NULL,
|
||||
member_login TEXT NOT NULL,
|
||||
member_channel_name TEXT NOT NULL,
|
||||
is_active INTEGER NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
updated_by_block_number INTEGER NOT NULL,
|
||||
PRIMARY KEY (
|
||||
owner_bch_name,
|
||||
channel_root_block_number,
|
||||
member_login,
|
||||
member_channel_name
|
||||
)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_chat200_state_owner
|
||||
ON chat200_state (owner_login, owner_bch_name);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_chat200_members_owner
|
||||
ON chat200_members_state (owner_bch_name, channel_root_block_number, is_active);
|
||||
""");
|
||||
}
|
||||
|
||||
private int getCurrentSchemaVersion() {
|
||||
try (Connection c = DriverManager.getConnection(jdbcUrl)) {
|
||||
if (!tableExists(c, DatabaseInitializer.DB_SCHEMA_VERSION_TABLE)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try (var ps = c.prepareStatement("""
|
||||
SELECT schema_version
|
||||
FROM db_schema_version
|
||||
WHERE id = 1
|
||||
LIMIT 1
|
||||
""");
|
||||
ResultSet rs = ps.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
return rs.getInt(1);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Cannot read DB schema version", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureActiveSessionsSessionTypeColumn(Connection c, Statement st) throws SQLException {
|
||||
if (columnExists(c, "active_sessions", "session_type")) return;
|
||||
st.executeUpdate("ALTER TABLE active_sessions ADD COLUMN session_type INTEGER NOT NULL DEFAULT 1");
|
||||
}
|
||||
|
||||
private static void ensureActiveSessionsClientPlatformColumn(Connection c, Statement st) throws SQLException {
|
||||
if (columnExists(c, "active_sessions", "client_platform")) return;
|
||||
st.executeUpdate("ALTER TABLE active_sessions ADD COLUMN client_platform TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
|
||||
private static void ensureEspPairingTables(Statement st) throws SQLException {
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS esp_pairing_settings (
|
||||
login TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,
|
||||
enabled INTEGER NOT NULL DEFAULT 0,
|
||||
password_hash TEXT NOT NULL DEFAULT '',
|
||||
ttl_seconds INTEGER NOT NULL DEFAULT 300,
|
||||
failed_attempts INTEGER NOT NULL DEFAULT 0,
|
||||
first_failed_at_ms INTEGER NOT NULL DEFAULT 0,
|
||||
blocked_until_ms INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS esp_pairing_requests (
|
||||
pairing_id TEXT NOT NULL PRIMARY KEY,
|
||||
login TEXT NOT NULL,
|
||||
requester_session_key TEXT NOT NULL,
|
||||
requester_session_type INTEGER NOT NULL DEFAULT 1,
|
||||
requester_client_platform TEXT NOT NULL DEFAULT '',
|
||||
payload_type INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
short_code TEXT NOT NULL,
|
||||
fingerprint_b58 TEXT NOT NULL,
|
||||
encrypted_payload TEXT,
|
||||
reject_reason TEXT,
|
||||
approved_by_session_id TEXT,
|
||||
created_at_ms INTEGER NOT NULL,
|
||||
expires_at_ms INTEGER NOT NULL,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
delivered_to_homeserver INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_esp_pairing_requests_login_status
|
||||
ON esp_pairing_requests (login, status, expires_at_ms);
|
||||
""");
|
||||
}
|
||||
|
||||
private static void ensureSignedMessagesRevisionColumn(Connection c, Statement st) throws SQLException {
|
||||
if (!tableExists(c, "signed_messages_v2")) return;
|
||||
if (!columnExists(c, "signed_messages_v2", "revision_time_ms")) {
|
||||
st.executeUpdate("ALTER TABLE signed_messages_v2 ADD COLUMN revision_time_ms INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureSignedMessagesReencryptedColumn(Connection c, Statement st) throws SQLException {
|
||||
if (!tableExists(c, "signed_messages_v2")) return;
|
||||
if (!columnExists(c, "signed_messages_v2", "reencrypted_at_ms")) {
|
||||
st.executeUpdate("ALTER TABLE signed_messages_v2 ADD COLUMN reencrypted_at_ms INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureSignedMessagesReadAtColumn(Connection c, Statement st) throws SQLException {
|
||||
if (!tableExists(c, "signed_messages_v2")) return;
|
||||
if (!columnExists(c, "signed_messages_v2", "read_at_ms")) {
|
||||
st.executeUpdate("ALTER TABLE signed_messages_v2 ADD COLUMN read_at_ms INTEGER");
|
||||
}
|
||||
}
|
||||
|
||||
private static void backfillSignedMessagesReadAt(Statement st) throws SQLException {
|
||||
st.executeUpdate("""
|
||||
UPDATE signed_messages_v2 AS content
|
||||
SET read_at_ms = (
|
||||
SELECT MIN(receipt.time_ms)
|
||||
FROM signed_messages_v2 AS receipt
|
||||
WHERE receipt.message_type IN (3, 4)
|
||||
AND receipt.receipt_ref_base_key = content.base_key
|
||||
)
|
||||
WHERE content.message_type IN (1, 2)
|
||||
AND (content.read_at_ms IS NULL OR content.read_at_ms <= 0)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM signed_messages_v2 AS receipt
|
||||
WHERE receipt.message_type IN (3, 4)
|
||||
AND receipt.receipt_ref_base_key = content.base_key
|
||||
);
|
||||
""");
|
||||
}
|
||||
|
||||
/**
|
||||
* Временная одноразовая миграция на переходе к SHiNE_DM v1:
|
||||
* старые строки signed_messages_v2 больше не гарантированно совместимы
|
||||
* с новым бинарным форматом и правилами tombstone/reencrypt.
|
||||
* Позже эту миграцию нужно убрать, чтобы случайно не очищать рабочие DM.
|
||||
*/
|
||||
private static void clearLegacySignedMessagesForDmV11(Connection c, Statement st) throws SQLException {
|
||||
if (tableExists(c, "signed_message_session_delivery")) {
|
||||
st.executeUpdate("DELETE FROM signed_message_session_delivery");
|
||||
}
|
||||
if (tableExists(c, "signed_messages_v2")) {
|
||||
st.executeUpdate("DELETE FROM signed_messages_v2");
|
||||
}
|
||||
}
|
||||
|
||||
private static void dropDmFileTables(Statement st) throws SQLException {
|
||||
st.executeUpdate("DROP INDEX IF EXISTS idx_dm_message_file_links_login");
|
||||
st.executeUpdate("DROP INDEX IF EXISTS idx_dm_message_file_links_message");
|
||||
st.executeUpdate("DROP TABLE IF EXISTS dm_message_file_links");
|
||||
st.executeUpdate("DROP TABLE IF EXISTS dm_files");
|
||||
}
|
||||
|
||||
private static boolean columnExists(Connection c, String tableName, String columnName) throws SQLException {
|
||||
try (Statement probe = c.createStatement();
|
||||
ResultSet rs = probe.executeQuery("PRAGMA table_info(" + tableName + ")")) {
|
||||
while (rs.next()) {
|
||||
if (columnName.equalsIgnoreCase(rs.getString("name"))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void setSchemaVersion(Connection c, int version) throws SQLException {
|
||||
try (var ps = c.prepareStatement("""
|
||||
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||
VALUES (1, ?, CAST(strftime('%s','now') AS INTEGER) * 1000)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
schema_version = excluded.schema_version,
|
||||
updated_at_ms = excluded.updated_at_ms
|
||||
""")) {
|
||||
ps.setInt(1, version);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureReactionsStateTable(Statement st) throws SQLException {
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS reactions_state (
|
||||
from_login TEXT NOT NULL,
|
||||
from_bch_name TEXT NOT NULL,
|
||||
reaction_type INTEGER NOT NULL,
|
||||
to_login TEXT NOT NULL,
|
||||
to_bch_name TEXT NOT NULL,
|
||||
to_block_number INTEGER NOT NULL,
|
||||
to_block_hash BLOB NOT NULL,
|
||||
last_sub_type INTEGER NOT NULL,
|
||||
UNIQUE (
|
||||
from_login,
|
||||
from_bch_name,
|
||||
reaction_type,
|
||||
to_login,
|
||||
to_bch_name,
|
||||
to_block_number,
|
||||
to_block_hash
|
||||
)
|
||||
);
|
||||
""");
|
||||
}
|
||||
|
||||
private static void ensureTestFreeAvatarUploadsTable(Statement st) throws SQLException {
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS test_free_avatar_uploads (
|
||||
login TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,
|
||||
used_count INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at_ms INTEGER NOT NULL,
|
||||
last_tx_id TEXT NOT NULL DEFAULT '',
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login)
|
||||
);
|
||||
""");
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_test_free_avatar_uploads_updated
|
||||
ON test_free_avatar_uploads (updated_at_ms);
|
||||
""");
|
||||
}
|
||||
|
||||
private static void ensureSyncServersTable(Statement st) throws SQLException {
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS sync_servers (
|
||||
login TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,
|
||||
server_address TEXT NOT NULL DEFAULT '',
|
||||
updated_at_ms INTEGER NOT NULL
|
||||
);
|
||||
""");
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_sync_servers_updated
|
||||
ON sync_servers (updated_at_ms);
|
||||
""");
|
||||
}
|
||||
|
||||
|
||||
private static void createConnectionsStateTable(Statement st) throws SQLException {
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS connections_state (
|
||||
login TEXT NOT NULL,
|
||||
rel_type INTEGER NOT NULL,
|
||||
to_login TEXT NOT NULL,
|
||||
to_bch_name TEXT NOT NULL,
|
||||
to_block_number INTEGER NOT NULL,
|
||||
to_block_hash BLOB NOT NULL,
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login),
|
||||
UNIQUE (login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash)
|
||||
);
|
||||
""");
|
||||
}
|
||||
|
||||
private static void ensureConnectionsIndexes(Statement st) throws SQLException {
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_connections_state_login
|
||||
ON connections_state (login);
|
||||
""");
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_connections_state_to_login
|
||||
ON connections_state (to_login);
|
||||
""");
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_connections_state_pair
|
||||
ON connections_state (login, to_login);
|
||||
""");
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_connections_state_target
|
||||
ON connections_state (login, rel_type, to_bch_name, to_block_number);
|
||||
""");
|
||||
}
|
||||
|
||||
private static void ensureReactionsIndexes(Statement st) throws SQLException {
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_reactions_state_target
|
||||
ON reactions_state (to_bch_name, to_block_number, to_block_hash);
|
||||
""");
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_reactions_state_actor
|
||||
ON reactions_state (from_login, from_bch_name, reaction_type);
|
||||
""");
|
||||
}
|
||||
|
||||
private static void ensureChannelNamesStateTable(Statement st) throws SQLException {
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS channel_names_state (
|
||||
slug TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
channel_description TEXT NOT NULL DEFAULT '',
|
||||
owner_login TEXT NOT NULL,
|
||||
owner_bch_name TEXT NOT NULL,
|
||||
channel_type_code INTEGER NOT NULL DEFAULT 1,
|
||||
channel_type_version INTEGER NOT NULL DEFAULT 1,
|
||||
channel_root_block_number INTEGER NOT NULL,
|
||||
channel_root_block_hash BLOB NOT NULL,
|
||||
created_at_ms INTEGER NOT NULL
|
||||
);
|
||||
""");
|
||||
}
|
||||
|
||||
private static void ensureChannelNamesDescriptionColumn(Connection c, Statement st) throws SQLException {
|
||||
boolean hasDescription = false;
|
||||
try (Statement probe = c.createStatement();
|
||||
ResultSet rs = probe.executeQuery("PRAGMA table_info(channel_names_state)")) {
|
||||
while (rs.next()) {
|
||||
String name = rs.getString("name");
|
||||
if ("channel_description".equalsIgnoreCase(name)) {
|
||||
hasDescription = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasDescription) {
|
||||
st.executeUpdate("ALTER TABLE channel_names_state ADD COLUMN channel_description TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureChannelNamesTypeColumns(Connection c, Statement st) throws SQLException {
|
||||
boolean hasTypeCode = false;
|
||||
boolean hasTypeVersion = false;
|
||||
|
||||
try (Statement probe = c.createStatement();
|
||||
ResultSet rs = probe.executeQuery("PRAGMA table_info(channel_names_state)")) {
|
||||
while (rs.next()) {
|
||||
String name = rs.getString("name");
|
||||
if ("channel_type_code".equalsIgnoreCase(name)) hasTypeCode = true;
|
||||
if ("channel_type_version".equalsIgnoreCase(name)) hasTypeVersion = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasTypeCode) {
|
||||
st.executeUpdate("ALTER TABLE channel_names_state ADD COLUMN channel_type_code INTEGER NOT NULL DEFAULT 1");
|
||||
}
|
||||
if (!hasTypeVersion) {
|
||||
st.executeUpdate("ALTER TABLE channel_names_state ADD COLUMN channel_type_version INTEGER NOT NULL DEFAULT 1");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureChannelNamesIndexes(Statement st) throws SQLException {
|
||||
st.executeUpdate("DROP INDEX IF EXISTS uq_channel_names_state_slug");
|
||||
st.executeUpdate("DROP INDEX IF EXISTS uq_channel_names_state_owner_slug");
|
||||
st.executeUpdate("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_names_state_owner_type_slug
|
||||
ON channel_names_state (owner_bch_name, channel_type_code, slug);
|
||||
""");
|
||||
st.executeUpdate("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_names_state_target
|
||||
ON channel_names_state (owner_bch_name, channel_root_block_number, channel_root_block_hash);
|
||||
""");
|
||||
st.executeUpdate("""
|
||||
CREATE INDEX IF NOT EXISTS idx_channel_names_state_owner
|
||||
ON channel_names_state (owner_login, owner_bch_name);
|
||||
""");
|
||||
}
|
||||
|
||||
private static void ensureSignedMessageReceiptUniq(Connection c, Statement st) throws SQLException {
|
||||
if (!tableExists(c, "signed_messages_v2")) return;
|
||||
|
||||
if (tableExists(c, "signed_message_session_delivery")) {
|
||||
st.executeUpdate("""
|
||||
DELETE FROM signed_message_session_delivery
|
||||
WHERE message_key IN (
|
||||
SELECT message_key
|
||||
FROM signed_messages_v2
|
||||
WHERE message_type IN (3, 4)
|
||||
AND receipt_ref_base_key IS NOT NULL
|
||||
AND rowid NOT IN (
|
||||
SELECT MIN(rowid)
|
||||
FROM signed_messages_v2
|
||||
WHERE message_type IN (3, 4)
|
||||
AND receipt_ref_base_key IS NOT NULL
|
||||
GROUP BY target_login, message_type, receipt_ref_base_key
|
||||
)
|
||||
);
|
||||
""");
|
||||
}
|
||||
|
||||
st.executeUpdate("""
|
||||
DELETE FROM signed_messages_v2
|
||||
WHERE message_type IN (3, 4)
|
||||
AND receipt_ref_base_key IS NOT NULL
|
||||
AND rowid NOT IN (
|
||||
SELECT MIN(rowid)
|
||||
FROM signed_messages_v2
|
||||
WHERE message_type IN (3, 4)
|
||||
AND receipt_ref_base_key IS NOT NULL
|
||||
GROUP BY target_login, message_type, receipt_ref_base_key
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_signed_messages_v2_receipt_incoming
|
||||
ON signed_messages_v2 (target_login, receipt_ref_base_key)
|
||||
WHERE message_type = 3 AND receipt_ref_base_key IS NOT NULL;
|
||||
""");
|
||||
st.executeUpdate("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_signed_messages_v2_receipt_outgoing
|
||||
ON signed_messages_v2 (target_login, receipt_ref_base_key)
|
||||
WHERE message_type = 4 AND receipt_ref_base_key IS NOT NULL;
|
||||
""");
|
||||
}
|
||||
|
||||
private static void rebuildConnectionsStateTable(Statement st) throws SQLException {
|
||||
st.executeUpdate("DROP TABLE IF EXISTS connections_state_v2");
|
||||
st.executeUpdate("""
|
||||
CREATE TABLE connections_state_v2 (
|
||||
login TEXT NOT NULL,
|
||||
rel_type INTEGER NOT NULL,
|
||||
to_login TEXT NOT NULL,
|
||||
to_bch_name TEXT NOT NULL,
|
||||
to_block_number INTEGER NOT NULL,
|
||||
to_block_hash BLOB NOT NULL,
|
||||
FOREIGN KEY (login) REFERENCES solana_users(login),
|
||||
UNIQUE (login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash)
|
||||
);
|
||||
""");
|
||||
|
||||
st.executeUpdate("""
|
||||
INSERT OR IGNORE INTO connections_state_v2
|
||||
(login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash)
|
||||
SELECT
|
||||
login,
|
||||
rel_type,
|
||||
to_login,
|
||||
to_bch_name,
|
||||
COALESCE(to_block_number, 0),
|
||||
COALESCE(to_block_hash, zeroblob(32))
|
||||
FROM connections_state
|
||||
WHERE login IS NOT NULL
|
||||
AND to_login IS NOT NULL
|
||||
AND to_bch_name IS NOT NULL;
|
||||
""");
|
||||
|
||||
st.executeUpdate("DROP TABLE connections_state");
|
||||
st.executeUpdate("ALTER TABLE connections_state_v2 RENAME TO connections_state");
|
||||
}
|
||||
|
||||
private static boolean tableExists(Connection c, String tableName) throws SQLException {
|
||||
String sql = "SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1";
|
||||
try (var ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, tableName);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
return rs.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean needsConnectionsStateUpgrade(Connection c) throws SQLException {
|
||||
boolean toBlockNumberNotNull = false;
|
||||
boolean toBlockHashNotNull = false;
|
||||
|
||||
try (Statement st = c.createStatement();
|
||||
ResultSet rs = st.executeQuery("PRAGMA table_info(connections_state)")) {
|
||||
while (rs.next()) {
|
||||
String name = rs.getString("name");
|
||||
int notNull = rs.getInt("notnull");
|
||||
if ("to_block_number".equalsIgnoreCase(name)) {
|
||||
toBlockNumberNotNull = notNull == 1;
|
||||
}
|
||||
if ("to_block_hash".equalsIgnoreCase(name)) {
|
||||
toBlockHashNotNull = notNull == 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !toBlockNumberNotNull || !toBlockHashNotNull;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ public final class ChannelNameRules {
|
||||
private static final int MAX_DISPLAY_NAME_LENGTH = 32;
|
||||
private static final Pattern DISPLAY_ALLOWED_PATTERN =
|
||||
Pattern.compile("^[A-Za-z0-9_-]+$");
|
||||
private static final Pattern PUBLIC_CHANNEL_ALLOWED_PATTERN =
|
||||
Pattern.compile("^[A-Za-z0-9_-]+$");
|
||||
|
||||
private ChannelNameRules() {}
|
||||
|
||||
@@ -34,6 +36,14 @@ public final class ChannelNameRules {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
public static String requireValidPublicDisplayNameForCreate(String rawName) {
|
||||
String normalized = requireValidDisplayNameForCreate(rawName);
|
||||
if (!PUBLIC_CHANNEL_ALLOWED_PATTERN.matcher(normalized).matches()) {
|
||||
throw new IllegalArgumentException("channelName contains unsupported characters");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
public static String toCanonicalSlug(String rawName) {
|
||||
String normalized = normalizeDisplayName(rawName);
|
||||
if (normalized.isEmpty()) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package shine.db.connection;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ConnectionFactory {
|
||||
Connection getConnection() throws SQLException;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package shine.db.connection;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Нейтральная точка доступа к JDBC-соединениям без привязки к конкретной БД.
|
||||
*
|
||||
* На первом этапе переносов этот интерфейс нужен как новая опора для DAO,
|
||||
* чтобы постепенно убрать прямую зависимость от DbController.
|
||||
*/
|
||||
public interface DbProvider extends ConnectionFactory, AutoCloseable {
|
||||
|
||||
@Override
|
||||
Connection getConnection() throws SQLException;
|
||||
|
||||
@Override
|
||||
default void close() throws Exception {
|
||||
// По умолчанию ресурсов на уровне provider нет.
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package shine.db.connection;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Базовый JDBC provider поверх DriverManager.
|
||||
*
|
||||
* Не знает ничего про конкретный доменный режим БД:
|
||||
* конкретные параметры и post-connect инициализация задаются снаружи.
|
||||
*/
|
||||
public final class DriverManagerDbProvider implements DbProvider {
|
||||
|
||||
private final String jdbcUrl;
|
||||
private final String user;
|
||||
private final String password;
|
||||
private final ConnectionInitializer initializer;
|
||||
|
||||
public DriverManagerDbProvider(String jdbcUrl,
|
||||
String user,
|
||||
String password,
|
||||
ConnectionInitializer initializer) {
|
||||
if (jdbcUrl == null || jdbcUrl.isBlank()) {
|
||||
throw new IllegalArgumentException("jdbcUrl is blank");
|
||||
}
|
||||
this.jdbcUrl = jdbcUrl;
|
||||
this.user = blankToNull(user);
|
||||
this.password = blankToNull(password);
|
||||
this.initializer = initializer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection getConnection() throws SQLException {
|
||||
Connection connection;
|
||||
if (user == null) {
|
||||
connection = DriverManager.getConnection(jdbcUrl);
|
||||
} else {
|
||||
connection = DriverManager.getConnection(jdbcUrl, user, password == null ? "" : password);
|
||||
}
|
||||
|
||||
if (initializer != null) {
|
||||
initializer.initialize(connection);
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
private static String blankToNull(String value) {
|
||||
if (value == null) return null;
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ConnectionInitializer {
|
||||
void initialize(Connection connection) throws SQLException;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.entities.ActiveSessionEntry;
|
||||
|
||||
import java.sql.*;
|
||||
@@ -17,7 +17,7 @@ import java.util.List;
|
||||
public final class ActiveSessionsDAO {
|
||||
|
||||
private static volatile ActiveSessionsDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private ActiveSessionsDAO() { }
|
||||
|
||||
@@ -137,7 +137,7 @@ public final class ActiveSessionsDAO {
|
||||
client_platform,
|
||||
user_language
|
||||
FROM active_sessions
|
||||
WHERE login = ? COLLATE NOCASE
|
||||
WHERE LOWER(login) = LOWER(?)
|
||||
""";
|
||||
|
||||
List<ActiveSessionEntry> result = new ArrayList<>();
|
||||
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.DatabaseInitializer;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
@@ -24,12 +24,12 @@ import java.sql.SQLException;
|
||||
* - эта схема проще и прозрачнее, чем много обратных триггеров по разным таблицам;
|
||||
* - если любой шаг не удался, делаем rollback и БД остаётся в исходном состоянии;
|
||||
* - файловые действия (.bch / .tmp_bch) сознательно НЕ входят в эту транзакцию:
|
||||
* SQLite не может атомарно закоммитить и SQL, и файловую систему сразу;
|
||||
* SQL-транзакция и файловая система не коммитятся атомарно вместе;
|
||||
* поэтому БД-чистка делается здесь, а файловая чистка будет следующим шагом
|
||||
* отдельным recovery/resync-слоем после успешного commit.
|
||||
*
|
||||
* Важный смысл текущей реализации:
|
||||
* - мы НЕ трогаем identity-слой (`solana_users`) и НЕ трогаем DM-таблицы;
|
||||
* - мы НЕ трогаем current users слой (`solana_user_pda_current`) и НЕ трогаем DM-таблицы;
|
||||
* - мы очищаем только блокчейн пользователя и derived-state, который строится из неё;
|
||||
* - висячие cross-chain ссылки в чужих blocks допускаются как нормальное поведение системы.
|
||||
*/
|
||||
@@ -39,7 +39,7 @@ public final class BlockchainResyncCleanupDAO {
|
||||
|
||||
private static volatile BlockchainResyncCleanupDAO instance;
|
||||
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private BlockchainResyncCleanupDAO() {}
|
||||
|
||||
@@ -307,7 +307,7 @@ public final class BlockchainResyncCleanupDAO {
|
||||
private int deleteConnectionsStateForLogin(Connection c, String login) throws SQLException {
|
||||
return executeDelete(c, """
|
||||
DELETE FROM connections_state
|
||||
WHERE login = ? COLLATE NOCASE
|
||||
WHERE LOWER(login) = LOWER(?)
|
||||
""", login);
|
||||
}
|
||||
|
||||
@@ -317,7 +317,7 @@ public final class BlockchainResyncCleanupDAO {
|
||||
private int deleteUsersParamsForLogin(Connection c, String login) throws SQLException {
|
||||
return executeDelete(c, """
|
||||
DELETE FROM users_params
|
||||
WHERE login = ? COLLATE NOCASE
|
||||
WHERE LOWER(login) = LOWER(?)
|
||||
""", login);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.entities.BlockchainStateEntry;
|
||||
|
||||
import java.sql.*;
|
||||
@@ -10,7 +10,7 @@ import java.util.List;
|
||||
public final class BlockchainStateDAO {
|
||||
|
||||
private static volatile BlockchainStateDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private BlockchainStateDAO() {}
|
||||
|
||||
@@ -75,7 +75,7 @@ public final class BlockchainStateDAO {
|
||||
last_block_hash,
|
||||
updated_at_ms
|
||||
FROM blockchain_state
|
||||
ORDER BY blockchain_name COLLATE NOCASE
|
||||
ORDER BY LOWER(blockchain_name)
|
||||
""";
|
||||
|
||||
List<BlockchainStateEntry> result = new ArrayList<>();
|
||||
@@ -142,7 +142,7 @@ public final class BlockchainStateDAO {
|
||||
* Строгая вставка state только если записи ещё нет.
|
||||
*
|
||||
* Нужна для recovery / resync:
|
||||
* - identity пользователя уже может существовать в solana_users;
|
||||
* - runtime-проекция пользователя уже может существовать в current users слое;
|
||||
* - в таком случае нам надо восстановить только blockchain_state;
|
||||
* - если запись уже есть, метод просто ничего не меняет.
|
||||
*/
|
||||
@@ -229,7 +229,7 @@ public final class BlockchainStateDAO {
|
||||
|
||||
private static void setBytesNullable(PreparedStatement ps, int index, byte[] b) throws SQLException {
|
||||
if (b != null) ps.setBytes(index, b);
|
||||
else ps.setNull(index, Types.BLOB);
|
||||
else ps.setNull(index, Types.BINARY);
|
||||
}
|
||||
|
||||
private static String nn(String s) { return s == null ? "" : s; }
|
||||
|
||||
@@ -2,7 +2,7 @@ package shine.db.dao;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.entities.BlockEntry;
|
||||
|
||||
import java.sql.*;
|
||||
@@ -22,7 +22,7 @@ import java.util.List;
|
||||
public final class BlocksDAO {
|
||||
|
||||
private static volatile BlocksDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final DbController db = DbController.getInstance();
|
||||
private static final Logger log = LoggerFactory.getLogger(BlocksDAO.class);
|
||||
|
||||
private BlocksDAO() { }
|
||||
@@ -90,7 +90,7 @@ public final class BlocksDAO {
|
||||
else ps.setNull(i++, Types.INTEGER);
|
||||
|
||||
if (e.getToBlockHash() != null) ps.setBytes(i++, e.getToBlockHash());
|
||||
else ps.setNull(i++, Types.BLOB);
|
||||
else ps.setNull(i++, Types.BINARY);
|
||||
|
||||
ps.setBytes(i++, e.getBlockHash());
|
||||
ps.setBytes(i++, e.getBlockSignature());
|
||||
@@ -106,7 +106,7 @@ public final class BlocksDAO {
|
||||
else ps.setNull(i++, Types.INTEGER);
|
||||
|
||||
if (e.getPrevLineHash() != null) ps.setBytes(i++, e.getPrevLineHash());
|
||||
else ps.setNull(i++, Types.BLOB);
|
||||
else ps.setNull(i++, Types.BINARY);
|
||||
|
||||
if (e.getThisLineNumber() != null) ps.setInt(i++, e.getThisLineNumber());
|
||||
else ps.setNull(i++, Types.INTEGER);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.entities.ChannelNameStateEntry;
|
||||
|
||||
import java.sql.Connection;
|
||||
@@ -11,7 +11,7 @@ import java.util.List;
|
||||
|
||||
public final class ChannelNameStateDAO {
|
||||
private static volatile ChannelNameStateDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private ChannelNameStateDAO() {}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.sql.CurrentUsersSql;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
@@ -13,16 +14,14 @@ import java.util.List;
|
||||
* ConnectionsStateDAO — чтение текущего состояния связей из connections_state.
|
||||
*
|
||||
* ВАЖНО:
|
||||
* - login в запросах может быть в любом регистре, поэтому в WHERE используем COLLATE NOCASE
|
||||
* - в ответах возвращаем логины в каноническом регистре через JOIN на solana_users
|
||||
*
|
||||
* ПРИМЕЧАНИЕ:
|
||||
* Таблица пользователей тут названа "solana_users". Если у тебя иначе — поменяй в SQL.
|
||||
* - login в запросах может быть в любом регистре;
|
||||
* - в ответах возвращаем логины в каноническом регистре через прямой lookup
|
||||
* по актуальным PDA-данным и manual fallback.
|
||||
*/
|
||||
public final class ConnectionsStateDAO {
|
||||
|
||||
private static volatile ConnectionsStateDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private ConnectionsStateDAO() {}
|
||||
|
||||
@@ -42,14 +41,17 @@ public final class ConnectionsStateDAO {
|
||||
String sql = """
|
||||
SELECT COALESCE(u_login.login, u_bch.login, cs.to_login) AS friend_login
|
||||
FROM connections_state cs
|
||||
LEFT JOIN solana_users u_login
|
||||
ON u_login.login = cs.to_login COLLATE NOCASE
|
||||
LEFT JOIN solana_users u_bch
|
||||
ON u_bch.blockchain_name = cs.to_bch_name COLLATE NOCASE
|
||||
WHERE cs.login = ? COLLATE NOCASE
|
||||
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 = ?
|
||||
ORDER BY friend_login
|
||||
""";
|
||||
""".formatted(
|
||||
CurrentUsersSql.usersSubquery("u_login"),
|
||||
CurrentUsersSql.usersSubquery("u_bch")
|
||||
);
|
||||
|
||||
List<String> out = new ArrayList<>();
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
@@ -72,17 +74,20 @@ public final class ConnectionsStateDAO {
|
||||
String sql = """
|
||||
SELECT COALESCE(u_actor.login, cs.login) AS friend_login
|
||||
FROM connections_state cs
|
||||
LEFT JOIN solana_users u_actor
|
||||
ON u_actor.login = cs.login COLLATE NOCASE
|
||||
LEFT JOIN solana_users u_target
|
||||
ON u_target.login = ? COLLATE NOCASE
|
||||
LEFT JOIN %s
|
||||
ON LOWER(u_actor.login) = LOWER(cs.login)
|
||||
LEFT JOIN %s
|
||||
ON LOWER(u_target.login) = LOWER(?)
|
||||
WHERE (
|
||||
cs.to_login = ? COLLATE NOCASE
|
||||
OR (u_target.blockchain_name IS NOT NULL AND cs.to_bch_name = u_target.blockchain_name COLLATE NOCASE)
|
||||
LOWER(cs.to_login) = LOWER(?)
|
||||
OR (u_target.blockchain_name IS NOT NULL AND LOWER(cs.to_bch_name) = LOWER(u_target.blockchain_name))
|
||||
)
|
||||
AND cs.rel_type = ?
|
||||
ORDER BY friend_login
|
||||
""";
|
||||
""".formatted(
|
||||
CurrentUsersSql.usersSubquery("u_actor"),
|
||||
CurrentUsersSql.usersSubquery("u_target")
|
||||
);
|
||||
|
||||
List<String> out = new ArrayList<>();
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
@@ -106,19 +111,19 @@ public final class ConnectionsStateDAO {
|
||||
String sql = """
|
||||
SELECT u.login AS friend_login
|
||||
FROM connections_state a
|
||||
JOIN solana_users u
|
||||
ON u.login = a.to_login COLLATE NOCASE
|
||||
WHERE a.login = ? COLLATE NOCASE
|
||||
JOIN %s
|
||||
ON LOWER(u.login) = LOWER(a.to_login)
|
||||
WHERE LOWER(a.login) = LOWER(?)
|
||||
AND a.rel_type = ?
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM connections_state b
|
||||
WHERE b.login = a.to_login COLLATE NOCASE
|
||||
AND b.to_login = a.login COLLATE NOCASE
|
||||
WHERE LOWER(b.login) = LOWER(a.to_login)
|
||||
AND LOWER(b.to_login) = LOWER(a.login)
|
||||
AND b.rel_type = a.rel_type
|
||||
)
|
||||
ORDER BY u.login
|
||||
""";
|
||||
""".formatted(CurrentUsersSql.usersSubquery("u"));
|
||||
|
||||
List<String> out = new ArrayList<>();
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
@@ -141,10 +146,22 @@ public final class ConnectionsStateDAO {
|
||||
String toBchName,
|
||||
Integer toBlockNumber,
|
||||
byte[] toBlockHash) throws SQLException {
|
||||
try (PreparedStatement deletePs = c.prepareStatement("""
|
||||
DELETE FROM connections_state
|
||||
WHERE LOWER(login) = LOWER(?)
|
||||
AND rel_type = ?
|
||||
AND LOWER(to_login) = LOWER(?)
|
||||
""")) {
|
||||
deletePs.setString(1, login);
|
||||
deletePs.setInt(2, relType);
|
||||
deletePs.setString(3, toLogin);
|
||||
deletePs.executeUpdate();
|
||||
}
|
||||
|
||||
String sql = """
|
||||
INSERT INTO connections_state (login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(login, rel_type, to_login) DO UPDATE SET
|
||||
ON CONFLICT(login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash) DO UPDATE SET
|
||||
to_bch_name=excluded.to_bch_name,
|
||||
to_block_number=excluded.to_block_number,
|
||||
to_block_hash=excluded.to_block_hash
|
||||
|
||||
+37
-61
@@ -1,19 +1,22 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.DbController;
|
||||
import shine.db.KeyEncodingUtil;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
import shine.db.sql.CurrentUsersSql;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* SolanaUsersDAO — локальная таблица пользователей из Solana.
|
||||
* CurrentUsersDAO — совместимый runtime-доступ к текущему срезу пользователей.
|
||||
*
|
||||
* Таблица: solana_users
|
||||
* Источник:
|
||||
* - solana_user_pda_current
|
||||
*
|
||||
* Колонки:
|
||||
* - login TEXT PRIMARY KEY (COLLATE NOCASE)
|
||||
* - login TEXT PRIMARY KEY
|
||||
* - blockchain_name TEXT NOT NULL
|
||||
* - solana_key TEXT NOT NULL
|
||||
* - blockchain_key TEXT NOT NULL
|
||||
@@ -23,59 +26,32 @@ import java.util.List;
|
||||
* - методы с Connection НЕ закрывают соединение
|
||||
* - методы без Connection сами открывают и закрывают соединение
|
||||
*/
|
||||
public final class SolanaUsersDAO {
|
||||
public final class CurrentUsersDAO {
|
||||
|
||||
private static volatile SolanaUsersDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private static volatile CurrentUsersDAO instance;
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private SolanaUsersDAO() {}
|
||||
private CurrentUsersDAO() {}
|
||||
|
||||
public static SolanaUsersDAO getInstance() {
|
||||
public static CurrentUsersDAO getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (SolanaUsersDAO.class) {
|
||||
if (instance == null) instance = new SolanaUsersDAO();
|
||||
synchronized (CurrentUsersDAO.class) {
|
||||
if (instance == null) instance = new CurrentUsersDAO();
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
// -------------------- INSERT --------------------
|
||||
|
||||
/** Вставка с внешним соединением. Соединение НЕ закрывает. */
|
||||
public void insert(Connection c, SolanaUserEntry user) throws SQLException {
|
||||
String sql = """
|
||||
INSERT INTO solana_users (
|
||||
login, blockchain_name, solana_key, blockchain_key, client_key
|
||||
) VALUES (?, ?, ?, ?, ?)
|
||||
""";
|
||||
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, user.getLogin());
|
||||
ps.setString(2, user.getBlockchainName());
|
||||
ps.setString(3, user.getSolanaKey());
|
||||
ps.setString(4, user.getBlockchainKey());
|
||||
ps.setString(5, user.getClientKey());
|
||||
ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/** Вставка без внешнего соединения. Сам открывает/закрывает. */
|
||||
public void insert(SolanaUserEntry user) throws SQLException {
|
||||
try (Connection c = db.getConnection()) {
|
||||
insert(c, user);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- EXISTS --------------------
|
||||
|
||||
/** Проверка существования по login (case-insensitive) с внешним соединением. Соединение НЕ закрывает. */
|
||||
public boolean existsByLogin(Connection c, String login) throws SQLException {
|
||||
String sql = """
|
||||
SELECT 1
|
||||
FROM solana_users
|
||||
FROM %s
|
||||
WHERE LOWER(login) = LOWER(?)
|
||||
LIMIT 1
|
||||
""";
|
||||
""".formatted(CurrentUsersSql.usersSubquery("su"));
|
||||
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, login);
|
||||
@@ -96,10 +72,10 @@ public final class SolanaUsersDAO {
|
||||
public boolean existsByBlockchainName(Connection c, String blockchainName) throws SQLException {
|
||||
String sql = """
|
||||
SELECT 1
|
||||
FROM solana_users
|
||||
FROM %s
|
||||
WHERE blockchain_name = ?
|
||||
LIMIT 1
|
||||
""";
|
||||
""".formatted(CurrentUsersSql.usersSubquery("su"));
|
||||
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, blockchainName);
|
||||
@@ -119,7 +95,7 @@ public final class SolanaUsersDAO {
|
||||
// -------------------- SELECT --------------------
|
||||
|
||||
/** Получить по login (case-insensitive) с внешним соединением. Соединение НЕ закрывает. */
|
||||
public SolanaUserEntry getByLogin(Connection c, String login) throws SQLException {
|
||||
public CurrentUserEntry getByLogin(Connection c, String login) throws SQLException {
|
||||
String sql = """
|
||||
SELECT
|
||||
login,
|
||||
@@ -127,9 +103,9 @@ public final class SolanaUsersDAO {
|
||||
solana_key,
|
||||
blockchain_key,
|
||||
client_key
|
||||
FROM solana_users
|
||||
FROM %s
|
||||
WHERE LOWER(login) = LOWER(?)
|
||||
""";
|
||||
""".formatted(CurrentUsersSql.usersSubquery("su"));
|
||||
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, login);
|
||||
@@ -141,14 +117,14 @@ public final class SolanaUsersDAO {
|
||||
}
|
||||
|
||||
/** Получить по login (case-insensitive) без внешнего соединения. Сам открывает/закрывает. */
|
||||
public SolanaUserEntry getByLogin(String login) throws SQLException {
|
||||
public CurrentUserEntry getByLogin(String login) throws SQLException {
|
||||
try (Connection c = db.getConnection()) {
|
||||
return getByLogin(c, login);
|
||||
}
|
||||
}
|
||||
|
||||
/** Получить по blockchain_name (case-sensitive) с внешним соединением. Соединение НЕ закрывает. */
|
||||
public SolanaUserEntry getByBlockchainName(Connection c, String blockchainName) throws SQLException {
|
||||
public CurrentUserEntry getByBlockchainName(Connection c, String blockchainName) throws SQLException {
|
||||
String sql = """
|
||||
SELECT
|
||||
login,
|
||||
@@ -156,9 +132,9 @@ public final class SolanaUsersDAO {
|
||||
solana_key,
|
||||
blockchain_key,
|
||||
client_key
|
||||
FROM solana_users
|
||||
FROM %s
|
||||
WHERE blockchain_name = ?
|
||||
""";
|
||||
""".formatted(CurrentUsersSql.usersSubquery("su"));
|
||||
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, blockchainName);
|
||||
@@ -170,14 +146,14 @@ public final class SolanaUsersDAO {
|
||||
}
|
||||
|
||||
/** Получить по blockchain_name без внешнего соединения. */
|
||||
public SolanaUserEntry getByBlockchainName(String blockchainName) throws SQLException {
|
||||
public CurrentUserEntry getByBlockchainName(String blockchainName) throws SQLException {
|
||||
try (Connection c = db.getConnection()) {
|
||||
return getByBlockchainName(c, blockchainName);
|
||||
}
|
||||
}
|
||||
|
||||
/** Поиск по префиксу с внешним соединением. Соединение НЕ закрывает. */
|
||||
public List<SolanaUserEntry> searchByLoginPrefix(Connection c, String prefix) throws SQLException {
|
||||
public List<CurrentUserEntry> searchByLoginPrefix(Connection c, String prefix) throws SQLException {
|
||||
String sql = """
|
||||
SELECT
|
||||
login,
|
||||
@@ -185,13 +161,13 @@ public final class SolanaUsersDAO {
|
||||
solana_key,
|
||||
blockchain_key,
|
||||
client_key
|
||||
FROM solana_users
|
||||
FROM %s
|
||||
WHERE LOWER(login) LIKE ?
|
||||
ORDER BY login
|
||||
LIMIT 5
|
||||
""";
|
||||
""".formatted(CurrentUsersSql.usersSubquery("su"));
|
||||
|
||||
List<SolanaUserEntry> result = new ArrayList<>();
|
||||
List<CurrentUserEntry> result = new ArrayList<>();
|
||||
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, prefix.toLowerCase() + "%");
|
||||
@@ -204,7 +180,7 @@ public final class SolanaUsersDAO {
|
||||
}
|
||||
|
||||
/** Поиск по префиксу без внешнего соединения. Сам открывает/закрывает. */
|
||||
public List<SolanaUserEntry> searchByLoginPrefix(String prefix) throws SQLException {
|
||||
public List<CurrentUserEntry> searchByLoginPrefix(String prefix) throws SQLException {
|
||||
try (Connection c = db.getConnection()) {
|
||||
return searchByLoginPrefix(c, prefix);
|
||||
}
|
||||
@@ -212,14 +188,14 @@ public final class SolanaUsersDAO {
|
||||
|
||||
// -------------------- MAPPER --------------------
|
||||
|
||||
private SolanaUserEntry mapRow(ResultSet rs) throws SQLException {
|
||||
SolanaUserEntry e = new SolanaUserEntry();
|
||||
private CurrentUserEntry mapRow(ResultSet rs) throws SQLException {
|
||||
CurrentUserEntry e = new CurrentUserEntry();
|
||||
|
||||
e.setLogin(rs.getString("login"));
|
||||
e.setBlockchainName(rs.getString("blockchain_name"));
|
||||
e.setSolanaKey(rs.getString("solana_key"));
|
||||
e.setBlockchainKey(rs.getString("blockchain_key"));
|
||||
e.setClientKey(rs.getString("client_key"));
|
||||
e.setSolanaKey(KeyEncodingUtil.normalizeKeyToBase64_32(rs.getString("solana_key")));
|
||||
e.setBlockchainKey(KeyEncodingUtil.normalizeKeyToBase64_32(rs.getString("blockchain_key")));
|
||||
e.setClientKey(KeyEncodingUtil.normalizeKeyToBase64_32(rs.getString("client_key")));
|
||||
|
||||
return e;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.entities.DirectMessageEntry;
|
||||
|
||||
import java.sql.Connection;
|
||||
@@ -8,7 +8,7 @@ import java.sql.PreparedStatement;
|
||||
|
||||
public final class DirectMessagesDAO {
|
||||
private static volatile DirectMessagesDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private DirectMessagesDAO() {}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.entities.EspPairingRequestEntry;
|
||||
|
||||
import java.sql.Connection;
|
||||
@@ -13,7 +13,7 @@ import java.util.List;
|
||||
public final class EspPairingRequestsDAO {
|
||||
|
||||
private static volatile EspPairingRequestsDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private EspPairingRequestsDAO() { }
|
||||
|
||||
@@ -110,7 +110,7 @@ public final class EspPairingRequestsDAO {
|
||||
payload_type, status, short_code, fingerprint_b58, encrypted_payload, reject_reason,
|
||||
approved_by_session_id, created_at_ms, expires_at_ms, updated_at_ms, delivered_to_homeserver
|
||||
FROM esp_pairing_requests
|
||||
WHERE login = ? COLLATE NOCASE
|
||||
WHERE LOWER(login) = LOWER(?)
|
||||
AND expires_at_ms > ?
|
||||
AND status = 'created'
|
||||
ORDER BY created_at_ms DESC
|
||||
@@ -131,7 +131,7 @@ public final class EspPairingRequestsDAO {
|
||||
StringBuilder sql = new StringBuilder("""
|
||||
SELECT COUNT(*)
|
||||
FROM esp_pairing_requests
|
||||
WHERE login = ? COLLATE NOCASE
|
||||
WHERE LOWER(login) = LOWER(?)
|
||||
AND created_at_ms >= ?
|
||||
AND status IN (
|
||||
""");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.entities.EspPairingSettingsEntry;
|
||||
|
||||
import java.sql.Connection;
|
||||
@@ -11,7 +11,7 @@ import java.sql.SQLException;
|
||||
public final class EspPairingSettingsDAO {
|
||||
|
||||
private static volatile EspPairingSettingsDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private EspPairingSettingsDAO() { }
|
||||
|
||||
@@ -74,7 +74,7 @@ public final class EspPairingSettingsDAO {
|
||||
String sql = """
|
||||
SELECT login, enabled, password_hash, ttl_seconds, failed_attempts, first_failed_at_ms, blocked_until_ms, updated_at_ms
|
||||
FROM esp_pairing_settings
|
||||
WHERE login = ? COLLATE NOCASE
|
||||
WHERE LOWER(login) = LOWER(?)
|
||||
LIMIT 1
|
||||
""";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.entities.IpGeoCacheEntry;
|
||||
|
||||
import java.sql.*;
|
||||
@@ -20,7 +20,7 @@ import java.sql.*;
|
||||
public final class IpGeoCacheDAO {
|
||||
|
||||
private static volatile IpGeoCacheDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private IpGeoCacheDAO() { }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.entities.PushTokenEntry;
|
||||
|
||||
import java.sql.Connection;
|
||||
@@ -11,7 +11,7 @@ import java.util.List;
|
||||
|
||||
public final class PushTokensDAO {
|
||||
private static volatile PushTokensDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private PushTokensDAO() {}
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.entities.SignedDirectMessageHistoryEntry;
|
||||
|
||||
import java.sql.Connection;
|
||||
@@ -8,7 +8,7 @@ import java.sql.PreparedStatement;
|
||||
|
||||
public final class SignedDirectMessagesHistoryDAO {
|
||||
private static volatile SignedDirectMessagesHistoryDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private SignedDirectMessagesHistoryDAO() {}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
|
||||
public final class SignedDmReplayDAO {
|
||||
private static volatile SignedDmReplayDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private SignedDmReplayDAO() {}
|
||||
|
||||
@@ -24,10 +24,11 @@ public final class SignedDmReplayDAO {
|
||||
cleanupExpired(nowMs - 15L * 60L * 1000L);
|
||||
try (Connection c = db.getConnection()) {
|
||||
String sql = """
|
||||
INSERT OR IGNORE INTO signed_direct_message_replay (
|
||||
from_login, time_ms, nonce, created_at_ms
|
||||
) VALUES (?, ?, ?, ?)
|
||||
""";
|
||||
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);
|
||||
|
||||
+88
-118
@@ -1,7 +1,7 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.entities.SignedMessageV2Entry;
|
||||
import shine.db.DbController;
|
||||
import shine.db.entities.SignedMessageEntry;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
@@ -11,9 +11,7 @@ import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class SignedMessagesV2DAO {
|
||||
private static final int SQLITE_BUSY_MAX_RETRIES = 6;
|
||||
private static final long SQLITE_BUSY_RETRY_BASE_DELAY_MS = 40L;
|
||||
public final class SignedMessagesDAO {
|
||||
|
||||
public enum ApplyStatus {
|
||||
APPLIED,
|
||||
@@ -26,33 +24,34 @@ public final class SignedMessagesV2DAO {
|
||||
}
|
||||
}
|
||||
|
||||
private static volatile SignedMessagesV2DAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private static volatile SignedMessagesDAO instance;
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private SignedMessagesV2DAO() {}
|
||||
private SignedMessagesDAO() {}
|
||||
|
||||
public static SignedMessagesV2DAO getInstance() {
|
||||
public static SignedMessagesDAO getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (SignedMessagesV2DAO.class) {
|
||||
if (instance == null) instance = new SignedMessagesV2DAO();
|
||||
synchronized (SignedMessagesDAO.class) {
|
||||
if (instance == null) instance = new SignedMessagesDAO();
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public ApplyStatus insertIfAbsent(SignedMessageV2Entry e) throws Exception {
|
||||
public ApplyStatus insertIfAbsent(SignedMessageEntry e) throws Exception {
|
||||
return withBusyRetry(() -> {
|
||||
try (Connection c = db.getConnection()) {
|
||||
if (isBlockedByConversationDelete(c, e.getFromLogin(), e.getToLogin(), e.getTimeMs())) {
|
||||
return ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE;
|
||||
}
|
||||
String sql = """
|
||||
INSERT OR IGNORE INTO signed_messages_v2 (
|
||||
INSERT INTO signed_messages (
|
||||
message_key, base_key, target_login, from_login, to_login,
|
||||
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
||||
raw_block, created_at_ms, source_api, origin_session_id,
|
||||
receipt_ref_base_key, receipt_ref_type, read_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT DO NOTHING
|
||||
""";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
bindSignedMessage(ps, e);
|
||||
@@ -66,7 +65,7 @@ public final class SignedMessagesV2DAO {
|
||||
});
|
||||
}
|
||||
|
||||
public boolean insertPairBothOrNothing(SignedMessageV2Entry first, SignedMessageV2Entry second) throws Exception {
|
||||
public boolean insertPairBothOrNothing(SignedMessageEntry first, SignedMessageEntry second) throws Exception {
|
||||
return withBusyRetry(() -> {
|
||||
try (Connection c = db.getConnection()) {
|
||||
boolean prevAutoCommit = c.getAutoCommit();
|
||||
@@ -95,7 +94,7 @@ public final class SignedMessagesV2DAO {
|
||||
});
|
||||
}
|
||||
|
||||
public ApplyStatus upsertContentPair(SignedMessageV2Entry incoming, SignedMessageV2Entry outgoing) throws Exception {
|
||||
public ApplyStatus upsertContentPair(SignedMessageEntry incoming, SignedMessageEntry outgoing) throws Exception {
|
||||
return withBusyRetry(() -> {
|
||||
try (Connection c = db.getConnection()) {
|
||||
boolean prevAutoCommit = c.getAutoCommit();
|
||||
@@ -136,7 +135,7 @@ public final class SignedMessagesV2DAO {
|
||||
});
|
||||
}
|
||||
|
||||
public ApplyStatus upsertIncomingCopy(SignedMessageV2Entry incoming) throws Exception {
|
||||
public ApplyStatus upsertIncomingCopy(SignedMessageEntry incoming) throws Exception {
|
||||
return withBusyRetry(() -> {
|
||||
try (Connection c = db.getConnection()) {
|
||||
boolean prevAutoCommit = c.getAutoCommit();
|
||||
@@ -173,7 +172,7 @@ public final class SignedMessagesV2DAO {
|
||||
});
|
||||
}
|
||||
|
||||
public ApplyStatus applyDeleteMessage(SignedMessageV2Entry tombstone) throws Exception {
|
||||
public ApplyStatus applyDeleteMessage(SignedMessageEntry tombstone) throws Exception {
|
||||
return withBusyRetry(() -> {
|
||||
try (Connection c = db.getConnection()) {
|
||||
boolean prevAutoCommit = c.getAutoCommit();
|
||||
@@ -204,7 +203,7 @@ public final class SignedMessagesV2DAO {
|
||||
});
|
||||
}
|
||||
|
||||
public ApplyStatus applyDeleteConversation(SignedMessageV2Entry tombstone) throws Exception {
|
||||
public ApplyStatus applyDeleteConversation(SignedMessageEntry tombstone) throws Exception {
|
||||
return withBusyRetry(() -> {
|
||||
try (Connection c = db.getConnection()) {
|
||||
boolean prevAutoCommit = c.getAutoCommit();
|
||||
@@ -232,7 +231,7 @@ public final class SignedMessagesV2DAO {
|
||||
});
|
||||
}
|
||||
|
||||
public SignedMessageV2Entry getByMessageKey(String messageKey) throws Exception {
|
||||
public SignedMessageEntry getByMessageKey(String messageKey) throws Exception {
|
||||
try (Connection c = db.getConnection()) {
|
||||
String sql = """
|
||||
SELECT
|
||||
@@ -240,9 +239,9 @@ public final class SignedMessagesV2DAO {
|
||||
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
||||
raw_block, created_at_ms, source_api, origin_session_id,
|
||||
receipt_ref_base_key, receipt_ref_type, read_at_ms
|
||||
FROM signed_messages_v2
|
||||
FROM %s
|
||||
WHERE message_key = ?
|
||||
""";
|
||||
""".formatted(messagesTable());
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, messageKey);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
@@ -253,7 +252,7 @@ public final class SignedMessagesV2DAO {
|
||||
}
|
||||
}
|
||||
|
||||
public SignedMessageV2Entry getLatestConversationDelete(String fromLogin, String toLogin) throws Exception {
|
||||
public SignedMessageEntry getLatestConversationDelete(String fromLogin, String toLogin) throws Exception {
|
||||
try (Connection c = db.getConnection()) {
|
||||
return getLatestConversationDelete(c, fromLogin, toLogin);
|
||||
}
|
||||
@@ -268,9 +267,10 @@ public final class SignedMessagesV2DAO {
|
||||
withBusyRetry(() -> {
|
||||
try (Connection c = db.getConnection()) {
|
||||
String sql = """
|
||||
INSERT OR IGNORE INTO signed_message_session_delivery (
|
||||
INSERT INTO signed_message_session_delivery (
|
||||
message_key, session_id, delivered, delivered_at_ms, created_at_ms
|
||||
) VALUES (?, ?, 0, NULL, ?)
|
||||
ON CONFLICT DO NOTHING
|
||||
""";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
for (String sessionId : sessionIds) {
|
||||
@@ -314,22 +314,23 @@ public final class SignedMessagesV2DAO {
|
||||
});
|
||||
}
|
||||
|
||||
public List<SignedMessageV2Entry> listPendingForSession(String login, String sessionId) throws Exception {
|
||||
public List<SignedMessageEntry> listPendingForSession(String login, String sessionId) throws Exception {
|
||||
return withBusyRetry(() -> {
|
||||
try (Connection c = db.getConnection()) {
|
||||
String fillSql = """
|
||||
INSERT OR IGNORE INTO signed_message_session_delivery (
|
||||
INSERT INTO signed_message_session_delivery (
|
||||
message_key, session_id, delivered, delivered_at_ms, created_at_ms
|
||||
)
|
||||
SELECT m.message_key, ?, 0, NULL, ?
|
||||
FROM signed_messages_v2 m
|
||||
FROM %s m
|
||||
WHERE (
|
||||
(m.message_type IN (1, 3) AND m.to_login = ? COLLATE NOCASE)
|
||||
OR (m.message_type IN (2, 4) AND m.from_login = ? COLLATE NOCASE)
|
||||
(m.message_type IN (1, 3) AND LOWER(m.to_login) = LOWER(?))
|
||||
OR (m.message_type IN (2, 4) AND LOWER(m.from_login) = LOWER(?))
|
||||
OR (m.message_type IN (5, 6, 7, 8)
|
||||
AND (m.from_login = ? COLLATE NOCASE OR m.to_login = ? COLLATE NOCASE))
|
||||
AND (LOWER(m.from_login) = LOWER(?) OR LOWER(m.to_login) = LOWER(?)))
|
||||
)
|
||||
""";
|
||||
ON CONFLICT DO NOTHING
|
||||
""".formatted(messagesTable());
|
||||
long now = System.currentTimeMillis();
|
||||
try (PreparedStatement ps = c.prepareStatement(fillSql)) {
|
||||
ps.setString(1, sessionId);
|
||||
@@ -347,13 +348,13 @@ public final class SignedMessagesV2DAO {
|
||||
m.time_ms, m.nonce, m.message_type, m.revision_time_ms, m.reencrypted_at_ms,
|
||||
m.raw_block, m.created_at_ms, m.source_api, m.origin_session_id,
|
||||
m.receipt_ref_base_key, m.receipt_ref_type, m.read_at_ms
|
||||
FROM signed_messages_v2 m
|
||||
FROM %s m
|
||||
JOIN signed_message_session_delivery d
|
||||
ON d.message_key = m.message_key
|
||||
WHERE d.session_id = ? AND d.delivered = 0
|
||||
ORDER BY m.time_ms ASC, m.revision_time_ms ASC, m.reencrypted_at_ms ASC, m.created_at_ms ASC
|
||||
""";
|
||||
List<SignedMessageV2Entry> out = new ArrayList<>();
|
||||
""".formatted(messagesTable());
|
||||
List<SignedMessageEntry> out = new ArrayList<>();
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, sessionId);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
@@ -365,7 +366,7 @@ public final class SignedMessagesV2DAO {
|
||||
});
|
||||
}
|
||||
|
||||
public List<SignedMessageV2Entry> listConversationPage(
|
||||
public List<SignedMessageEntry> listConversationPage(
|
||||
String login,
|
||||
String peerLogin,
|
||||
long beforeTimeMs,
|
||||
@@ -379,12 +380,12 @@ public final class SignedMessagesV2DAO {
|
||||
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
||||
raw_block, created_at_ms, source_api, origin_session_id,
|
||||
receipt_ref_base_key, receipt_ref_type, read_at_ms
|
||||
FROM signed_messages_v2
|
||||
WHERE target_login = ? COLLATE NOCASE
|
||||
FROM %s
|
||||
WHERE LOWER(target_login) = LOWER(?)
|
||||
AND message_type IN (1, 2)
|
||||
AND (
|
||||
(from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
||||
OR (from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
||||
(LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||
)
|
||||
AND (
|
||||
? <= 0
|
||||
@@ -393,8 +394,8 @@ public final class SignedMessagesV2DAO {
|
||||
)
|
||||
ORDER BY time_ms DESC, message_key DESC
|
||||
LIMIT ?
|
||||
""";
|
||||
List<SignedMessageV2Entry> out = new ArrayList<>();
|
||||
""".formatted(messagesTable());
|
||||
List<SignedMessageEntry> out = new ArrayList<>();
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, login);
|
||||
ps.setString(2, login);
|
||||
@@ -415,9 +416,9 @@ public final class SignedMessagesV2DAO {
|
||||
}
|
||||
}
|
||||
|
||||
private void upsertMessage(Connection c, SignedMessageV2Entry e) throws SQLException {
|
||||
private void upsertMessage(Connection c, SignedMessageEntry e) throws SQLException {
|
||||
String sql = """
|
||||
INSERT INTO signed_messages_v2 (
|
||||
INSERT INTO %s (
|
||||
message_key, base_key, target_login, from_login, to_login,
|
||||
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
||||
raw_block, created_at_ms, source_api, origin_session_id,
|
||||
@@ -439,15 +440,15 @@ public final class SignedMessagesV2DAO {
|
||||
origin_session_id = excluded.origin_session_id,
|
||||
receipt_ref_base_key = excluded.receipt_ref_base_key,
|
||||
receipt_ref_type = excluded.receipt_ref_type,
|
||||
read_at_ms = COALESCE(signed_messages_v2.read_at_ms, excluded.read_at_ms)
|
||||
""";
|
||||
read_at_ms = COALESCE(%s.read_at_ms, excluded.read_at_ms)
|
||||
""".formatted(messagesTable(), messagesTable());
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
bindSignedMessage(ps, e);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private void markMessageReadByReceipt(Connection c, SignedMessageV2Entry entry) throws SQLException {
|
||||
private void markMessageReadByReceipt(Connection c, SignedMessageEntry entry) throws SQLException {
|
||||
if (entry == null) return;
|
||||
int messageType = entry.getMessageType();
|
||||
if (messageType != 3 && messageType != 4) return;
|
||||
@@ -456,7 +457,7 @@ public final class SignedMessagesV2DAO {
|
||||
long readAtMs = entry.getTimeMs();
|
||||
if (readAtMs <= 0) return;
|
||||
try (PreparedStatement ps = c.prepareStatement("""
|
||||
UPDATE signed_messages_v2
|
||||
UPDATE %s
|
||||
SET read_at_ms = CASE
|
||||
WHEN read_at_ms IS NULL OR read_at_ms <= 0 THEN ?
|
||||
WHEN read_at_ms > ? THEN ?
|
||||
@@ -464,7 +465,7 @@ public final class SignedMessagesV2DAO {
|
||||
END
|
||||
WHERE base_key = ?
|
||||
AND message_type IN (1, 2)
|
||||
""")) {
|
||||
""".formatted(messagesTable()))) {
|
||||
ps.setLong(1, readAtMs);
|
||||
ps.setLong(2, readAtMs);
|
||||
ps.setLong(3, readAtMs);
|
||||
@@ -476,10 +477,10 @@ public final class SignedMessagesV2DAO {
|
||||
private RevisionMarker getRevisionMarkerByMessageKey(Connection c, String messageKey) throws SQLException {
|
||||
String sql = """
|
||||
SELECT revision_time_ms, reencrypted_at_ms
|
||||
FROM signed_messages_v2
|
||||
FROM %s
|
||||
WHERE message_key = ?
|
||||
LIMIT 1
|
||||
""";
|
||||
""".formatted(messagesTable());
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, messageKey);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
@@ -492,12 +493,12 @@ public final class SignedMessagesV2DAO {
|
||||
private RevisionMarker getCurrentContentMarker(Connection c, String baseKey) throws SQLException {
|
||||
String sql = """
|
||||
SELECT revision_time_ms, reencrypted_at_ms
|
||||
FROM signed_messages_v2
|
||||
FROM %s
|
||||
WHERE base_key = ?
|
||||
AND message_type IN (1, 2)
|
||||
ORDER BY revision_time_ms DESC, reencrypted_at_ms DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
""".formatted(messagesTable());
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, baseKey);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
@@ -510,11 +511,11 @@ public final class SignedMessagesV2DAO {
|
||||
private boolean hasMessageDeleteTombstone(Connection c, String baseKey) throws SQLException {
|
||||
String sql = """
|
||||
SELECT 1
|
||||
FROM signed_messages_v2
|
||||
FROM %s
|
||||
WHERE base_key = ?
|
||||
AND message_type IN (5, 6)
|
||||
LIMIT 1
|
||||
""";
|
||||
""".formatted(messagesTable());
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, baseKey);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
@@ -531,13 +532,13 @@ public final class SignedMessagesV2DAO {
|
||||
private Long getLatestConversationDeleteBoundary(Connection c, String fromLogin, String toLogin) throws SQLException {
|
||||
String sql = """
|
||||
SELECT MAX(time_ms)
|
||||
FROM signed_messages_v2
|
||||
FROM %s
|
||||
WHERE message_type IN (7, 8)
|
||||
AND (
|
||||
(from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
||||
OR (from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
||||
(LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||
)
|
||||
""";
|
||||
""".formatted(messagesTable());
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, fromLogin);
|
||||
ps.setString(2, toLogin);
|
||||
@@ -551,22 +552,22 @@ public final class SignedMessagesV2DAO {
|
||||
}
|
||||
}
|
||||
|
||||
private SignedMessageV2Entry getLatestConversationDelete(Connection c, String fromLogin, String toLogin) throws Exception {
|
||||
private SignedMessageEntry getLatestConversationDelete(Connection c, String fromLogin, String toLogin) throws Exception {
|
||||
String sql = """
|
||||
SELECT
|
||||
message_key, base_key, target_login, from_login, to_login,
|
||||
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
||||
raw_block, created_at_ms, source_api, origin_session_id,
|
||||
receipt_ref_base_key, receipt_ref_type
|
||||
FROM signed_messages_v2
|
||||
receipt_ref_base_key, receipt_ref_type, read_at_ms
|
||||
FROM %s
|
||||
WHERE message_type IN (7, 8)
|
||||
AND (
|
||||
(from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
||||
OR (from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
||||
(LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||
)
|
||||
ORDER BY time_ms DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
""".formatted(messagesTable());
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, fromLogin);
|
||||
ps.setString(2, toLogin);
|
||||
@@ -582,16 +583,16 @@ public final class SignedMessagesV2DAO {
|
||||
private void deleteMessageContentAndReceipts(Connection c, String baseKey) throws SQLException {
|
||||
deleteDeliveryRowsByMessageSelection(c, """
|
||||
SELECT message_key
|
||||
FROM signed_messages_v2
|
||||
FROM %s
|
||||
WHERE (base_key = ? AND message_type IN (1, 2))
|
||||
OR (receipt_ref_base_key = ? AND message_type IN (3, 4))
|
||||
""", baseKey, baseKey);
|
||||
""".formatted(messagesTable()), baseKey, baseKey);
|
||||
|
||||
try (PreparedStatement ps = c.prepareStatement("""
|
||||
DELETE FROM signed_messages_v2
|
||||
DELETE FROM %s
|
||||
WHERE (base_key = ? AND message_type IN (1, 2))
|
||||
OR (receipt_ref_base_key = ? AND message_type IN (3, 4))
|
||||
""")) {
|
||||
""".formatted(messagesTable()))) {
|
||||
ps.setString(1, baseKey);
|
||||
ps.setString(2, baseKey);
|
||||
ps.executeUpdate();
|
||||
@@ -601,22 +602,22 @@ public final class SignedMessagesV2DAO {
|
||||
private void deleteConversationHistoryBefore(Connection c, String fromLogin, String toLogin, long boundaryTimeMs) throws SQLException {
|
||||
deleteDeliveryRowsByMessageSelection(c, """
|
||||
SELECT message_key
|
||||
FROM signed_messages_v2
|
||||
FROM %s
|
||||
WHERE time_ms < ?
|
||||
AND (
|
||||
(from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
||||
OR (from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
||||
(LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||
)
|
||||
""", boundaryTimeMs, fromLogin, toLogin, toLogin, fromLogin);
|
||||
""".formatted(messagesTable()), boundaryTimeMs, fromLogin, toLogin, toLogin, fromLogin);
|
||||
|
||||
try (PreparedStatement ps = c.prepareStatement("""
|
||||
DELETE FROM signed_messages_v2
|
||||
DELETE FROM %s
|
||||
WHERE time_ms < ?
|
||||
AND (
|
||||
(from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
||||
OR (from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
||||
(LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||
)
|
||||
""")) {
|
||||
""".formatted(messagesTable()))) {
|
||||
ps.setLong(1, boundaryTimeMs);
|
||||
ps.setString(2, fromLogin);
|
||||
ps.setString(3, toLogin);
|
||||
@@ -645,22 +646,22 @@ public final class SignedMessagesV2DAO {
|
||||
}
|
||||
}
|
||||
|
||||
private int insertStrict(Connection c, SignedMessageV2Entry e) throws SQLException {
|
||||
private int insertStrict(Connection c, SignedMessageEntry e) throws SQLException {
|
||||
String sql = """
|
||||
INSERT INTO signed_messages_v2 (
|
||||
INSERT INTO %s (
|
||||
message_key, base_key, target_login, from_login, to_login,
|
||||
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
||||
raw_block, created_at_ms, source_api, origin_session_id,
|
||||
receipt_ref_base_key, receipt_ref_type, read_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""";
|
||||
""".formatted(messagesTable());
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
bindSignedMessage(ps, e);
|
||||
return ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private void bindSignedMessage(PreparedStatement ps, SignedMessageV2Entry e) throws SQLException {
|
||||
private void bindSignedMessage(PreparedStatement ps, SignedMessageEntry e) throws SQLException {
|
||||
ps.setString(1, e.getMessageKey());
|
||||
ps.setString(2, e.getBaseKey());
|
||||
ps.setString(3, e.getTargetLogin());
|
||||
@@ -703,43 +704,8 @@ public final class SignedMessagesV2DAO {
|
||||
return msg.contains("constraint") || msg.contains("unique") || msg.contains("primary key");
|
||||
}
|
||||
|
||||
private boolean isBusyLock(SQLException ex) {
|
||||
Throwable current = ex;
|
||||
while (current != null) {
|
||||
String msg = String.valueOf(current.getMessage()).toLowerCase();
|
||||
if (msg.contains("sqlite_busy") || msg.contains("database is locked") || msg.contains("database table is locked")) {
|
||||
return true;
|
||||
}
|
||||
current = current.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void sleepBeforeBusyRetry(int attempt) throws SQLException {
|
||||
long delayMs = SQLITE_BUSY_RETRY_BASE_DELAY_MS * (1L << Math.min(attempt, 4));
|
||||
try {
|
||||
Thread.sleep(delayMs);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
SQLException sqlEx = new SQLException("Interrupted while retrying SQLite busy lock", ie);
|
||||
throw sqlEx;
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T withBusyRetry(SqlWork<T> work) throws Exception {
|
||||
SQLException lastBusy = null;
|
||||
for (int attempt = 0; attempt < SQLITE_BUSY_MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
return work.run();
|
||||
} catch (SQLException ex) {
|
||||
if (!isBusyLock(ex) || attempt >= SQLITE_BUSY_MAX_RETRIES - 1) {
|
||||
throw ex;
|
||||
}
|
||||
lastBusy = ex;
|
||||
sleepBeforeBusyRetry(attempt);
|
||||
}
|
||||
}
|
||||
throw lastBusy == null ? new SQLException("SQLite busy retry failed") : lastBusy;
|
||||
return work.run();
|
||||
}
|
||||
|
||||
private int compareMarkers(RevisionMarker left, RevisionMarker right) {
|
||||
@@ -748,8 +714,12 @@ public final class SignedMessagesV2DAO {
|
||||
return Long.compare(left.reencryptedAtMs, right.reencryptedAtMs);
|
||||
}
|
||||
|
||||
private SignedMessageV2Entry mapRow(ResultSet rs) throws Exception {
|
||||
SignedMessageV2Entry e = new SignedMessageV2Entry();
|
||||
private String messagesTable() {
|
||||
return "signed_messages";
|
||||
}
|
||||
|
||||
private SignedMessageEntry mapRow(ResultSet rs) throws Exception {
|
||||
SignedMessageEntry e = new SignedMessageEntry();
|
||||
e.setMessageKey(rs.getString("message_key"));
|
||||
e.setBaseKey(rs.getString("base_key"));
|
||||
e.setTargetLogin(rs.getString("target_login"));
|
||||
@@ -773,7 +743,7 @@ public final class SignedMessagesV2DAO {
|
||||
}
|
||||
|
||||
private record RevisionMarker(long revisionTimeMs, long reencryptedAtMs) {
|
||||
private static RevisionMarker of(SignedMessageV2Entry entry) {
|
||||
private static RevisionMarker of(SignedMessageEntry entry) {
|
||||
return new RevisionMarker(entry.getRevisionTimeMs(), entry.getReencryptedAtMs());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.DbController;
|
||||
import shine.db.KeyEncodingUtil;
|
||||
import shine.db.entities.SolanaUserPdaCurrentEntry;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Доступ к актуальной PostgreSQL-таблице пользователей, поддерживаемой sync-модулем.
|
||||
*/
|
||||
public final class SolanaUserPdaCurrentDAO {
|
||||
|
||||
private static volatile SolanaUserPdaCurrentDAO instance;
|
||||
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private SolanaUserPdaCurrentDAO() {}
|
||||
|
||||
public static SolanaUserPdaCurrentDAO getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (SolanaUserPdaCurrentDAO.class) {
|
||||
if (instance == null) {
|
||||
instance = new SolanaUserPdaCurrentDAO();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public SolanaUserPdaCurrentEntry getByBlockchainName(String blockchainName) throws SQLException {
|
||||
try (Connection c = db.getConnection()) {
|
||||
return getByBlockchainName(c, blockchainName);
|
||||
}
|
||||
}
|
||||
|
||||
public SolanaUserPdaCurrentEntry getByBlockchainName(Connection c, String blockchainName) throws SQLException {
|
||||
String sql = """
|
||||
SELECT
|
||||
login,
|
||||
blockchain_name,
|
||||
blockchain_key,
|
||||
paid_limit_bytes
|
||||
FROM solana_user_pda_current
|
||||
WHERE blockchain_name = ?
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, blockchainName);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (!rs.next()) {
|
||||
return null;
|
||||
}
|
||||
return mapRow(rs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SolanaUserPdaCurrentEntry mapRow(ResultSet rs) throws SQLException {
|
||||
SolanaUserPdaCurrentEntry entry = new SolanaUserPdaCurrentEntry();
|
||||
entry.setLogin(rs.getString("login"));
|
||||
entry.setBlockchainName(rs.getString("blockchain_name"));
|
||||
entry.setBlockchainKey(KeyEncodingUtil.normalizeKeyToBase64_32(rs.getString("blockchain_key")));
|
||||
entry.setPaidLimitBytes(rs.getLong("paid_limit_bytes"));
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.MsgSubType;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.ArrayList;
|
||||
@@ -25,7 +25,7 @@ import java.util.List;
|
||||
public final class SubscriptionsDAO {
|
||||
|
||||
private static volatile SubscriptionsDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private SubscriptionsDAO() {}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.entities.SyncServerEntry;
|
||||
|
||||
import java.sql.Connection;
|
||||
@@ -17,7 +17,7 @@ import java.util.List;
|
||||
public final class SyncServersDAO {
|
||||
|
||||
private static volatile SyncServersDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private SyncServersDAO() {}
|
||||
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.entities.TestFreeAvatarUploadEntry;
|
||||
|
||||
import java.sql.Connection;
|
||||
@@ -11,7 +11,7 @@ import java.sql.SQLException;
|
||||
public final class TestFreeAvatarUploadsDAO {
|
||||
|
||||
private static volatile TestFreeAvatarUploadsDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private TestFreeAvatarUploadsDAO() {
|
||||
}
|
||||
@@ -29,7 +29,7 @@ public final class TestFreeAvatarUploadsDAO {
|
||||
String sql = """
|
||||
SELECT login, used_count, updated_at_ms, last_tx_id
|
||||
FROM test_free_avatar_uploads
|
||||
WHERE login = ? COLLATE NOCASE
|
||||
WHERE LOWER(login) = LOWER(?)
|
||||
LIMIT 1
|
||||
""";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
|
||||
import java.sql.*;
|
||||
|
||||
/**
|
||||
* UserCreateDAO — атомарное добавление пользователя:
|
||||
* - solana_users (login, blockchain_name, solana_key, blockchain_key, client_key)
|
||||
* - blockchain_state (blockchain_name, login, blockchain_key, size_limit, ... last_block_number=-1 ...)
|
||||
*
|
||||
* ВАЖНО:
|
||||
* - только INSERT (без перезаписи существующих записей)
|
||||
* - если login или blockchainName заняты — возвращаем false (пользователь уже есть/занято)
|
||||
*/
|
||||
public final class UserCreateDAO {
|
||||
|
||||
private static volatile UserCreateDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
|
||||
|
||||
private UserCreateDAO() {}
|
||||
|
||||
public static UserCreateDAO getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (UserCreateDAO.class) {
|
||||
if (instance == null) instance = new UserCreateDAO();
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true если добавили; false если занято (login уже есть или blockchainName уже существует).
|
||||
*/
|
||||
public boolean insertUserWithBlockchain(
|
||||
String login,
|
||||
String blockchainName,
|
||||
String solanaKey,
|
||||
String blockchainKey,
|
||||
String clientKey,
|
||||
long sizeLimit,
|
||||
long nowMs
|
||||
) throws SQLException {
|
||||
|
||||
try (Connection c = db.getConnection()) {
|
||||
boolean oldAuto = c.getAutoCommit();
|
||||
c.setAutoCommit(false);
|
||||
|
||||
try {
|
||||
// 1) solana_users
|
||||
SolanaUserEntry u = new SolanaUserEntry();
|
||||
u.setLogin(login);
|
||||
u.setBlockchainName(blockchainName);
|
||||
u.setSolanaKey(solanaKey);
|
||||
u.setBlockchainKey(blockchainKey);
|
||||
u.setClientKey(clientKey);
|
||||
|
||||
usersDao.insert(c, u); // если login занят (NOCASE) или blockchainName (unique) -> constraint
|
||||
|
||||
// 2) blockchain_state — строго INSERT, без UPSERT (иначе можно перезаписать существующую цепочку)
|
||||
insertBlockchainStateStrict(
|
||||
c,
|
||||
blockchainName,
|
||||
login,
|
||||
blockchainKey,
|
||||
sizeLimit,
|
||||
nowMs
|
||||
);
|
||||
|
||||
c.commit();
|
||||
return true;
|
||||
|
||||
} catch (SQLException e) {
|
||||
c.rollback();
|
||||
|
||||
String msg = e.getMessage() == null ? "" : e.getMessage().toLowerCase();
|
||||
if (msg.contains("constraint")) {
|
||||
return false;
|
||||
}
|
||||
throw e;
|
||||
|
||||
} finally {
|
||||
c.setAutoCommit(oldAuto);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void insertBlockchainStateStrict(
|
||||
Connection c,
|
||||
String blockchainName,
|
||||
String login,
|
||||
String blockchainKey,
|
||||
long sizeLimit,
|
||||
long nowMs
|
||||
) throws SQLException {
|
||||
|
||||
String sql = """
|
||||
INSERT INTO blockchain_state (
|
||||
blockchain_name,
|
||||
login,
|
||||
blockchain_key,
|
||||
size_limit,
|
||||
file_size_bytes,
|
||||
last_block_number,
|
||||
last_block_hash,
|
||||
updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""";
|
||||
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
int i = 1;
|
||||
ps.setString(i++, blockchainName);
|
||||
ps.setString(i++, login);
|
||||
ps.setString(i++, blockchainKey);
|
||||
|
||||
ps.setLong(i++, sizeLimit);
|
||||
ps.setLong(i++, 0L);
|
||||
|
||||
ps.setInt(i++, -1);
|
||||
ps.setNull(i++, Types.BLOB); // старт: блоков ещё нет
|
||||
ps.setLong(i++, nowMs);
|
||||
|
||||
ps.executeUpdate(); // если blockchainName занят -> constraint (PK)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.entities.UserParamEntry;
|
||||
|
||||
import java.sql.*;
|
||||
@@ -21,7 +21,7 @@ import java.util.List;
|
||||
public final class UserParamsDAO {
|
||||
|
||||
private static volatile UserParamsDAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private UserParamsDAO() { }
|
||||
|
||||
@@ -89,7 +89,7 @@ public final class UserParamsDAO {
|
||||
client_key,
|
||||
signature
|
||||
FROM users_params
|
||||
WHERE login = ? COLLATE NOCASE AND param = ?
|
||||
WHERE LOWER(login) = LOWER(?) AND param = ?
|
||||
LIMIT 1
|
||||
""";
|
||||
|
||||
@@ -120,7 +120,7 @@ public final class UserParamsDAO {
|
||||
client_key,
|
||||
signature
|
||||
FROM users_params
|
||||
WHERE login = ? COLLATE NOCASE
|
||||
WHERE LOWER(login) = LOWER(?)
|
||||
ORDER BY time_ms DESC
|
||||
""";
|
||||
|
||||
|
||||
+7
-6
@@ -3,18 +3,19 @@ package shine.db.entities;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* SolanaUserEntry — локальная запись пользователя из Solana.
|
||||
* CurrentUserEntry — локальная runtime-проекция текущего состояния пользователя.
|
||||
*
|
||||
* Таблица: solana_users
|
||||
* Источник:
|
||||
* - solana_user_pda_current
|
||||
*
|
||||
* Поля:
|
||||
* - login — PRIMARY KEY (TEXT) (case-insensitive на уровне COLLATE NOCASE)
|
||||
* - login — PRIMARY KEY (TEXT)
|
||||
* - blockchain_name — TEXT NOT NULL
|
||||
* - solana_key — TEXT NOT NULL
|
||||
* - blockchain_key — TEXT NOT NULL
|
||||
* - client_key — TEXT NOT NULL
|
||||
*/
|
||||
public class SolanaUserEntry {
|
||||
public class CurrentUserEntry {
|
||||
|
||||
private String login;
|
||||
|
||||
@@ -29,9 +30,9 @@ public class SolanaUserEntry {
|
||||
/** Ключ устройства (публичный ключ устройства) */
|
||||
private String clientKey;
|
||||
|
||||
public SolanaUserEntry() {}
|
||||
public CurrentUserEntry() {}
|
||||
|
||||
public SolanaUserEntry(String login,
|
||||
public CurrentUserEntry(String login,
|
||||
String blockchainName,
|
||||
String solanaKey,
|
||||
String blockchainKey,
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
package shine.db.entities;
|
||||
|
||||
public class SignedMessageV2Entry {
|
||||
public class SignedMessageEntry {
|
||||
private String messageKey;
|
||||
private String baseKey;
|
||||
private String targetLogin;
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package shine.db.entities;
|
||||
|
||||
/**
|
||||
* Минимальный срез текущей записи пользователя из PostgreSQL sync-таблицы.
|
||||
*
|
||||
* Источник: solana_user_pda_current
|
||||
*/
|
||||
public final class SolanaUserPdaCurrentEntry {
|
||||
|
||||
private String login;
|
||||
private String blockchainName;
|
||||
private String blockchainKey;
|
||||
private long paidLimitBytes;
|
||||
|
||||
public String getLogin() {
|
||||
return login;
|
||||
}
|
||||
|
||||
public void setLogin(String login) {
|
||||
this.login = login;
|
||||
}
|
||||
|
||||
public String getBlockchainName() {
|
||||
return blockchainName;
|
||||
}
|
||||
|
||||
public void setBlockchainName(String blockchainName) {
|
||||
this.blockchainName = blockchainName;
|
||||
}
|
||||
|
||||
public String getBlockchainKey() {
|
||||
return blockchainKey;
|
||||
}
|
||||
|
||||
public void setBlockchainKey(String blockchainKey) {
|
||||
this.blockchainKey = blockchainKey;
|
||||
}
|
||||
|
||||
public long getPaidLimitBytes() {
|
||||
return paidLimitBytes;
|
||||
}
|
||||
|
||||
public void setPaidLimitBytes(long paidLimitBytes) {
|
||||
this.paidLimitBytes = paidLimitBytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package shine.db.sql;
|
||||
|
||||
public final class CurrentUsersSql {
|
||||
|
||||
private CurrentUsersSql() {}
|
||||
|
||||
public static String usersSubquery(String alias) {
|
||||
if (alias == null || alias.isBlank()) {
|
||||
throw new IllegalArgumentException("alias is blank");
|
||||
}
|
||||
return """
|
||||
(
|
||||
SELECT
|
||||
current_users.login AS login,
|
||||
current_users.blockchain_name AS blockchain_name,
|
||||
current_users.client_key AS solana_key,
|
||||
current_users.blockchain_key AS blockchain_key,
|
||||
current_users.client_key AS client_key
|
||||
FROM solana_user_pda_current current_users
|
||||
) %s
|
||||
""".formatted(alias);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_access_servers_current (
|
||||
user_login TEXT NOT NULL REFERENCES solana_user_pda_current(login) ON DELETE CASCADE,
|
||||
server_login TEXT NOT NULL REFERENCES solana_user_pda_current(login) ON DELETE CASCADE,
|
||||
server_url TEXT NOT NULL,
|
||||
server_client_key TEXT NOT NULL,
|
||||
user_record_number INTEGER NOT NULL,
|
||||
user_updated_at_ms BIGINT NOT NULL,
|
||||
server_record_number INTEGER NOT NULL,
|
||||
server_updated_at_ms BIGINT NOT NULL,
|
||||
refreshed_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (user_login, server_login)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_access_servers_user
|
||||
ON user_access_servers_current(user_login);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_access_servers_server
|
||||
ON user_access_servers_current(server_login);
|
||||
|
||||
CREATE OR REPLACE FUNCTION shine_refresh_user_access_servers_for_user(p_user_login TEXT)
|
||||
RETURNS VOID
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF p_user_login IS NULL OR btrim(p_user_login) = '' THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
DELETE FROM user_access_servers_current
|
||||
WHERE LOWER(user_login) = LOWER(p_user_login);
|
||||
|
||||
INSERT INTO user_access_servers_current (
|
||||
user_login,
|
||||
server_login,
|
||||
server_url,
|
||||
server_client_key,
|
||||
user_record_number,
|
||||
user_updated_at_ms,
|
||||
server_record_number,
|
||||
server_updated_at_ms,
|
||||
refreshed_at_ms
|
||||
)
|
||||
SELECT
|
||||
u.login,
|
||||
s.login,
|
||||
s.server_address,
|
||||
s.client_key,
|
||||
u.record_number,
|
||||
u.updated_at_ms,
|
||||
s.record_number,
|
||||
s.updated_at_ms,
|
||||
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||
FROM solana_user_pda_current u
|
||||
CROSS JOIN LATERAL jsonb_array_elements_text(
|
||||
CASE
|
||||
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||
ELSE u.access_servers_json::jsonb
|
||||
END
|
||||
) AS access_server(login_value)
|
||||
JOIN solana_user_pda_current s
|
||||
ON LOWER(s.login) = LOWER(btrim(access_server.login_value))
|
||||
AND s.is_server = TRUE
|
||||
AND btrim(COALESCE(s.server_address, '')) <> ''
|
||||
WHERE LOWER(u.login) = LOWER(p_user_login)
|
||||
ON CONFLICT (user_login, server_login) DO UPDATE SET
|
||||
server_url = EXCLUDED.server_url,
|
||||
server_client_key = EXCLUDED.server_client_key,
|
||||
user_record_number = EXCLUDED.user_record_number,
|
||||
user_updated_at_ms = EXCLUDED.user_updated_at_ms,
|
||||
server_record_number = EXCLUDED.server_record_number,
|
||||
server_updated_at_ms = EXCLUDED.server_updated_at_ms,
|
||||
refreshed_at_ms = EXCLUDED.refreshed_at_ms;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION shine_refresh_user_access_servers_for_server(p_server_login TEXT)
|
||||
RETURNS VOID
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
affected_user RECORD;
|
||||
BEGIN
|
||||
IF p_server_login IS NULL OR btrim(p_server_login) = '' THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
DELETE FROM user_access_servers_current
|
||||
WHERE LOWER(server_login) = LOWER(p_server_login);
|
||||
|
||||
FOR affected_user IN
|
||||
SELECT u.login
|
||||
FROM solana_user_pda_current u
|
||||
CROSS JOIN LATERAL jsonb_array_elements_text(
|
||||
CASE
|
||||
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||
ELSE u.access_servers_json::jsonb
|
||||
END
|
||||
) AS access_server(login_value)
|
||||
WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login)
|
||||
LOOP
|
||||
PERFORM shine_refresh_user_access_servers_for_user(affected_user.login);
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION shine_refresh_user_access_servers_all()
|
||||
RETURNS VOID
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
affected_user RECORD;
|
||||
BEGIN
|
||||
TRUNCATE TABLE user_access_servers_current;
|
||||
|
||||
FOR affected_user IN
|
||||
SELECT login
|
||||
FROM solana_user_pda_current
|
||||
LOOP
|
||||
PERFORM shine_refresh_user_access_servers_for_user(affected_user.login);
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION trg_refresh_user_access_servers_from_user_pda_row()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
PERFORM shine_refresh_user_access_servers_for_user(OLD.login);
|
||||
PERFORM shine_refresh_user_access_servers_for_server(OLD.login);
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
|
||||
IF TG_OP = 'UPDATE' AND LOWER(OLD.login) <> LOWER(NEW.login) THEN
|
||||
PERFORM shine_refresh_user_access_servers_for_user(OLD.login);
|
||||
PERFORM shine_refresh_user_access_servers_for_server(OLD.login);
|
||||
END IF;
|
||||
|
||||
PERFORM shine_refresh_user_access_servers_for_user(NEW.login);
|
||||
PERFORM shine_refresh_user_access_servers_for_server(NEW.login);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION trg_refresh_user_access_servers_from_user_pda_truncate()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
TRUNCATE TABLE user_access_servers_current;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_refresh_user_access_servers_row ON solana_user_pda_current;
|
||||
CREATE TRIGGER trg_refresh_user_access_servers_row
|
||||
AFTER INSERT OR UPDATE OR DELETE ON solana_user_pda_current
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trg_refresh_user_access_servers_from_user_pda_row();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_refresh_user_access_servers_truncate ON solana_user_pda_current;
|
||||
CREATE TRIGGER trg_refresh_user_access_servers_truncate
|
||||
AFTER TRUNCATE ON solana_user_pda_current
|
||||
FOR EACH STATEMENT
|
||||
EXECUTE FUNCTION trg_refresh_user_access_servers_from_user_pda_truncate();
|
||||
|
||||
SELECT shine_refresh_user_access_servers_all();
|
||||
|
||||
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||
VALUES (1, 2, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
schema_version = EXCLUDED.schema_version,
|
||||
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||
|
||||
COMMIT;
|
||||
File diff suppressed because it is too large
Load Diff
+9
-9
@@ -1,7 +1,7 @@
|
||||
package server.logic.ws_protocol.JSON;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
import shine.db.entities.ActiveSessionEntry;
|
||||
|
||||
/**
|
||||
@@ -25,8 +25,8 @@ public class ConnectionContext {
|
||||
public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1; // выполнен challenge (AuthChallenge или SessionChallenge)
|
||||
public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь
|
||||
|
||||
// Полный пользователь из БД (solana_users)
|
||||
private SolanaUserEntry solanaUserEntry;
|
||||
// Полный пользователь из runtime БД (current users / solana_user_pda_current)
|
||||
private CurrentUserEntry currentUserEntry;
|
||||
|
||||
// Активная сессия из БД (active_sessions)
|
||||
private ActiveSessionEntry activeSessionEntry;
|
||||
@@ -89,12 +89,12 @@ public class ConnectionContext {
|
||||
|
||||
// --- SolanaUser / ActiveSession ---
|
||||
|
||||
public SolanaUserEntry getSolanaUser() {
|
||||
return solanaUserEntry;
|
||||
public CurrentUserEntry getCurrentUser() {
|
||||
return currentUserEntry;
|
||||
}
|
||||
|
||||
public void setSolanaUser(SolanaUserEntry solanaUserEntry) {
|
||||
this.solanaUserEntry = solanaUserEntry;
|
||||
public void setCurrentUser(CurrentUserEntry currentUserEntry) {
|
||||
this.currentUserEntry = currentUserEntry;
|
||||
}
|
||||
|
||||
public ActiveSessionEntry getActiveSession() {
|
||||
@@ -108,7 +108,7 @@ public class ConnectionContext {
|
||||
// --- Удобный геттер для логина ---
|
||||
|
||||
public String getLogin() {
|
||||
return solanaUserEntry != null ? solanaUserEntry.getLogin() : null;
|
||||
return currentUserEntry != null ? currentUserEntry.getLogin() : null;
|
||||
}
|
||||
|
||||
// --- sessionId ---
|
||||
@@ -176,7 +176,7 @@ public class ConnectionContext {
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
solanaUserEntry = null;
|
||||
currentUserEntry = null;
|
||||
activeSessionEntry = null;
|
||||
|
||||
sessionId = null;
|
||||
|
||||
-5
@@ -43,9 +43,6 @@ import server.logic.ws_protocol.JSON.handlers.blockchain.Net_GetBlockchainBlock_
|
||||
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_GetBlockchainBlock_Request;
|
||||
|
||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request;
|
||||
|
||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_GetUser_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_TestGetFreeAvatarQuota_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_TestUploadFreeAvatar_Handler;
|
||||
@@ -142,7 +139,6 @@ public final class JsonHandlerRegistry {
|
||||
}
|
||||
|
||||
private static final Map<String, JsonMessageHandler> HANDLERS = Map.ofEntries(
|
||||
Map.entry("AddUser", new Net_AddUser_Handler()),
|
||||
Map.entry("GetUser", new Net_GetUser_Handler()),
|
||||
Map.entry("SearchUsers", new Net_SearchUsers_Handler()),
|
||||
Map.entry("TestGetFreeAvatarQuota", new Net_TestGetFreeAvatarQuota_Handler()),
|
||||
@@ -223,7 +219,6 @@ public final class JsonHandlerRegistry {
|
||||
);
|
||||
|
||||
private static final Map<String, Class<? extends Net_Request>> REQUEST_TYPES = Map.ofEntries(
|
||||
Map.entry("AddUser", Net_AddUser_Request.class),
|
||||
Map.entry("GetUser", Net_GetUser_Request.class),
|
||||
Map.entry("SearchUsers", Net_SearchUsers_Request.class),
|
||||
Map.entry("TestGetFreeAvatarQuota", Net_TestGetFreeAvatarQuota_Request.class),
|
||||
|
||||
+6
-22
@@ -9,8 +9,8 @@ import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Re
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.dao.CurrentUsersDAO;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -26,7 +26,7 @@ import java.security.SecureRandom;
|
||||
*
|
||||
* Что делает:
|
||||
* 1) Проверяет login.
|
||||
* 2) Находит пользователя (solana_users).
|
||||
* 2) Находит пользователя в текущем PDA snapshot.
|
||||
* 3) Пишет solanaUser в ctx, ставит AUTH_STATUS_AUTH_IN_PROGRESS.
|
||||
* 4) Генерирует authNonce (base64url(32)) и сохраняет в ctx.authNonce.
|
||||
*/
|
||||
@@ -60,33 +60,17 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
|
||||
);
|
||||
}
|
||||
|
||||
SolanaUserEntry solanaUserEntry = SolanaUsersDAO.getInstance().getByLogin(login);
|
||||
if (solanaUserEntry == null) {
|
||||
try {
|
||||
solanaUserEntry = SolanaUserPdaImportService.findOrImportByLogin(login);
|
||||
if (solanaUserEntry != null) {
|
||||
log.info("AuthChallenge: пользователь {} импортирован из Solana PDA", solanaUserEntry.getLogin());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("AuthChallenge: ошибка lazy-import пользователя {} из Solana", login, e);
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.SERVER_DATA_ERROR,
|
||||
"SOLANA_IMPORT_FAILED",
|
||||
"Ошибка проверки пользователя в Solana"
|
||||
);
|
||||
}
|
||||
}
|
||||
CurrentUserEntry solanaUserEntry = CurrentUsersDAO.getInstance().getByLogin(login);
|
||||
if (solanaUserEntry == null) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
"UNKNOWN_USER",
|
||||
"Пользователь с таким логином не найден"
|
||||
);
|
||||
}
|
||||
|
||||
ctx.setSolanaUser(solanaUserEntry);
|
||||
ctx.setCurrentUser(solanaUserEntry);
|
||||
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS);
|
||||
|
||||
byte[] buf = new byte[32];
|
||||
|
||||
+3
-3
@@ -18,7 +18,7 @@ import server.logic.ws_protocol.WireCodes;
|
||||
import server.ws.WsConnectionUtils;
|
||||
import shine.db.dao.ActiveSessionsDAO;
|
||||
import shine.db.entities.ActiveSessionEntry;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
@@ -42,7 +42,7 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
|
||||
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
||||
Net_CloseActiveSession_Request req = (Net_CloseActiveSession_Request) baseReq;
|
||||
|
||||
if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
|
||||
if (ctx == null || ctx.getCurrentUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
@@ -51,7 +51,7 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
|
||||
);
|
||||
}
|
||||
|
||||
SolanaUserEntry user = ctx.getSolanaUser();
|
||||
CurrentUserEntry user = ctx.getCurrentUser();
|
||||
String currentLogin = user.getLogin();
|
||||
|
||||
String targetSessionId = req.getSessionId();
|
||||
|
||||
+6
-6
@@ -15,9 +15,9 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import server.ws.WsConnectionUtils;
|
||||
import shine.db.dao.ActiveSessionsDAO;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.dao.CurrentUsersDAO;
|
||||
import shine.db.entities.ActiveSessionEntry;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
import shine.geo.ClientInfoService;
|
||||
import shine.geo.GeoLookupService;
|
||||
import utils.crypto.Ed25519Util;
|
||||
@@ -58,7 +58,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
Net_CreateAuthSession_Request req = (Net_CreateAuthSession_Request) baseReq;
|
||||
|
||||
if (ctx == null
|
||||
|| ctx.getSolanaUser() == null
|
||||
|| ctx.getCurrentUser() == null
|
||||
|| ctx.getAuthNonce() == null
|
||||
|| ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {
|
||||
|
||||
@@ -72,7 +72,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
return err;
|
||||
}
|
||||
|
||||
SolanaUserEntry userFromContext = ctx.getSolanaUser();
|
||||
CurrentUserEntry userFromContext = ctx.getCurrentUser();
|
||||
String loginFromContext = userFromContext.getLogin();
|
||||
String loginFromReq = req.getLogin();
|
||||
if (loginFromReq == null || loginFromReq.isBlank()) {
|
||||
@@ -97,9 +97,9 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
||||
return err;
|
||||
}
|
||||
|
||||
SolanaUserEntry user;
|
||||
CurrentUserEntry user;
|
||||
try {
|
||||
user = SolanaUsersDAO.getInstance().getByLogin(loginFromContext);
|
||||
user = CurrentUsersDAO.getInstance().getByLogin(loginFromContext);
|
||||
} catch (SQLException e) {
|
||||
Net_Response err = NetExceptionResponseFactory.error(
|
||||
req,
|
||||
|
||||
+3
-3
@@ -14,7 +14,7 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.ActiveSessionsDAO;
|
||||
import shine.db.entities.ActiveSessionEntry;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
import shine.geo.GeoLookupService;
|
||||
|
||||
import java.sql.SQLException;
|
||||
@@ -36,7 +36,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
|
||||
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
||||
Net_ListSessions_Request req = (Net_ListSessions_Request) baseReq;
|
||||
|
||||
if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
|
||||
if (ctx == null || ctx.getCurrentUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
WireCodes.Status.UNVERIFIED,
|
||||
@@ -45,7 +45,7 @@ public class Net_ListSessions_Handler implements JsonMessageHandler {
|
||||
);
|
||||
}
|
||||
|
||||
SolanaUserEntry user = ctx.getSolanaUser();
|
||||
CurrentUserEntry user = ctx.getCurrentUser();
|
||||
String currentLogin = user.getLogin();
|
||||
|
||||
List<ActiveSessionEntry> sessions;
|
||||
|
||||
+5
-5
@@ -14,9 +14,9 @@ import server.logic.ws_protocol.JSON.utils.AuthKeyUtils;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.ActiveSessionsDAO;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.dao.CurrentUsersDAO;
|
||||
import shine.db.entities.ActiveSessionEntry;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
import shine.geo.ClientInfoService;
|
||||
import shine.geo.GeoLookupService;
|
||||
import utils.crypto.Ed25519Util;
|
||||
@@ -188,9 +188,9 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler {
|
||||
ctx.setSessionLoginNonceExpiresAtMs(0);
|
||||
|
||||
// подтягиваем пользователя
|
||||
SolanaUserEntry user;
|
||||
CurrentUserEntry user;
|
||||
try {
|
||||
user = SolanaUsersDAO.getInstance().getByLogin(session.getLogin());
|
||||
user = CurrentUsersDAO.getInstance().getByLogin(session.getLogin());
|
||||
} catch (SQLException e) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
@@ -294,7 +294,7 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler {
|
||||
|
||||
// ctx
|
||||
ctx.setActiveSession(session);
|
||||
ctx.setSolanaUser(user);
|
||||
ctx.setCurrentUser(user);
|
||||
ctx.setSessionId(sessionId);
|
||||
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
|
||||
|
||||
|
||||
+3
-3
@@ -15,10 +15,10 @@ import server.logic.ws_protocol.JSON.utils.NetIdGenerator;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.EspPairingRequestsDAO;
|
||||
import shine.db.dao.EspPairingSettingsDAO;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.dao.CurrentUsersDAO;
|
||||
import shine.db.entities.EspPairingRequestEntry;
|
||||
import shine.db.entities.EspPairingSettingsEntry;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -60,7 +60,7 @@ public class Net_StartEspPairing_Handler implements JsonMessageHandler {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_PASSWORD_HASH_FORMAT", "passwordHash должен быть пустым или иметь формат sha256$<64 hex>");
|
||||
}
|
||||
|
||||
SolanaUserEntry user = SolanaUsersDAO.getInstance().getByLogin(login);
|
||||
CurrentUserEntry user = CurrentUsersDAO.getInstance().getByLogin(login);
|
||||
if (user == null) {
|
||||
return NetExceptionResponseFactory.error(req, 422, "PAIRING_NOT_AVAILABLE", "Для этого login pairing недоступен");
|
||||
}
|
||||
|
||||
+1
-33
@@ -4,9 +4,6 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.dao.UserCreateDAO;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import utils.config.SolanaProgramsConfig;
|
||||
|
||||
import java.net.URI;
|
||||
@@ -22,8 +19,7 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Lazy-import пользователя из Solana PDA в локальную БД сервера.
|
||||
* Используется при входе, если в solana_users нет записи по login.
|
||||
* Чтение пользовательских и серверных PDA из Solana.
|
||||
*/
|
||||
public final class SolanaUserPdaImportService {
|
||||
|
||||
@@ -34,34 +30,6 @@ public final class SolanaUserPdaImportService {
|
||||
|
||||
private SolanaUserPdaImportService() {}
|
||||
|
||||
public static SolanaUserEntry findOrImportByLogin(String loginRaw) throws Exception {
|
||||
String login = normalizeLogin(loginRaw);
|
||||
if (login == null) return null;
|
||||
|
||||
SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
|
||||
SolanaUserEntry existing = usersDao.getByLogin(login);
|
||||
if (existing != null) return existing;
|
||||
|
||||
ParsedSolanaUser parsed = fetchFromSolana(login);
|
||||
if (parsed == null) return null;
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
long sizeLimit = parsed.paidLimitBytes > 0 ? parsed.paidLimitBytes : 100_000L;
|
||||
boolean inserted = UserCreateDAO.getInstance().insertUserWithBlockchain(
|
||||
parsed.login,
|
||||
parsed.blockchainName,
|
||||
parsed.clientKeyB64, // в текущей модели solanaKey = clientKey
|
||||
parsed.blockchainKeyB64,
|
||||
parsed.clientKeyB64,
|
||||
sizeLimit,
|
||||
now
|
||||
);
|
||||
if (!inserted) {
|
||||
return usersDao.getByLogin(login);
|
||||
}
|
||||
return usersDao.getByLogin(login);
|
||||
}
|
||||
|
||||
public static SessionTypeCheckResult checkSessionTypeAgainstPda(String loginRaw, String sessionKeyApi, int requestedSessionType) throws Exception {
|
||||
String login = normalizeLogin(loginRaw);
|
||||
if (login == null) return SessionTypeCheckResult.noRecord();
|
||||
|
||||
+4
-40
@@ -33,46 +33,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
1. Добавление пользователя (AddUser)
|
||||
1. Пользователь уже существует в актуальном PDA snapshot
|
||||
|
||||
Назначение: создать локальную запись пользователя с двумя ключами — solanaKey и clientKey.
|
||||
|
||||
📤 Запрос клиента
|
||||
{
|
||||
"op": "AddUser",
|
||||
"requestId": "req-1",
|
||||
"payload": {
|
||||
"login": "anya4",
|
||||
"loginId": 100212,
|
||||
"bchId": 4222,
|
||||
"solanaKey": "BASE64_LOGIN_KEY",
|
||||
"clientKey": "BASE64_DEVICE_KEY",
|
||||
"bchLimit": 1000000
|
||||
}
|
||||
}
|
||||
|
||||
🖥 Действия сервера
|
||||
|
||||
Проверяет корректность данных.
|
||||
|
||||
Вставляет запись в таблицу:
|
||||
|
||||
CREATE TABLE solana_users (
|
||||
login TEXT NOT NULL,
|
||||
loginId INTEGER PRIMARY KEY,
|
||||
bchId INTEGER NOT NULL,
|
||||
solanaKey TEXT,
|
||||
clientKey TEXT,
|
||||
bchLimit INTEGER
|
||||
);
|
||||
|
||||
📥 Ответ
|
||||
{
|
||||
"op": "AddUser",
|
||||
"requestId": "req-1",
|
||||
"status": 200,
|
||||
"payload": { "ok": true }
|
||||
}
|
||||
Назначение: перед началом auth-flow сервер уже должен видеть пользователя
|
||||
в актуальном `solana_user_pda_current`.
|
||||
|
||||
2. Шаг 1 — запрос временного пароля сессии
|
||||
AuthChallenge
|
||||
@@ -173,4 +137,4 @@ pushAuthKey TEXT
|
||||
}
|
||||
|
||||
📘 Итоговая схема создания сессии
|
||||
AddUser → AuthChallenge → CreateAuthSession → Session Created
|
||||
PDA snapshot ready → AuthChallenge → CreateAuthSession → Session Created
|
||||
|
||||
+9
-8
@@ -279,18 +279,19 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
ChannelNameStateEntry channelNameStateEntry = null;
|
||||
Chat200CreateSeed chat200CreateSeed = null;
|
||||
if (block.body instanceof CreateChannelBody createChannelBody) {
|
||||
int channelTypeCode = Short.toUnsignedInt(createChannelBody.channelTypeCode);
|
||||
int channelTypeVersion = Short.toUnsignedInt(createChannelBody.channelTypeVersion);
|
||||
final String normalizedName;
|
||||
final String slug;
|
||||
try {
|
||||
normalizedName = ChannelNameRules.requireValidDisplayNameForCreate(createChannelBody.channelName);
|
||||
normalizedName = channelTypeCode == (CreateChannelBody.CHANNEL_TYPE_PERSONAL & 0xFFFF)
|
||||
? ChannelNameRules.requireValidDisplayNameForCreate(createChannelBody.channelName)
|
||||
: ChannelNameRules.requireValidPublicDisplayNameForCreate(createChannelBody.channelName);
|
||||
slug = ChannelNameRules.toCanonicalSlug(normalizedName);
|
||||
} catch (IllegalArgumentException badName) {
|
||||
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_channel_name", serverLastNum, serverLastHashHex);
|
||||
}
|
||||
|
||||
int channelTypeCode = Short.toUnsignedInt(createChannelBody.channelTypeCode);
|
||||
int channelTypeVersion = Short.toUnsignedInt(createChannelBody.channelTypeVersion);
|
||||
|
||||
try {
|
||||
if (channelNameStateDAO.existsByOwnerTypeAndSlug(blockchainName, channelTypeCode, slug)) {
|
||||
return new AddBlockResult(409, "channel_name_already_exists", serverLastNum, serverLastHashHex);
|
||||
@@ -544,7 +545,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
private void upsertChat200StateFromCreate(Chat200CreateSeed seed) throws Exception {
|
||||
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection();
|
||||
try (Connection c = shine.db.DbController.getInstance().getConnection();
|
||||
PreparedStatement ps = c.prepareStatement("""
|
||||
INSERT INTO chat200_state (
|
||||
owner_login, owner_bch_name, channel_root_block_number, channel_root_block_hash,
|
||||
@@ -586,7 +587,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
|
||||
long updatedAtMs = block.timestamp * 1000L;
|
||||
if ("desc".equals(cmd.command)) {
|
||||
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection();
|
||||
try (Connection c = shine.db.DbController.getInstance().getConnection();
|
||||
PreparedStatement ps = c.prepareStatement("""
|
||||
UPDATE chat200_state
|
||||
SET chat_title = ?, updated_at_ms = ?
|
||||
@@ -606,7 +607,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
String memberChannel = cmd.arg2 == null ? "" : cmd.arg2.trim();
|
||||
if (memberLogin.isBlank() || memberChannel.isBlank()) return;
|
||||
|
||||
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection();
|
||||
try (Connection c = shine.db.DbController.getInstance().getConnection();
|
||||
PreparedStatement ps = c.prepareStatement("""
|
||||
INSERT INTO chat200_members_state (
|
||||
owner_bch_name, channel_root_block_number, member_login, member_channel_name,
|
||||
@@ -630,7 +631,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
private boolean isChat200Channel(String ownerBch, int rootBlockNumber) throws Exception {
|
||||
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection();
|
||||
try (Connection c = shine.db.DbController.getInstance().getConnection();
|
||||
PreparedStatement ps = c.prepareStatement("""
|
||||
SELECT channel_type_code
|
||||
FROM channel_names_state
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ public final class BlockchainWriter {
|
||||
prepareWriteArtifacts(blockchainName, block.blockNumber, blockHashHex, candidateBytes);
|
||||
|
||||
boolean committed = false;
|
||||
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) {
|
||||
try (Connection c = shine.db.DbController.getInstance().getConnection()) {
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
// 1) insert block
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ import blockchain.BchBlockEntry;
|
||||
import blockchain.body.CreateChannelBody;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.channels.ChannelNameRules;
|
||||
import shine.db.dao.ChannelNameStateDAO;
|
||||
import shine.db.entities.ChannelNameStateEntry;
|
||||
@@ -48,7 +48,7 @@ public final class ChannelNamesStateBootstrapper {
|
||||
ORDER BY bch_name, block_number
|
||||
""";
|
||||
|
||||
try (Connection c = SqliteDbController.getInstance().getConnection()) {
|
||||
try (Connection c = DbController.getInstance().getConnection()) {
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
|
||||
+5
-3
@@ -8,6 +8,7 @@ import blockchain.body.TextLineBody;
|
||||
import blockchain.body.TextReplyBody;
|
||||
import shine.db.channels.ChannelNameRules;
|
||||
import shine.db.MsgSubType;
|
||||
import shine.db.sql.CurrentUsersSql;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
@@ -30,7 +31,8 @@ final class ChannelsReadSupport {
|
||||
private ChannelsReadSupport() {}
|
||||
|
||||
static String canonicalLogin(Connection c, String anyCaseLogin) throws SQLException {
|
||||
String sql = "SELECT login FROM solana_users WHERE login = ? COLLATE NOCASE LIMIT 1";
|
||||
String sql = ("SELECT login FROM " + CurrentUsersSql.usersSubquery("su")
|
||||
+ " WHERE LOWER(login) = LOWER(?) LIMIT 1");
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, anyCaseLogin);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
@@ -411,7 +413,7 @@ final class ChannelsReadSupport {
|
||||
String partnerBchSql = """
|
||||
SELECT blockchain_name
|
||||
FROM blockchain_state
|
||||
WHERE login = ? COLLATE NOCASE
|
||||
WHERE LOWER(login) = LOWER(?)
|
||||
ORDER BY blockchain_name
|
||||
LIMIT 1
|
||||
""";
|
||||
@@ -462,7 +464,7 @@ final class ChannelsReadSupport {
|
||||
String sql = """
|
||||
SELECT msg_sub_type
|
||||
FROM blocks
|
||||
WHERE login = ? COLLATE NOCASE
|
||||
WHERE LOWER(login) = LOWER(?)
|
||||
AND msg_type = ?
|
||||
AND to_bch_name = ?
|
||||
AND to_block_number = ?
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelMe
|
||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelMessages_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.MsgSubType;
|
||||
import utils.blockchain.BlockchainNameUtil;
|
||||
import blockchain.body.CreateChannelBody;
|
||||
@@ -39,7 +39,7 @@ public class Net_GetChannelMessages_Handler implements JsonMessageHandler {
|
||||
|
||||
boolean asc = req.getSort() == null || !"desc".equalsIgnoreCase(req.getSort());
|
||||
|
||||
try (Connection c = SqliteDbController.getInstance().getConnection()) {
|
||||
try (Connection c = DbController.getInstance().getConnection()) {
|
||||
String viewerLogin = ctx != null ? ctx.getLogin() : null;
|
||||
if (viewerLogin == null || viewerLogin.isBlank()) {
|
||||
viewerLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin());
|
||||
|
||||
+6
-6
@@ -10,7 +10,7 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelsC
|
||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelsCounters_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.MsgSubType;
|
||||
|
||||
import java.sql.Connection;
|
||||
@@ -26,7 +26,7 @@ public class Net_GetChannelsCounters_Handler implements JsonMessageHandler {
|
||||
if (req.getLogin() == null || req.getLogin().isBlank()) {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля: login");
|
||||
}
|
||||
try (Connection c = SqliteDbController.getInstance().getConnection()) {
|
||||
try (Connection c = DbController.getInstance().getConnection()) {
|
||||
String canonicalLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin().trim());
|
||||
if (canonicalLogin == null) {
|
||||
return NetExceptionResponseFactory.error(req, 404, "user_not_found", "Пользователь не найден");
|
||||
@@ -52,7 +52,7 @@ public class Net_GetChannelsCounters_Handler implements JsonMessageHandler {
|
||||
String sql = """
|
||||
SELECT COUNT(*)
|
||||
FROM connections_state
|
||||
WHERE login = ? COLLATE NOCASE
|
||||
WHERE LOWER(login) = LOWER(?)
|
||||
AND rel_type = ?
|
||||
""";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
@@ -68,7 +68,7 @@ public class Net_GetChannelsCounters_Handler implements JsonMessageHandler {
|
||||
String sql = """
|
||||
SELECT COUNT(*)
|
||||
FROM channel_names_state
|
||||
WHERE owner_login = ? COLLATE NOCASE
|
||||
WHERE LOWER(owner_login) = LOWER(?)
|
||||
AND channel_type_code = ?
|
||||
""";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
@@ -81,7 +81,7 @@ public class Net_GetChannelsCounters_Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
private int countMyChannels(Connection c, String login) throws Exception {
|
||||
String bchCountSql = "SELECT COUNT(*) FROM blockchain_state WHERE login = ? COLLATE NOCASE";
|
||||
String bchCountSql = "SELECT COUNT(*) FROM blockchain_state WHERE LOWER(login) = LOWER(?)";
|
||||
int stories = 0;
|
||||
try (PreparedStatement ps = c.prepareStatement(bchCountSql)) {
|
||||
ps.setString(1, login);
|
||||
@@ -92,7 +92,7 @@ public class Net_GetChannelsCounters_Handler implements JsonMessageHandler {
|
||||
String namedSql = """
|
||||
SELECT COUNT(*)
|
||||
FROM channel_names_state
|
||||
WHERE owner_login = ? COLLATE NOCASE
|
||||
WHERE LOWER(owner_login) = LOWER(?)
|
||||
AND channel_type_code IN (1,100,200)
|
||||
""";
|
||||
int named = 0;
|
||||
|
||||
+3
-4
@@ -10,7 +10,7 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetGroupDial
|
||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetGroupDialog_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.channels.ChannelNameRules;
|
||||
|
||||
import java.sql.Connection;
|
||||
@@ -33,7 +33,7 @@ public class Net_GetGroupDialog_Handler implements JsonMessageHandler {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля group");
|
||||
}
|
||||
|
||||
try (Connection c = SqliteDbController.getInstance().getConnection()) {
|
||||
try (Connection c = DbController.getInstance().getConnection()) {
|
||||
Net_GetGroupDialog_Response resp = new Net_GetGroupDialog_Response();
|
||||
resp.setOp(req.getOp());
|
||||
resp.setRequestId(req.getRequestId());
|
||||
@@ -115,7 +115,7 @@ public class Net_GetGroupDialog_Handler implements JsonMessageHandler {
|
||||
String canonicalLogin = ChannelsReadSupport.canonicalLogin(c, ref.memberLogin);
|
||||
if (canonicalLogin == null || canonicalLogin.isBlank()) return null;
|
||||
|
||||
String bchSql = "SELECT blockchain_name FROM blockchain_state WHERE login = ? COLLATE NOCASE ORDER BY blockchain_name LIMIT 1";
|
||||
String bchSql = "SELECT blockchain_name FROM blockchain_state WHERE LOWER(login) = LOWER(?) ORDER BY blockchain_name LIMIT 1";
|
||||
String memberBch = null;
|
||||
try (PreparedStatement ps = c.prepareStatement(bchSql)) {
|
||||
ps.setString(1, canonicalLogin);
|
||||
@@ -214,4 +214,3 @@ public class Net_GetGroupDialog_Handler implements JsonMessageHandler {
|
||||
String text;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+49
-6
@@ -12,7 +12,7 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetMessageTh
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.MsgSubType;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
@@ -28,21 +28,21 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
|
||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||
Net_GetMessageThread_Request req = (Net_GetMessageThread_Request) baseRequest;
|
||||
if (req.getMessage() == null || req.getMessage().getBlockchainName() == null || req.getMessage().getBlockNumber() == null) {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля message");
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля message");
|
||||
}
|
||||
|
||||
int depthUp = req.getDepthUp() == null ? 20 : Math.max(0, req.getDepthUp());
|
||||
int depthDown = req.getDepthDown() == null ? 2 : Math.max(0, req.getDepthDown());
|
||||
int childLimit = req.getLimitChildrenPerNode() == null ? 50 : Math.max(1, req.getLimitChildrenPerNode());
|
||||
|
||||
try (Connection c = SqliteDbController.getInstance().getConnection()) {
|
||||
try (Connection c = DbController.getInstance().getConnection()) {
|
||||
String viewerLogin = ctx != null ? ctx.getLogin() : null;
|
||||
if (viewerLogin == null || viewerLogin.isBlank()) {
|
||||
viewerLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin());
|
||||
}
|
||||
PostRow focusRow = findByNumber(c, req.getMessage().getBlockchainName(), req.getMessage().getBlockNumber());
|
||||
PostRow focusRow = findFocusRow(c, req.getMessage());
|
||||
if (focusRow == null) {
|
||||
return NetExceptionResponseFactory.error(req, 404, "message_not_found", "Сообщение не найдено");
|
||||
return NetExceptionResponseFactory.error(req, 404, "message_not_found", "Сообщение не найдено");
|
||||
}
|
||||
|
||||
Net_GetMessageThread_Response resp = new Net_GetMessageThread_Response();
|
||||
@@ -67,10 +67,26 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
|
||||
return resp;
|
||||
} catch (Exception e) {
|
||||
log.error("GetMessageThread failed", e);
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "internal_error", "Внутренняя ошибка сервера");
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "internal_error", "Внутренняя ошибка сервера");
|
||||
}
|
||||
}
|
||||
|
||||
private PostRow findFocusRow(Connection c, Net_GetMessageThread_Request.MessageSelector selector) throws Exception {
|
||||
String blockchainName = String.valueOf(selector.getBlockchainName() == null ? "" : selector.getBlockchainName()).trim();
|
||||
int blockNumber = selector.getBlockNumber();
|
||||
byte[] blockHash = parseOptionalHash(selector.getBlockHash());
|
||||
|
||||
if (blockHash != null) {
|
||||
PostRow exact = findByNumberAndHash(c, blockchainName, blockNumber, blockHash);
|
||||
if (exact != null) return exact;
|
||||
}
|
||||
|
||||
PostRow byNumber = findByNumber(c, blockchainName, blockNumber);
|
||||
if (byNumber != null) return byNumber;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<Net_GetMessageThread_Response.MessageNodeTree> loadChildren(Connection c, PostRow parent, int depthDown, int childLimit, String viewerLogin) throws Exception {
|
||||
if (depthDown <= 0) return List.of();
|
||||
List<PostRow> replies = findReplies(c, parent.bchName, parent.blockNumber, parent.blockHash, childLimit);
|
||||
@@ -123,6 +139,33 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private PostRow findByNumberAndHash(Connection c, String bchName, int blockNumber, byte[] blockHash) throws Exception {
|
||||
String sql = """
|
||||
SELECT login,bch_name,block_number,block_hash,block_bytes,to_bch_name,to_block_number,to_block_hash,line_code,msg_sub_type,this_line_number
|
||||
FROM blocks
|
||||
WHERE bch_name=? AND block_number=? AND block_hash=?
|
||||
LIMIT 1
|
||||
""";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, bchName);
|
||||
ps.setInt(2, blockNumber);
|
||||
ps.setBytes(3, blockHash);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
return rs.next() ? mapRow(rs) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] parseOptionalHash(String hex) {
|
||||
String value = String.valueOf(hex == null ? "" : hex).trim();
|
||||
if (value.isEmpty()) return null;
|
||||
try {
|
||||
return ChannelsReadSupport.hexToBytes(value);
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private PostRow mapRow(ResultSet rs) throws Exception {
|
||||
PostRow row = new PostRow();
|
||||
row.login = rs.getString("login");
|
||||
|
||||
+3
-4
@@ -10,7 +10,7 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListGroupCha
|
||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListGroupChats200_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
@@ -27,7 +27,7 @@ public class Net_ListGroupChats200_Handler implements JsonMessageHandler {
|
||||
if (req.getLogin() == null || req.getLogin().isBlank()) {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля: login");
|
||||
}
|
||||
try (Connection c = SqliteDbController.getInstance().getConnection()) {
|
||||
try (Connection c = DbController.getInstance().getConnection()) {
|
||||
String canonicalLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin().trim());
|
||||
if (canonicalLogin == null) {
|
||||
return NetExceptionResponseFactory.error(req, 404, "user_not_found", "Пользователь не найден");
|
||||
@@ -59,7 +59,7 @@ public class Net_ListGroupChats200_Handler implements JsonMessageHandler {
|
||||
) m
|
||||
ON m.owner_bch_name = s.owner_bch_name
|
||||
AND m.channel_root_block_number = s.channel_root_block_number
|
||||
WHERE s.owner_login = ? COLLATE NOCASE
|
||||
WHERE LOWER(s.owner_login) = LOWER(?)
|
||||
ORDER BY s.updated_at_ms DESC, s.channel_root_block_number DESC
|
||||
""";
|
||||
List<Net_ListGroupChats200_Response.Row> out = new ArrayList<>();
|
||||
@@ -83,4 +83,3 @@ public class Net_ListGroupChats200_Handler implements JsonMessageHandler {
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListSubscrip
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.MsgSubType;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
@@ -29,7 +29,7 @@ public class Net_ListSubscriptionsFeed_Handler implements JsonMessageHandler {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля: login");
|
||||
}
|
||||
|
||||
try (Connection c = SqliteDbController.getInstance().getConnection()) {
|
||||
try (Connection c = DbController.getInstance().getConnection()) {
|
||||
String canonicalLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin().trim());
|
||||
if (canonicalLogin == null) {
|
||||
return NetExceptionResponseFactory.error(req, 404, "user_not_found", "Пользователь не найден");
|
||||
|
||||
+4
-4
@@ -11,7 +11,7 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_MarkChannelM
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.MsgSubType;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
@@ -37,7 +37,7 @@ public class Net_MarkChannelMessagesSeen_Handler implements JsonMessageHandler {
|
||||
return ok;
|
||||
}
|
||||
|
||||
try (Connection c = SqliteDbController.getInstance().getConnection()) {
|
||||
try (Connection c = DbController.getInstance().getConnection()) {
|
||||
String viewerLogin = ctx != null ? ctx.getLogin() : null;
|
||||
if (viewerLogin == null || viewerLogin.isBlank()) {
|
||||
viewerLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin());
|
||||
@@ -64,9 +64,10 @@ public class Net_MarkChannelMessagesSeen_Handler implements JsonMessageHandler {
|
||||
""".formatted(strictChannelMatch ? "AND line_code = ?" : "");
|
||||
|
||||
String insertSql = """
|
||||
INSERT OR IGNORE INTO message_views_state (
|
||||
INSERT INTO message_views_state (
|
||||
viewer_login, to_bch_name, to_block_number, to_block_hash, first_seen_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT DO NOTHING
|
||||
""";
|
||||
|
||||
int seenAccepted = 0;
|
||||
@@ -132,4 +133,3 @@ public class Net_MarkChannelMessagesSeen_Handler implements JsonMessageHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-6
@@ -9,6 +9,7 @@ import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_AddCloseF
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.MsgSubType;
|
||||
import shine.db.sql.CurrentUsersSql;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
@@ -31,7 +32,7 @@ public class Net_AddCloseFriend_Handler implements JsonMessageHandler {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "Нельзя добавить себя");
|
||||
}
|
||||
|
||||
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) {
|
||||
try (Connection c = shine.db.DbController.getInstance().getConnection()) {
|
||||
String canonicalTo = findCanonicalLogin(c, toLogin);
|
||||
if (canonicalTo == null) {
|
||||
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
||||
@@ -42,8 +43,7 @@ public class Net_AddCloseFriend_Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
// Idempotent insert for close-friend relation.
|
||||
// Using INSERT OR IGNORE avoids ON CONFLICT(column list) mismatches
|
||||
// across DB instances with different UNIQUE schemas.
|
||||
// Rely on PostgreSQL ON CONFLICT DO NOTHING.
|
||||
insertCloseFriendIgnoreDuplicate(c, from, canonicalTo, targetBch);
|
||||
|
||||
Net_AddCloseFriend_Response resp = new Net_AddCloseFriend_Response();
|
||||
@@ -58,7 +58,8 @@ public class Net_AddCloseFriend_Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
private String findCanonicalLogin(Connection c, String login) throws Exception {
|
||||
String sql = "SELECT login FROM solana_users WHERE login = ? COLLATE NOCASE LIMIT 1";
|
||||
String sql = "SELECT login FROM " + CurrentUsersSql.usersSubquery("su")
|
||||
+ " WHERE LOWER(login) = LOWER(?) LIMIT 1";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, login);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
@@ -68,7 +69,7 @@ public class Net_AddCloseFriend_Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
private String findPrimaryBlockchain(Connection c, String login) throws Exception {
|
||||
String sql = "SELECT blockchain_name FROM blockchain_state WHERE login = ? COLLATE NOCASE ORDER BY blockchain_name LIMIT 1";
|
||||
String sql = "SELECT blockchain_name FROM blockchain_state WHERE LOWER(login) = LOWER(?) ORDER BY blockchain_name LIMIT 1";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, login);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
@@ -82,10 +83,11 @@ public class Net_AddCloseFriend_Handler implements JsonMessageHandler {
|
||||
String toLogin,
|
||||
String toBchName) throws Exception {
|
||||
String sql = """
|
||||
INSERT OR IGNORE INTO connections_state (
|
||||
INSERT INTO connections_state (
|
||||
login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash) DO NOTHING
|
||||
""";
|
||||
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
|
||||
+8
-10
@@ -11,8 +11,9 @@ import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriend
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.MsgSubType;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.dao.ConnectionsStateDAO;
|
||||
import shine.db.sql.CurrentUsersSql;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
@@ -26,10 +27,7 @@ import java.util.List;
|
||||
*
|
||||
* ВАЖНО:
|
||||
* - login в запросе может быть любым регистром
|
||||
* - в ответе возвращаем канонический регистр (как в solana_users.login)
|
||||
*
|
||||
* ПРИМЕЧАНИЕ:
|
||||
* Таблица пользователей тут названа "solana_users". Если у тебя иначе — поменяй SQL.
|
||||
* - в ответе возвращаем канонический регистр логина.
|
||||
*/
|
||||
public class Net_GetFriendsLists_Handler implements JsonMessageHandler {
|
||||
|
||||
@@ -51,12 +49,12 @@ public class Net_GetFriendsLists_Handler implements JsonMessageHandler {
|
||||
final String loginAnyCase = req.getLogin().trim();
|
||||
|
||||
try {
|
||||
SqliteDbController db = SqliteDbController.getInstance();
|
||||
DbController db = DbController.getInstance();
|
||||
ConnectionsStateDAO dao = ConnectionsStateDAO.getInstance();
|
||||
|
||||
try (Connection c = db.getConnection()) {
|
||||
|
||||
// 1) Канонизируем login через solana_users (NOCASE)
|
||||
// 1) Канонизируем login через current users слой (NOCASE)
|
||||
String canonicalLogin = findCanonicalLogin(c, loginAnyCase);
|
||||
if (canonicalLogin == null) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
@@ -99,10 +97,10 @@ public class Net_GetFriendsLists_Handler implements JsonMessageHandler {
|
||||
private String findCanonicalLogin(Connection c, String loginAnyCase) throws Exception {
|
||||
String sql = """
|
||||
SELECT login
|
||||
FROM solana_users
|
||||
WHERE login = ? COLLATE NOCASE
|
||||
FROM %s
|
||||
WHERE LOWER(login) = LOWER(?)
|
||||
LIMIT 1
|
||||
""";
|
||||
""".formatted(CurrentUsersSql.usersSubquery("su"));
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, loginAnyCase);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
|
||||
+9
-7
@@ -10,6 +10,7 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.MsgSubType;
|
||||
import shine.db.dao.ConnectionsStateDAO;
|
||||
import shine.db.sql.CurrentUsersSql;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
@@ -39,7 +40,7 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) {
|
||||
try (Connection c = shine.db.DbController.getInstance().getConnection()) {
|
||||
String canonicalLogin = findCanonicalLogin(c, requestedLogin);
|
||||
if (canonicalLogin == null) {
|
||||
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
||||
@@ -113,7 +114,8 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
private String findCanonicalLogin(Connection c, String loginAnyCase) throws Exception {
|
||||
String sql = "SELECT login FROM solana_users WHERE login = ? COLLATE NOCASE LIMIT 1";
|
||||
String sql = "SELECT login FROM " + CurrentUsersSql.usersSubquery("su")
|
||||
+ " WHERE LOWER(login) = LOWER(?) LIMIT 1";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, loginAnyCase);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
@@ -168,19 +170,19 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
|
||||
MAX(CASE WHEN up.param = 'official' THEN up.value END) AS official_value,
|
||||
MAX(CASE WHEN up.param = 'shine' THEN up.value END) AS shine_value,
|
||||
MAX(CASE WHEN up.param = 'ava' THEN up.value END) AS avatar_value
|
||||
FROM solana_users su
|
||||
FROM %s
|
||||
LEFT JOIN users_params up
|
||||
ON up.login = su.login COLLATE NOCASE
|
||||
ON LOWER(up.login) = LOWER(su.login)
|
||||
AND up.param IN ('gender', 'official', 'shine', 'ava')
|
||||
WHERE su.login COLLATE NOCASE IN (%s)
|
||||
WHERE LOWER(su.login) IN (%s)
|
||||
GROUP BY su.login
|
||||
ORDER BY su.login
|
||||
""".formatted(String.join(", ", placeholders));
|
||||
""".formatted(CurrentUsersSql.usersSubquery("su"), String.join(", ", placeholders));
|
||||
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
int i = 1;
|
||||
for (String login : logins) {
|
||||
ps.setString(i, login);
|
||||
ps.setString(i, normKey(login));
|
||||
i += 1;
|
||||
}
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ public class Net_ListContacts_Handler implements JsonMessageHandler {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "NOT_AUTHENTICATED", "Требуется авторизация");
|
||||
}
|
||||
|
||||
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) {
|
||||
try (Connection c = shine.db.DbController.getInstance().getConnection()) {
|
||||
List<String> contacts = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, ctx.getLogin(), MsgSubType.CONNECTION_CONTACT);
|
||||
Net_ListContacts_Response resp = new Net_ListContacts_Response();
|
||||
resp.setOp(req.getOp());
|
||||
|
||||
+6
-6
@@ -11,19 +11,19 @@ import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetSyncUserPro
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.BlockchainStateDAO;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.dao.CurrentUsersDAO;
|
||||
import shine.db.entities.BlockchainStateEntry;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
|
||||
/**
|
||||
* GetSyncUserProfile — server-to-server профиль пользователя для межсерверной синхронизации.
|
||||
* Нужен, чтобы принимающий сервер мог создать локальные solana_users + blockchain_state
|
||||
* без прямого запроса в Solana RPC.
|
||||
* Нужен, чтобы принимающий сервер мог создать локальную runtime-проекцию пользователя
|
||||
* и blockchain_state без прямого запроса в Solana RPC.
|
||||
*/
|
||||
public final class Net_GetSyncUserProfile_Handler implements JsonMessageHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Net_GetSyncUserProfile_Handler.class);
|
||||
private final SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
|
||||
private final CurrentUsersDAO usersDAO = CurrentUsersDAO.getInstance();
|
||||
private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
|
||||
|
||||
@Override
|
||||
@@ -41,7 +41,7 @@ public final class Net_GetSyncUserProfile_Handler implements JsonMessageHandler
|
||||
}
|
||||
|
||||
try {
|
||||
SolanaUserEntry user = usersDAO.getByLogin(login);
|
||||
CurrentUserEntry user = usersDAO.getByLogin(login);
|
||||
|
||||
Net_GetSyncUserProfile_Response resp = new Net_GetSyncUserProfile_Response();
|
||||
resp.setOp(req.getOp());
|
||||
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.tempToTest;
|
||||
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
|
||||
/**
|
||||
* AddUser отключен: регистрация работает через Solana.
|
||||
*/
|
||||
public class Net_AddUser_Handler implements JsonMessageHandler {
|
||||
|
||||
@Override
|
||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||
Net_AddUser_Request req = (Net_AddUser_Request) baseRequest;
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
410,
|
||||
"ADD_USER_DISABLED",
|
||||
"Серверная регистрация AddUser отключена. Используйте регистрацию через Solana."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-8
@@ -5,16 +5,15 @@ import org.slf4j.LoggerFactory;
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.SolanaUserPdaImportService;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.BlockchainStateDAO;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.dao.CurrentUsersDAO;
|
||||
import shine.db.entities.BlockchainStateEntry;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.Arrays;
|
||||
@@ -38,14 +37,11 @@ public class Net_GetUser_Handler implements JsonMessageHandler {
|
||||
);
|
||||
}
|
||||
|
||||
SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
|
||||
CurrentUsersDAO usersDAO = CurrentUsersDAO.getInstance();
|
||||
BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
|
||||
|
||||
try {
|
||||
SolanaUserEntry u = usersDAO.getByLogin(req.getLogin());
|
||||
if (u == null) {
|
||||
u = SolanaUserPdaImportService.findOrImportByLogin(req.getLogin());
|
||||
}
|
||||
CurrentUserEntry u = usersDAO.getByLogin(req.getLogin());
|
||||
|
||||
Net_GetUser_Response resp = new Net_GetUser_Response();
|
||||
resp.setOp(req.getOp());
|
||||
|
||||
+5
-5
@@ -10,8 +10,8 @@ import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUser
|
||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUsers_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.dao.CurrentUsersDAO;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
@@ -37,11 +37,11 @@ public class Net_SearchUsers_Handler implements JsonMessageHandler {
|
||||
String prefix = req.getPrefix().trim();
|
||||
|
||||
try {
|
||||
SolanaUsersDAO dao = SolanaUsersDAO.getInstance();
|
||||
List<SolanaUserEntry> users = dao.searchByLoginPrefix(prefix); // case-insensitive + LIMIT 5
|
||||
CurrentUsersDAO dao = CurrentUsersDAO.getInstance();
|
||||
List<CurrentUserEntry> users = dao.searchByLoginPrefix(prefix); // case-insensitive + LIMIT 5
|
||||
|
||||
List<String> logins = new ArrayList<>();
|
||||
for (SolanaUserEntry u : users) {
|
||||
for (CurrentUserEntry u : users) {
|
||||
if (u != null && u.getLogin() != null) {
|
||||
logins.add(u.getLogin()); // регистр как в БД
|
||||
}
|
||||
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
|
||||
/**
|
||||
* Запрос AddUser — временная/тестовая регистрация локального пользователя.
|
||||
*
|
||||
* Клиент отправляет:
|
||||
*
|
||||
* {
|
||||
* "op": "AddUser",
|
||||
* "requestId": "test-add-1",
|
||||
* "payload": {
|
||||
* "login": "anya",
|
||||
* "blockchainName": "anya-001",
|
||||
* "solanaKey": "base64-ed25519-public-key-login",
|
||||
* "blockchainKey": "base64-ed25519-public-key-blockchain",
|
||||
* "clientKey": "base64-ed25519-public-key-device",
|
||||
* "bchLimit": 1000000
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Все поля лежат внутри payload.
|
||||
*/
|
||||
public class Net_AddUser_Request extends Net_Request {
|
||||
|
||||
private String login;
|
||||
private String blockchainName;
|
||||
|
||||
/** Ключ пользователя Solana (публичный ключ логина) */
|
||||
private String solanaKey;
|
||||
|
||||
/** Ключ блокчейна (публичный ключ блокчейна) */
|
||||
private String blockchainKey;
|
||||
|
||||
/** Ключ устройства (публичный ключ устройства) */
|
||||
private String clientKey;
|
||||
|
||||
private Integer bchLimit;
|
||||
|
||||
public String getLogin() { return login; }
|
||||
public void setLogin(String login) { this.login = login; }
|
||||
|
||||
public String getBlockchainName() { return blockchainName; }
|
||||
public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
|
||||
|
||||
public String getSolanaKey() { return solanaKey; }
|
||||
public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; }
|
||||
|
||||
public String getBlockchainKey() { return blockchainKey; }
|
||||
public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
|
||||
|
||||
public String getClientKey() { return clientKey; }
|
||||
public void setClientKey(String clientKey) { this.clientKey = clientKey; }
|
||||
|
||||
public Integer getBchLimit() { return bchLimit; }
|
||||
public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; }
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
// file: server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_AddUser_Response.java
|
||||
package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
|
||||
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
|
||||
/**
|
||||
* Успешный ответ на AddUser.
|
||||
*
|
||||
* Сейчас дополнительных полей нет — достаточно status=200.
|
||||
*
|
||||
* Пример:
|
||||
* {
|
||||
* "op": "AddUser",
|
||||
* "requestId": "test-add-1",
|
||||
* "status": 200,
|
||||
* "payload": { }
|
||||
* }
|
||||
*/
|
||||
public class Net_AddUser_Response extends Net_Response {
|
||||
// При необходимости сюда можно добавить, например, флаг created/updated и т.п.
|
||||
}
|
||||
+2
-2
@@ -10,7 +10,7 @@ import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserPar
|
||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.DbController;
|
||||
import shine.db.dao.UserParamsDAO;
|
||||
import shine.db.entities.UserParamEntry;
|
||||
|
||||
@@ -48,7 +48,7 @@ public class Net_GetUserParam_Handler implements JsonMessageHandler {
|
||||
String param = req.getParam().trim();
|
||||
|
||||
try {
|
||||
SqliteDbController db = SqliteDbController.getInstance();
|
||||
DbController db = DbController.getInstance();
|
||||
UserParamsDAO dao = UserParamsDAO.getInstance();
|
||||
|
||||
try (Connection c = db.getConnection()) {
|
||||
|
||||
+5
-5
@@ -10,10 +10,10 @@ import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserPa
|
||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.DbController;
|
||||
import shine.db.dao.CurrentUsersDAO;
|
||||
import shine.db.dao.UserParamsDAO;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
import shine.db.entities.UserParamEntry;
|
||||
|
||||
import java.sql.Connection;
|
||||
@@ -50,7 +50,7 @@ public class Net_ListUserParams_Handler implements JsonMessageHandler {
|
||||
String login = req.getLogin().trim();
|
||||
|
||||
try {
|
||||
SqliteDbController db = SqliteDbController.getInstance();
|
||||
DbController db = DbController.getInstance();
|
||||
UserParamsDAO dao = UserParamsDAO.getInstance();
|
||||
|
||||
List<UserParamEntry> entries;
|
||||
@@ -63,7 +63,7 @@ public class Net_ListUserParams_Handler implements JsonMessageHandler {
|
||||
resp.setRequestId(req.getRequestId());
|
||||
resp.setStatus(WireCodes.Status.OK);
|
||||
|
||||
SolanaUserEntry user = SolanaUsersDAO.getInstance().getByLogin(login);
|
||||
CurrentUserEntry user = CurrentUsersDAO.getInstance().getByLogin(login);
|
||||
resp.setLogin(user != null && user.getLogin() != null ? user.getLogin() : login);
|
||||
|
||||
List<Net_ListUserParams_Response.Item> items = new ArrayList<>();
|
||||
|
||||
+6
-6
@@ -11,10 +11,10 @@ import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUser
|
||||
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.SqliteDbController;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.DbController;
|
||||
import shine.db.dao.CurrentUsersDAO;
|
||||
import shine.db.dao.UserParamsDAO;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
import shine.db.entities.UserParamEntry;
|
||||
import utils.config.ShineSignatureConstants;
|
||||
import utils.crypto.Ed25519Util;
|
||||
@@ -104,13 +104,13 @@ public class Net_UpsertUserParam_Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
// ---------------- DB checks + upsert ----------------
|
||||
SqliteDbController db = SqliteDbController.getInstance();
|
||||
SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
|
||||
DbController db = DbController.getInstance();
|
||||
CurrentUsersDAO usersDAO = CurrentUsersDAO.getInstance();
|
||||
UserParamsDAO paramsDAO = UserParamsDAO.getInstance();
|
||||
|
||||
try (Connection c = db.getConnection()) {
|
||||
// 1) user exists
|
||||
SolanaUserEntry user = usersDAO.getByLogin(c, login);
|
||||
CurrentUserEntry user = usersDAO.getByLogin(c, login);
|
||||
if (user == null) {
|
||||
return NetExceptionResponseFactory.error(
|
||||
req,
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_AckSessionDelivery_Re
|
||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_AckSessionDelivery_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.SignedMessagesV2DAO;
|
||||
import shine.db.dao.SignedMessagesDAO;
|
||||
|
||||
public class Net_AckSessionDelivery_Handler implements JsonMessageHandler {
|
||||
@Override
|
||||
@@ -22,7 +22,7 @@ public class Net_AckSessionDelivery_Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
String messageKey = req.getMessageKey().trim();
|
||||
SignedMessagesV2DAO.getInstance().markDelivered(messageKey, ctx.getSessionId(), System.currentTimeMillis());
|
||||
SignedMessagesDAO.getInstance().markDelivered(messageKey, ctx.getSessionId(), System.currentTimeMillis());
|
||||
|
||||
Net_AckSessionDelivery_Response resp = new Net_AckSessionDelivery_Response();
|
||||
resp.setOp(req.getOp());
|
||||
|
||||
+3
-3
@@ -15,9 +15,9 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.JSON.utils.NetIdGenerator;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.ActiveSessionsDAO;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.dao.CurrentUsersDAO;
|
||||
import shine.db.entities.ActiveSessionEntry;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -42,7 +42,7 @@ public class Net_CallInviteBroadcast_Handler implements JsonMessageHandler {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "toLogin/callId/type=100 обязательны");
|
||||
}
|
||||
|
||||
SolanaUserEntry targetUser = SolanaUsersDAO.getInstance().getByLogin(toRequest);
|
||||
CurrentUserEntry targetUser = CurrentUsersDAO.getInstance().getByLogin(toRequest);
|
||||
if (targetUser == null) {
|
||||
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
||||
}
|
||||
|
||||
+3
-3
@@ -17,9 +17,9 @@ import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.JSON.utils.NetIdGenerator;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.ActiveSessionsDAO;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.dao.CurrentUsersDAO;
|
||||
import shine.db.entities.ActiveSessionEntry;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -50,7 +50,7 @@ public class Net_CallSignalToSession_Handler implements JsonMessageHandler {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "toLogin/targetSessionId/callId/type обязательны");
|
||||
}
|
||||
|
||||
SolanaUserEntry targetUser = SolanaUsersDAO.getInstance().getByLogin(toRequest);
|
||||
CurrentUserEntry targetUser = CurrentUsersDAO.getInstance().getByLogin(toRequest);
|
||||
if (targetUser == null) {
|
||||
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
||||
}
|
||||
|
||||
+4
-4
@@ -9,8 +9,8 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_DeleteConversation_Re
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import server.sync.DmFederationService;
|
||||
import shine.db.dao.SignedMessagesV2DAO;
|
||||
import shine.db.entities.SignedMessageV2Entry;
|
||||
import shine.db.dao.SignedMessagesDAO;
|
||||
import shine.db.entities.SignedMessageEntry;
|
||||
|
||||
public class Net_DeleteConversation_Handler implements JsonMessageHandler {
|
||||
@Override
|
||||
@@ -38,8 +38,8 @@ public class Net_DeleteConversation_Handler implements JsonMessageHandler {
|
||||
return NetExceptionResponseFactory.error(req, status, code, "Сообщение не прошло проверку");
|
||||
}
|
||||
|
||||
SignedMessageV2Entry entry = SignedMessagesCore.toEntry(block, "DeleteConversation", null);
|
||||
SignedMessagesV2DAO.ApplyStatus status = SignedMessagesV2DAO.getInstance().applyDeleteConversation(entry);
|
||||
SignedMessageEntry entry = SignedMessagesCore.toEntry(block, "DeleteConversation", null);
|
||||
SignedMessagesDAO.ApplyStatus status = SignedMessagesDAO.getInstance().applyDeleteConversation(entry);
|
||||
|
||||
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
||||
if (status.applied()) {
|
||||
|
||||
+4
-4
@@ -9,8 +9,8 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_DeleteMessage_Respons
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import server.sync.DmFederationService;
|
||||
import shine.db.dao.SignedMessagesV2DAO;
|
||||
import shine.db.entities.SignedMessageV2Entry;
|
||||
import shine.db.dao.SignedMessagesDAO;
|
||||
import shine.db.entities.SignedMessageEntry;
|
||||
|
||||
public class Net_DeleteMessage_Handler implements JsonMessageHandler {
|
||||
@Override
|
||||
@@ -38,8 +38,8 @@ public class Net_DeleteMessage_Handler implements JsonMessageHandler {
|
||||
return NetExceptionResponseFactory.error(req, status, code, "Сообщение не прошло проверку");
|
||||
}
|
||||
|
||||
SignedMessageV2Entry entry = SignedMessagesCore.toEntry(block, "DeleteMessage", null);
|
||||
SignedMessagesV2DAO.ApplyStatus status = SignedMessagesV2DAO.getInstance().applyDeleteMessage(entry);
|
||||
SignedMessageEntry entry = SignedMessagesCore.toEntry(block, "DeleteMessage", null);
|
||||
SignedMessagesDAO.ApplyStatus status = SignedMessagesDAO.getInstance().applyDeleteMessage(entry);
|
||||
|
||||
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
||||
if (status.applied()) {
|
||||
|
||||
+4
-4
@@ -10,8 +10,8 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDirectMessages_Req
|
||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDirectMessages_Response;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.SignedMessagesV2DAO;
|
||||
import shine.db.entities.SignedMessageV2Entry;
|
||||
import shine.db.dao.SignedMessagesDAO;
|
||||
import shine.db.entities.SignedMessageEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
@@ -43,7 +43,7 @@ public class Net_GetDirectMessages_Handler implements JsonMessageHandler {
|
||||
String beforeMessageKey = req.getBeforeMessageKey() == null ? "" : req.getBeforeMessageKey().trim();
|
||||
|
||||
try {
|
||||
List<SignedMessageV2Entry> page = SignedMessagesV2DAO.getInstance().listConversationPage(
|
||||
List<SignedMessageEntry> page = SignedMessagesDAO.getInstance().listConversationPage(
|
||||
login,
|
||||
peerLogin,
|
||||
beforeTimeMs,
|
||||
@@ -66,7 +66,7 @@ public class Net_GetDirectMessages_Handler implements JsonMessageHandler {
|
||||
resp.setHasMore(hasMore);
|
||||
|
||||
List<Net_GetDirectMessages_Response.MessageItem> items = new ArrayList<>();
|
||||
for (SignedMessageV2Entry entry : page) {
|
||||
for (SignedMessageEntry entry : page) {
|
||||
Net_GetDirectMessages_Response.MessageItem item = new Net_GetDirectMessages_Response.MessageItem();
|
||||
item.setMessageKey(entry.getMessageKey());
|
||||
item.setBaseKey(entry.getBaseKey());
|
||||
|
||||
+8
-8
@@ -8,8 +8,8 @@ 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.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.SignedMessagesV2DAO;
|
||||
import shine.db.entities.SignedMessageV2Entry;
|
||||
import shine.db.dao.SignedMessagesDAO;
|
||||
import shine.db.entities.SignedMessageEntry;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
@@ -40,21 +40,21 @@ public class Net_ReceiveIncomingMessage_Handler implements JsonMessageHandler {
|
||||
return NetExceptionResponseFactory.error(req, status, code, "Сообщение не прошло проверку");
|
||||
}
|
||||
|
||||
final SignedMessageV2Entry entry;
|
||||
final SignedMessageEntry entry;
|
||||
try {
|
||||
entry = SignedMessagesCore.toEntry(incoming, "ReceiveIncomingMessage", null);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, ex.getMessage(), "Некорректный payload подтверждения");
|
||||
}
|
||||
|
||||
SignedMessagesV2DAO.ApplyStatus status = incoming.isContentType()
|
||||
? SignedMessagesV2DAO.getInstance().upsertIncomingCopy(entry)
|
||||
: SignedMessagesV2DAO.getInstance().insertIfAbsent(entry);
|
||||
SignedMessagesDAO.ApplyStatus status = incoming.isContentType()
|
||||
? SignedMessagesDAO.getInstance().upsertIncomingCopy(entry)
|
||||
: SignedMessagesDAO.getInstance().insertIfAbsent(entry);
|
||||
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
||||
if (status.applied()) {
|
||||
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, incoming);
|
||||
}
|
||||
if (status == SignedMessagesV2DAO.ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE) {
|
||||
if (status == SignedMessagesDAO.ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE) {
|
||||
bounceConversationDeleteIfKnown(incoming.fromLogin, incoming.toLogin);
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ public class Net_ReceiveIncomingMessage_Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
private void bounceConversationDeleteIfKnown(String fromLogin, String toLogin) throws Exception {
|
||||
SignedMessageV2Entry tombstone = SignedMessagesV2DAO.getInstance().getLatestConversationDelete(fromLogin, toLogin);
|
||||
SignedMessageEntry tombstone = SignedMessagesDAO.getInstance().getLatestConversationDelete(fromLogin, toLogin);
|
||||
if (tombstone == null || tombstone.getRawBlock() == null || tombstone.getRawBlock().length == 0) return;
|
||||
server.sync.DmFederationService.fanOutDeleteConversation(
|
||||
tombstone.getFromLogin(),
|
||||
|
||||
+4
-4
@@ -18,11 +18,11 @@ import shine.db.dao.ActiveSessionsDAO;
|
||||
import shine.db.dao.DirectMessagesDAO;
|
||||
import shine.db.dao.SignedDirectMessagesHistoryDAO;
|
||||
import shine.db.dao.SignedDmReplayDAO;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.dao.CurrentUsersDAO;
|
||||
import shine.db.entities.ActiveSessionEntry;
|
||||
import shine.db.entities.DirectMessageEntry;
|
||||
import shine.db.entities.SignedDirectMessageHistoryEntry;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
import utils.crypto.Ed25519Util;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -54,8 +54,8 @@ public class Net_SendDirectMessage_Handler implements JsonMessageHandler {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, ex.getMessage(), "Некорректный формат пакета");
|
||||
}
|
||||
|
||||
SolanaUserEntry fromUser = SolanaUsersDAO.getInstance().getByLogin(packet.fromLogin);
|
||||
SolanaUserEntry toUser = SolanaUsersDAO.getInstance().getByLogin(packet.toLogin);
|
||||
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 пользователь не найден");
|
||||
}
|
||||
|
||||
+11
-11
@@ -9,8 +9,8 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_SendMessagePair_Respo
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import server.sync.DmFederationService;
|
||||
import shine.db.dao.SignedMessagesV2DAO;
|
||||
import shine.db.entities.SignedMessageV2Entry;
|
||||
import shine.db.dao.SignedMessagesDAO;
|
||||
import shine.db.entities.SignedMessageEntry;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
@@ -41,8 +41,8 @@ public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
||||
return NetExceptionResponseFactory.error(req, status, code, "Сообщение не прошло проверку");
|
||||
}
|
||||
|
||||
SignedMessageV2Entry incomingEntry;
|
||||
SignedMessageV2Entry outgoingEntry;
|
||||
SignedMessageEntry incomingEntry;
|
||||
SignedMessageEntry outgoingEntry;
|
||||
try {
|
||||
String sourceApi = "SendMessagePair";
|
||||
String originSessionId = (ctx != null && !isBlank(ctx.getSessionId())) ? ctx.getSessionId() : null;
|
||||
@@ -52,15 +52,15 @@ public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, ex.getMessage(), "Некорректный payload подтверждения");
|
||||
}
|
||||
|
||||
SignedMessagesV2DAO.ApplyStatus pairStatus;
|
||||
SignedMessagesDAO.ApplyStatus pairStatus;
|
||||
if (incoming.isContentType()) {
|
||||
pairStatus = SignedMessagesV2DAO.getInstance().upsertContentPair(
|
||||
pairStatus = SignedMessagesDAO.getInstance().upsertContentPair(
|
||||
incomingEntry, outgoingEntry
|
||||
);
|
||||
} else {
|
||||
pairStatus = SignedMessagesV2DAO.getInstance().insertPairBothOrNothing(incomingEntry, outgoingEntry)
|
||||
? SignedMessagesV2DAO.ApplyStatus.APPLIED
|
||||
: SignedMessagesV2DAO.ApplyStatus.DUPLICATE_OR_OLDER;
|
||||
pairStatus = SignedMessagesDAO.getInstance().insertPairBothOrNothing(incomingEntry, outgoingEntry)
|
||||
? SignedMessagesDAO.ApplyStatus.APPLIED
|
||||
: SignedMessagesDAO.ApplyStatus.DUPLICATE_OR_OLDER;
|
||||
}
|
||||
|
||||
SignedMessagesRealtime.DeliveryCounters inCounters = new SignedMessagesRealtime.DeliveryCounters();
|
||||
@@ -77,7 +77,7 @@ public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
||||
outCounters = SignedMessagesRealtime.deliverToRelevantSessions(outgoingEntry, outgoing, excludeSessionId);
|
||||
}
|
||||
|
||||
if (pairStatus == SignedMessagesV2DAO.ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE) {
|
||||
if (pairStatus == SignedMessagesDAO.ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE) {
|
||||
bounceConversationDeleteIfKnown(incoming.fromLogin, incoming.toLogin);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
private void bounceConversationDeleteIfKnown(String fromLogin, String toLogin) throws Exception {
|
||||
SignedMessageV2Entry tombstone = SignedMessagesV2DAO.getInstance().getLatestConversationDelete(fromLogin, toLogin);
|
||||
SignedMessageEntry tombstone = SignedMessagesDAO.getInstance().getLatestConversationDelete(fromLogin, toLogin);
|
||||
if (tombstone == null || tombstone.getRawBlock() == null || tombstone.getRawBlock().length == 0) return;
|
||||
DmFederationService.fanOutDeleteConversation(
|
||||
tombstone.getFromLogin(),
|
||||
|
||||
+4
-4
@@ -16,9 +16,9 @@ import server.logic.ws_protocol.JSON.push.WsEventSender;
|
||||
import server.logic.ws_protocol.JSON.utils.AuthKeyUtils;
|
||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||
import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.dao.CurrentUsersDAO;
|
||||
import shine.db.entities.ActiveSessionEntry;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
import utils.crypto.Ed25519Util;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
@@ -74,12 +74,12 @@ public class Net_SendSignal_Handler implements JsonMessageHandler {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "TIME_SKEW", "Время клиента отличается от сервера более чем на 30 секунд");
|
||||
}
|
||||
|
||||
SolanaUserEntry senderUser = ctx.getSolanaUser();
|
||||
CurrentUserEntry senderUser = ctx.getCurrentUser();
|
||||
if (senderUser == null || senderUser.getClientKey() == null || senderUser.getClientKey().isBlank()) {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.SERVER_DATA_ERROR, "NO_CLIENT_KEY", "Для пользователя не найден client key");
|
||||
}
|
||||
|
||||
SolanaUserEntry targetUser = SolanaUsersDAO.getInstance().getByLogin(toRequest);
|
||||
CurrentUserEntry targetUser = CurrentUsersDAO.getInstance().getByLogin(toRequest);
|
||||
if (targetUser == null) {
|
||||
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
||||
}
|
||||
|
||||
+10
-14
@@ -1,9 +1,8 @@
|
||||
package server.logic.ws_protocol.JSON.messages;
|
||||
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.SolanaUserPdaImportService;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.entities.SignedMessageV2Entry;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.dao.CurrentUsersDAO;
|
||||
import shine.db.entities.SignedMessageEntry;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
import utils.crypto.Ed25519Util;
|
||||
|
||||
import java.util.Base64;
|
||||
@@ -47,25 +46,22 @@ final class SignedMessagesCore {
|
||||
}
|
||||
|
||||
static void verifyUsersAndSignature(SignedMessageBlock block) throws Exception {
|
||||
SolanaUserEntry from = resolveUser(block.fromLogin);
|
||||
SolanaUserEntry to = resolveUser(block.toLogin);
|
||||
CurrentUserEntry from = resolveUser(block.fromLogin);
|
||||
CurrentUserEntry to = resolveUser(block.toLogin);
|
||||
if (from == null || to == null) {
|
||||
throw new IllegalArgumentException("USER_NOT_FOUND");
|
||||
}
|
||||
|
||||
String signerLogin = block.isSignedByRecipient() ? block.toLogin : block.fromLogin;
|
||||
SolanaUserEntry signer = signerLogin.equalsIgnoreCase(block.fromLogin) ? from : to;
|
||||
CurrentUserEntry signer = signerLogin.equalsIgnoreCase(block.fromLogin) ? from : to;
|
||||
byte[] pubKey32 = Ed25519Util.keyFromBase64(signer.getClientKey());
|
||||
if (!Ed25519Util.verify(block.signedBody, block.signature64, pubKey32)) {
|
||||
throw new IllegalArgumentException("BAD_SIGNATURE");
|
||||
}
|
||||
}
|
||||
|
||||
private static SolanaUserEntry resolveUser(String login) throws Exception {
|
||||
SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
|
||||
SolanaUserEntry user = usersDao.getByLogin(login);
|
||||
if (user != null) return user;
|
||||
return SolanaUserPdaImportService.findOrImportByLogin(login);
|
||||
private static CurrentUserEntry resolveUser(String login) throws Exception {
|
||||
return CurrentUsersDAO.getInstance().getByLogin(login);
|
||||
}
|
||||
|
||||
static void validatePair(SignedMessageBlock incoming, SignedMessageBlock outgoing) {
|
||||
@@ -100,11 +96,11 @@ final class SignedMessagesCore {
|
||||
}
|
||||
}
|
||||
|
||||
static SignedMessageV2Entry toEntry(SignedMessageBlock block, String sourceApi, String originSessionId) {
|
||||
static SignedMessageEntry toEntry(SignedMessageBlock block, String sourceApi, String originSessionId) {
|
||||
String baseKey = SignedMessageKeys.baseKey(block.toLogin, block.fromLogin, block.timeMs, block.nonce);
|
||||
String messageKey = SignedMessageKeys.messageKey(block.toLogin, block.fromLogin, block.timeMs, block.nonce, block.messageType);
|
||||
|
||||
SignedMessageV2Entry entry = new SignedMessageV2Entry();
|
||||
SignedMessageEntry entry = new SignedMessageEntry();
|
||||
entry.setMessageKey(messageKey);
|
||||
entry.setBaseKey(baseKey);
|
||||
entry.setTargetLogin(primaryTargetLogin(block));
|
||||
|
||||
+11
-11
@@ -9,9 +9,9 @@ import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.push.WebPushSender;
|
||||
import server.logic.ws_protocol.JSON.push.WsEventSender;
|
||||
import shine.db.dao.ActiveSessionsDAO;
|
||||
import shine.db.dao.SignedMessagesV2DAO;
|
||||
import shine.db.dao.SignedMessagesDAO;
|
||||
import shine.db.entities.ActiveSessionEntry;
|
||||
import shine.db.entities.SignedMessageV2Entry;
|
||||
import shine.db.entities.SignedMessageEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
@@ -37,12 +37,12 @@ public final class SignedMessagesRealtime {
|
||||
|
||||
private SignedMessagesRealtime() {}
|
||||
|
||||
static DeliveryCounters deliverToRelevantSessions(SignedMessageV2Entry message, SignedMessageBlock block) throws Exception {
|
||||
static DeliveryCounters deliverToRelevantSessions(SignedMessageEntry message, SignedMessageBlock block) throws Exception {
|
||||
return deliverToRelevantSessions(message, block, null);
|
||||
}
|
||||
|
||||
static DeliveryCounters deliverToRelevantSessions(
|
||||
SignedMessageV2Entry message,
|
||||
SignedMessageEntry message,
|
||||
SignedMessageBlock block,
|
||||
String excludeSessionId
|
||||
) throws Exception {
|
||||
@@ -58,7 +58,7 @@ public final class SignedMessagesRealtime {
|
||||
}
|
||||
sessionIdsToTrack.add(sessionId);
|
||||
}
|
||||
SignedMessagesV2DAO.getInstance().ensureDeliveryRows(message.getMessageKey(), sessionIdsToTrack, now);
|
||||
SignedMessagesDAO.getInstance().ensureDeliveryRows(message.getMessageKey(), sessionIdsToTrack, now);
|
||||
for (ActiveSessionEntry s : sessions) {
|
||||
String sessionId = s.getSessionId();
|
||||
if (excludeSessionId != null && excludeSessionId.equals(sessionId)) {
|
||||
@@ -97,9 +97,9 @@ public final class SignedMessagesRealtime {
|
||||
|
||||
private static void dispatchPendingForSession(String login, String sessionId) {
|
||||
try {
|
||||
List<SignedMessageV2Entry> pending = SignedMessagesV2DAO.getInstance()
|
||||
List<SignedMessageEntry> pending = SignedMessagesDAO.getInstance()
|
||||
.listPendingForSession(login, sessionId);
|
||||
for (SignedMessageV2Entry e : pending) {
|
||||
for (SignedMessageEntry e : pending) {
|
||||
sendEventToSessionIfOnline(sessionId, login, e, true);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -110,7 +110,7 @@ public final class SignedMessagesRealtime {
|
||||
private static boolean sendEventToSessionIfOnline(
|
||||
String sessionId,
|
||||
String actualTargetLogin,
|
||||
SignedMessageV2Entry message,
|
||||
SignedMessageEntry message,
|
||||
boolean backlog
|
||||
) {
|
||||
ConnectionContext targetCtx = ActiveConnectionsRegistry.getInstance().getBySessionId(sessionId);
|
||||
@@ -134,14 +134,14 @@ public final class SignedMessagesRealtime {
|
||||
return WsEventSender.sendEvent(targetCtx, "SignedMessageArrived", message.getMessageKey(), payload);
|
||||
}
|
||||
|
||||
private static boolean shouldPushNewIncomingMessage(String targetLogin, SignedMessageV2Entry message, SignedMessageBlock block) {
|
||||
private static boolean shouldPushNewIncomingMessage(String targetLogin, SignedMessageEntry message, SignedMessageBlock block) {
|
||||
if (block == null) return false;
|
||||
if (message.getMessageType() != SignedMessageBlock.TYPE_INCOMING_TEXT) return false;
|
||||
if (!targetLogin.equalsIgnoreCase(message.getToLogin())) return false;
|
||||
return block.revisionTimeMs == 0;
|
||||
}
|
||||
|
||||
private static boolean pushNewMessageNotification(ActiveSessionEntry session, SignedMessageV2Entry message) {
|
||||
private static boolean pushNewMessageNotification(ActiveSessionEntry session, SignedMessageEntry message) {
|
||||
try {
|
||||
if (session == null) return false;
|
||||
if (isBlank(session.getPushEndpoint()) || isBlank(session.getPushP256dhKey()) || isBlank(session.getPushAuthKey())) {
|
||||
@@ -160,7 +160,7 @@ public final class SignedMessagesRealtime {
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> targetLoginsForMessage(SignedMessageV2Entry message) {
|
||||
private static List<String> targetLoginsForMessage(SignedMessageEntry message) {
|
||||
Set<String> out = new LinkedHashSet<>();
|
||||
int type = message.getMessageType();
|
||||
if (type == SignedMessageBlock.TYPE_INCOMING_TEXT || type == SignedMessageBlock.TYPE_READ_INCOMING) {
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
////package server.logic.ws_protocol.JSON.utils;
|
||||
//
|
||||
//import shine.db.entities.SolanaUserEntry;
|
||||
//import shine.db.entities.CurrentUserEntry;
|
||||
//import utils.crypto.Ed25519Util;
|
||||
//
|
||||
//import java.nio.charset.StandardCharsets;
|
||||
@@ -35,7 +35,7 @@
|
||||
// * Подпись проверяется над preimageCreateAuthSession(...).
|
||||
// */
|
||||
// public static boolean verifyCreateAuthSessionSignature(
|
||||
// SolanaUserEntry user,
|
||||
// CurrentUserEntry user,
|
||||
// String login,
|
||||
// String authNonce,
|
||||
// long timeMs,
|
||||
|
||||
+13
-1
@@ -114,6 +114,12 @@ public final class AddBlockSyncService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstTry.serverAlreadyHasBlock(blockNumber)) {
|
||||
log.info("AddBlock sync skipped: partner already has block. partner={} blockchainName={} blockNumber={} remoteLast={}",
|
||||
partner.getLogin(), blockchainName, blockNumber, firstTry.serverLastGlobalNumber());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!firstTry.needsBackfill()) {
|
||||
log.warn("AddBlock sync failed without backfill: partner={} blockchainName={} blockNumber={} code={}",
|
||||
partner.getLogin(), blockchainName, blockNumber, firstTry.code());
|
||||
@@ -123,7 +129,7 @@ public final class AddBlockSyncService {
|
||||
int remoteLast = firstTry.serverLastGlobalNumber();
|
||||
int fromBlockNumber = remoteLast + 1;
|
||||
if (fromBlockNumber > blockNumber) {
|
||||
log.warn("AddBlock sync inconsistent backfill window: partner={} blockchainName={} remoteLast={} target={}",
|
||||
log.info("AddBlock sync skipped: partner already caught up during backfill window. partner={} blockchainName={} remoteLast={} target={}",
|
||||
partner.getLogin(), blockchainName, remoteLast, blockNumber);
|
||||
return;
|
||||
}
|
||||
@@ -295,6 +301,12 @@ public final class AddBlockSyncService {
|
||||
boolean needsBackfill() {
|
||||
return !ok && ("bad_prev_hash".equalsIgnoreCase(code) || "bad_block_number".equalsIgnoreCase(code));
|
||||
}
|
||||
|
||||
boolean serverAlreadyHasBlock(int targetBlockNumber) {
|
||||
return !ok
|
||||
&& serverLastGlobalNumber != Integer.MIN_VALUE
|
||||
&& serverLastGlobalNumber >= targetBlockNumber;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SyncWsListener implements WebSocket.Listener {
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
import shine.db.entities.CurrentUserEntry;
|
||||
|
||||
import java.net.SocketAddress;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
@@ -57,7 +57,7 @@ public final class WsConnectionUtils {
|
||||
final String sessionId = safeString(ctx.getSessionId());
|
||||
final int authStatus = safeAuthStatus(ctx);
|
||||
|
||||
final SolanaUserEntry user = ctx.getSolanaUser();
|
||||
final CurrentUserEntry user = ctx.getCurrentUser();
|
||||
final String login = (user != null ? safeString(user.getLogin()) : "");
|
||||
|
||||
final String activeSessionId =
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
}
|
||||
|
||||
group = 'shine'
|
||||
version = '1.0.0'
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':shine-server-config')
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.2'
|
||||
implementation 'org.postgresql:postgresql:42.7.7'
|
||||
implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1'
|
||||
implementation 'org.slf4j:slf4j-api:2.0.16'
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
+1444
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,234 @@
|
||||
package sync.config;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
public record AppConfig(
|
||||
String rpcUrl,
|
||||
String websocketUrl,
|
||||
String programId,
|
||||
String databaseUrl,
|
||||
String databaseUser,
|
||||
String databasePassword,
|
||||
Duration pollInterval,
|
||||
String commitment
|
||||
) {
|
||||
|
||||
public static final String DEFAULT_PROGRAM_ID =
|
||||
"SHiNEPr1APdAgNBteUyBXcNovaHctpSjUu8oH2ZJdN6";
|
||||
|
||||
public static final String FIXED_COMMITMENT =
|
||||
"confirmed";
|
||||
|
||||
public static final String ENABLED_KEY =
|
||||
"solana.users.sync.enabled";
|
||||
public static final String RPC_URL_KEY =
|
||||
"solana.users.sync.rpcUrl";
|
||||
public static final String WEBSOCKET_URL_KEY =
|
||||
"solana.users.sync.wsUrl";
|
||||
public static final String PROGRAM_ID_KEY =
|
||||
"solana.users.sync.programId";
|
||||
public static final String DATABASE_URL_KEY =
|
||||
"solana.users.sync.databaseUrl";
|
||||
public static final String DATABASE_USER_KEY =
|
||||
"solana.users.sync.dbUser";
|
||||
public static final String DATABASE_PASSWORD_KEY =
|
||||
"solana.users.sync.dbPassword";
|
||||
public static final String POLL_INTERVAL_KEY =
|
||||
"solana.users.sync.pollIntervalSeconds";
|
||||
public static final String LEGACY_SOLANA_RPC_URL_KEY =
|
||||
"solana.rpcUrl";
|
||||
|
||||
public static boolean isEnabled(
|
||||
utils.config.AppConfig serverConfig
|
||||
) {
|
||||
String value =
|
||||
trimToNull(
|
||||
serverConfig.getParam(ENABLED_KEY)
|
||||
);
|
||||
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean.parseBoolean(value);
|
||||
}
|
||||
|
||||
public static AppConfig fromServerConfig(
|
||||
utils.config.AppConfig serverConfig
|
||||
) {
|
||||
|
||||
String rpcUrl =
|
||||
firstRequired(
|
||||
serverConfig,
|
||||
"Solana users sync RPC URL",
|
||||
RPC_URL_KEY,
|
||||
LEGACY_SOLANA_RPC_URL_KEY
|
||||
);
|
||||
|
||||
String websocketUrl =
|
||||
requireParam(
|
||||
serverConfig,
|
||||
WEBSOCKET_URL_KEY
|
||||
);
|
||||
|
||||
String programId =
|
||||
optionalParam(
|
||||
serverConfig,
|
||||
PROGRAM_ID_KEY
|
||||
);
|
||||
|
||||
if (programId == null) {
|
||||
programId =
|
||||
DEFAULT_PROGRAM_ID;
|
||||
}
|
||||
|
||||
String databaseUrl =
|
||||
requireParam(
|
||||
serverConfig,
|
||||
DATABASE_URL_KEY
|
||||
);
|
||||
|
||||
String databaseUser =
|
||||
requireParam(
|
||||
serverConfig,
|
||||
DATABASE_USER_KEY
|
||||
);
|
||||
|
||||
String databasePassword =
|
||||
requireParam(
|
||||
serverConfig,
|
||||
DATABASE_PASSWORD_KEY
|
||||
);
|
||||
|
||||
long pollIntervalSeconds =
|
||||
parsePositiveLong(
|
||||
optionalParam(
|
||||
serverConfig,
|
||||
POLL_INTERVAL_KEY
|
||||
),
|
||||
300L,
|
||||
POLL_INTERVAL_KEY
|
||||
);
|
||||
|
||||
return new AppConfig(
|
||||
rpcUrl,
|
||||
websocketUrl,
|
||||
programId,
|
||||
databaseUrl,
|
||||
databaseUser,
|
||||
databasePassword,
|
||||
Duration.ofSeconds(
|
||||
pollIntervalSeconds
|
||||
),
|
||||
FIXED_COMMITMENT
|
||||
);
|
||||
}
|
||||
|
||||
private static long parsePositiveLong(
|
||||
String rawValue,
|
||||
long defaultValue,
|
||||
String envName
|
||||
) {
|
||||
|
||||
if (rawValue == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
long value =
|
||||
Long.parseLong(
|
||||
rawValue
|
||||
);
|
||||
|
||||
if (value <= 0L) {
|
||||
throw new IllegalArgumentException(
|
||||
envName + " must be > 0"
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
|
||||
} catch (NumberFormatException exception) {
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
envName + " must be a positive integer",
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static String firstRequired(
|
||||
utils.config.AppConfig serverConfig,
|
||||
String humanName,
|
||||
String... names
|
||||
) {
|
||||
|
||||
for (String name : names) {
|
||||
String value =
|
||||
optionalParam(
|
||||
serverConfig,
|
||||
name
|
||||
);
|
||||
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException(
|
||||
"Missing required server config: "
|
||||
+ humanName
|
||||
);
|
||||
}
|
||||
|
||||
private static String requireParam(
|
||||
utils.config.AppConfig serverConfig,
|
||||
String name
|
||||
) {
|
||||
|
||||
String value =
|
||||
optionalParam(
|
||||
serverConfig,
|
||||
name
|
||||
);
|
||||
|
||||
if (value == null) {
|
||||
throw new IllegalStateException(
|
||||
"Missing required server config: "
|
||||
+ name
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String optionalParam(
|
||||
utils.config.AppConfig serverConfig,
|
||||
String name
|
||||
) {
|
||||
|
||||
String value =
|
||||
serverConfig.getParam(name);
|
||||
|
||||
return trimToNull(value);
|
||||
}
|
||||
|
||||
private static String trimToNull(
|
||||
String value
|
||||
) {
|
||||
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String trimmed =
|
||||
value.trim();
|
||||
|
||||
if (trimmed.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package sync.model;
|
||||
|
||||
public record ProgramAccountUpdate(
|
||||
String address,
|
||||
String owner,
|
||||
long lamports,
|
||||
long slot,
|
||||
String dataBase64,
|
||||
boolean executable,
|
||||
Long rentEpoch
|
||||
) {
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package sync.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record SnapshotResult(
|
||||
long snapshotSlot,
|
||||
List<ProgramAccountUpdate> accounts
|
||||
) {
|
||||
}
|
||||
+822
@@ -0,0 +1,822 @@
|
||||
package sync.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sync.codec.ShineUsersCodec;
|
||||
import sync.config.AppConfig;
|
||||
import sync.model.ProgramAccountUpdate;
|
||||
import sync.model.SnapshotResult;
|
||||
import sync.source.AccountUpdateListener;
|
||||
import sync.source.ConnectionListener;
|
||||
import sync.source.rpc.SolanaRpcClient;
|
||||
import sync.source.rpc.SolanaWebSocketClient;
|
||||
import sync.storage.postgres.PostgresStorageRepository;
|
||||
import sync.util.SolanaPdaUtil;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class SolanaUsersSyncService
|
||||
implements AutoCloseable {
|
||||
|
||||
private static final int FULL_SNAPSHOT_PROGRESS_STEP =
|
||||
100;
|
||||
|
||||
private static final Logger log =
|
||||
LoggerFactory.getLogger(
|
||||
SolanaUsersSyncService.class
|
||||
);
|
||||
|
||||
private final AppConfig config;
|
||||
private final ObjectMapper mapper;
|
||||
private final PostgresStorageRepository storage;
|
||||
private final SolanaRpcClient rpcClient;
|
||||
private final SolanaWebSocketClient webSocketClient;
|
||||
private final ExecutorService syncExecutor;
|
||||
private final ScheduledExecutorService pollScheduler;
|
||||
private final CompletableFuture<Void> readyFuture =
|
||||
new CompletableFuture<>();
|
||||
private final AtomicBoolean closed =
|
||||
new AtomicBoolean(false);
|
||||
private final AtomicBoolean syncRequested =
|
||||
new AtomicBoolean(false);
|
||||
private final AtomicBoolean syncWorkerScheduled =
|
||||
new AtomicBoolean(false);
|
||||
private final String economyConfigPda;
|
||||
|
||||
private volatile boolean initialSyncCompleted =
|
||||
false;
|
||||
|
||||
public SolanaUsersSyncService(
|
||||
AppConfig config
|
||||
) throws Exception {
|
||||
|
||||
this.config =
|
||||
config;
|
||||
|
||||
this.mapper =
|
||||
new ObjectMapper();
|
||||
|
||||
this.storage =
|
||||
new PostgresStorageRepository(
|
||||
config.databaseUrl(),
|
||||
config.databaseUser(),
|
||||
config.databasePassword(),
|
||||
mapper
|
||||
);
|
||||
|
||||
this.rpcClient =
|
||||
new SolanaRpcClient(
|
||||
config.rpcUrl(),
|
||||
config.programId(),
|
||||
config.commitment()
|
||||
);
|
||||
|
||||
this.webSocketClient =
|
||||
new SolanaWebSocketClient(
|
||||
config.websocketUrl(),
|
||||
config.programId(),
|
||||
config.commitment()
|
||||
);
|
||||
|
||||
this.syncExecutor =
|
||||
Executors.newSingleThreadExecutor(
|
||||
runnable -> {
|
||||
Thread thread =
|
||||
new Thread(
|
||||
runnable,
|
||||
"solana-users-sync-worker"
|
||||
);
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}
|
||||
);
|
||||
|
||||
this.pollScheduler =
|
||||
Executors.newSingleThreadScheduledExecutor(
|
||||
runnable -> {
|
||||
Thread thread =
|
||||
new Thread(
|
||||
runnable,
|
||||
"solana-users-sync-periodic"
|
||||
);
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}
|
||||
);
|
||||
|
||||
this.economyConfigPda =
|
||||
SolanaPdaUtil.findProgramAddress(
|
||||
List.of(
|
||||
"shine_users_economy_config"
|
||||
.getBytes(StandardCharsets.UTF_8)
|
||||
),
|
||||
config.programId()
|
||||
);
|
||||
}
|
||||
|
||||
public void start()
|
||||
throws Exception {
|
||||
|
||||
log.info(
|
||||
"Starting sync service. programId={} economyConfigPda={} pollInterval={}",
|
||||
config.programId(),
|
||||
economyConfigPda,
|
||||
config.pollInterval()
|
||||
);
|
||||
|
||||
storage.updateLifecycleState(
|
||||
"STARTING",
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
webSocketClient.start(
|
||||
new AccountUpdateListener() {
|
||||
@Override
|
||||
public void onAccountUpdate(
|
||||
ProgramAccountUpdate update
|
||||
) {
|
||||
log.debug(
|
||||
"Realtime notification received. address={} slot={}",
|
||||
update.address(),
|
||||
update.slot()
|
||||
);
|
||||
requestSync(
|
||||
"realtime"
|
||||
);
|
||||
}
|
||||
},
|
||||
new ConnectionListener() {
|
||||
@Override
|
||||
public void onConnected(
|
||||
boolean firstConnection
|
||||
) {
|
||||
log.info(
|
||||
"Solana websocket connected. firstConnection={}",
|
||||
firstConnection
|
||||
);
|
||||
requestSync(
|
||||
firstConnection
|
||||
? "initial-connect"
|
||||
: "reconnect"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnected(
|
||||
Throwable cause
|
||||
) {
|
||||
log.warn(
|
||||
"Solana websocket disconnected: {}",
|
||||
cause == null
|
||||
? "unknown"
|
||||
: cause.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
long periodSeconds =
|
||||
config.pollInterval()
|
||||
.getSeconds();
|
||||
|
||||
pollScheduler.scheduleWithFixedDelay(
|
||||
() -> requestSync(
|
||||
"periodic"
|
||||
),
|
||||
periodSeconds,
|
||||
periodSeconds,
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
}
|
||||
|
||||
public void awaitReady()
|
||||
throws Exception {
|
||||
readyFuture.get();
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return readyFuture.isDone()
|
||||
&& !readyFuture.isCompletedExceptionally();
|
||||
}
|
||||
|
||||
private void requestSync(
|
||||
String reason
|
||||
) {
|
||||
|
||||
if (closed.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
syncRequested.set(true);
|
||||
|
||||
if (!syncWorkerScheduled.compareAndSet(
|
||||
false,
|
||||
true
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
syncExecutor.submit(
|
||||
() -> runSyncLoop(reason)
|
||||
);
|
||||
}
|
||||
|
||||
private void runSyncLoop(
|
||||
String firstReason
|
||||
) {
|
||||
|
||||
String reason =
|
||||
firstReason;
|
||||
|
||||
try {
|
||||
|
||||
while (!closed.get()) {
|
||||
|
||||
boolean shouldRun =
|
||||
syncRequested.getAndSet(false);
|
||||
|
||||
if (!shouldRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
performSync(reason);
|
||||
reason = "coalesced";
|
||||
}
|
||||
|
||||
} catch (Exception exception) {
|
||||
|
||||
log.error(
|
||||
"Sync loop failed",
|
||||
exception
|
||||
);
|
||||
|
||||
try {
|
||||
storage.updateLifecycleState(
|
||||
"FAILED",
|
||||
false,
|
||||
exception.getMessage(),
|
||||
System.currentTimeMillis(),
|
||||
null
|
||||
);
|
||||
} catch (Exception storageException) {
|
||||
log.error(
|
||||
"Failed to persist sync failure state",
|
||||
storageException
|
||||
);
|
||||
}
|
||||
|
||||
readyFuture.completeExceptionally(
|
||||
exception
|
||||
);
|
||||
|
||||
} finally {
|
||||
|
||||
syncWorkerScheduled.set(false);
|
||||
|
||||
if (syncRequested.get()
|
||||
&& !closed.get()
|
||||
&& syncWorkerScheduled.compareAndSet(
|
||||
false,
|
||||
true
|
||||
)) {
|
||||
syncExecutor.submit(
|
||||
() -> runSyncLoop("rescheduled")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void performSync(
|
||||
String reason
|
||||
) throws Exception {
|
||||
|
||||
long nowMs =
|
||||
System.currentTimeMillis();
|
||||
|
||||
PostgresStorageRepository.SyncStateSnapshot state =
|
||||
storage.loadState();
|
||||
|
||||
log.info(
|
||||
"Starting history sync. reason={} lastSeenSignature={}",
|
||||
reason,
|
||||
state.lastSeenSignature()
|
||||
);
|
||||
|
||||
storage.updateLifecycleState(
|
||||
initialSyncCompleted
|
||||
? "SYNCING"
|
||||
: "BOOTSTRAPPING",
|
||||
false,
|
||||
null,
|
||||
nowMs,
|
||||
null
|
||||
);
|
||||
|
||||
SolanaRpcClient.SignatureFetchResult fetchResult =
|
||||
rpcClient.getSignaturesForAddressSince(
|
||||
economyConfigPda,
|
||||
state.lastSeenSignature()
|
||||
);
|
||||
|
||||
if (state.lastSeenSignature() != null
|
||||
&& !fetchResult.anchorFound()) {
|
||||
|
||||
log.error(
|
||||
"History anchor signature not found anymore: {}. Running current-state full snapshot fallback.",
|
||||
state.lastSeenSignature()
|
||||
);
|
||||
|
||||
runFullSnapshotFallback(
|
||||
state,
|
||||
fetchResult,
|
||||
nowMs
|
||||
);
|
||||
|
||||
markReadyAfterSync(
|
||||
state,
|
||||
nowMs
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (fetchResult.signatures().isEmpty()) {
|
||||
|
||||
PostgresStorageRepository.SyncStateSnapshot newState =
|
||||
new PostgresStorageRepository.SyncStateSnapshot(
|
||||
"READY",
|
||||
true,
|
||||
nowMs,
|
||||
nowMs,
|
||||
state.lastSeenSignature(),
|
||||
state.lastSeenSlot(),
|
||||
state.lastRelevantSignature(),
|
||||
state.lastRelevantSlot(),
|
||||
null,
|
||||
state.economyConfigState(),
|
||||
nowMs
|
||||
);
|
||||
|
||||
storage.applyHistoryBatch(
|
||||
List.of(),
|
||||
List.of(),
|
||||
newState
|
||||
);
|
||||
|
||||
log.info(
|
||||
"History sync completed with no new transactions."
|
||||
);
|
||||
|
||||
markReadyAfterSync(
|
||||
newState,
|
||||
nowMs
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
List<SolanaRpcClient.SignatureRecord> chronologicalSignatures =
|
||||
new ArrayList<>(
|
||||
fetchResult.signatures()
|
||||
);
|
||||
|
||||
Collections.reverse(
|
||||
chronologicalSignatures
|
||||
);
|
||||
|
||||
List<ParsedTxEnvelope> envelopes =
|
||||
new ArrayList<>();
|
||||
|
||||
Set<String> updatePdaAddresses =
|
||||
new LinkedHashSet<>();
|
||||
|
||||
for (SolanaRpcClient.SignatureRecord signatureRecord : chronologicalSignatures) {
|
||||
|
||||
JsonNode transaction =
|
||||
rpcClient.getTransactionJsonParsed(
|
||||
signatureRecord.signature()
|
||||
);
|
||||
|
||||
ParsedTxEnvelope envelope =
|
||||
parseTransactionEnvelope(
|
||||
signatureRecord,
|
||||
transaction
|
||||
);
|
||||
|
||||
envelopes.add(
|
||||
envelope
|
||||
);
|
||||
|
||||
if (envelope.parsedInstruction() != null
|
||||
&& envelope.parsedInstruction().kind() == ShineUsersCodec.TxKind.UPDATE_USER_PDA
|
||||
&& envelope.parsedInstruction().affectedPdaAddress() != null) {
|
||||
updatePdaAddresses.add(
|
||||
envelope.parsedInstruction()
|
||||
.affectedPdaAddress()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, ShineUsersCodec.UserPdaSnapshot> currentSnapshots =
|
||||
storage.getCurrentSnapshots(
|
||||
updatePdaAddresses
|
||||
);
|
||||
|
||||
List<PostgresStorageRepository.TxHistoryEntry> txEntries =
|
||||
new ArrayList<>();
|
||||
|
||||
List<ShineUsersCodec.UserPdaSnapshot> snapshotsToPersist =
|
||||
new ArrayList<>();
|
||||
|
||||
ShineUsersCodec.EconomyConfigState economyState =
|
||||
state.economyConfigState();
|
||||
|
||||
String lastRelevantSignature =
|
||||
state.lastRelevantSignature();
|
||||
|
||||
Long lastRelevantSlot =
|
||||
state.lastRelevantSlot();
|
||||
|
||||
for (ParsedTxEnvelope envelope : envelopes) {
|
||||
|
||||
ShineUsersCodec.ParsedInstruction parsedInstruction =
|
||||
envelope.parsedInstruction();
|
||||
|
||||
String txKind =
|
||||
parsedInstruction == null
|
||||
? "failed_or_unavailable"
|
||||
: parsedInstruction.kind().name();
|
||||
|
||||
boolean relevant =
|
||||
false;
|
||||
|
||||
String affectedPdaAddress =
|
||||
null;
|
||||
|
||||
String affectedLogin =
|
||||
null;
|
||||
|
||||
if (parsedInstruction != null) {
|
||||
|
||||
if (parsedInstruction.kind() == ShineUsersCodec.TxKind.INIT_USERS_ECONOMY_CONFIG
|
||||
|| parsedInstruction.kind() == ShineUsersCodec.TxKind.UPDATE_USERS_ECONOMY_CONFIG) {
|
||||
economyState =
|
||||
parsedInstruction.economyConfigState();
|
||||
}
|
||||
|
||||
if (parsedInstruction.relevant()
|
||||
&& parsedInstruction.userPdaMutation() != null) {
|
||||
|
||||
relevant = true;
|
||||
affectedPdaAddress = parsedInstruction.affectedPdaAddress();
|
||||
affectedLogin = parsedInstruction.affectedLogin();
|
||||
|
||||
ShineUsersCodec.UserPdaSnapshot snapshot;
|
||||
|
||||
if (parsedInstruction.kind() == ShineUsersCodec.TxKind.CREATE_USER_PDA) {
|
||||
|
||||
if (economyState == null) {
|
||||
economyState =
|
||||
ShineUsersCodec.EconomyConfigState.initial();
|
||||
log.warn(
|
||||
"Economy config state was absent while processing create tx {}. Falling back to initial constants.",
|
||||
envelope.signatureRecord().signature()
|
||||
);
|
||||
}
|
||||
|
||||
snapshot =
|
||||
ShineUsersCodec.buildCreateSnapshot(
|
||||
parsedInstruction.userPdaMutation(),
|
||||
economyState,
|
||||
envelope.signatureRecord().signature(),
|
||||
envelope.signatureRecord().slot()
|
||||
);
|
||||
|
||||
} else {
|
||||
|
||||
ShineUsersCodec.UserPdaSnapshot previous =
|
||||
currentSnapshots.get(
|
||||
affectedPdaAddress
|
||||
);
|
||||
|
||||
if (previous == null) {
|
||||
throw new IllegalStateException(
|
||||
"Missing previous snapshot for update PDA " +
|
||||
affectedPdaAddress
|
||||
);
|
||||
}
|
||||
|
||||
snapshot =
|
||||
ShineUsersCodec.buildUpdateSnapshot(
|
||||
parsedInstruction.userPdaMutation(),
|
||||
previous,
|
||||
envelope.signatureRecord().signature(),
|
||||
envelope.signatureRecord().slot()
|
||||
);
|
||||
}
|
||||
|
||||
currentSnapshots.put(
|
||||
snapshot.pdaAddress(),
|
||||
snapshot
|
||||
);
|
||||
|
||||
snapshotsToPersist.add(
|
||||
snapshot
|
||||
);
|
||||
|
||||
lastRelevantSignature =
|
||||
envelope.signatureRecord().signature();
|
||||
|
||||
lastRelevantSlot =
|
||||
envelope.signatureRecord().slot();
|
||||
}
|
||||
}
|
||||
|
||||
txEntries.add(
|
||||
new PostgresStorageRepository.TxHistoryEntry(
|
||||
envelope.signatureRecord().signature(),
|
||||
envelope.signatureRecord().slot(),
|
||||
envelope.signatureRecord().blockTime(),
|
||||
txKind,
|
||||
relevant,
|
||||
affectedPdaAddress,
|
||||
affectedLogin,
|
||||
envelope.rawTransactionJson(),
|
||||
nowMs
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
SolanaRpcClient.SignatureRecord newestSeen =
|
||||
fetchResult.signatures()
|
||||
.get(0);
|
||||
|
||||
PostgresStorageRepository.SyncStateSnapshot newState =
|
||||
new PostgresStorageRepository.SyncStateSnapshot(
|
||||
"READY",
|
||||
true,
|
||||
nowMs,
|
||||
nowMs,
|
||||
newestSeen.signature(),
|
||||
newestSeen.slot(),
|
||||
lastRelevantSignature,
|
||||
lastRelevantSlot,
|
||||
null,
|
||||
economyState,
|
||||
nowMs
|
||||
);
|
||||
|
||||
storage.applyHistoryBatch(
|
||||
txEntries,
|
||||
snapshotsToPersist,
|
||||
newState
|
||||
);
|
||||
|
||||
log.info(
|
||||
"History sync completed. txCount={} relevantCount={} latestSignature={}",
|
||||
txEntries.size(),
|
||||
snapshotsToPersist.size(),
|
||||
newestSeen.signature()
|
||||
);
|
||||
|
||||
markReadyAfterSync(
|
||||
newState,
|
||||
nowMs
|
||||
);
|
||||
}
|
||||
|
||||
private void runFullSnapshotFallback(
|
||||
PostgresStorageRepository.SyncStateSnapshot state,
|
||||
SolanaRpcClient.SignatureFetchResult fetchResult,
|
||||
long nowMs
|
||||
) throws Exception {
|
||||
|
||||
log.warn(
|
||||
"Starting full snapshot fallback because incremental history anchor is unavailable."
|
||||
);
|
||||
|
||||
SnapshotResult snapshotResult =
|
||||
rpcClient.loadFullSnapshot();
|
||||
|
||||
log.info(
|
||||
"Full snapshot downloaded. snapshotSlot={} rawAccounts={}",
|
||||
snapshotResult.snapshotSlot(),
|
||||
snapshotResult.accounts().size()
|
||||
);
|
||||
|
||||
List<ShineUsersCodec.UserPdaSnapshot> currentSnapshots =
|
||||
new ArrayList<>();
|
||||
|
||||
int processedAccounts =
|
||||
0;
|
||||
|
||||
for (ProgramAccountUpdate account : snapshotResult.accounts()) {
|
||||
|
||||
processedAccounts++;
|
||||
|
||||
try {
|
||||
currentSnapshots.add(
|
||||
ShineUsersCodec.parseUserPdaAccount(
|
||||
account.address(),
|
||||
account.slot(),
|
||||
account.dataBase64(),
|
||||
state.lastSeenSignature()
|
||||
)
|
||||
);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
if (processedAccounts == 1
|
||||
|| processedAccounts % FULL_SNAPSHOT_PROGRESS_STEP == 0
|
||||
|| processedAccounts == snapshotResult.accounts().size()) {
|
||||
log.info(
|
||||
"Full snapshot parse progress: {}/{} accounts, {} user PDA snapshots accepted.",
|
||||
processedAccounts,
|
||||
snapshotResult.accounts().size(),
|
||||
currentSnapshots.size()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String newestSignature =
|
||||
fetchResult.signatures().isEmpty()
|
||||
? state.lastSeenSignature()
|
||||
: fetchResult.signatures().get(0).signature();
|
||||
|
||||
Long newestSlot =
|
||||
fetchResult.signatures().isEmpty()
|
||||
? state.lastSeenSlot()
|
||||
: fetchResult.signatures().get(0).slot();
|
||||
|
||||
PostgresStorageRepository.SyncStateSnapshot newState =
|
||||
new PostgresStorageRepository.SyncStateSnapshot(
|
||||
"READY",
|
||||
true,
|
||||
nowMs,
|
||||
nowMs,
|
||||
newestSignature,
|
||||
newestSlot,
|
||||
state.lastRelevantSignature(),
|
||||
state.lastRelevantSlot(),
|
||||
"history_anchor_missing_full_snapshot_fallback",
|
||||
state.economyConfigState(),
|
||||
nowMs
|
||||
);
|
||||
|
||||
storage.replaceCurrentFromFullSnapshot(
|
||||
currentSnapshots,
|
||||
newState
|
||||
);
|
||||
|
||||
log.warn(
|
||||
"Full snapshot fallback completed. currentSnapshots={} newestSignature={} newestSlot={}",
|
||||
currentSnapshots.size(),
|
||||
newestSignature,
|
||||
newestSlot
|
||||
);
|
||||
}
|
||||
|
||||
private void markReadyAfterSync(
|
||||
PostgresStorageRepository.SyncStateSnapshot state,
|
||||
long nowMs
|
||||
) throws Exception {
|
||||
|
||||
initialSyncCompleted = true;
|
||||
|
||||
if (!readyFuture.isDone()) {
|
||||
readyFuture.complete(null);
|
||||
log.info(
|
||||
"Sync service entered READY state."
|
||||
);
|
||||
}
|
||||
|
||||
storage.updateLifecycleState(
|
||||
"READY",
|
||||
true,
|
||||
state.lastError(),
|
||||
nowMs,
|
||||
nowMs
|
||||
);
|
||||
}
|
||||
|
||||
private ParsedTxEnvelope parseTransactionEnvelope(
|
||||
SolanaRpcClient.SignatureRecord signatureRecord,
|
||||
JsonNode transaction
|
||||
) {
|
||||
|
||||
if (signatureRecord.failed()) {
|
||||
return new ParsedTxEnvelope(
|
||||
signatureRecord,
|
||||
new ShineUsersCodec.ParsedInstruction(
|
||||
ShineUsersCodec.TxKind.OTHER,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
),
|
||||
transaction == null
|
||||
? "{\"failed\":true,\"error\":" +
|
||||
String.valueOf(signatureRecord.errorJson()) + "}"
|
||||
: transaction.toString()
|
||||
);
|
||||
}
|
||||
|
||||
if (transaction == null
|
||||
|| transaction.isNull()) {
|
||||
return new ParsedTxEnvelope(
|
||||
signatureRecord,
|
||||
null,
|
||||
"{\"transaction\":null}"
|
||||
);
|
||||
}
|
||||
|
||||
JsonNode instructions =
|
||||
transaction.path("transaction")
|
||||
.path("message")
|
||||
.path("instructions");
|
||||
|
||||
if (instructions.isArray()) {
|
||||
for (JsonNode instruction : instructions) {
|
||||
ShineUsersCodec.ParsedInstruction parsedInstruction =
|
||||
ShineUsersCodec.parseShineUsersInstruction(
|
||||
instruction,
|
||||
config.programId()
|
||||
);
|
||||
|
||||
if (parsedInstruction != null) {
|
||||
return new ParsedTxEnvelope(
|
||||
signatureRecord,
|
||||
parsedInstruction,
|
||||
transaction.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ParsedTxEnvelope(
|
||||
signatureRecord,
|
||||
new ShineUsersCodec.ParsedInstruction(
|
||||
ShineUsersCodec.TxKind.OTHER,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
),
|
||||
transaction.toString()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
if (!closed.compareAndSet(
|
||||
false,
|
||||
true
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
pollScheduler.shutdownNow();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
webSocketClient.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
syncExecutor.shutdownNow();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
rpcClient.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
storage.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private record ParsedTxEnvelope(
|
||||
SolanaRpcClient.SignatureRecord signatureRecord,
|
||||
ShineUsersCodec.ParsedInstruction parsedInstruction,
|
||||
String rawTransactionJson
|
||||
) {
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package sync.source;
|
||||
|
||||
import sync.model.ProgramAccountUpdate;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface AccountUpdateListener {
|
||||
|
||||
void onAccountUpdate(
|
||||
ProgramAccountUpdate update
|
||||
);
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package sync.source;
|
||||
|
||||
public interface ConnectionListener {
|
||||
|
||||
void onConnected(
|
||||
boolean firstConnection
|
||||
);
|
||||
|
||||
void onDisconnected(
|
||||
Throwable cause
|
||||
);
|
||||
}
|
||||
+737
@@ -0,0 +1,737 @@
|
||||
package sync.source.rpc;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import okhttp3.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sync.model.ProgramAccountUpdate;
|
||||
import sync.model.SnapshotResult;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
|
||||
public final class SolanaRpcClient
|
||||
implements AutoCloseable {
|
||||
|
||||
private static final Logger log =
|
||||
LoggerFactory.getLogger(
|
||||
SolanaRpcClient.class
|
||||
);
|
||||
|
||||
private static final MediaType JSON =
|
||||
MediaType.get("application/json");
|
||||
|
||||
private static final int SIGNATURE_PAGE_SIZE =
|
||||
1000;
|
||||
|
||||
private static final int ACCOUNT_BATCH_SIZE =
|
||||
100;
|
||||
|
||||
private final String rpcUrl;
|
||||
private final String programId;
|
||||
private final String commitment;
|
||||
private final ObjectMapper mapper =
|
||||
new ObjectMapper();
|
||||
private final OkHttpClient httpClient =
|
||||
new OkHttpClient.Builder()
|
||||
.callTimeout(Duration.ofMinutes(2))
|
||||
.build();
|
||||
|
||||
public SolanaRpcClient(
|
||||
String rpcUrl,
|
||||
String programId,
|
||||
String commitment
|
||||
) {
|
||||
this.rpcUrl = rpcUrl;
|
||||
this.programId = programId;
|
||||
this.commitment = commitment;
|
||||
}
|
||||
|
||||
public SnapshotResult loadFullSnapshot()
|
||||
throws IOException {
|
||||
|
||||
log.info(
|
||||
"Requesting full snapshot via getProgramAccounts. programId={}",
|
||||
programId
|
||||
);
|
||||
|
||||
Map<String, Object> payload =
|
||||
Map.of(
|
||||
"jsonrpc", "2.0",
|
||||
"id", 100,
|
||||
"method", "getProgramAccounts",
|
||||
"params", List.of(
|
||||
programId,
|
||||
Map.of(
|
||||
"encoding", "base64",
|
||||
"commitment", commitment,
|
||||
"withContext", true
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
JsonNode root =
|
||||
executeRpc(payload);
|
||||
|
||||
log.info(
|
||||
"Full snapshot RPC response received. Parsing account list..."
|
||||
);
|
||||
|
||||
JsonNode result =
|
||||
root.path("result");
|
||||
|
||||
long snapshotSlot =
|
||||
result.path("context")
|
||||
.path("slot")
|
||||
.asLong(-1);
|
||||
|
||||
if (snapshotSlot < 0) {
|
||||
throw new IOException(
|
||||
"Missing context.slot in getProgramAccounts"
|
||||
);
|
||||
}
|
||||
|
||||
JsonNode values =
|
||||
result.path("value");
|
||||
|
||||
if (!values.isArray()) {
|
||||
throw new IOException(
|
||||
"Unexpected getProgramAccounts response"
|
||||
);
|
||||
}
|
||||
|
||||
List<ProgramAccountUpdate> accounts =
|
||||
new ArrayList<>();
|
||||
|
||||
for (JsonNode item : values) {
|
||||
|
||||
JsonNode account =
|
||||
item.path("account");
|
||||
|
||||
JsonNode data =
|
||||
account.path("data");
|
||||
|
||||
if (!data.isArray()
|
||||
|| data.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
accounts.add(
|
||||
parseAccount(
|
||||
item.path("pubkey")
|
||||
.asText(),
|
||||
account,
|
||||
snapshotSlot
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
log.info(
|
||||
"Full snapshot RPC parsed. accounts={} snapshotSlot={}",
|
||||
accounts.size(),
|
||||
snapshotSlot
|
||||
);
|
||||
|
||||
return new SnapshotResult(
|
||||
snapshotSlot,
|
||||
accounts
|
||||
);
|
||||
}
|
||||
|
||||
public SignatureFetchResult getSignaturesForAddressSince(
|
||||
String address,
|
||||
String knownSignature
|
||||
) throws IOException {
|
||||
|
||||
List<SignatureRecord> signatures =
|
||||
new ArrayList<>();
|
||||
|
||||
String before =
|
||||
null;
|
||||
|
||||
boolean anchorFound =
|
||||
knownSignature == null;
|
||||
|
||||
while (true) {
|
||||
|
||||
Map<String, Object> options =
|
||||
new LinkedHashMap<>();
|
||||
|
||||
options.put(
|
||||
"limit",
|
||||
SIGNATURE_PAGE_SIZE
|
||||
);
|
||||
|
||||
options.put(
|
||||
"commitment",
|
||||
commitment
|
||||
);
|
||||
|
||||
if (before != null) {
|
||||
options.put(
|
||||
"before",
|
||||
before
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, Object> payload =
|
||||
Map.of(
|
||||
"jsonrpc", "2.0",
|
||||
"id", 103,
|
||||
"method", "getSignaturesForAddress",
|
||||
"params", List.of(
|
||||
address,
|
||||
options
|
||||
)
|
||||
);
|
||||
|
||||
JsonNode root =
|
||||
executeRpc(payload);
|
||||
|
||||
JsonNode values =
|
||||
root.path("result");
|
||||
|
||||
if (!values.isArray()) {
|
||||
throw new IOException(
|
||||
"Unexpected getSignaturesForAddress response"
|
||||
);
|
||||
}
|
||||
|
||||
if (values.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (JsonNode item : values) {
|
||||
|
||||
String signature =
|
||||
item.path("signature")
|
||||
.asText("");
|
||||
|
||||
if (signature.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (knownSignature != null
|
||||
&& knownSignature.equals(signature)) {
|
||||
anchorFound = true;
|
||||
return new SignatureFetchResult(
|
||||
signatures,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
Long blockTime =
|
||||
item.hasNonNull("blockTime")
|
||||
? item.get("blockTime").asLong()
|
||||
: null;
|
||||
|
||||
signatures.add(
|
||||
new SignatureRecord(
|
||||
signature,
|
||||
item.path("slot")
|
||||
.asLong(-1),
|
||||
blockTime,
|
||||
item.hasNonNull("err"),
|
||||
item.path("err").toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
JsonNode last =
|
||||
values.get(values.size() - 1);
|
||||
|
||||
before =
|
||||
last.path("signature")
|
||||
.asText("");
|
||||
|
||||
if (before.isBlank()
|
||||
|| values.size() < SIGNATURE_PAGE_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new SignatureFetchResult(
|
||||
signatures,
|
||||
anchorFound
|
||||
);
|
||||
}
|
||||
|
||||
public JsonNode getTransactionJsonParsed(
|
||||
String signature
|
||||
) throws IOException {
|
||||
|
||||
Map<String, Object> payload =
|
||||
Map.of(
|
||||
"jsonrpc", "2.0",
|
||||
"id", 104,
|
||||
"method", "getTransaction",
|
||||
"params", List.of(
|
||||
signature,
|
||||
Map.of(
|
||||
"encoding",
|
||||
"jsonParsed",
|
||||
"commitment",
|
||||
commitment,
|
||||
"maxSupportedTransactionVersion",
|
||||
0
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
JsonNode root =
|
||||
executeRpc(payload);
|
||||
|
||||
JsonNode result =
|
||||
root.get("result");
|
||||
|
||||
if (result == null
|
||||
|| result.isNull()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public long getCurrentSlot()
|
||||
throws IOException {
|
||||
|
||||
Map<String, Object> payload =
|
||||
Map.of(
|
||||
"jsonrpc", "2.0",
|
||||
"id", 101,
|
||||
"method", "getSlot",
|
||||
"params", List.of(
|
||||
Map.of(
|
||||
"commitment",
|
||||
commitment
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
JsonNode root =
|
||||
executeRpc(payload);
|
||||
|
||||
long slot =
|
||||
root.path("result")
|
||||
.asLong(-1);
|
||||
|
||||
if (slot < 0) {
|
||||
throw new IOException(
|
||||
"Invalid getSlot response"
|
||||
);
|
||||
}
|
||||
|
||||
return slot;
|
||||
}
|
||||
|
||||
public AccountBatchResult getCurrentAccounts(
|
||||
Collection<String> addresses,
|
||||
long recoverySlot
|
||||
) throws IOException {
|
||||
|
||||
List<ProgramAccountUpdate> updates =
|
||||
new ArrayList<>();
|
||||
|
||||
List<String> missingAddresses =
|
||||
new ArrayList<>();
|
||||
|
||||
List<String> addressList =
|
||||
new ArrayList<>(addresses);
|
||||
|
||||
for (int offset = 0;
|
||||
offset < addressList.size();
|
||||
offset += ACCOUNT_BATCH_SIZE) {
|
||||
|
||||
int end =
|
||||
Math.min(
|
||||
offset + ACCOUNT_BATCH_SIZE,
|
||||
addressList.size()
|
||||
);
|
||||
|
||||
List<String> batch =
|
||||
addressList.subList(
|
||||
offset,
|
||||
end
|
||||
);
|
||||
|
||||
Map<String, Object> payload =
|
||||
Map.of(
|
||||
"jsonrpc", "2.0",
|
||||
"id", 105,
|
||||
"method", "getMultipleAccounts",
|
||||
"params", List.of(
|
||||
batch,
|
||||
Map.of(
|
||||
"encoding",
|
||||
"base64",
|
||||
"commitment",
|
||||
commitment
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
JsonNode root =
|
||||
executeRpc(payload);
|
||||
|
||||
JsonNode values =
|
||||
root.path("result")
|
||||
.path("value");
|
||||
|
||||
if (!values.isArray()) {
|
||||
throw new IOException(
|
||||
"Unexpected getMultipleAccounts response"
|
||||
);
|
||||
}
|
||||
|
||||
if (values.size() != batch.size()) {
|
||||
throw new IOException(
|
||||
"Unexpected getMultipleAccounts account count"
|
||||
);
|
||||
}
|
||||
|
||||
for (int i = 0; i < batch.size(); i++) {
|
||||
|
||||
String address =
|
||||
batch.get(i);
|
||||
|
||||
JsonNode account =
|
||||
values.get(i);
|
||||
|
||||
if (account == null
|
||||
|| account.isNull()) {
|
||||
missingAddresses.add(address);
|
||||
continue;
|
||||
}
|
||||
|
||||
String owner =
|
||||
account.path("owner")
|
||||
.asText("");
|
||||
|
||||
if (!programId.equals(owner)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
JsonNode data =
|
||||
account.path("data");
|
||||
|
||||
if (!data.isArray()
|
||||
|| data.isEmpty()) {
|
||||
throw new IOException(
|
||||
"Unexpected account.data format for "
|
||||
+ address
|
||||
);
|
||||
}
|
||||
|
||||
updates.add(
|
||||
parseAccount(
|
||||
address,
|
||||
account,
|
||||
recoverySlot
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new AccountBatchResult(
|
||||
updates,
|
||||
missingAddresses
|
||||
);
|
||||
}
|
||||
|
||||
public long getFirstAvailableBlock()
|
||||
throws IOException {
|
||||
|
||||
Map<String, Object> payload =
|
||||
Map.of(
|
||||
"jsonrpc", "2.0",
|
||||
"id", 102,
|
||||
"method", "getFirstAvailableBlock"
|
||||
);
|
||||
|
||||
JsonNode root =
|
||||
executeRpc(payload);
|
||||
|
||||
return root.path("result")
|
||||
.asLong(-1);
|
||||
}
|
||||
|
||||
public List<SignatureInfo> getSignaturesAfterSlot(
|
||||
long fromSlot,
|
||||
long targetSlot
|
||||
) throws IOException {
|
||||
|
||||
List<SignatureInfo> signatures =
|
||||
new ArrayList<>();
|
||||
|
||||
String before =
|
||||
null;
|
||||
|
||||
boolean lowerBoundaryReached =
|
||||
false;
|
||||
|
||||
while (!lowerBoundaryReached) {
|
||||
|
||||
Map<String, Object> options =
|
||||
new LinkedHashMap<>();
|
||||
|
||||
options.put("limit", SIGNATURE_PAGE_SIZE);
|
||||
options.put("commitment", commitment);
|
||||
|
||||
if (before != null) {
|
||||
options.put("before", before);
|
||||
}
|
||||
|
||||
Map<String, Object> payload =
|
||||
Map.of(
|
||||
"jsonrpc", "2.0",
|
||||
"id", 106,
|
||||
"method", "getSignaturesForAddress",
|
||||
"params", List.of(
|
||||
programId,
|
||||
options
|
||||
)
|
||||
);
|
||||
|
||||
JsonNode root =
|
||||
executeRpc(payload);
|
||||
|
||||
JsonNode values =
|
||||
root.path("result");
|
||||
|
||||
if (!values.isArray()) {
|
||||
throw new IOException(
|
||||
"Unexpected getSignaturesForAddress response"
|
||||
);
|
||||
}
|
||||
|
||||
if (values.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (JsonNode item : values) {
|
||||
|
||||
long slot =
|
||||
item.path("slot")
|
||||
.asLong(-1);
|
||||
|
||||
if (slot < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (slot <= fromSlot) {
|
||||
lowerBoundaryReached = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (slot > targetSlot) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String signature =
|
||||
item.path("signature")
|
||||
.asText("");
|
||||
|
||||
if (signature.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
JsonNode error =
|
||||
item.get("err");
|
||||
|
||||
if (error != null
|
||||
&& !error.isNull()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
signatures.add(
|
||||
new SignatureInfo(
|
||||
signature,
|
||||
slot
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
JsonNode last =
|
||||
values.get(values.size() - 1);
|
||||
|
||||
before =
|
||||
last.path("signature")
|
||||
.asText("");
|
||||
|
||||
if (before.isBlank()
|
||||
|| values.size() < SIGNATURE_PAGE_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return signatures;
|
||||
}
|
||||
|
||||
public Set<String> getTouchedAddresses(
|
||||
List<SignatureInfo> signatures
|
||||
) throws IOException {
|
||||
|
||||
Set<String> addresses =
|
||||
new LinkedHashSet<>();
|
||||
|
||||
for (SignatureInfo signatureInfo : signatures) {
|
||||
|
||||
JsonNode transaction =
|
||||
getTransactionJsonParsed(
|
||||
signatureInfo.signature()
|
||||
);
|
||||
|
||||
if (transaction == null
|
||||
|| transaction.isNull()) {
|
||||
throw new IOException(
|
||||
"Transaction unavailable during recovery: "
|
||||
+ signatureInfo.signature()
|
||||
);
|
||||
}
|
||||
|
||||
JsonNode accountKeys =
|
||||
transaction.path("transaction")
|
||||
.path("message")
|
||||
.path("accountKeys");
|
||||
|
||||
if (!accountKeys.isArray()) {
|
||||
throw new IOException(
|
||||
"Missing accountKeys for transaction: "
|
||||
+ signatureInfo.signature()
|
||||
);
|
||||
}
|
||||
|
||||
for (JsonNode keyNode : accountKeys) {
|
||||
|
||||
String pubkey;
|
||||
|
||||
if (keyNode.isTextual()) {
|
||||
pubkey = keyNode.asText();
|
||||
} else {
|
||||
pubkey = keyNode.path("pubkey")
|
||||
.asText("");
|
||||
}
|
||||
|
||||
if (!pubkey.isBlank()) {
|
||||
addresses.add(pubkey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addresses.remove(programId);
|
||||
return addresses;
|
||||
}
|
||||
|
||||
private ProgramAccountUpdate parseAccount(
|
||||
String address,
|
||||
JsonNode account,
|
||||
long slot
|
||||
) {
|
||||
|
||||
JsonNode data =
|
||||
account.path("data");
|
||||
|
||||
return new ProgramAccountUpdate(
|
||||
address,
|
||||
account.path("owner")
|
||||
.asText(),
|
||||
account.path("lamports")
|
||||
.asLong(),
|
||||
slot,
|
||||
data.get(0)
|
||||
.asText(),
|
||||
account.path("executable")
|
||||
.asBoolean(false),
|
||||
account.hasNonNull("rentEpoch")
|
||||
? account.get("rentEpoch").asLong()
|
||||
: null
|
||||
);
|
||||
}
|
||||
|
||||
private JsonNode executeRpc(
|
||||
Map<String, Object> payload
|
||||
) throws IOException {
|
||||
|
||||
Request request =
|
||||
new Request.Builder()
|
||||
.url(rpcUrl)
|
||||
.post(
|
||||
RequestBody.create(
|
||||
mapper.writeValueAsBytes(payload),
|
||||
JSON
|
||||
)
|
||||
)
|
||||
.build();
|
||||
|
||||
try (Response response =
|
||||
httpClient.newCall(request).execute()) {
|
||||
|
||||
if (!response.isSuccessful()) {
|
||||
throw new IOException(
|
||||
"Solana RPC HTTP error: " + response.code()
|
||||
);
|
||||
}
|
||||
|
||||
ResponseBody body =
|
||||
response.body();
|
||||
|
||||
if (body == null) {
|
||||
throw new IOException(
|
||||
"Solana RPC returned empty body"
|
||||
);
|
||||
}
|
||||
|
||||
JsonNode root =
|
||||
mapper.readTree(
|
||||
body.string()
|
||||
);
|
||||
|
||||
if (root.has("error")) {
|
||||
throw new IOException(
|
||||
"Solana RPC error: " + root.get("error")
|
||||
);
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
httpClient.dispatcher()
|
||||
.executorService()
|
||||
.shutdown();
|
||||
httpClient.connectionPool()
|
||||
.evictAll();
|
||||
}
|
||||
|
||||
public record SignatureRecord(
|
||||
String signature,
|
||||
long slot,
|
||||
Long blockTime,
|
||||
boolean failed,
|
||||
String errorJson
|
||||
) {
|
||||
}
|
||||
|
||||
public record SignatureFetchResult(
|
||||
List<SignatureRecord> signatures,
|
||||
boolean anchorFound
|
||||
) {
|
||||
}
|
||||
|
||||
public record AccountBatchResult(
|
||||
List<ProgramAccountUpdate> updates,
|
||||
List<String> missingAddresses
|
||||
) {
|
||||
}
|
||||
|
||||
public record SignatureInfo(
|
||||
String signature,
|
||||
long slot
|
||||
) {
|
||||
}
|
||||
}
|
||||
+542
@@ -0,0 +1,542 @@
|
||||
package sync.source.rpc;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import okhttp3.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sync.model.ProgramAccountUpdate;
|
||||
import sync.source.AccountUpdateListener;
|
||||
import sync.source.ConnectionListener;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public final class SolanaWebSocketClient
|
||||
extends WebSocketListener
|
||||
implements AutoCloseable {
|
||||
|
||||
private static final Logger log =
|
||||
LoggerFactory.getLogger(
|
||||
SolanaWebSocketClient.class
|
||||
);
|
||||
|
||||
private final String websocketUrl;
|
||||
private final String programId;
|
||||
private final String commitment;
|
||||
|
||||
private final ObjectMapper mapper =
|
||||
new ObjectMapper();
|
||||
|
||||
private final OkHttpClient httpClient;
|
||||
|
||||
private final ScheduledExecutorService scheduler;
|
||||
|
||||
private final AtomicBoolean closed =
|
||||
new AtomicBoolean(false);
|
||||
|
||||
private final AtomicInteger reconnectAttempt =
|
||||
new AtomicInteger(0);
|
||||
|
||||
private final AtomicBoolean everSubscribed =
|
||||
new AtomicBoolean(false);
|
||||
|
||||
private volatile WebSocket webSocket;
|
||||
|
||||
private volatile AccountUpdateListener
|
||||
accountUpdateListener;
|
||||
|
||||
private volatile ConnectionListener
|
||||
connectionListener;
|
||||
|
||||
public SolanaWebSocketClient(
|
||||
String websocketUrl,
|
||||
String programId,
|
||||
String commitment
|
||||
) {
|
||||
this.websocketUrl = websocketUrl;
|
||||
this.programId = programId;
|
||||
this.commitment = commitment;
|
||||
|
||||
this.httpClient =
|
||||
new OkHttpClient.Builder()
|
||||
.readTimeout(
|
||||
Duration.ZERO
|
||||
)
|
||||
.pingInterval(
|
||||
Duration.ofSeconds(20)
|
||||
)
|
||||
.build();
|
||||
|
||||
this.scheduler =
|
||||
Executors
|
||||
.newSingleThreadScheduledExecutor(
|
||||
runnable -> {
|
||||
|
||||
Thread thread =
|
||||
new Thread(
|
||||
runnable,
|
||||
"solana-rpc-reconnect"
|
||||
);
|
||||
|
||||
thread.setDaemon(
|
||||
true
|
||||
);
|
||||
|
||||
return thread;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public void start(
|
||||
AccountUpdateListener accountUpdateListener,
|
||||
ConnectionListener connectionListener
|
||||
) {
|
||||
|
||||
this.accountUpdateListener =
|
||||
accountUpdateListener;
|
||||
|
||||
this.connectionListener =
|
||||
connectionListener;
|
||||
|
||||
connect();
|
||||
}
|
||||
|
||||
private void connect() {
|
||||
|
||||
if (closed.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(
|
||||
"Connecting to Solana WebSocket: {}",
|
||||
websocketUrl
|
||||
);
|
||||
|
||||
Request request =
|
||||
new Request.Builder()
|
||||
.url(
|
||||
websocketUrl
|
||||
)
|
||||
.build();
|
||||
|
||||
this.webSocket =
|
||||
httpClient.newWebSocket(
|
||||
request,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(
|
||||
WebSocket webSocket,
|
||||
Response response
|
||||
) {
|
||||
|
||||
reconnectAttempt.set(
|
||||
0
|
||||
);
|
||||
|
||||
log.info(
|
||||
"WebSocket connected"
|
||||
);
|
||||
|
||||
sendProgramSubscribe(
|
||||
webSocket
|
||||
);
|
||||
}
|
||||
|
||||
private void sendProgramSubscribe(
|
||||
WebSocket webSocket
|
||||
) {
|
||||
|
||||
try {
|
||||
|
||||
Map<String, Object> request =
|
||||
Map.of(
|
||||
"jsonrpc",
|
||||
"2.0",
|
||||
"id",
|
||||
1,
|
||||
"method",
|
||||
"programSubscribe",
|
||||
"params",
|
||||
List.of(
|
||||
programId,
|
||||
Map.of(
|
||||
"encoding",
|
||||
"base64",
|
||||
"commitment",
|
||||
commitment
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
String payload =
|
||||
mapper.writeValueAsString(
|
||||
request
|
||||
);
|
||||
|
||||
if (!webSocket.send(
|
||||
payload
|
||||
)) {
|
||||
throw new IllegalStateException(
|
||||
"WebSocket rejected subscription request"
|
||||
);
|
||||
}
|
||||
|
||||
log.info(
|
||||
"Subscription request sent for program: {}",
|
||||
programId
|
||||
);
|
||||
|
||||
} catch (Exception exception) {
|
||||
|
||||
log.error(
|
||||
"Failed to send subscription request",
|
||||
exception
|
||||
);
|
||||
|
||||
webSocket.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(
|
||||
WebSocket webSocket,
|
||||
String text
|
||||
) {
|
||||
|
||||
try {
|
||||
|
||||
JsonNode root =
|
||||
mapper.readTree(
|
||||
text
|
||||
);
|
||||
|
||||
if (
|
||||
root.has("id")
|
||||
&& root.has("result")
|
||||
&& root.get("id").asInt() == 1
|
||||
) {
|
||||
|
||||
log.info(
|
||||
"Subscribed successfully. subscriptionId={}",
|
||||
root.get("result").asText()
|
||||
);
|
||||
|
||||
boolean firstConnection =
|
||||
everSubscribed
|
||||
.compareAndSet(
|
||||
false,
|
||||
true
|
||||
);
|
||||
|
||||
ConnectionListener listener =
|
||||
connectionListener;
|
||||
|
||||
if (listener != null) {
|
||||
listener.onConnected(
|
||||
firstConnection
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.has("error")) {
|
||||
|
||||
log.error(
|
||||
"Solana websocket RPC error: {}",
|
||||
root.get("error")
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!"programNotification"
|
||||
.equals(
|
||||
root
|
||||
.path("method")
|
||||
.asText()
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ProgramAccountUpdate update =
|
||||
parseProgramNotification(
|
||||
root
|
||||
);
|
||||
|
||||
log.debug(
|
||||
"Account update. address={} slot={} base64Chars={}",
|
||||
update.address(),
|
||||
update.slot(),
|
||||
update.dataBase64().length()
|
||||
);
|
||||
|
||||
AccountUpdateListener listener =
|
||||
accountUpdateListener;
|
||||
|
||||
if (listener != null) {
|
||||
listener.onAccountUpdate(
|
||||
update
|
||||
);
|
||||
}
|
||||
|
||||
} catch (Exception exception) {
|
||||
|
||||
log.error(
|
||||
"Failed to process WebSocket message. raw={}",
|
||||
text,
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private ProgramAccountUpdate parseProgramNotification(
|
||||
JsonNode root
|
||||
) {
|
||||
|
||||
JsonNode result =
|
||||
root
|
||||
.path("params")
|
||||
.path("result");
|
||||
|
||||
JsonNode context =
|
||||
result.path("context");
|
||||
|
||||
JsonNode value =
|
||||
result.path("value");
|
||||
|
||||
JsonNode account =
|
||||
value.path("account");
|
||||
|
||||
JsonNode data =
|
||||
account.path("data");
|
||||
|
||||
if (!data.isArray()
|
||||
|| data.isEmpty()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unexpected account.data format"
|
||||
);
|
||||
}
|
||||
|
||||
return new ProgramAccountUpdate(
|
||||
requiredText(
|
||||
value,
|
||||
"pubkey"
|
||||
),
|
||||
requiredText(
|
||||
account,
|
||||
"owner"
|
||||
),
|
||||
requiredLong(
|
||||
account,
|
||||
"lamports"
|
||||
),
|
||||
requiredLong(
|
||||
context,
|
||||
"slot"
|
||||
),
|
||||
data.get(0).asText(),
|
||||
account.path(
|
||||
"executable"
|
||||
).asBoolean(false),
|
||||
account.hasNonNull(
|
||||
"rentEpoch"
|
||||
)
|
||||
? account
|
||||
.get("rentEpoch")
|
||||
.asLong()
|
||||
: null
|
||||
);
|
||||
}
|
||||
|
||||
private String requiredText(
|
||||
JsonNode node,
|
||||
String field
|
||||
) {
|
||||
|
||||
JsonNode value =
|
||||
node.get(field);
|
||||
|
||||
if (value == null
|
||||
|| value.isNull()
|
||||
|| value.asText().isBlank()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Missing field: "
|
||||
+ field
|
||||
);
|
||||
}
|
||||
|
||||
return value.asText();
|
||||
}
|
||||
|
||||
private long requiredLong(
|
||||
JsonNode node,
|
||||
String field
|
||||
) {
|
||||
|
||||
JsonNode value =
|
||||
node.get(field);
|
||||
|
||||
if (value == null
|
||||
|| !value.isNumber()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Missing numeric field: "
|
||||
+ field
|
||||
);
|
||||
}
|
||||
|
||||
return value.asLong();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosed(
|
||||
WebSocket webSocket,
|
||||
int code,
|
||||
String reason
|
||||
) {
|
||||
|
||||
log.warn(
|
||||
"WebSocket closed. code={} reason={}",
|
||||
code,
|
||||
reason
|
||||
);
|
||||
|
||||
notifyDisconnected(
|
||||
new IllegalStateException(
|
||||
"WebSocket closed: "
|
||||
+ reason
|
||||
)
|
||||
);
|
||||
|
||||
scheduleReconnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(
|
||||
WebSocket webSocket,
|
||||
Throwable throwable,
|
||||
Response response
|
||||
) {
|
||||
|
||||
log.error(
|
||||
"WebSocket failure",
|
||||
throwable
|
||||
);
|
||||
|
||||
if (response != null) {
|
||||
log.error(
|
||||
"WebSocket HTTP status: {}",
|
||||
response.code()
|
||||
);
|
||||
}
|
||||
|
||||
notifyDisconnected(
|
||||
throwable
|
||||
);
|
||||
|
||||
scheduleReconnect();
|
||||
}
|
||||
|
||||
private void notifyDisconnected(
|
||||
Throwable cause
|
||||
) {
|
||||
|
||||
ConnectionListener listener =
|
||||
connectionListener;
|
||||
|
||||
if (listener != null) {
|
||||
|
||||
try {
|
||||
listener.onDisconnected(
|
||||
cause
|
||||
);
|
||||
} catch (Exception exception) {
|
||||
log.error(
|
||||
"Connection listener failed",
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleReconnect() {
|
||||
|
||||
if (closed.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int attempt =
|
||||
reconnectAttempt
|
||||
.incrementAndGet();
|
||||
|
||||
long delaySeconds =
|
||||
switch (
|
||||
Math.min(
|
||||
attempt,
|
||||
5
|
||||
)
|
||||
) {
|
||||
case 1 -> 1;
|
||||
case 2 -> 2;
|
||||
case 3 -> 5;
|
||||
case 4 -> 10;
|
||||
default -> 30;
|
||||
};
|
||||
|
||||
log.warn(
|
||||
"Reconnect scheduled in {} seconds (attempt {})",
|
||||
delaySeconds,
|
||||
attempt
|
||||
);
|
||||
|
||||
scheduler.schedule(
|
||||
this::connect,
|
||||
delaySeconds,
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
if (!closed.compareAndSet(
|
||||
false,
|
||||
true
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
WebSocket socket =
|
||||
this.webSocket;
|
||||
|
||||
if (socket != null) {
|
||||
socket.close(
|
||||
1000,
|
||||
"Application shutdown"
|
||||
);
|
||||
}
|
||||
|
||||
scheduler.shutdownNow();
|
||||
|
||||
httpClient
|
||||
.dispatcher()
|
||||
.executorService()
|
||||
.shutdown();
|
||||
|
||||
httpClient
|
||||
.connectionPool()
|
||||
.evictAll();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user