SHA256
Compare commits
52
Commits
01321f5200
..
main
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
7d27bfdcaf | ||
|
|
023d61a1e9 | ||
|
|
85d90a7f95 | ||
|
|
e0295eebde | ||
|
|
ebc9143593 | ||
|
|
e7c8fd748c | ||
|
|
aff601f61a | ||
|
|
0c5089fa79 | ||
|
|
e5556f3706 | ||
|
|
09c7f8541e | ||
|
|
8eb2ef438c | ||
|
|
34abe14151 | ||
|
|
90e37c198f | ||
|
|
1373283947 | ||
|
|
065616a18b | ||
|
|
53cd55a2a0 | ||
|
|
079be37fff | ||
|
|
806a8c57d4 | ||
|
|
797e769cc3 | ||
|
|
822a72e246 | ||
|
|
58b564b385 | ||
|
|
7aa9e2f6a4 | ||
|
|
f645686d87 | ||
|
|
6fcaa5d3d4 | ||
|
|
5d19e17a0b | ||
|
|
52a9f1ac1d | ||
|
|
3dec5e3225 | ||
|
|
90ded1612a | ||
|
|
7ab9fb7c28 | ||
|
|
6f499c68e2 | ||
|
|
19beba8fd7 | ||
|
|
f7e33802c8 | ||
|
|
c03d94105c | ||
|
|
632d56737b | ||
|
|
8801b93973 | ||
|
|
390a2b988e | ||
|
|
a66d7a64e6 | ||
|
|
a8596d3350 | ||
|
|
8a5dabee95 | ||
|
|
da859468d3 | ||
|
|
3e4583bbe9 | ||
|
|
b47440dd72 | ||
|
|
357a05f7ec | ||
|
|
f2fdb8195c | ||
|
|
8a0275d962 | ||
|
|
ef1fd9f579 | ||
|
|
f6b95d94bc | ||
|
|
2a65e6d359 | ||
|
|
83f87ba22c | ||
|
|
78ddb8ffb6 | ||
|
|
db37f35d09 | ||
|
|
d8f5fe4fd0 |
@@ -23,7 +23,7 @@ SHiNE-server — серверная часть мессенджера SHiNE: Web
|
|||||||
|
|
||||||
- **адрес сервера** (URL WebSocket/HTTPS, например `https://shineup.me/ws`);
|
- **адрес сервера** (URL WebSocket/HTTPS, например `https://shineup.me/ws`);
|
||||||
- **список серверов синхронизации** (`sync_servers`) — логины SHiNE-аккаунтов серверов-партнёров,
|
- **список серверов синхронизации** (`sync_servers`) — логины SHiNE-аккаунтов серверов-партнёров,
|
||||||
с которыми синхронизируются блоки и DM;
|
с которыми синхронизируются пользовательские блокчейны;
|
||||||
- **корневой ключ** сервера (`root_key`).
|
- **корневой ключ** сервера (`root_key`).
|
||||||
|
|
||||||
Клиенты читают PDA напрямую из Solana, чтобы узнать адрес сервера и при необходимости подключиться.
|
Клиенты читают PDA напрямую из Solana, чтобы узнать адрес сервера и при необходимости подключиться.
|
||||||
@@ -49,7 +49,9 @@ shine-UI/server-ui.html
|
|||||||
|
|
||||||
## Синхронизация с партнёрскими серверами
|
## Синхронизация с партнёрскими серверами
|
||||||
|
|
||||||
Сервер должен синхронизировать блоки блокчейна и DM с серверами-партнёрами из `sync_servers`.
|
Сервер должен синхронизировать блоки пользовательских блокчейнов с
|
||||||
|
серверами-партнёрами из `sync_servers`. DM между партнёрами не реплицируются:
|
||||||
|
они доставляются только на первый access-сервер получателя.
|
||||||
Детали: `docs/Blockchain/sync-between-servers.md`
|
Детали: `docs/Blockchain/sync-between-servers.md`
|
||||||
|
|
||||||
## Деплой
|
## Деплой
|
||||||
|
|||||||
@@ -102,16 +102,23 @@ public final class MsgSubType {
|
|||||||
/** Удалить из близких друзей (close friend). */
|
/** Удалить из близких друзей (close friend). */
|
||||||
public static final short CONNECTION_UNCLOSE_FRIEND = 11;
|
public static final short CONNECTION_UNCLOSE_FRIEND = 11;
|
||||||
|
|
||||||
|
/** Добавить в друзья. */
|
||||||
|
public static final short CONNECTION_FRIEND = 14;
|
||||||
|
/** Удалить из друзей. */
|
||||||
|
public static final short CONNECTION_UNFRIEND = 15;
|
||||||
|
|
||||||
/** Добавить в контакты. */
|
/** Добавить в контакты. */
|
||||||
public static final short CONNECTION_CONTACT = 20;
|
public static final short CONNECTION_CONTACT = 20;
|
||||||
/** Удалить из контактов. */
|
/** Удалить из контактов. */
|
||||||
public static final short CONNECTION_UNCONTACT = 21;
|
public static final short CONNECTION_UNCONTACT = 21;
|
||||||
|
|
||||||
|
// FOLLOW на публичный канал используется. Старый FOLLOW пользователя (target header/block 0) считается legacy. DO NOT REMOVE.
|
||||||
/** Подписаться (follow). */
|
/** Подписаться (follow). */
|
||||||
public static final short CONNECTION_FOLLOW = 30;
|
public static final short CONNECTION_FOLLOW = 30;
|
||||||
/** Отписаться (unfollow). */
|
/** Отписаться (unfollow). */
|
||||||
public static final short CONNECTION_UNFOLLOW = 31;
|
public static final short CONNECTION_UNFOLLOW = 31;
|
||||||
|
|
||||||
|
// Родственные связи пока не используются в UI. Сохраняем коды. DO NOT REMOVE.
|
||||||
/** Добавить связь "жена/муж". */
|
/** Добавить связь "жена/муж". */
|
||||||
public static final short CONNECTION_SPOUSE = 40;
|
public static final short CONNECTION_SPOUSE = 40;
|
||||||
/** Удалить связь "жена/муж". */
|
/** Удалить связь "жена/муж". */
|
||||||
@@ -132,9 +139,8 @@ public final class MsgSubType {
|
|||||||
/** Удалить связь "брат/сестра". */
|
/** Удалить связь "брат/сестра". */
|
||||||
public static final short CONNECTION_UNSIBLING = 55;
|
public static final short CONNECTION_UNSIBLING = 55;
|
||||||
|
|
||||||
/** Просто знаю этого человека. */
|
// Legacy. Допускаем старые блоки 60/61, но новый UI их не создаёт и не использует. DO NOT REMOVE.
|
||||||
public static final short CONNECTION_KNOWN_PERSON = 60;
|
public static final short CONNECTION_KNOWN_PERSON = 60;
|
||||||
/** Не знаю этого человека. */
|
|
||||||
public static final short CONNECTION_UNKNOWN_PERSON = 61;
|
public static final short CONNECTION_UNKNOWN_PERSON = 61;
|
||||||
|
|
||||||
/** Точно уверен, что сияющий. */
|
/** Точно уверен, что сияющий. */
|
||||||
@@ -142,11 +148,17 @@ public final class MsgSubType {
|
|||||||
/** Не подтверждаю, что сияющий. */
|
/** Не подтверждаю, что сияющий. */
|
||||||
public static final short CONNECTION_SHINE_UNCONFIRMED = 71;
|
public static final short CONNECTION_SHINE_UNCONFIRMED = 71;
|
||||||
|
|
||||||
|
// Пока не используется в UI. Сохраняем для совместимости протокола/блокчейна. DO NOT REMOVE.
|
||||||
/** Мало знаком, но видел сияющим. */
|
/** Мало знаком, но видел сияющим. */
|
||||||
public static final short CONNECTION_SHINE_SEEN = 74;
|
public static final short CONNECTION_SHINE_SEEN = 74;
|
||||||
/** Не отмечаю, что видел сияющим. */
|
/** Не отмечаю, что видел сияющим. */
|
||||||
public static final short CONNECTION_SHINE_UNSEEN = 75;
|
public static final short CONNECTION_SHINE_UNSEEN = 75;
|
||||||
|
|
||||||
|
/** Подтверждаю, что это действительно официальный аккаунт этого человека. */
|
||||||
|
public static final short CONNECTION_OFFICIAL_ACCOUNT_CONFIRMED = 80;
|
||||||
|
/** Снимаю подтверждение официального аккаунта этого человека. */
|
||||||
|
public static final short CONNECTION_OFFICIAL_ACCOUNT_UNCONFIRMED = 81;
|
||||||
|
|
||||||
/* ===================== USER_PARAM (msg_type=4) ===================== */
|
/* ===================== USER_PARAM (msg_type=4) ===================== */
|
||||||
|
|
||||||
/** Параметр профиля key/value (обе строки). */
|
/** Параметр профиля key/value (обе строки). */
|
||||||
|
|||||||
+9
-3
@@ -20,9 +20,11 @@ import java.util.Objects;
|
|||||||
* PARENT=50, UNPARENT=51
|
* PARENT=50, UNPARENT=51
|
||||||
* CHILD=52, UNCHILD=53
|
* CHILD=52, UNCHILD=53
|
||||||
* SIBLING=54, UNSIBLING=55
|
* SIBLING=54, UNSIBLING=55
|
||||||
* KNOWN_PERSON=60, UNKNOWN_PERSON=61
|
* FRIEND=14, UNFRIEND=15
|
||||||
|
* KNOWN_PERSON=60, UNKNOWN_PERSON=61 (legacy; accepted but not used by current UI)
|
||||||
* SHINE_CONFIRMED=70, SHINE_UNCONFIRMED=71
|
* SHINE_CONFIRMED=70, SHINE_UNCONFIRMED=71
|
||||||
* SHINE_SEEN=74, SHINE_UNSEEN=75
|
* SHINE_SEEN=74, SHINE_UNSEEN=75 (currently not used by UI)
|
||||||
|
* OFFICIAL_ACCOUNT_CONFIRMED=80, OFFICIAL_ACCOUNT_UNCONFIRMED=81
|
||||||
*
|
*
|
||||||
* bodyBytes (BigEndian), новый формат (toLogin НЕ ХРАНИМ):
|
* bodyBytes (BigEndian), новый формат (toLogin НЕ ХРАНИМ):
|
||||||
* [4] lineCode
|
* [4] lineCode
|
||||||
@@ -185,6 +187,8 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasL
|
|||||||
int v = st & 0xFFFF;
|
int v = st & 0xFFFF;
|
||||||
return v == (MsgSubType.CONNECTION_CLOSE_FRIEND & 0xFFFF)
|
return v == (MsgSubType.CONNECTION_CLOSE_FRIEND & 0xFFFF)
|
||||||
|| v == (MsgSubType.CONNECTION_UNCLOSE_FRIEND & 0xFFFF)
|
|| v == (MsgSubType.CONNECTION_UNCLOSE_FRIEND & 0xFFFF)
|
||||||
|
|| v == (MsgSubType.CONNECTION_FRIEND & 0xFFFF)
|
||||||
|
|| v == (MsgSubType.CONNECTION_UNFRIEND & 0xFFFF)
|
||||||
|| v == (MsgSubType.CONNECTION_CONTACT & 0xFFFF)
|
|| v == (MsgSubType.CONNECTION_CONTACT & 0xFFFF)
|
||||||
|| v == (MsgSubType.CONNECTION_UNCONTACT & 0xFFFF)
|
|| v == (MsgSubType.CONNECTION_UNCONTACT & 0xFFFF)
|
||||||
|| v == (MsgSubType.CONNECTION_FOLLOW & 0xFFFF)
|
|| v == (MsgSubType.CONNECTION_FOLLOW & 0xFFFF)
|
||||||
@@ -202,7 +206,9 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasL
|
|||||||
|| v == (MsgSubType.CONNECTION_SHINE_CONFIRMED & 0xFFFF)
|
|| v == (MsgSubType.CONNECTION_SHINE_CONFIRMED & 0xFFFF)
|
||||||
|| v == (MsgSubType.CONNECTION_SHINE_UNCONFIRMED & 0xFFFF)
|
|| v == (MsgSubType.CONNECTION_SHINE_UNCONFIRMED & 0xFFFF)
|
||||||
|| v == (MsgSubType.CONNECTION_SHINE_SEEN & 0xFFFF)
|
|| v == (MsgSubType.CONNECTION_SHINE_SEEN & 0xFFFF)
|
||||||
|| v == (MsgSubType.CONNECTION_SHINE_UNSEEN & 0xFFFF);
|
|| v == (MsgSubType.CONNECTION_SHINE_UNSEEN & 0xFFFF)
|
||||||
|
|| v == (MsgSubType.CONNECTION_OFFICIAL_ACCOUNT_CONFIRMED & 0xFFFF)
|
||||||
|
|| v == (MsgSubType.CONNECTION_OFFICIAL_ACCOUNT_UNCONFIRMED & 0xFFFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ public final class DatabaseInitializer {
|
|||||||
public static final int SCHEMA_VERSION_13 = 13;
|
public static final int SCHEMA_VERSION_13 = 13;
|
||||||
public static final int SCHEMA_VERSION_14 = 14;
|
public static final int SCHEMA_VERSION_14 = 14;
|
||||||
public static final int SCHEMA_VERSION_15 = 15;
|
public static final int SCHEMA_VERSION_15 = 15;
|
||||||
|
public static final int SCHEMA_VERSION_16 = 16;
|
||||||
|
public static final int SCHEMA_VERSION_17 = 17;
|
||||||
|
public static final int SCHEMA_VERSION_18 = 18;
|
||||||
|
public static final int SCHEMA_VERSION_19 = 19;
|
||||||
|
public static final int SCHEMA_VERSION_20 = 20;
|
||||||
public static final String POSTGRES_SCHEMA_RESOURCE = "postgres/schema_v1.sql";
|
public static final String POSTGRES_SCHEMA_RESOURCE = "postgres/schema_v1.sql";
|
||||||
public static final String POSTGRES_MIGRATION_V2_RESOURCE = "postgres/migration_v2.sql";
|
public static final String POSTGRES_MIGRATION_V2_RESOURCE = "postgres/migration_v2.sql";
|
||||||
public static final String POSTGRES_MIGRATION_V3_RESOURCE = "postgres/migration_v3.sql";
|
public static final String POSTGRES_MIGRATION_V3_RESOURCE = "postgres/migration_v3.sql";
|
||||||
@@ -47,6 +52,11 @@ public final class DatabaseInitializer {
|
|||||||
public static final String POSTGRES_MIGRATION_V13_RESOURCE = "postgres/migration_v13.sql";
|
public static final String POSTGRES_MIGRATION_V13_RESOURCE = "postgres/migration_v13.sql";
|
||||||
public static final String POSTGRES_MIGRATION_V14_RESOURCE = "postgres/migration_v14.sql";
|
public static final String POSTGRES_MIGRATION_V14_RESOURCE = "postgres/migration_v14.sql";
|
||||||
public static final String POSTGRES_MIGRATION_V15_RESOURCE = "postgres/migration_v15.sql";
|
public static final String POSTGRES_MIGRATION_V15_RESOURCE = "postgres/migration_v15.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V16_RESOURCE = "postgres/migration_v16.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V17_RESOURCE = "postgres/migration_v17.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V18_RESOURCE = "postgres/migration_v18.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V19_RESOURCE = "postgres/migration_v19.sql";
|
||||||
|
public static final String POSTGRES_MIGRATION_V20_RESOURCE = "postgres/migration_v20.sql";
|
||||||
|
|
||||||
private DatabaseInitializer() {}
|
private DatabaseInitializer() {}
|
||||||
|
|
||||||
@@ -68,12 +78,17 @@ public final class DatabaseInitializer {
|
|||||||
public static final short CONNECTION_CLOSE_FRIEND = 10;
|
public static final short CONNECTION_CLOSE_FRIEND = 10;
|
||||||
public static final short CONNECTION_UNCLOSE_FRIEND = 11;
|
public static final short CONNECTION_UNCLOSE_FRIEND = 11;
|
||||||
|
|
||||||
|
public static final short CONNECTION_FRIEND = 14;
|
||||||
|
public static final short CONNECTION_UNFRIEND = 15;
|
||||||
|
|
||||||
public static final short CONNECTION_CONTACT = 20;
|
public static final short CONNECTION_CONTACT = 20;
|
||||||
public static final short CONNECTION_UNCONTACT = 21;
|
public static final short CONNECTION_UNCONTACT = 21;
|
||||||
|
|
||||||
|
// FOLLOW на публичный канал используется; user-to-user FOLLOW legacy. DO NOT REMOVE.
|
||||||
public static final short CONNECTION_FOLLOW = 30;
|
public static final short CONNECTION_FOLLOW = 30;
|
||||||
public static final short CONNECTION_UNFOLLOW = 31;
|
public static final short CONNECTION_UNFOLLOW = 31;
|
||||||
|
|
||||||
|
// Родственные связи пока не используются в UI. DO NOT REMOVE.
|
||||||
public static final short CONNECTION_SPOUSE = 40;
|
public static final short CONNECTION_SPOUSE = 40;
|
||||||
public static final short CONNECTION_UNSPOUSE = 41;
|
public static final short CONNECTION_UNSPOUSE = 41;
|
||||||
|
|
||||||
@@ -86,15 +101,20 @@ public final class DatabaseInitializer {
|
|||||||
public static final short CONNECTION_SIBLING = 54;
|
public static final short CONNECTION_SIBLING = 54;
|
||||||
public static final short CONNECTION_UNSIBLING = 55;
|
public static final short CONNECTION_UNSIBLING = 55;
|
||||||
|
|
||||||
|
// Legacy 60/61: новый UI не использует. DO NOT REMOVE.
|
||||||
public static final short CONNECTION_KNOWN_PERSON = 60;
|
public static final short CONNECTION_KNOWN_PERSON = 60;
|
||||||
public static final short CONNECTION_UNKNOWN_PERSON = 61;
|
public static final short CONNECTION_UNKNOWN_PERSON = 61;
|
||||||
|
|
||||||
public static final short CONNECTION_SHINE_CONFIRMED = 70;
|
public static final short CONNECTION_SHINE_CONFIRMED = 70;
|
||||||
public static final short CONNECTION_SHINE_UNCONFIRMED = 71;
|
public static final short CONNECTION_SHINE_UNCONFIRMED = 71;
|
||||||
|
|
||||||
|
// Пока не используется в UI. DO NOT REMOVE.
|
||||||
public static final short CONNECTION_SHINE_SEEN = 74;
|
public static final short CONNECTION_SHINE_SEEN = 74;
|
||||||
public static final short CONNECTION_SHINE_UNSEEN = 75;
|
public static final short CONNECTION_SHINE_UNSEEN = 75;
|
||||||
|
|
||||||
|
public static final short CONNECTION_OFFICIAL_ACCOUNT_CONFIRMED = 80;
|
||||||
|
public static final short CONNECTION_OFFICIAL_ACCOUNT_UNCONFIRMED = 81;
|
||||||
|
|
||||||
public static void ensurePostgresSchemaInitialized(String jdbcUrl,
|
public static void ensurePostgresSchemaInitialized(String jdbcUrl,
|
||||||
String user,
|
String user,
|
||||||
String password) throws SQLException {
|
String password) throws SQLException {
|
||||||
@@ -166,6 +186,26 @@ public final class DatabaseInitializer {
|
|||||||
runSqlScript(conn, POSTGRES_MIGRATION_V15_RESOURCE);
|
runSqlScript(conn, POSTGRES_MIGRATION_V15_RESOURCE);
|
||||||
currentVersion = SCHEMA_VERSION_15;
|
currentVersion = SCHEMA_VERSION_15;
|
||||||
}
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_16) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V16_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_16;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_17) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V17_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_17;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_18) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V18_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_18;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_19) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V19_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_19;
|
||||||
|
}
|
||||||
|
if (currentVersion < SCHEMA_VERSION_20) {
|
||||||
|
runSqlScript(conn, POSTGRES_MIGRATION_V20_RESOURCE);
|
||||||
|
currentVersion = SCHEMA_VERSION_20;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,10 +58,10 @@ public final class MsgSubType {
|
|||||||
/* ===================== CONNECTION (msg_type=3) ===================== */
|
/* ===================== CONNECTION (msg_type=3) ===================== */
|
||||||
/**
|
/**
|
||||||
* Совпадает с ConnectionBody:
|
* Совпадает с ConnectionBody:
|
||||||
* SET: CLOSE_FRIEND=10, CONTACT=20, FOLLOW=30, SPOUSE=40, PARENT=50, CHILD=52, SIBLING=54,
|
* SET: CLOSE_FRIEND=10, FRIEND=14, CONTACT=20, FOLLOW=30, SPOUSE=40, PARENT=50, CHILD=52, SIBLING=54,
|
||||||
* KNOWN_PERSON=60, SHINE_CONFIRMED=70, SHINE_SEEN=74
|
* KNOWN_PERSON(legacy)=60, SHINE_CONFIRMED=70, SHINE_SEEN=74, OFFICIAL_ACCOUNT_CONFIRMED=80
|
||||||
* UNSET: UNCLOSE_FRIEND=11, UNCONTACT=21, UNFOLLOW=31, UNSPOUSE=41, UNPARENT=51, UNCHILD=53, UNSIBLING=55,
|
* UNSET: UNCLOSE_FRIEND=11, UNFRIEND=15, UNCONTACT=21, UNFOLLOW=31, UNSPOUSE=41, UNPARENT=51, UNCHILD=53, UNSIBLING=55,
|
||||||
* UNKNOWN_PERSON=61, SHINE_UNCONFIRMED=71, SHINE_UNSEEN=75
|
* UNKNOWN_PERSON(legacy)=61, SHINE_UNCONFIRMED=71, SHINE_UNSEEN=75, OFFICIAL_ACCOUNT_UNCONFIRMED=81
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Добавить в близкие друзья (close friend). */
|
/** Добавить в близкие друзья (close friend). */
|
||||||
@@ -70,18 +70,26 @@ public final class MsgSubType {
|
|||||||
/** Удалить из близких друзей (close friend). */
|
/** Удалить из близких друзей (close friend). */
|
||||||
public static final short CONNECTION_UNCLOSE_FRIEND = 11;
|
public static final short CONNECTION_UNCLOSE_FRIEND = 11;
|
||||||
|
|
||||||
|
/** Добавить в друзья. */
|
||||||
|
public static final short CONNECTION_FRIEND = 14;
|
||||||
|
|
||||||
|
/** Удалить из друзей. */
|
||||||
|
public static final short CONNECTION_UNFRIEND = 15;
|
||||||
|
|
||||||
/** Добавить в контакты. */
|
/** Добавить в контакты. */
|
||||||
public static final short CONNECTION_CONTACT = 20;
|
public static final short CONNECTION_CONTACT = 20;
|
||||||
|
|
||||||
/** Удалить из контактов. */
|
/** Удалить из контактов. */
|
||||||
public static final short CONNECTION_UNCONTACT = 21;
|
public static final short CONNECTION_UNCONTACT = 21;
|
||||||
|
|
||||||
|
// FOLLOW на публичный канал используется. Старый FOLLOW пользователя считается legacy. DO NOT REMOVE.
|
||||||
/** Подписаться (follow). */
|
/** Подписаться (follow). */
|
||||||
public static final short CONNECTION_FOLLOW = 30;
|
public static final short CONNECTION_FOLLOW = 30;
|
||||||
|
|
||||||
/** Отписаться (unfollow). */
|
/** Отписаться (unfollow). */
|
||||||
public static final short CONNECTION_UNFOLLOW = 31;
|
public static final short CONNECTION_UNFOLLOW = 31;
|
||||||
|
|
||||||
|
// Родственные связи пока не используются в UI. Сохраняем коды. DO NOT REMOVE.
|
||||||
/** Добавить связь "жена/муж". */
|
/** Добавить связь "жена/муж". */
|
||||||
public static final short CONNECTION_SPOUSE = 40;
|
public static final short CONNECTION_SPOUSE = 40;
|
||||||
|
|
||||||
@@ -106,10 +114,8 @@ public final class MsgSubType {
|
|||||||
/** Удалить связь "брат/сестра". */
|
/** Удалить связь "брат/сестра". */
|
||||||
public static final short CONNECTION_UNSIBLING = 55;
|
public static final short CONNECTION_UNSIBLING = 55;
|
||||||
|
|
||||||
/** Просто знаю этого человека. */
|
// Legacy 60/61: принимаем старые данные, но новый UI их не использует. DO NOT REMOVE.
|
||||||
public static final short CONNECTION_KNOWN_PERSON = 60;
|
public static final short CONNECTION_KNOWN_PERSON = 60;
|
||||||
|
|
||||||
/** Не знаю этого человека. */
|
|
||||||
public static final short CONNECTION_UNKNOWN_PERSON = 61;
|
public static final short CONNECTION_UNKNOWN_PERSON = 61;
|
||||||
|
|
||||||
/** Точно уверен, что сияющий. */
|
/** Точно уверен, что сияющий. */
|
||||||
@@ -118,12 +124,19 @@ public final class MsgSubType {
|
|||||||
/** Не подтверждаю, что сияющий. */
|
/** Не подтверждаю, что сияющий. */
|
||||||
public static final short CONNECTION_SHINE_UNCONFIRMED = 71;
|
public static final short CONNECTION_SHINE_UNCONFIRMED = 71;
|
||||||
|
|
||||||
|
// Пока не используется в UI. Сохраняем для совместимости. DO NOT REMOVE.
|
||||||
/** Мало знаком, но видел сияющим. */
|
/** Мало знаком, но видел сияющим. */
|
||||||
public static final short CONNECTION_SHINE_SEEN = 74;
|
public static final short CONNECTION_SHINE_SEEN = 74;
|
||||||
|
|
||||||
/** Не отмечаю, что видел сияющим. */
|
/** Не отмечаю, что видел сияющим. */
|
||||||
public static final short CONNECTION_SHINE_UNSEEN = 75;
|
public static final short CONNECTION_SHINE_UNSEEN = 75;
|
||||||
|
|
||||||
|
/** Подтверждение официального аккаунта человека. */
|
||||||
|
public static final short CONNECTION_OFFICIAL_ACCOUNT_CONFIRMED = 80;
|
||||||
|
|
||||||
|
/** Снятие подтверждения официального аккаунта человека. */
|
||||||
|
public static final short CONNECTION_OFFICIAL_ACCOUNT_UNCONFIRMED = 81;
|
||||||
|
|
||||||
/* ===================== USER_PARAM (msg_type=4) ===================== */
|
/* ===================== USER_PARAM (msg_type=4) ===================== */
|
||||||
|
|
||||||
/** Параметр профиля key/value (обе строки). */
|
/** Параметр профиля key/value (обе строки). */
|
||||||
|
|||||||
+19
@@ -426,6 +426,25 @@ public final class BlockchainResyncCleanupDAO {
|
|||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("SELECT shine_refresh_user_profile(login) FROM solana_user_pda_current")) {
|
||||||
|
ps.execute();
|
||||||
|
}
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("SELECT shine_refresh_user_stats(login) FROM solana_user_pda_current")) {
|
||||||
|
ps.execute();
|
||||||
|
}
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
|
UPDATE message_stats ms SET
|
||||||
|
primary_likes_count=(SELECT COUNT(*)::INTEGER FROM reactions_state rs WHERE rs.reaction_type=1 AND rs.last_sub_type=1
|
||||||
|
AND rs.to_login=ms.to_login AND rs.to_bch_name=ms.to_bch_name AND rs.to_block_number=ms.to_block_number AND rs.to_block_hash=ms.to_block_hash
|
||||||
|
AND shine_is_primary(rs.from_login)),
|
||||||
|
shining_likes_count=(SELECT COUNT(*)::INTEGER FROM reactions_state rs WHERE rs.reaction_type=1 AND rs.last_sub_type=1
|
||||||
|
AND rs.to_login=ms.to_login AND rs.to_bch_name=ms.to_bch_name AND rs.to_block_number=ms.to_block_number AND rs.to_block_hash=ms.to_block_hash
|
||||||
|
AND shine_is_primary(rs.from_login) AND shine_is_shining(rs.from_login))
|
||||||
|
""")) {
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement("""
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
INSERT INTO channel_stats_state (
|
INSERT INTO channel_stats_state (
|
||||||
owner_bch_name,
|
owner_bch_name,
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ public final class DmDialogStateDAO {
|
|||||||
|
|
||||||
Map<String, DialogSummary> byPeer = new LinkedHashMap<>();
|
Map<String, DialogSummary> byPeer = new LinkedHashMap<>();
|
||||||
List<String> contacts = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, cleanOwner, MsgSubType.CONNECTION_CONTACT);
|
List<String> contacts = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, cleanOwner, MsgSubType.CONNECTION_CONTACT);
|
||||||
|
List<String> friends = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, cleanOwner, MsgSubType.CONNECTION_FRIEND);
|
||||||
List<String> closeFriends = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, cleanOwner, MsgSubType.CONNECTION_CLOSE_FRIEND);
|
List<String> closeFriends = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, cleanOwner, MsgSubType.CONNECTION_CLOSE_FRIEND);
|
||||||
|
|
||||||
try (PreparedStatement ps = c.prepareStatement("""
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
@@ -76,16 +77,18 @@ public final class DmDialogStateDAO {
|
|||||||
ps.setString(1, cleanOwner);
|
ps.setString(1, cleanOwner);
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
|
long lastMessageTimeMs = rs.getLong("last_message_time_ms");
|
||||||
|
int unreadCount = rs.getInt("unread_count");
|
||||||
DialogSummary row = new DialogSummary(
|
DialogSummary row = new DialogSummary(
|
||||||
rs.getString("owner_login"),
|
rs.getString("owner_login"),
|
||||||
rs.getString("peer_login"),
|
rs.getString("peer_login"),
|
||||||
normalizeRelationFlag(rs.getString("relation_flag")),
|
normalizeRelationFlag(rs.getString("relation_flag")),
|
||||||
rs.getString("last_message_blob_b64"),
|
rs.getString("last_message_blob_b64"),
|
||||||
rs.getLong("last_message_time_ms"),
|
lastMessageTimeMs,
|
||||||
rs.getInt("unread_count"),
|
unreadCount,
|
||||||
rs.getLong("last_read_receipt_time_ms"),
|
rs.getLong("last_read_receipt_time_ms"),
|
||||||
rs.getLong("updated_at_ms"),
|
rs.getLong("updated_at_ms"),
|
||||||
true
|
lastMessageTimeMs > 0 || unreadCount > 0
|
||||||
);
|
);
|
||||||
byPeer.put(normKey(row.peerLogin()), row);
|
byPeer.put(normKey(row.peerLogin()), row);
|
||||||
}
|
}
|
||||||
@@ -95,6 +98,9 @@ public final class DmDialogStateDAO {
|
|||||||
for (String peer : contacts) {
|
for (String peer : contacts) {
|
||||||
addOrUpdateSummary(byPeer, cleanOwner, peer, "contact", false);
|
addOrUpdateSummary(byPeer, cleanOwner, peer, "contact", false);
|
||||||
}
|
}
|
||||||
|
for (String peer : friends) {
|
||||||
|
addOrUpdateSummary(byPeer, cleanOwner, peer, "friend", false);
|
||||||
|
}
|
||||||
for (String peer : closeFriends) {
|
for (String peer : closeFriends) {
|
||||||
addOrUpdateSummary(byPeer, cleanOwner, peer, "close_friend", false);
|
addOrUpdateSummary(byPeer, cleanOwner, peer, "close_friend", false);
|
||||||
}
|
}
|
||||||
@@ -286,6 +292,9 @@ public final class DmDialogStateDAO {
|
|||||||
if (ConnectionsStateDAO.getInstance().hasOutgoingByRelTypeCanonical(c, ownerLogin, peerLogin, MsgSubType.CONNECTION_CLOSE_FRIEND)) {
|
if (ConnectionsStateDAO.getInstance().hasOutgoingByRelTypeCanonical(c, ownerLogin, peerLogin, MsgSubType.CONNECTION_CLOSE_FRIEND)) {
|
||||||
return "close_friend";
|
return "close_friend";
|
||||||
}
|
}
|
||||||
|
if (ConnectionsStateDAO.getInstance().hasOutgoingByRelTypeCanonical(c, ownerLogin, peerLogin, MsgSubType.CONNECTION_FRIEND)) {
|
||||||
|
return "friend";
|
||||||
|
}
|
||||||
if (ConnectionsStateDAO.getInstance().hasOutgoingByRelTypeCanonical(c, ownerLogin, peerLogin, MsgSubType.CONNECTION_CONTACT)) {
|
if (ConnectionsStateDAO.getInstance().hasOutgoingByRelTypeCanonical(c, ownerLogin, peerLogin, MsgSubType.CONNECTION_CONTACT)) {
|
||||||
return "contact";
|
return "contact";
|
||||||
}
|
}
|
||||||
@@ -357,6 +366,8 @@ public final class DmDialogStateDAO {
|
|||||||
String nextRelation = current.relationFlag();
|
String nextRelation = current.relationFlag();
|
||||||
if ("close_friend".equalsIgnoreCase(relationFlag) || "close_friend".equalsIgnoreCase(nextRelation)) {
|
if ("close_friend".equalsIgnoreCase(relationFlag) || "close_friend".equalsIgnoreCase(nextRelation)) {
|
||||||
nextRelation = "close_friend";
|
nextRelation = "close_friend";
|
||||||
|
} else if ("friend".equalsIgnoreCase(relationFlag) || "friend".equalsIgnoreCase(nextRelation)) {
|
||||||
|
nextRelation = "friend";
|
||||||
} else if ("contact".equalsIgnoreCase(relationFlag) || "contact".equalsIgnoreCase(nextRelation)) {
|
} else if ("contact".equalsIgnoreCase(relationFlag) || "contact".equalsIgnoreCase(nextRelation)) {
|
||||||
nextRelation = "contact";
|
nextRelation = "contact";
|
||||||
} else {
|
} else {
|
||||||
@@ -433,7 +444,7 @@ public final class DmDialogStateDAO {
|
|||||||
|
|
||||||
private String normalizeRelationFlag(String value) {
|
private String normalizeRelationFlag(String value) {
|
||||||
String clean = normalize(value).toLowerCase(Locale.ROOT);
|
String clean = normalize(value).toLowerCase(Locale.ROOT);
|
||||||
if ("close_friend".equals(clean) || "contact".equals(clean)) return clean;
|
if ("close_friend".equals(clean) || "friend".equals(clean) || "contact".equals(clean)) return clean;
|
||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+42
@@ -36,6 +36,48 @@ public final class UserAccessServersCurrentDAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает первый access server пользователя ровно в порядке
|
||||||
|
* solana_user_pda_current.access_servers_json (access_servers[0]).
|
||||||
|
*
|
||||||
|
* В user_access_servers_current порядок массива намеренно не хранится,
|
||||||
|
* поэтому для auth-routing первичный сервер читается напрямую из PDA snapshot.
|
||||||
|
*/
|
||||||
|
public UserAccessServerRouteEntry findPrimaryByUserLogin(String userLogin) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT
|
||||||
|
u.login AS user_login,
|
||||||
|
s.login AS server_login,
|
||||||
|
s.server_address AS server_url,
|
||||||
|
s.client_key AS server_client_key
|
||||||
|
FROM solana_user_pda_current u
|
||||||
|
CROSS JOIN LATERAL (
|
||||||
|
SELECT BTRIM(a.login_value) AS server_login
|
||||||
|
FROM jsonb_array_elements_text(
|
||||||
|
CASE
|
||||||
|
WHEN BTRIM(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||||
|
ELSE u.access_servers_json::jsonb
|
||||||
|
END
|
||||||
|
) WITH ORDINALITY AS a(login_value, ord)
|
||||||
|
WHERE a.ord = 1
|
||||||
|
) primary_access
|
||||||
|
JOIN solana_user_pda_current s
|
||||||
|
ON s.normalized_login = LOWER(primary_access.server_login)
|
||||||
|
AND s.is_server = TRUE
|
||||||
|
AND BTRIM(COALESCE(s.server_address, '')) <> ''
|
||||||
|
WHERE u.normalized_login = LOWER(BTRIM(?))
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
try (Connection c = db.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, userLogin);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (!rs.next()) return null;
|
||||||
|
return mapRow(rs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public List<UserAccessServerRouteEntry> listByUserLogin(Connection c, String userLogin) throws SQLException {
|
public List<UserAccessServerRouteEntry> listByUserLogin(Connection c, String userLogin) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT user_login, server_login, server_url, server_client_key
|
SELECT user_login, server_login, server_url, server_client_key
|
||||||
|
|||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package shine.db.dao;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
|
||||||
|
public final class UserNotificationSeenStateDAO {
|
||||||
|
private static final UserNotificationSeenStateDAO INSTANCE = new UserNotificationSeenStateDAO();
|
||||||
|
private UserNotificationSeenStateDAO() {}
|
||||||
|
public static UserNotificationSeenStateDAO getInstance() { return INSTANCE; }
|
||||||
|
|
||||||
|
public long getSeenAt(Connection c, String login, String category) throws Exception {
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("SELECT seen_at_ms FROM user_notification_seen_state WHERE owner_login=? AND category=?")) {
|
||||||
|
ps.setString(1, login); ps.setString(2, category);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) { return rs.next() ? rs.getLong(1) : 0L; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long advance(Connection c, String login, String category, long seenAtMs, long signedAtMs, byte[] signedBlob) throws Exception {
|
||||||
|
String sql = """
|
||||||
|
INSERT INTO user_notification_seen_state(owner_login, category, seen_at_ms, signed_blob, signed_at_ms, updated_at_ms)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(owner_login, category) DO UPDATE SET
|
||||||
|
seen_at_ms=EXCLUDED.seen_at_ms, signed_blob=EXCLUDED.signed_blob,
|
||||||
|
signed_at_ms=EXCLUDED.signed_at_ms, updated_at_ms=EXCLUDED.updated_at_ms
|
||||||
|
WHERE user_notification_seen_state.seen_at_ms < EXCLUDED.seen_at_ms
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, login); ps.setString(2, category); ps.setLong(3, seenAtMs);
|
||||||
|
ps.setBytes(4, signedBlob); ps.setLong(5, signedAtMs); ps.setLong(6, System.currentTimeMillis());
|
||||||
|
ps.executeUpdate();
|
||||||
|
}
|
||||||
|
return getSeenAt(c, login, category);
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -84,6 +84,33 @@ public final class UserNotificationsStateDAO {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<UserNotificationEntry> listVisible(Connection c, String ownerLogin, String kind, long seenAtMs, long cutoffMs) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT owner_login, notification_kind, created_at_ms, source_login, source_bch_name,
|
||||||
|
source_block_number, source_block_hash, target_login, target_bch_name,
|
||||||
|
target_block_number, target_block_hash, source_msg_sub_type, source_text
|
||||||
|
FROM user_notifications_state
|
||||||
|
WHERE owner_login = ? AND notification_kind = ?
|
||||||
|
AND (created_at_ms > ? OR created_at_ms >= ?)
|
||||||
|
ORDER BY created_at_ms DESC, source_block_number DESC
|
||||||
|
""";
|
||||||
|
List<UserNotificationEntry> out = new ArrayList<>();
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, ownerLogin); ps.setString(2, kind); ps.setLong(3, Math.max(0, seenAtMs));
|
||||||
|
ps.setLong(4, Math.max(0, cutoffMs));
|
||||||
|
try (ResultSet rs = ps.executeQuery()) { while (rs.next()) out.add(mapRow(rs)); }
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long countUnseen(Connection c, String ownerLogin, String kind, long seenAtMs) throws SQLException {
|
||||||
|
String sql = "SELECT COUNT(*) FROM user_notifications_state WHERE owner_login = ? AND notification_kind = ? AND created_at_ms > ?";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, ownerLogin); ps.setString(2, kind); ps.setLong(3, Math.max(0, seenAtMs));
|
||||||
|
try (ResultSet rs = ps.executeQuery()) { return rs.next() ? rs.getLong(1) : 0L; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static UserNotificationEntry mapRow(ResultSet rs) throws SQLException {
|
private static UserNotificationEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
UserNotificationEntry e = new UserNotificationEntry();
|
UserNotificationEntry e = new UserNotificationEntry();
|
||||||
e.setOwnerLogin(rs.getString("owner_login"));
|
e.setOwnerLogin(rs.getString("owner_login"));
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ public final class UserParamsDAO {
|
|||||||
WHERE users_params.time_ms < excluded.time_ms
|
WHERE users_params.time_ms < excluded.time_ms
|
||||||
""";
|
""";
|
||||||
|
|
||||||
|
int changed;
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, e.getLogin());
|
ps.setString(1, e.getLogin());
|
||||||
ps.setString(2, e.getParam());
|
ps.setString(2, e.getParam());
|
||||||
@@ -67,8 +68,12 @@ public final class UserParamsDAO {
|
|||||||
if (e.getSignature() != null) ps.setString(6, e.getSignature());
|
if (e.getSignature() != null) ps.setString(6, e.getSignature());
|
||||||
else ps.setNull(6, Types.VARCHAR);
|
else ps.setNull(6, Types.VARCHAR);
|
||||||
|
|
||||||
return ps.executeUpdate();
|
changed = ps.executeUpdate();
|
||||||
}
|
}
|
||||||
|
if (changed > 0 && affectsFastProfile(e.getParam())) {
|
||||||
|
refreshFastProfileAndDerivedState(c, e.getLogin(), e.getParam());
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int upsertIfNewer(UserParamEntry e) throws SQLException {
|
public int upsertIfNewer(UserParamEntry e) throws SQLException {
|
||||||
@@ -77,6 +82,32 @@ public final class UserParamsDAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static boolean affectsFastProfile(String param) {
|
||||||
|
if (param == null) return false;
|
||||||
|
return switch (param) {
|
||||||
|
case "first_name", "last_name", "ava", "account_role", "shine" -> true;
|
||||||
|
default -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void refreshFastProfileAndDerivedState(Connection c, String login, String param) throws SQLException {
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("SELECT shine_refresh_user_profile(?)")) {
|
||||||
|
ps.setString(1, login);
|
||||||
|
ps.execute();
|
||||||
|
}
|
||||||
|
if ("account_role".equals(param) || "shine".equals(param)) {
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("SELECT shine_refresh_related_user_stats(?)")) {
|
||||||
|
ps.setString(1, login);
|
||||||
|
ps.execute();
|
||||||
|
}
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("SELECT shine_refresh_message_qualified_likes_for_actor(?)")) {
|
||||||
|
ps.setString(1, login);
|
||||||
|
ps.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------- SELECT --------------------
|
// -------------------- SELECT --------------------
|
||||||
|
|
||||||
public UserParamEntry getByLoginAndParam(Connection c, String login, String param) throws SQLException {
|
public UserParamEntry getByLoginAndParam(Connection c, String login, String param) throws SQLException {
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package shine.db.dao;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import shine.db.MsgSubType;
|
||||||
|
|
||||||
|
/** Fast denormalized projection for user cards shown in dialogs/relations/profile lists. */
|
||||||
|
public final class UserProfileStateDAO {
|
||||||
|
private static volatile UserProfileStateDAO instance;
|
||||||
|
private UserProfileStateDAO() {}
|
||||||
|
public static UserProfileStateDAO getInstance() {
|
||||||
|
if (instance == null) synchronized (UserProfileStateDAO.class) {
|
||||||
|
if (instance == null) instance = new UserProfileStateDAO();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProfileCard get(Connection c, String login) throws SQLException {
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
|
SELECT login, first_name, last_name, ava_ar, account_role, shine_status
|
||||||
|
FROM user_profile_state WHERE LOWER(login)=LOWER(?) LIMIT 1
|
||||||
|
""")) {
|
||||||
|
ps.setString(1, login);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (!rs.next()) return new ProfileCard(login, "", "", "", null, null);
|
||||||
|
return map(rs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Effective outgoing social relation with priority close_friend > friend > contact > none.
|
||||||
|
*
|
||||||
|
* Use the canonical relation lookup instead of comparing only connections_state.to_login:
|
||||||
|
* older/current blocks may address a user by blockchain name, so a direct to_login-only
|
||||||
|
* query can incorrectly report "none" even when ListContacts/Connections sees the relation.
|
||||||
|
*/
|
||||||
|
public String getEffectiveRelationType(Connection c, String ownerLogin, String targetLogin) throws SQLException {
|
||||||
|
if (ownerLogin == null || ownerLogin.isBlank() || targetLogin == null || targetLogin.isBlank()) return "none";
|
||||||
|
ConnectionsStateDAO relations = ConnectionsStateDAO.getInstance();
|
||||||
|
if (relations.hasOutgoingByRelTypeCanonical(c, ownerLogin, targetLogin, MsgSubType.CONNECTION_CLOSE_FRIEND)) {
|
||||||
|
return "close_friend";
|
||||||
|
}
|
||||||
|
if (relations.hasOutgoingByRelTypeCanonical(c, ownerLogin, targetLogin, MsgSubType.CONNECTION_FRIEND)) {
|
||||||
|
return "friend";
|
||||||
|
}
|
||||||
|
if (relations.hasOutgoingByRelTypeCanonical(c, ownerLogin, targetLogin, MsgSubType.CONNECTION_CONTACT)) {
|
||||||
|
return "contact";
|
||||||
|
}
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RelationCard> listRelations(Connection c, String ownerLogin, String listType, int limit, int offset) throws SQLException {
|
||||||
|
int safeLimit = Math.max(1, Math.min(limit <= 0 ? 100 : limit, 500));
|
||||||
|
int safeOffset = Math.max(0, offset);
|
||||||
|
String where = switch (String.valueOf(listType)) {
|
||||||
|
case "friends" -> "LOWER(cs.login)=LOWER(?) AND cs.rel_type=14 AND NOT EXISTS (SELECT 1 FROM connections_state cf WHERE LOWER(cf.login)=LOWER(cs.login) AND LOWER(cf.to_login)=LOWER(cs.to_login) AND cf.rel_type=10)";
|
||||||
|
case "close_friends" -> "LOWER(cs.login)=LOWER(?) AND cs.rel_type=10";
|
||||||
|
case "primary_received" -> "LOWER(cs.to_login)=LOWER(?) AND cs.rel_type=80 AND shine_is_primary(cs.login) AND shine_target_accepts_primary_confirmation(cs.to_login)";
|
||||||
|
case "primary_given" -> "LOWER(cs.login)=LOWER(?) AND cs.rel_type=80 AND shine_is_primary(cs.login) AND shine_target_accepts_primary_confirmation(cs.to_login)";
|
||||||
|
case "shine_received" -> "LOWER(cs.to_login)=LOWER(?) AND cs.rel_type=70 AND shine_is_primary(cs.login) AND shine_is_shining(cs.login) AND shine_target_accepts_shine_confirmation(cs.to_login)";
|
||||||
|
case "shine_given" -> "LOWER(cs.login)=LOWER(?) AND cs.rel_type=70 AND shine_is_primary(cs.login) AND shine_is_shining(cs.login) AND shine_target_accepts_shine_confirmation(cs.to_login)";
|
||||||
|
default -> throw new IllegalArgumentException("Unsupported listType: " + listType);
|
||||||
|
};
|
||||||
|
boolean received = "primary_received".equals(listType) || "shine_received".equals(listType);
|
||||||
|
String personExpr = received ? "cs.login" : "cs.to_login";
|
||||||
|
String sql = """
|
||||||
|
SELECT DISTINCT ON (LOWER(%s))
|
||||||
|
%s AS person_login,
|
||||||
|
COALESCE(up.first_name,'') AS first_name,
|
||||||
|
COALESCE(up.last_name,'') AS last_name,
|
||||||
|
COALESCE(up.ava_ar,'') AS ava_ar,
|
||||||
|
up.account_role, up.shine_status,
|
||||||
|
EXISTS(SELECT 1 FROM connections_state x WHERE LOWER(x.login)=LOWER(?) AND LOWER(x.to_login)=LOWER(%s) AND x.rel_type=80
|
||||||
|
AND shine_is_primary(x.login) AND shine_target_accepts_primary_confirmation(x.to_login)) AS primary_confirmed,
|
||||||
|
EXISTS(SELECT 1 FROM connections_state x WHERE LOWER(x.login)=LOWER(?) AND LOWER(x.to_login)=LOWER(%s) AND x.rel_type=70
|
||||||
|
AND shine_is_primary(x.login) AND shine_is_shining(x.login) AND shine_target_accepts_shine_confirmation(x.to_login)) AS shine_confirmed,
|
||||||
|
CASE
|
||||||
|
WHEN EXISTS(SELECT 1 FROM connections_state x WHERE LOWER(x.login)=LOWER(?) AND LOWER(x.to_login)=LOWER(%s) AND x.rel_type=10) THEN 'close_friend'
|
||||||
|
WHEN EXISTS(SELECT 1 FROM connections_state x WHERE LOWER(x.login)=LOWER(?) AND LOWER(x.to_login)=LOWER(%s) AND x.rel_type=14) THEN 'friend'
|
||||||
|
WHEN EXISTS(SELECT 1 FROM connections_state x WHERE LOWER(x.login)=LOWER(?) AND LOWER(x.to_login)=LOWER(%s) AND x.rel_type=20) THEN 'contact'
|
||||||
|
ELSE 'none' END AS relation_type,
|
||||||
|
COALESCE(us.primary_confirmations_received_count,0) AS primary_confirmations_count,
|
||||||
|
COALESCE(us.shine_confirmations_received_count,0) AS shine_confirmations_count
|
||||||
|
FROM connections_state cs
|
||||||
|
LEFT JOIN user_profile_state up ON LOWER(up.login)=LOWER(%s)
|
||||||
|
LEFT JOIN user_stats_state us ON LOWER(us.login)=LOWER(%s)
|
||||||
|
WHERE %s
|
||||||
|
ORDER BY LOWER(%s), %s
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
""".formatted(personExpr, personExpr, personExpr, personExpr, personExpr, personExpr, personExpr, personExpr, personExpr, where, personExpr, personExpr);
|
||||||
|
List<RelationCard> out = new ArrayList<>();
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
int i=1;
|
||||||
|
ps.setString(i++, ownerLogin); ps.setString(i++, ownerLogin); ps.setString(i++, ownerLogin); ps.setString(i++, ownerLogin); ps.setString(i++, ownerLogin);
|
||||||
|
ps.setString(i++, ownerLogin);
|
||||||
|
ps.setInt(i++, safeLimit); ps.setInt(i, safeOffset);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) out.add(new RelationCard(
|
||||||
|
rs.getString("person_login"), rs.getString("first_name"), rs.getString("last_name"), rs.getString("ava_ar"),
|
||||||
|
rs.getString("account_role"), rs.getString("shine_status"), rs.getBoolean("primary_confirmed"), rs.getBoolean("shine_confirmed"),
|
||||||
|
rs.getString("relation_type"), rs.getInt("primary_confirmations_count"), rs.getInt("shine_confirmations_count")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChannelCard> listPublicChannels(Connection c, String login, String mode, int limit, int offset) throws SQLException {
|
||||||
|
int safeLimit=Math.max(1,Math.min(limit<=0?100:limit,500)), safeOffset=Math.max(0,offset);
|
||||||
|
String sql;
|
||||||
|
if ("owned".equals(mode)) {
|
||||||
|
sql="""
|
||||||
|
SELECT cn.owner_login, cn.slug, cn.display_name, cn.ava_ar, cn.owner_bch_name, cn.channel_root_block_number, encode(cn.channel_root_block_hash,'hex') root_hash
|
||||||
|
FROM channel_names_state cn WHERE LOWER(cn.owner_login)=LOWER(?) AND cn.channel_type_code=1
|
||||||
|
ORDER BY cn.display_name, cn.slug LIMIT ? OFFSET ?
|
||||||
|
""";
|
||||||
|
} else if ("following".equals(mode)) {
|
||||||
|
sql="""
|
||||||
|
SELECT DISTINCT cn.owner_login, cn.slug, cn.display_name, cn.ava_ar, cn.owner_bch_name, cn.channel_root_block_number, encode(cn.channel_root_block_hash,'hex') root_hash
|
||||||
|
FROM connections_state cs JOIN channel_names_state cn ON cn.owner_bch_name=cs.to_bch_name AND cn.channel_root_block_number=cs.to_block_number AND cn.channel_root_block_hash=cs.to_block_hash
|
||||||
|
WHERE LOWER(cs.login)=LOWER(?) AND cs.rel_type=30 AND cn.channel_type_code=1
|
||||||
|
ORDER BY cn.display_name, cn.slug LIMIT ? OFFSET ?
|
||||||
|
""";
|
||||||
|
} else throw new IllegalArgumentException("Unsupported channel mode: "+mode);
|
||||||
|
List<ChannelCard> out=new ArrayList<>();
|
||||||
|
try(PreparedStatement ps=c.prepareStatement(sql)){ ps.setString(1,login);ps.setInt(2,safeLimit);ps.setInt(3,safeOffset);
|
||||||
|
try(ResultSet rs=ps.executeQuery()){while(rs.next())out.add(new ChannelCard(rs.getString("owner_login"),rs.getString("slug"),rs.getString("display_name"),rs.getString("ava_ar"),rs.getString("owner_bch_name"),rs.getInt("channel_root_block_number"),rs.getString("root_hash")));}}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProfileCard map(ResultSet rs)throws SQLException{return new ProfileCard(rs.getString("login"),rs.getString("first_name"),rs.getString("last_name"),rs.getString("ava_ar"),rs.getString("account_role"),rs.getString("shine_status"));}
|
||||||
|
public record ProfileCard(String login,String firstName,String lastName,String avatarAr,String accountRole,String shineStatus){}
|
||||||
|
public record RelationCard(String login,String firstName,String lastName,String avatarAr,String accountRole,String shineStatus,boolean primaryConfirmed,boolean shineConfirmed,String relationType,int primaryConfirmationsCount,int shineConfirmationsCount){}
|
||||||
|
public record ChannelCard(String ownerLogin,String slug,String displayName,String avatarAr,String ownerBlockchainName,int rootBlockNumber,String rootBlockHashHex){}
|
||||||
|
}
|
||||||
@@ -12,15 +12,11 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UserSettingsDAO — хранение пользовательских настроек.
|
* Локальное хранение подписанных пользовательских настроек.
|
||||||
*
|
* Запись уникальна по login + setting_type + setting_key и обновляется только
|
||||||
* Правило:
|
* более новым time_ms. Межсерверной репликации настроек нет.
|
||||||
* - уникальность: login + setting_type + setting_key
|
|
||||||
* - запись обновляется только если time_ms новее
|
|
||||||
* - synced=true означает, что значение уже дошло до второго сервера
|
|
||||||
*/
|
*/
|
||||||
public final class UserSettingsDAO {
|
public final class UserSettingsDAO {
|
||||||
|
|
||||||
private static volatile UserSettingsDAO instance;
|
private static volatile UserSettingsDAO instance;
|
||||||
private final DbController db = DbController.getInstance();
|
private final DbController db = DbController.getInstance();
|
||||||
|
|
||||||
@@ -39,16 +35,15 @@ public final class UserSettingsDAO {
|
|||||||
String sql = """
|
String sql = """
|
||||||
INSERT INTO user_settings (
|
INSERT INTO user_settings (
|
||||||
login, setting_type, setting_key, time_ms,
|
login, setting_type, setting_key, time_ms,
|
||||||
value_text, value_num, client_key, signature, synced
|
value_text, value_num, client_key, signature
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT (login, setting_type, setting_key)
|
ON CONFLICT (login, setting_type, setting_key)
|
||||||
DO UPDATE SET
|
DO UPDATE SET
|
||||||
time_ms = EXCLUDED.time_ms,
|
time_ms = EXCLUDED.time_ms,
|
||||||
value_text = EXCLUDED.value_text,
|
value_text = EXCLUDED.value_text,
|
||||||
value_num = EXCLUDED.value_num,
|
value_num = EXCLUDED.value_num,
|
||||||
client_key = EXCLUDED.client_key,
|
client_key = EXCLUDED.client_key,
|
||||||
signature = EXCLUDED.signature,
|
signature = EXCLUDED.signature
|
||||||
synced = EXCLUDED.synced
|
|
||||||
WHERE user_settings.time_ms < EXCLUDED.time_ms
|
WHERE user_settings.time_ms < EXCLUDED.time_ms
|
||||||
""";
|
""";
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
@@ -58,27 +53,20 @@ public final class UserSettingsDAO {
|
|||||||
ps.setLong(4, e.getTimeMs());
|
ps.setLong(4, e.getTimeMs());
|
||||||
ps.setString(5, e.getValueText() == null ? "" : e.getValueText());
|
ps.setString(5, e.getValueText() == null ? "" : e.getValueText());
|
||||||
ps.setLong(6, e.getValueNum());
|
ps.setLong(6, e.getValueNum());
|
||||||
|
|
||||||
if (e.getClientKey() == null || e.getClientKey().isBlank()) ps.setNull(7, Types.VARCHAR);
|
if (e.getClientKey() == null || e.getClientKey().isBlank()) ps.setNull(7, Types.VARCHAR);
|
||||||
else ps.setString(7, e.getClientKey());
|
else ps.setString(7, e.getClientKey());
|
||||||
|
|
||||||
if (e.getSignature() == null || e.getSignature().isBlank()) ps.setNull(8, Types.VARCHAR);
|
if (e.getSignature() == null || e.getSignature().isBlank()) ps.setNull(8, Types.VARCHAR);
|
||||||
else ps.setString(8, e.getSignature());
|
else ps.setString(8, e.getSignature());
|
||||||
|
|
||||||
ps.setBoolean(9, e.isSynced());
|
|
||||||
return ps.executeUpdate();
|
return ps.executeUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int upsertIfNewer(UserSettingEntry e) throws SQLException {
|
public UserSettingEntry getByLoginTypeKey(
|
||||||
try (Connection c = db.getConnection()) {
|
Connection c, String login, int settingType, String settingKey
|
||||||
return upsertIfNewer(c, e);
|
) throws SQLException {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public UserSettingEntry getByLoginTypeKey(Connection c, String login, int settingType, String settingKey) throws SQLException {
|
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT login, setting_type, setting_key, time_ms, value_text, value_num, client_key, signature, synced
|
SELECT login, setting_type, setting_key, time_ms,
|
||||||
|
value_text, value_num, client_key, signature
|
||||||
FROM user_settings
|
FROM user_settings
|
||||||
WHERE LOWER(login) = LOWER(?) AND setting_type = ? AND setting_key = ?
|
WHERE LOWER(login) = LOWER(?) AND setting_type = ? AND setting_key = ?
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
@@ -93,15 +81,10 @@ public final class UserSettingsDAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserSettingEntry getByLoginTypeKey(String login, int settingType, String settingKey) throws SQLException {
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
return getByLoginTypeKey(c, login, settingType, settingKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<UserSettingEntry> getByLogin(Connection c, String login) throws SQLException {
|
public List<UserSettingEntry> getByLogin(Connection c, String login) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT login, setting_type, setting_key, time_ms, value_text, value_num, client_key, signature, synced
|
SELECT login, setting_type, setting_key, time_ms,
|
||||||
|
value_text, value_num, client_key, signature
|
||||||
FROM user_settings
|
FROM user_settings
|
||||||
WHERE LOWER(login) = LOWER(?)
|
WHERE LOWER(login) = LOWER(?)
|
||||||
ORDER BY time_ms DESC, setting_type ASC, setting_key ASC
|
ORDER BY time_ms DESC, setting_type ASC, setting_key ASC
|
||||||
@@ -122,92 +105,6 @@ public final class UserSettingsDAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<UserSettingEntry> listNewerThan(Connection c, String login, long afterTimeMs, String afterSettingKey, int limit) throws SQLException {
|
|
||||||
String sql = """
|
|
||||||
SELECT login, setting_type, setting_key, time_ms, value_text, value_num, client_key, signature, synced
|
|
||||||
FROM user_settings
|
|
||||||
WHERE LOWER(login) = LOWER(?)
|
|
||||||
AND (
|
|
||||||
time_ms > ?
|
|
||||||
OR (time_ms = ? AND setting_key > ?)
|
|
||||||
)
|
|
||||||
ORDER BY time_ms ASC, setting_key ASC
|
|
||||||
LIMIT ?
|
|
||||||
""";
|
|
||||||
List<UserSettingEntry> out = new ArrayList<>();
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
|
||||||
ps.setString(1, login);
|
|
||||||
ps.setLong(2, Math.max(0L, afterTimeMs));
|
|
||||||
ps.setLong(3, Math.max(0L, afterTimeMs));
|
|
||||||
ps.setString(4, afterSettingKey == null ? "" : afterSettingKey);
|
|
||||||
ps.setInt(5, Math.max(1, limit));
|
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
|
||||||
while (rs.next()) out.add(mapRow(rs));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<UserSettingEntry> listUnsyncedByLogin(Connection c, String login, int limit) throws SQLException {
|
|
||||||
String sql = """
|
|
||||||
SELECT login, setting_type, setting_key, time_ms, value_text, value_num, client_key, signature, synced
|
|
||||||
FROM user_settings
|
|
||||||
WHERE LOWER(login) = LOWER(?)
|
|
||||||
AND synced = FALSE
|
|
||||||
ORDER BY time_ms ASC, setting_key ASC
|
|
||||||
LIMIT ?
|
|
||||||
""";
|
|
||||||
List<UserSettingEntry> out = new ArrayList<>();
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
|
||||||
ps.setString(1, login);
|
|
||||||
ps.setInt(2, Math.max(1, limit));
|
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
|
||||||
while (rs.next()) out.add(mapRow(rs));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int markSynced(Connection c, String login, int settingType, String settingKey) throws SQLException {
|
|
||||||
String sql = """
|
|
||||||
UPDATE user_settings
|
|
||||||
SET synced = TRUE
|
|
||||||
WHERE LOWER(login) = LOWER(?)
|
|
||||||
AND setting_type = ?
|
|
||||||
AND setting_key = ?
|
|
||||||
""";
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
|
||||||
ps.setString(1, login);
|
|
||||||
ps.setInt(2, settingType);
|
|
||||||
ps.setString(3, settingKey);
|
|
||||||
return ps.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int markAllUnsynced(Connection c, String login) throws SQLException {
|
|
||||||
String sql = """
|
|
||||||
UPDATE user_settings
|
|
||||||
SET synced = FALSE
|
|
||||||
WHERE LOWER(login) = LOWER(?)
|
|
||||||
""";
|
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
|
||||||
ps.setString(1, login);
|
|
||||||
return ps.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int markAllUnsynced() throws SQLException {
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
return markAllUnsynced(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int markAllUnsynced(Connection c) throws SQLException {
|
|
||||||
try (PreparedStatement ps = c.prepareStatement("UPDATE user_settings SET synced = FALSE")) {
|
|
||||||
return ps.executeUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static UserSettingEntry mapRow(ResultSet rs) throws SQLException {
|
private static UserSettingEntry mapRow(ResultSet rs) throws SQLException {
|
||||||
UserSettingEntry e = new UserSettingEntry();
|
UserSettingEntry e = new UserSettingEntry();
|
||||||
e.setLogin(rs.getString("login"));
|
e.setLogin(rs.getString("login"));
|
||||||
@@ -216,16 +113,12 @@ public final class UserSettingsDAO {
|
|||||||
e.setTimeMs(rs.getLong("time_ms"));
|
e.setTimeMs(rs.getLong("time_ms"));
|
||||||
e.setValueText(rs.getString("value_text"));
|
e.setValueText(rs.getString("value_text"));
|
||||||
e.setValueNum(rs.getLong("value_num"));
|
e.setValueNum(rs.getLong("value_num"));
|
||||||
|
|
||||||
String clientKey = rs.getString("client_key");
|
String clientKey = rs.getString("client_key");
|
||||||
if (rs.wasNull()) clientKey = null;
|
if (rs.wasNull()) clientKey = null;
|
||||||
e.setClientKey(clientKey);
|
e.setClientKey(clientKey);
|
||||||
|
|
||||||
String signature = rs.getString("signature");
|
String signature = rs.getString("signature");
|
||||||
if (rs.wasNull()) signature = null;
|
if (rs.wasNull()) signature = null;
|
||||||
e.setSignature(signature);
|
e.setSignature(signature);
|
||||||
|
|
||||||
e.setSynced(rs.getBoolean("synced"));
|
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-27
@@ -1,19 +1,6 @@
|
|||||||
package shine.db.entities;
|
package shine.db.entities;
|
||||||
|
|
||||||
/**
|
/** Одна локально сохранённая подписанная пользовательская настройка. */
|
||||||
* UserSettingEntry — одна пользовательская настройка.
|
|
||||||
*
|
|
||||||
* Таблица: user_settings
|
|
||||||
* - login TEXT NOT NULL
|
|
||||||
* - setting_type INTEGER NOT NULL
|
|
||||||
* - setting_key TEXT NOT NULL
|
|
||||||
* - time_ms BIGINT NOT NULL
|
|
||||||
* - value_text TEXT NOT NULL
|
|
||||||
* - value_num BIGINT NOT NULL
|
|
||||||
* - client_key TEXT NOT NULL
|
|
||||||
* - signature TEXT NOT NULL
|
|
||||||
* - synced BOOLEAN NOT NULL
|
|
||||||
*/
|
|
||||||
public class UserSettingEntry {
|
public class UserSettingEntry {
|
||||||
private String login;
|
private String login;
|
||||||
private int settingType;
|
private int settingType;
|
||||||
@@ -23,11 +10,19 @@ public class UserSettingEntry {
|
|||||||
private long valueNum;
|
private long valueNum;
|
||||||
private String clientKey;
|
private String clientKey;
|
||||||
private String signature;
|
private String signature;
|
||||||
private boolean synced;
|
|
||||||
|
|
||||||
public UserSettingEntry() {}
|
public UserSettingEntry() {}
|
||||||
|
|
||||||
public UserSettingEntry(String login, int settingType, String settingKey, long timeMs, String valueText, long valueNum, String clientKey, String signature, boolean synced) {
|
public UserSettingEntry(
|
||||||
|
String login,
|
||||||
|
int settingType,
|
||||||
|
String settingKey,
|
||||||
|
long timeMs,
|
||||||
|
String valueText,
|
||||||
|
long valueNum,
|
||||||
|
String clientKey,
|
||||||
|
String signature
|
||||||
|
) {
|
||||||
this.login = login;
|
this.login = login;
|
||||||
this.settingType = settingType;
|
this.settingType = settingType;
|
||||||
this.settingKey = settingKey;
|
this.settingKey = settingKey;
|
||||||
@@ -36,33 +31,22 @@ public class UserSettingEntry {
|
|||||||
this.valueNum = valueNum;
|
this.valueNum = valueNum;
|
||||||
this.clientKey = clientKey;
|
this.clientKey = clientKey;
|
||||||
this.signature = signature;
|
this.signature = signature;
|
||||||
this.synced = synced;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
public String getLogin() { return login; }
|
||||||
public void setLogin(String login) { this.login = login; }
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
public int getSettingType() { return settingType; }
|
public int getSettingType() { return settingType; }
|
||||||
public void setSettingType(int settingType) { this.settingType = settingType; }
|
public void setSettingType(int settingType) { this.settingType = settingType; }
|
||||||
|
|
||||||
public String getSettingKey() { return settingKey; }
|
public String getSettingKey() { return settingKey; }
|
||||||
public void setSettingKey(String settingKey) { this.settingKey = settingKey; }
|
public void setSettingKey(String settingKey) { this.settingKey = settingKey; }
|
||||||
|
|
||||||
public long getTimeMs() { return timeMs; }
|
public long getTimeMs() { return timeMs; }
|
||||||
public void setTimeMs(long timeMs) { this.timeMs = timeMs; }
|
public void setTimeMs(long timeMs) { this.timeMs = timeMs; }
|
||||||
|
|
||||||
public String getValueText() { return valueText; }
|
public String getValueText() { return valueText; }
|
||||||
public void setValueText(String valueText) { this.valueText = valueText; }
|
public void setValueText(String valueText) { this.valueText = valueText; }
|
||||||
|
|
||||||
public long getValueNum() { return valueNum; }
|
public long getValueNum() { return valueNum; }
|
||||||
public void setValueNum(long valueNum) { this.valueNum = valueNum; }
|
public void setValueNum(long valueNum) { this.valueNum = valueNum; }
|
||||||
|
|
||||||
public String getClientKey() { return clientKey; }
|
public String getClientKey() { return clientKey; }
|
||||||
public void setClientKey(String clientKey) { this.clientKey = clientKey; }
|
public void setClientKey(String clientKey) { this.clientKey = clientKey; }
|
||||||
|
|
||||||
public String getSignature() { return signature; }
|
public String getSignature() { return signature; }
|
||||||
public void setSignature(String signature) { this.signature = signature; }
|
public void setSignature(String signature) { this.signature = signature; }
|
||||||
|
|
||||||
public boolean isSynced() { return synced; }
|
|
||||||
public void setSynced(boolean synced) { this.synced = synced; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- У пользователя действует только первый access_server из Solana PDA.
|
||||||
|
CREATE OR REPLACE FUNCTION shine_refresh_user_access_servers_for_user(p_user_login TEXT)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF p_user_login IS NULL OR btrim(p_user_login) = '' THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
DELETE FROM user_access_servers_current
|
||||||
|
WHERE LOWER(user_login) = LOWER(p_user_login);
|
||||||
|
|
||||||
|
INSERT INTO user_access_servers_current (
|
||||||
|
user_login,
|
||||||
|
server_login,
|
||||||
|
server_url,
|
||||||
|
server_client_key,
|
||||||
|
user_record_number,
|
||||||
|
user_updated_at_ms,
|
||||||
|
server_record_number,
|
||||||
|
server_updated_at_ms,
|
||||||
|
refreshed_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
u.login,
|
||||||
|
s.login,
|
||||||
|
s.server_address,
|
||||||
|
s.client_key,
|
||||||
|
u.record_number,
|
||||||
|
u.updated_at_ms,
|
||||||
|
s.record_number,
|
||||||
|
s.updated_at_ms,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
FROM solana_user_pda_current u
|
||||||
|
CROSS JOIN LATERAL (
|
||||||
|
SELECT login_value
|
||||||
|
FROM jsonb_array_elements_text(
|
||||||
|
CASE
|
||||||
|
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||||
|
ELSE u.access_servers_json::jsonb
|
||||||
|
END
|
||||||
|
) WITH ORDINALITY AS access_server(login_value, ord)
|
||||||
|
WHERE ord = 1
|
||||||
|
) AS access_server
|
||||||
|
JOIN solana_user_pda_current s
|
||||||
|
ON LOWER(s.login) = LOWER(btrim(access_server.login_value))
|
||||||
|
AND s.is_server = TRUE
|
||||||
|
AND btrim(COALESCE(s.server_address, '')) <> ''
|
||||||
|
WHERE LOWER(u.login) = LOWER(p_user_login)
|
||||||
|
ON CONFLICT (user_login, server_login) DO UPDATE SET
|
||||||
|
server_url = EXCLUDED.server_url,
|
||||||
|
server_client_key = EXCLUDED.server_client_key,
|
||||||
|
user_record_number = EXCLUDED.user_record_number,
|
||||||
|
user_updated_at_ms = EXCLUDED.user_updated_at_ms,
|
||||||
|
server_record_number = EXCLUDED.server_record_number,
|
||||||
|
server_updated_at_ms = EXCLUDED.server_updated_at_ms,
|
||||||
|
refreshed_at_ms = EXCLUDED.refreshed_at_ms;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_refresh_user_access_servers_for_server(p_server_login TEXT)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
affected_user RECORD;
|
||||||
|
BEGIN
|
||||||
|
IF p_server_login IS NULL OR btrim(p_server_login) = '' THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
DELETE FROM user_access_servers_current
|
||||||
|
WHERE LOWER(server_login) = LOWER(p_server_login);
|
||||||
|
|
||||||
|
FOR affected_user IN
|
||||||
|
SELECT u.login
|
||||||
|
FROM solana_user_pda_current u
|
||||||
|
CROSS JOIN LATERAL (
|
||||||
|
SELECT login_value
|
||||||
|
FROM jsonb_array_elements_text(
|
||||||
|
CASE
|
||||||
|
WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb
|
||||||
|
ELSE u.access_servers_json::jsonb
|
||||||
|
END
|
||||||
|
) WITH ORDINALITY AS access_server(login_value, ord)
|
||||||
|
WHERE ord = 1
|
||||||
|
) AS access_server
|
||||||
|
WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login)
|
||||||
|
LOOP
|
||||||
|
PERFORM shine_refresh_user_access_servers_for_user(affected_user.login);
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS dm_sync_outbox;
|
||||||
|
DROP TABLE IF EXISTS dm_sync_peer_state;
|
||||||
|
DROP TABLE IF EXISTS user_settings_sync_peer_state;
|
||||||
|
DROP INDEX IF EXISTS idx_user_settings_synced;
|
||||||
|
ALTER TABLE user_settings DROP COLUMN IF EXISTS synced;
|
||||||
|
|
||||||
|
SELECT shine_refresh_user_access_servers_all();
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 16, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Add active FRIEND 14/15 and OFFICIAL_ACCOUNT confirmation 80/81 to connection state.
|
||||||
|
-- Legacy 60/61 remain accepted at protocol level; this migration does not reinterpret existing rows.
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_blocks_connection_state_ai()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
resolved_login TEXT;
|
||||||
|
positive_rel_type INTEGER;
|
||||||
|
existed_before BOOLEAN;
|
||||||
|
target_channel_type INTEGER;
|
||||||
|
BEGIN
|
||||||
|
IF NEW.msg_type <> 3 THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
resolved_login := shine_resolve_login(NEW.to_login, NEW.to_bch_name);
|
||||||
|
|
||||||
|
IF NEW.msg_sub_type IN (10, 14, 20, 30, 40, 50, 52, 54, 60, 70, 74, 80) THEN
|
||||||
|
IF resolved_login IS NULL OR NEW.to_bch_name IS NULL THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM connections_state
|
||||||
|
WHERE login = NEW.login
|
||||||
|
AND rel_type = NEW.msg_sub_type
|
||||||
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'))
|
||||||
|
)
|
||||||
|
INTO existed_before;
|
||||||
|
|
||||||
|
IF NOT existed_before THEN
|
||||||
|
IF NEW.msg_sub_type = 10 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
close_friends_count = user_stats_state.close_friends_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF NEW.msg_sub_type = 30 THEN
|
||||||
|
IF NEW.to_block_number IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = user_stats_state.following_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF NEW.to_block_number = 0 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_users_count = user_stats_state.following_users_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSE
|
||||||
|
SELECT cn.channel_type_code
|
||||||
|
INTO target_channel_type
|
||||||
|
FROM channel_names_state cn
|
||||||
|
WHERE cn.owner_bch_name = NEW.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = NEW.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = NEW.to_block_hash
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF target_channel_type = 1 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = user_stats_state.following_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.to_bch_name,
|
||||||
|
NEW.to_block_number,
|
||||||
|
NEW.to_block_hash,
|
||||||
|
resolved_login,
|
||||||
|
target_channel_type,
|
||||||
|
1,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (owner_bch_name, channel_root_block_number, channel_root_block_hash) DO UPDATE SET
|
||||||
|
owner_login = EXCLUDED.owner_login,
|
||||||
|
channel_type_code = EXCLUDED.channel_type_code,
|
||||||
|
subscribers_count = channel_stats_state.subscribers_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF target_channel_type IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = user_stats_state.following_channels_count + 1,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
DELETE FROM connections_state
|
||||||
|
WHERE login = NEW.login
|
||||||
|
AND rel_type = NEW.msg_sub_type
|
||||||
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'));
|
||||||
|
|
||||||
|
INSERT INTO connections_state (
|
||||||
|
login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
NEW.msg_sub_type,
|
||||||
|
resolved_login,
|
||||||
|
NEW.to_bch_name,
|
||||||
|
COALESCE(NEW.to_block_number, 0),
|
||||||
|
COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'))
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
positive_rel_type := CASE NEW.msg_sub_type
|
||||||
|
WHEN 11 THEN 10
|
||||||
|
WHEN 15 THEN 14
|
||||||
|
WHEN 21 THEN 20
|
||||||
|
WHEN 31 THEN 30
|
||||||
|
WHEN 41 THEN 40
|
||||||
|
WHEN 51 THEN 50
|
||||||
|
WHEN 53 THEN 52
|
||||||
|
WHEN 55 THEN 54
|
||||||
|
WHEN 61 THEN 60
|
||||||
|
WHEN 71 THEN 70
|
||||||
|
WHEN 75 THEN 74
|
||||||
|
WHEN 81 THEN 80
|
||||||
|
ELSE NULL
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF positive_rel_type IS NULL OR resolved_login IS NULL THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM connections_state
|
||||||
|
WHERE login = NEW.login
|
||||||
|
AND rel_type = positive_rel_type
|
||||||
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'))
|
||||||
|
)
|
||||||
|
INTO existed_before;
|
||||||
|
|
||||||
|
IF NOT existed_before THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF positive_rel_type = 10 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
close_friends_count = GREATEST(0, user_stats_state.close_friends_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF positive_rel_type = 30 THEN
|
||||||
|
IF NEW.to_block_number IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = GREATEST(0, user_stats_state.following_channels_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF NEW.to_block_number = 0 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_users_count = GREATEST(0, user_stats_state.following_users_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSE
|
||||||
|
SELECT cn.channel_type_code
|
||||||
|
INTO target_channel_type
|
||||||
|
FROM channel_names_state cn
|
||||||
|
WHERE cn.owner_bch_name = NEW.to_bch_name
|
||||||
|
AND cn.channel_root_block_number = NEW.to_block_number
|
||||||
|
AND cn.channel_root_block_hash = NEW.to_block_hash
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF target_channel_type = 1 THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = GREATEST(0, user_stats_state.following_channels_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state (
|
||||||
|
owner_bch_name,
|
||||||
|
channel_root_block_number,
|
||||||
|
channel_root_block_hash,
|
||||||
|
owner_login,
|
||||||
|
channel_type_code,
|
||||||
|
subscribers_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.to_bch_name,
|
||||||
|
NEW.to_block_number,
|
||||||
|
NEW.to_block_hash,
|
||||||
|
resolved_login,
|
||||||
|
target_channel_type,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (owner_bch_name, channel_root_block_number, channel_root_block_hash) DO UPDATE SET
|
||||||
|
owner_login = EXCLUDED.owner_login,
|
||||||
|
channel_type_code = EXCLUDED.channel_type_code,
|
||||||
|
subscribers_count = GREATEST(0, channel_stats_state.subscribers_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
ELSIF target_channel_type IS NULL THEN
|
||||||
|
INSERT INTO user_stats_state (
|
||||||
|
login,
|
||||||
|
owned_public_channels_count,
|
||||||
|
following_users_count,
|
||||||
|
following_channels_count,
|
||||||
|
close_friends_count,
|
||||||
|
updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
NEW.login,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||||
|
)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
following_channels_count = GREATEST(0, user_stats_state.following_channels_count - 1),
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
DELETE FROM connections_state
|
||||||
|
WHERE login = NEW.login
|
||||||
|
AND rel_type = positive_rel_type
|
||||||
|
AND to_login = resolved_login
|
||||||
|
AND to_bch_name = NEW.to_bch_name
|
||||||
|
AND to_block_number = COALESCE(NEW.to_block_number, 0)
|
||||||
|
AND to_block_hash = COALESCE(NEW.to_block_hash, decode(repeat('00', 32), 'hex'));
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
|
VALUES (1, 17, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
schema_version = EXCLUDED.schema_version,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- v18: fast profile projection, effective social/voting statistics and qualified likes.
|
||||||
|
-- Source of truth remains users_params / connections_state / reactions_state / channel_names_state.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_profile_state (
|
||||||
|
login TEXT PRIMARY KEY,
|
||||||
|
first_name TEXT NOT NULL DEFAULT '',
|
||||||
|
last_name TEXT NOT NULL DEFAULT '',
|
||||||
|
ava_ar TEXT NOT NULL DEFAULT '',
|
||||||
|
account_role TEXT,
|
||||||
|
shine_status TEXT,
|
||||||
|
updated_at_ms BIGINT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_profile_state_name
|
||||||
|
ON user_profile_state (LOWER(last_name), LOWER(first_name), LOWER(login));
|
||||||
|
|
||||||
|
ALTER TABLE user_stats_state
|
||||||
|
ADD COLUMN IF NOT EXISTS friends_count INTEGER NOT NULL DEFAULT 0 CHECK (friends_count >= 0),
|
||||||
|
ADD COLUMN IF NOT EXISTS primary_confirmations_received_count INTEGER NOT NULL DEFAULT 0 CHECK (primary_confirmations_received_count >= 0),
|
||||||
|
ADD COLUMN IF NOT EXISTS primary_confirmations_given_count INTEGER NOT NULL DEFAULT 0 CHECK (primary_confirmations_given_count >= 0),
|
||||||
|
ADD COLUMN IF NOT EXISTS shine_confirmations_received_count INTEGER NOT NULL DEFAULT 0 CHECK (shine_confirmations_received_count >= 0),
|
||||||
|
ADD COLUMN IF NOT EXISTS shine_confirmations_given_count INTEGER NOT NULL DEFAULT 0 CHECK (shine_confirmations_given_count >= 0);
|
||||||
|
|
||||||
|
ALTER TABLE message_stats
|
||||||
|
ADD COLUMN IF NOT EXISTS primary_likes_count INTEGER NOT NULL DEFAULT 0 CHECK (primary_likes_count >= 0),
|
||||||
|
ADD COLUMN IF NOT EXISTS shining_likes_count INTEGER NOT NULL DEFAULT 0 CHECK (shining_likes_count >= 0);
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_refresh_user_profile(p_login TEXT)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_first_name TEXT := '';
|
||||||
|
v_last_name TEXT := '';
|
||||||
|
v_ava_ar TEXT := '';
|
||||||
|
v_account_role TEXT := NULL;
|
||||||
|
v_shine TEXT := NULL;
|
||||||
|
v_updated BIGINT := 0;
|
||||||
|
BEGIN
|
||||||
|
IF p_login IS NULL OR btrim(p_login) = '' THEN RETURN; END IF;
|
||||||
|
|
||||||
|
SELECT COALESCE(MAX(value) FILTER (WHERE param = 'first_name'), ''),
|
||||||
|
COALESCE(MAX(value) FILTER (WHERE param = 'last_name'), ''),
|
||||||
|
COALESCE(MAX(value) FILTER (WHERE param = 'ava'), ''),
|
||||||
|
MAX(value) FILTER (WHERE param = 'account_role'),
|
||||||
|
MAX(value) FILTER (WHERE param = 'shine'),
|
||||||
|
COALESCE(MAX(time_ms), 0)
|
||||||
|
INTO v_first_name, v_last_name, v_ava_ar, v_account_role, v_shine, v_updated
|
||||||
|
FROM users_params
|
||||||
|
WHERE LOWER(login) = LOWER(p_login);
|
||||||
|
|
||||||
|
v_account_role := LOWER(BTRIM(COALESCE(v_account_role, '')));
|
||||||
|
IF v_account_role NOT IN ('primary', 'non_voting') THEN v_account_role := NULL; END IF;
|
||||||
|
|
||||||
|
v_shine := LOWER(BTRIM(COALESCE(v_shine, '')));
|
||||||
|
IF v_shine = 'yes' THEN v_shine := 'shining'; END IF; -- legacy compatibility
|
||||||
|
IF v_shine NOT IN ('shining', 'unknown', 'not_interested') THEN v_shine := NULL; END IF;
|
||||||
|
|
||||||
|
INSERT INTO user_profile_state(login, first_name, last_name, ava_ar, account_role, shine_status, updated_at_ms)
|
||||||
|
VALUES (p_login, v_first_name, v_last_name, v_ava_ar, v_account_role, v_shine, v_updated)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
first_name = EXCLUDED.first_name,
|
||||||
|
last_name = EXCLUDED.last_name,
|
||||||
|
ava_ar = EXCLUDED.ava_ar,
|
||||||
|
account_role = EXCLUDED.account_role,
|
||||||
|
shine_status = EXCLUDED.shine_status,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_is_primary(p_login TEXT)
|
||||||
|
RETURNS BOOLEAN
|
||||||
|
LANGUAGE sql
|
||||||
|
STABLE
|
||||||
|
AS $$
|
||||||
|
SELECT COALESCE((SELECT account_role = 'primary' FROM user_profile_state WHERE LOWER(login)=LOWER(p_login) LIMIT 1), FALSE)
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_is_shining(p_login TEXT)
|
||||||
|
RETURNS BOOLEAN
|
||||||
|
LANGUAGE sql
|
||||||
|
STABLE
|
||||||
|
AS $$
|
||||||
|
SELECT COALESCE((SELECT shine_status = 'shining' FROM user_profile_state WHERE LOWER(login)=LOWER(p_login) LIMIT 1), FALSE)
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_target_accepts_primary_confirmation(p_login TEXT)
|
||||||
|
RETURNS BOOLEAN
|
||||||
|
LANGUAGE sql
|
||||||
|
STABLE
|
||||||
|
AS $$
|
||||||
|
SELECT COALESCE((SELECT account_role IS DISTINCT FROM 'non_voting' FROM user_profile_state WHERE LOWER(login)=LOWER(p_login) LIMIT 1), TRUE)
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_target_accepts_shine_confirmation(p_login TEXT)
|
||||||
|
RETURNS BOOLEAN
|
||||||
|
LANGUAGE sql
|
||||||
|
STABLE
|
||||||
|
AS $$
|
||||||
|
SELECT COALESCE((SELECT shine_status IS DISTINCT FROM 'not_interested' FROM user_profile_state WHERE LOWER(login)=LOWER(p_login) LIMIT 1), TRUE)
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_refresh_user_stats(p_login TEXT)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_now BIGINT := CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT);
|
||||||
|
BEGIN
|
||||||
|
IF p_login IS NULL OR btrim(p_login) = '' THEN RETURN; END IF;
|
||||||
|
|
||||||
|
INSERT INTO user_stats_state(login, owned_public_channels_count, following_users_count,
|
||||||
|
following_channels_count, close_friends_count, friends_count,
|
||||||
|
primary_confirmations_received_count, primary_confirmations_given_count,
|
||||||
|
shine_confirmations_received_count, shine_confirmations_given_count, updated_at_ms)
|
||||||
|
SELECT p_login,
|
||||||
|
(SELECT COUNT(*)::INT FROM channel_names_state cn WHERE LOWER(cn.owner_login)=LOWER(p_login) AND cn.channel_type_code=1),
|
||||||
|
0,
|
||||||
|
(SELECT COUNT(*)::INT FROM connections_state cs JOIN channel_names_state cn
|
||||||
|
ON cn.owner_bch_name=cs.to_bch_name AND cn.channel_root_block_number=cs.to_block_number AND cn.channel_root_block_hash=cs.to_block_hash
|
||||||
|
WHERE LOWER(cs.login)=LOWER(p_login) AND cs.rel_type=30 AND cn.channel_type_code=1),
|
||||||
|
(SELECT COUNT(DISTINCT LOWER(cs.to_login))::INT FROM connections_state cs WHERE LOWER(cs.login)=LOWER(p_login) AND cs.rel_type=10),
|
||||||
|
(SELECT COUNT(DISTINCT LOWER(cs.to_login))::INT FROM connections_state cs
|
||||||
|
WHERE LOWER(cs.login)=LOWER(p_login) AND cs.rel_type=14
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM connections_state cf WHERE LOWER(cf.login)=LOWER(p_login) AND LOWER(cf.to_login)=LOWER(cs.to_login) AND cf.rel_type=10)),
|
||||||
|
(SELECT COUNT(DISTINCT LOWER(cs.login))::INT FROM connections_state cs
|
||||||
|
WHERE LOWER(cs.to_login)=LOWER(p_login) AND cs.rel_type=80
|
||||||
|
AND shine_is_primary(cs.login) AND shine_target_accepts_primary_confirmation(p_login)),
|
||||||
|
(SELECT COUNT(DISTINCT LOWER(cs.to_login))::INT FROM connections_state cs
|
||||||
|
WHERE LOWER(cs.login)=LOWER(p_login) AND cs.rel_type=80
|
||||||
|
AND shine_is_primary(p_login) AND shine_target_accepts_primary_confirmation(cs.to_login)),
|
||||||
|
(SELECT COUNT(DISTINCT LOWER(cs.login))::INT FROM connections_state cs
|
||||||
|
WHERE LOWER(cs.to_login)=LOWER(p_login) AND cs.rel_type=70
|
||||||
|
AND shine_is_primary(cs.login) AND shine_is_shining(cs.login) AND shine_target_accepts_shine_confirmation(p_login)),
|
||||||
|
(SELECT COUNT(DISTINCT LOWER(cs.to_login))::INT FROM connections_state cs
|
||||||
|
WHERE LOWER(cs.login)=LOWER(p_login) AND cs.rel_type=70
|
||||||
|
AND shine_is_primary(p_login) AND shine_is_shining(p_login) AND shine_target_accepts_shine_confirmation(cs.to_login)),
|
||||||
|
v_now
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
owned_public_channels_count=EXCLUDED.owned_public_channels_count,
|
||||||
|
following_users_count=0,
|
||||||
|
following_channels_count=EXCLUDED.following_channels_count,
|
||||||
|
close_friends_count=EXCLUDED.close_friends_count,
|
||||||
|
friends_count=EXCLUDED.friends_count,
|
||||||
|
primary_confirmations_received_count=EXCLUDED.primary_confirmations_received_count,
|
||||||
|
primary_confirmations_given_count=EXCLUDED.primary_confirmations_given_count,
|
||||||
|
shine_confirmations_received_count=EXCLUDED.shine_confirmations_received_count,
|
||||||
|
shine_confirmations_given_count=EXCLUDED.shine_confirmations_given_count,
|
||||||
|
updated_at_ms=EXCLUDED.updated_at_ms;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_refresh_related_user_stats(p_login TEXT)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE r RECORD;
|
||||||
|
BEGIN
|
||||||
|
PERFORM shine_refresh_user_stats(p_login);
|
||||||
|
FOR r IN
|
||||||
|
SELECT DISTINCT x.login FROM (
|
||||||
|
SELECT cs.login FROM connections_state cs WHERE LOWER(cs.to_login)=LOWER(p_login) AND cs.rel_type IN (70,80)
|
||||||
|
UNION
|
||||||
|
SELECT cs.to_login AS login FROM connections_state cs WHERE LOWER(cs.login)=LOWER(p_login) AND cs.rel_type IN (70,80)
|
||||||
|
) x
|
||||||
|
LOOP
|
||||||
|
PERFORM shine_refresh_user_stats(r.login);
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_refresh_message_qualified_likes_for_actor(p_login TEXT)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE r RECORD;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN
|
||||||
|
SELECT DISTINCT rs.to_login, rs.to_bch_name, rs.to_block_number, rs.to_block_hash
|
||||||
|
FROM reactions_state rs
|
||||||
|
WHERE LOWER(rs.from_login)=LOWER(p_login) AND rs.reaction_type=1 AND rs.last_sub_type=1
|
||||||
|
LOOP
|
||||||
|
UPDATE message_stats ms SET
|
||||||
|
primary_likes_count=(SELECT COUNT(*)::INT FROM reactions_state x WHERE x.reaction_type=1 AND x.last_sub_type=1
|
||||||
|
AND x.to_login=r.to_login AND x.to_bch_name=r.to_bch_name AND x.to_block_number=r.to_block_number AND x.to_block_hash=r.to_block_hash
|
||||||
|
AND shine_is_primary(x.from_login)),
|
||||||
|
shining_likes_count=(SELECT COUNT(*)::INT FROM reactions_state x WHERE x.reaction_type=1 AND x.last_sub_type=1
|
||||||
|
AND x.to_login=r.to_login AND x.to_bch_name=r.to_bch_name AND x.to_block_number=r.to_block_number AND x.to_block_hash=r.to_block_hash
|
||||||
|
AND shine_is_primary(x.from_login) AND shine_is_shining(x.from_login))
|
||||||
|
WHERE ms.to_login=r.to_login AND ms.to_bch_name=r.to_bch_name AND ms.to_block_number=r.to_block_number AND ms.to_block_hash=r.to_block_hash;
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_refresh_channel_follow_stats(p_bch_name TEXT, p_block_number INTEGER, p_block_hash BYTEA)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_owner_login TEXT;
|
||||||
|
v_channel_type INTEGER;
|
||||||
|
v_count INTEGER := 0;
|
||||||
|
v_now BIGINT := CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT);
|
||||||
|
BEGIN
|
||||||
|
IF p_bch_name IS NULL OR p_block_number IS NULL OR p_block_hash IS NULL OR p_block_number <= 0 THEN RETURN; END IF;
|
||||||
|
|
||||||
|
SELECT cn.owner_login, cn.channel_type_code
|
||||||
|
INTO v_owner_login, v_channel_type
|
||||||
|
FROM channel_names_state cn
|
||||||
|
WHERE cn.owner_bch_name=p_bch_name
|
||||||
|
AND cn.channel_root_block_number=p_block_number
|
||||||
|
AND cn.channel_root_block_hash=p_block_hash
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF v_channel_type IS NULL THEN RETURN; END IF;
|
||||||
|
|
||||||
|
SELECT COUNT(DISTINCT LOWER(cs.login))::INT
|
||||||
|
INTO v_count
|
||||||
|
FROM connections_state cs
|
||||||
|
WHERE cs.rel_type=30
|
||||||
|
AND cs.to_bch_name=p_bch_name
|
||||||
|
AND cs.to_block_number=p_block_number
|
||||||
|
AND cs.to_block_hash=p_block_hash;
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state(owner_bch_name,channel_root_block_number,channel_root_block_hash,owner_login,channel_type_code,subscribers_count,updated_at_ms)
|
||||||
|
VALUES(p_bch_name,p_block_number,p_block_hash,COALESCE(v_owner_login,''),v_channel_type,v_count,v_now)
|
||||||
|
ON CONFLICT(owner_bch_name,channel_root_block_number,channel_root_block_hash) DO UPDATE SET
|
||||||
|
owner_login=EXCLUDED.owner_login,
|
||||||
|
channel_type_code=EXCLUDED.channel_type_code,
|
||||||
|
subscribers_count=EXCLUDED.subscribers_count,
|
||||||
|
updated_at_ms=EXCLUDED.updated_at_ms;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Replace incremental channel-name counter with idempotent recomputation.
|
||||||
|
CREATE OR REPLACE FUNCTION shine_channel_names_state_stats_ai()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.channel_type_code = 1 THEN
|
||||||
|
PERFORM shine_refresh_user_stats(NEW.owner_login);
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Rebuild connection state without incremental user counters; then recompute affected users.
|
||||||
|
CREATE OR REPLACE FUNCTION shine_blocks_connection_state_ai()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
resolved_login TEXT;
|
||||||
|
positive_rel_type INTEGER;
|
||||||
|
BEGIN
|
||||||
|
IF NEW.msg_type <> 3 THEN RETURN NEW; END IF;
|
||||||
|
resolved_login := shine_resolve_login(NEW.to_login, NEW.to_bch_name);
|
||||||
|
IF resolved_login IS NULL OR NEW.to_bch_name IS NULL THEN RETURN NEW; END IF;
|
||||||
|
|
||||||
|
IF NEW.msg_sub_type IN (10,14,20,30,40,50,52,54,60,70,74,80) THEN
|
||||||
|
DELETE FROM connections_state WHERE login=NEW.login AND rel_type=NEW.msg_sub_type AND to_login=resolved_login
|
||||||
|
AND to_bch_name=NEW.to_bch_name AND to_block_number=COALESCE(NEW.to_block_number,0)
|
||||||
|
AND to_block_hash=COALESCE(NEW.to_block_hash,decode(repeat('00',32),'hex'));
|
||||||
|
INSERT INTO connections_state(login,rel_type,to_login,to_bch_name,to_block_number,to_block_hash)
|
||||||
|
VALUES(NEW.login,NEW.msg_sub_type,resolved_login,NEW.to_bch_name,COALESCE(NEW.to_block_number,0),COALESCE(NEW.to_block_hash,decode(repeat('00',32),'hex')));
|
||||||
|
ELSE
|
||||||
|
positive_rel_type := CASE NEW.msg_sub_type WHEN 11 THEN 10 WHEN 15 THEN 14 WHEN 21 THEN 20 WHEN 31 THEN 30 WHEN 41 THEN 40
|
||||||
|
WHEN 51 THEN 50 WHEN 53 THEN 52 WHEN 55 THEN 54 WHEN 61 THEN 60 WHEN 71 THEN 70 WHEN 75 THEN 74 WHEN 81 THEN 80 ELSE NULL END;
|
||||||
|
IF positive_rel_type IS NULL THEN RETURN NEW; END IF;
|
||||||
|
DELETE FROM connections_state WHERE login=NEW.login AND rel_type=positive_rel_type AND to_login=resolved_login
|
||||||
|
AND to_bch_name=NEW.to_bch_name AND to_block_number=COALESCE(NEW.to_block_number,0)
|
||||||
|
AND to_block_hash=COALESCE(NEW.to_block_hash,decode(repeat('00',32),'hex'));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
PERFORM shine_refresh_user_stats(NEW.login);
|
||||||
|
PERFORM shine_refresh_user_stats(resolved_login);
|
||||||
|
IF NEW.msg_sub_type IN (30,31) AND NEW.to_block_number IS NOT NULL AND NEW.to_block_number > 0 AND NEW.to_block_hash IS NOT NULL THEN
|
||||||
|
PERFORM shine_refresh_channel_follow_stats(NEW.to_bch_name, NEW.to_block_number, NEW.to_block_hash);
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_blocks_message_stats_like_ai()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE previous_sub_type INTEGER;
|
||||||
|
BEGIN
|
||||||
|
IF NEW.msg_type<>2 OR NEW.msg_sub_type NOT IN (1,2) THEN RETURN NEW; END IF;
|
||||||
|
IF NEW.to_login IS NULL OR NEW.to_bch_name IS NULL OR NEW.to_block_number IS NULL OR NEW.to_block_hash IS NULL THEN RETURN NEW; END IF;
|
||||||
|
|
||||||
|
INSERT INTO message_stats(to_login,to_bch_name,to_block_number,to_block_hash,likes_count,primary_likes_count,shining_likes_count,replies_count,edits_count)
|
||||||
|
VALUES(NEW.to_login,NEW.to_bch_name,NEW.to_block_number,NEW.to_block_hash,0,0,0,0,0)
|
||||||
|
ON CONFLICT(to_login,to_bch_name,to_block_number,to_block_hash) DO NOTHING;
|
||||||
|
|
||||||
|
SELECT last_sub_type INTO previous_sub_type FROM reactions_state WHERE from_login=NEW.login AND from_bch_name=NEW.bch_name AND reaction_type=1
|
||||||
|
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 LIMIT 1;
|
||||||
|
|
||||||
|
IF NEW.msg_sub_type=1 AND previous_sub_type IS DISTINCT FROM 1 THEN
|
||||||
|
UPDATE message_stats SET likes_count=likes_count+1,
|
||||||
|
primary_likes_count=primary_likes_count + CASE WHEN shine_is_primary(NEW.login) THEN 1 ELSE 0 END,
|
||||||
|
shining_likes_count=shining_likes_count + CASE WHEN shine_is_primary(NEW.login) AND shine_is_shining(NEW.login) 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;
|
||||||
|
ELSIF NEW.msg_sub_type=2 AND previous_sub_type=1 THEN
|
||||||
|
UPDATE message_stats SET likes_count=GREATEST(0,likes_count-1),
|
||||||
|
primary_likes_count=GREATEST(0,primary_likes_count - CASE WHEN shine_is_primary(NEW.login) THEN 1 ELSE 0 END),
|
||||||
|
shining_likes_count=GREATEST(0,shining_likes_count - CASE WHEN shine_is_primary(NEW.login) AND shine_is_shining(NEW.login) 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;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
INSERT INTO reactions_state(from_login,from_bch_name,reaction_type,to_login,to_bch_name,to_block_number,to_block_hash,last_sub_type)
|
||||||
|
VALUES(NEW.login,NEW.bch_name,1,NEW.to_login,NEW.to_bch_name,NEW.to_block_number,NEW.to_block_hash,NEW.msg_sub_type)
|
||||||
|
ON CONFLICT(from_login,from_bch_name,reaction_type,to_login,to_bch_name,to_block_number,to_block_hash) DO UPDATE SET last_sub_type=EXCLUDED.last_sub_type;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Initial projection/rebuild.
|
||||||
|
DO $$ DECLARE r RECORD; BEGIN
|
||||||
|
FOR r IN SELECT DISTINCT login FROM users_params LOOP PERFORM shine_refresh_user_profile(r.login); END LOOP;
|
||||||
|
END $$;
|
||||||
|
DO $$ DECLARE r RECORD; BEGIN
|
||||||
|
FOR r IN SELECT login FROM solana_user_pda_current LOOP PERFORM shine_refresh_user_stats(r.login); END LOOP;
|
||||||
|
END $$;
|
||||||
|
DO $$ DECLARE r RECORD; BEGIN
|
||||||
|
FOR r IN SELECT owner_bch_name, channel_root_block_number, channel_root_block_hash FROM channel_names_state WHERE channel_type_code=1 LOOP
|
||||||
|
PERFORM shine_refresh_channel_follow_stats(r.owner_bch_name, r.channel_root_block_number, r.channel_root_block_hash);
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
UPDATE message_stats ms SET
|
||||||
|
primary_likes_count=(SELECT COUNT(*)::INT FROM reactions_state rs WHERE rs.reaction_type=1 AND rs.last_sub_type=1
|
||||||
|
AND rs.to_login=ms.to_login AND rs.to_bch_name=ms.to_bch_name AND rs.to_block_number=ms.to_block_number AND rs.to_block_hash=ms.to_block_hash
|
||||||
|
AND shine_is_primary(rs.from_login)),
|
||||||
|
shining_likes_count=(SELECT COUNT(*)::INT FROM reactions_state rs WHERE rs.reaction_type=1 AND rs.last_sub_type=1
|
||||||
|
AND rs.to_login=ms.to_login AND rs.to_bch_name=ms.to_bch_name AND rs.to_block_number=ms.to_block_number AND rs.to_block_hash=ms.to_block_hash
|
||||||
|
AND shine_is_primary(rs.from_login) AND shine_is_shining(rs.from_login));
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version(id,schema_version,updated_at_ms)
|
||||||
|
VALUES(1,18,CAST(EXTRACT(EPOCH FROM clock_timestamp())*1000 AS BIGINT))
|
||||||
|
ON CONFLICT(id) DO UPDATE SET schema_version=EXCLUDED.schema_version, updated_at_ms=EXCLUDED.updated_at_ms;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- v19: skip user_stats refresh for logins that are absent from solana_user_pda_current.
|
||||||
|
-- This keeps bootstrap resilient to legacy rows that are still present in blocks.
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_refresh_user_stats(p_login TEXT)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_now BIGINT := CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT);
|
||||||
|
BEGIN
|
||||||
|
IF p_login IS NULL OR btrim(p_login) = '' THEN RETURN; END IF;
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM solana_user_pda_current
|
||||||
|
WHERE login = p_login
|
||||||
|
LIMIT 1
|
||||||
|
) THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
INSERT INTO user_stats_state(login, owned_public_channels_count, following_users_count,
|
||||||
|
following_channels_count, close_friends_count, friends_count,
|
||||||
|
primary_confirmations_received_count, primary_confirmations_given_count,
|
||||||
|
shine_confirmations_received_count, shine_confirmations_given_count, updated_at_ms)
|
||||||
|
SELECT p_login,
|
||||||
|
(SELECT COUNT(*)::INT FROM channel_names_state cn WHERE LOWER(cn.owner_login)=LOWER(p_login) AND cn.channel_type_code=1),
|
||||||
|
0,
|
||||||
|
(SELECT COUNT(*)::INT FROM connections_state cs JOIN channel_names_state cn
|
||||||
|
ON cn.owner_bch_name=cs.to_bch_name AND cn.channel_root_block_number=cs.to_block_number AND cn.channel_root_block_hash=cs.to_block_hash
|
||||||
|
WHERE LOWER(cs.login)=LOWER(p_login) AND cs.rel_type=30 AND cn.channel_type_code=1),
|
||||||
|
(SELECT COUNT(DISTINCT LOWER(cs.to_login))::INT FROM connections_state cs WHERE LOWER(cs.login)=LOWER(p_login) AND cs.rel_type=10),
|
||||||
|
(SELECT COUNT(DISTINCT LOWER(cs.to_login))::INT FROM connections_state cs
|
||||||
|
WHERE LOWER(cs.login)=LOWER(p_login) AND cs.rel_type=14
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM connections_state cf WHERE LOWER(cf.login)=LOWER(p_login) AND LOWER(cf.to_login)=LOWER(cs.to_login) AND cf.rel_type=10)),
|
||||||
|
(SELECT COUNT(DISTINCT LOWER(cs.login))::INT FROM connections_state cs
|
||||||
|
WHERE LOWER(cs.to_login)=LOWER(p_login) AND cs.rel_type=80
|
||||||
|
AND shine_is_primary(cs.login) AND shine_target_accepts_primary_confirmation(p_login)),
|
||||||
|
(SELECT COUNT(DISTINCT LOWER(cs.to_login))::INT FROM connections_state cs
|
||||||
|
WHERE LOWER(cs.login)=LOWER(p_login) AND cs.rel_type=80
|
||||||
|
AND shine_is_primary(p_login) AND shine_target_accepts_primary_confirmation(cs.to_login)),
|
||||||
|
(SELECT COUNT(DISTINCT LOWER(cs.login))::INT FROM connections_state cs
|
||||||
|
WHERE LOWER(cs.to_login)=LOWER(p_login) AND cs.rel_type=70
|
||||||
|
AND shine_is_primary(cs.login) AND shine_is_shining(cs.login) AND shine_target_accepts_shine_confirmation(p_login)),
|
||||||
|
(SELECT COUNT(DISTINCT LOWER(cs.to_login))::INT FROM connections_state cs
|
||||||
|
WHERE LOWER(cs.login)=LOWER(p_login) AND cs.rel_type=70
|
||||||
|
AND shine_is_primary(p_login) AND shine_is_shining(p_login) AND shine_target_accepts_shine_confirmation(cs.to_login)),
|
||||||
|
v_now
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
owned_public_channels_count=EXCLUDED.owned_public_channels_count,
|
||||||
|
following_users_count=0,
|
||||||
|
following_channels_count=EXCLUDED.following_channels_count,
|
||||||
|
close_friends_count=EXCLUDED.close_friends_count,
|
||||||
|
friends_count=EXCLUDED.friends_count,
|
||||||
|
primary_confirmations_received_count=EXCLUDED.primary_confirmations_received_count,
|
||||||
|
primary_confirmations_given_count=EXCLUDED.primary_confirmations_given_count,
|
||||||
|
shine_confirmations_received_count=EXCLUDED.shine_confirmations_received_count,
|
||||||
|
shine_confirmations_given_count=EXCLUDED.shine_confirmations_given_count,
|
||||||
|
updated_at_ms=EXCLUDED.updated_at_ms;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_refresh_related_user_stats(p_login TEXT)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE r RECORD;
|
||||||
|
BEGIN
|
||||||
|
PERFORM shine_refresh_user_stats(p_login);
|
||||||
|
FOR r IN
|
||||||
|
SELECT DISTINCT x.login FROM (
|
||||||
|
SELECT cs.login FROM connections_state cs WHERE LOWER(cs.to_login)=LOWER(p_login) AND cs.rel_type IN (70,80)
|
||||||
|
UNION
|
||||||
|
SELECT cs.to_login AS login FROM connections_state cs WHERE LOWER(cs.login)=LOWER(p_login) AND cs.rel_type IN (70,80)
|
||||||
|
) x
|
||||||
|
LOOP
|
||||||
|
PERFORM shine_refresh_user_stats(r.login);
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
-- Notifications v2: three categories + signed seen watermarks.
|
||||||
|
|
||||||
|
ALTER TABLE user_notifications_state
|
||||||
|
DROP CONSTRAINT IF EXISTS user_notifications_state_notification_kind_check;
|
||||||
|
ALTER TABLE user_notifications_state
|
||||||
|
ADD CONSTRAINT user_notifications_state_notification_kind_check
|
||||||
|
CHECK (notification_kind IN ('reply', 'connection', 'event'));
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_notification_seen_state (
|
||||||
|
owner_login VARCHAR(60) NOT NULL,
|
||||||
|
category TEXT NOT NULL CHECK (category IN ('replies', 'connections', 'events')),
|
||||||
|
seen_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
signed_blob BYTEA NOT NULL,
|
||||||
|
signed_at_ms BIGINT NOT NULL,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (owner_login, category)
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE db_schema_version SET schema_version = 20 WHERE id = 1;
|
||||||
@@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS db_schema_version (
|
|||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||||
VALUES (1, 13, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
VALUES (1, 20, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
schema_version = EXCLUDED.schema_version,
|
schema_version = EXCLUDED.schema_version,
|
||||||
updated_at_ms = EXCLUDED.updated_at_ms;
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
@@ -177,7 +177,7 @@ BEGIN
|
|||||||
ELSE u.access_servers_json::jsonb
|
ELSE u.access_servers_json::jsonb
|
||||||
END
|
END
|
||||||
) WITH ORDINALITY AS access_server(login_value, ord)
|
) WITH ORDINALITY AS access_server(login_value, ord)
|
||||||
WHERE ord <= 2
|
WHERE ord = 1
|
||||||
) AS access_server
|
) AS access_server
|
||||||
JOIN solana_user_pda_current s
|
JOIN solana_user_pda_current s
|
||||||
ON LOWER(s.login) = LOWER(btrim(access_server.login_value))
|
ON LOWER(s.login) = LOWER(btrim(access_server.login_value))
|
||||||
@@ -220,7 +220,7 @@ BEGIN
|
|||||||
ELSE u.access_servers_json::jsonb
|
ELSE u.access_servers_json::jsonb
|
||||||
END
|
END
|
||||||
) WITH ORDINALITY AS access_server(login_value, ord)
|
) WITH ORDINALITY AS access_server(login_value, ord)
|
||||||
WHERE ord <= 2
|
WHERE ord = 1
|
||||||
) AS access_server
|
) AS access_server
|
||||||
WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login)
|
WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login)
|
||||||
LOOP
|
LOOP
|
||||||
@@ -416,35 +416,15 @@ CREATE TABLE IF NOT EXISTS user_settings (
|
|||||||
value_num BIGINT NOT NULL DEFAULT 0,
|
value_num BIGINT NOT NULL DEFAULT 0,
|
||||||
client_key TEXT NOT NULL DEFAULT '',
|
client_key TEXT NOT NULL DEFAULT '',
|
||||||
signature TEXT NOT NULL DEFAULT '',
|
signature TEXT NOT NULL DEFAULT '',
|
||||||
synced BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
UNIQUE (login, setting_type, setting_key)
|
UNIQUE (login, setting_type, setting_key)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_settings_login
|
CREATE INDEX IF NOT EXISTS idx_user_settings_login
|
||||||
ON user_settings(login);
|
ON user_settings(login);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_settings_synced
|
|
||||||
ON user_settings(login, synced, time_ms);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_settings_type_key
|
CREATE INDEX IF NOT EXISTS idx_user_settings_type_key
|
||||||
ON user_settings(setting_type, setting_key);
|
ON user_settings(setting_type, setting_key);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS user_settings_sync_peer_state (
|
|
||||||
owner_login TEXT NOT NULL,
|
|
||||||
remote_server_login TEXT NOT NULL,
|
|
||||||
remote_server_url TEXT NOT NULL,
|
|
||||||
cursor_time_ms BIGINT NOT NULL DEFAULT 0,
|
|
||||||
cursor_setting_key TEXT NOT NULL DEFAULT '',
|
|
||||||
bootstrap_completed BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
last_sync_at_ms BIGINT,
|
|
||||||
last_error TEXT,
|
|
||||||
updated_at_ms BIGINT NOT NULL,
|
|
||||||
PRIMARY KEY (owner_login, remote_server_login)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_settings_sync_peer_state_owner
|
|
||||||
ON user_settings_sync_peer_state(owner_login);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ip_geo_cache (
|
CREATE TABLE IF NOT EXISTS ip_geo_cache (
|
||||||
ip TEXT PRIMARY KEY,
|
ip TEXT PRIMARY KEY,
|
||||||
geo TEXT,
|
geo TEXT,
|
||||||
@@ -785,7 +765,7 @@ CREATE INDEX IF NOT EXISTS idx_signed_message_delivery_session
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS user_notifications_state (
|
CREATE TABLE IF NOT EXISTS user_notifications_state (
|
||||||
owner_login TEXT NOT NULL REFERENCES solana_user_pda_current(normalized_login),
|
owner_login TEXT NOT NULL REFERENCES solana_user_pda_current(normalized_login),
|
||||||
notification_kind TEXT NOT NULL CHECK (notification_kind IN ('reply', 'connection')),
|
notification_kind TEXT NOT NULL CHECK (notification_kind IN ('reply', 'connection', 'event')),
|
||||||
created_at_ms BIGINT NOT NULL,
|
created_at_ms BIGINT NOT NULL,
|
||||||
source_login TEXT NOT NULL,
|
source_login TEXT NOT NULL,
|
||||||
source_bch_name TEXT NOT NULL,
|
source_bch_name TEXT NOT NULL,
|
||||||
@@ -824,22 +804,6 @@ CREATE TABLE IF NOT EXISTS dm_dialog_state (
|
|||||||
CREATE INDEX IF NOT EXISTS idx_dm_dialog_state_owner_last_time
|
CREATE INDEX IF NOT EXISTS idx_dm_dialog_state_owner_last_time
|
||||||
ON dm_dialog_state(owner_login, last_message_time_ms DESC, peer_login);
|
ON dm_dialog_state(owner_login, last_message_time_ms DESC, peer_login);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS dm_sync_peer_state (
|
|
||||||
owner_login TEXT NOT NULL,
|
|
||||||
remote_server_login TEXT NOT NULL,
|
|
||||||
remote_server_url TEXT NOT NULL,
|
|
||||||
cursor_stored_at_ms BIGINT NOT NULL DEFAULT 0,
|
|
||||||
cursor_message_key TEXT NOT NULL DEFAULT '',
|
|
||||||
bootstrap_completed BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
last_sync_at_ms BIGINT,
|
|
||||||
last_error TEXT,
|
|
||||||
updated_at_ms BIGINT NOT NULL,
|
|
||||||
PRIMARY KEY (owner_login, remote_server_login)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_dm_sync_peer_state_owner
|
|
||||||
ON dm_sync_peer_state(owner_login);
|
|
||||||
|
|
||||||
-- Изменяемое состояние доставки исходящей пары. Подписанные блоки остаются
|
-- Изменяемое состояние доставки исходящей пары. Подписанные блоки остаются
|
||||||
-- неизменяемыми; эта таблица описывает только сетевую доставку пары 1/2 или 3/4.
|
-- неизменяемыми; эта таблица описывает только сетевую доставку пары 1/2 или 3/4.
|
||||||
CREATE TABLE IF NOT EXISTS dm_delivery_state (
|
CREATE TABLE IF NOT EXISTS dm_delivery_state (
|
||||||
@@ -868,23 +832,6 @@ CREATE INDEX IF NOT EXISTS idx_dm_delivery_state_due
|
|||||||
CREATE INDEX IF NOT EXISTS idx_dm_delivery_state_base
|
CREATE INDEX IF NOT EXISTS idx_dm_delivery_state_base
|
||||||
ON dm_delivery_state(base_key, from_login);
|
ON dm_delivery_state(base_key, from_login);
|
||||||
|
|
||||||
-- У одного пользователя теперь максимум один второй access-сервер, поэтому
|
|
||||||
-- достаточно одного флага ACK на событие. Курсор по времени больше не нужен.
|
|
||||||
CREATE TABLE IF NOT EXISTS dm_sync_outbox (
|
|
||||||
owner_login TEXT NOT NULL,
|
|
||||||
primary_message_key TEXT NOT NULL,
|
|
||||||
event_id TEXT NOT NULL,
|
|
||||||
secondary_message_key TEXT,
|
|
||||||
synced BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
created_at_ms BIGINT NOT NULL,
|
|
||||||
updated_at_ms BIGINT NOT NULL,
|
|
||||||
PRIMARY KEY (owner_login, primary_message_key)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_dm_sync_outbox_unsynced
|
|
||||||
ON dm_sync_outbox(owner_login, created_at_ms, primary_message_key)
|
|
||||||
WHERE synced = FALSE;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS message_views_state (
|
CREATE TABLE IF NOT EXISTS message_views_state (
|
||||||
viewer_login TEXT NOT NULL,
|
viewer_login TEXT NOT NULL,
|
||||||
to_bch_name TEXT NOT NULL,
|
to_bch_name TEXT NOT NULL,
|
||||||
@@ -1166,7 +1113,7 @@ BEGIN
|
|||||||
|
|
||||||
resolved_login := shine_resolve_login(NEW.to_login, NEW.to_bch_name);
|
resolved_login := shine_resolve_login(NEW.to_login, NEW.to_bch_name);
|
||||||
|
|
||||||
IF NEW.msg_sub_type IN (10, 20, 30, 40, 50, 52, 54, 60, 70, 74) THEN
|
IF NEW.msg_sub_type IN (10, 14, 20, 30, 40, 50, 52, 54, 60, 70, 74, 80) THEN
|
||||||
IF resolved_login IS NULL OR NEW.to_bch_name IS NULL THEN
|
IF resolved_login IS NULL OR NEW.to_bch_name IS NULL THEN
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END IF;
|
END IF;
|
||||||
@@ -1341,6 +1288,7 @@ BEGIN
|
|||||||
|
|
||||||
positive_rel_type := CASE NEW.msg_sub_type
|
positive_rel_type := CASE NEW.msg_sub_type
|
||||||
WHEN 11 THEN 10
|
WHEN 11 THEN 10
|
||||||
|
WHEN 15 THEN 14
|
||||||
WHEN 21 THEN 20
|
WHEN 21 THEN 20
|
||||||
WHEN 31 THEN 30
|
WHEN 31 THEN 30
|
||||||
WHEN 41 THEN 40
|
WHEN 41 THEN 40
|
||||||
@@ -1350,6 +1298,7 @@ BEGIN
|
|||||||
WHEN 61 THEN 60
|
WHEN 61 THEN 60
|
||||||
WHEN 71 THEN 70
|
WHEN 71 THEN 70
|
||||||
WHEN 75 THEN 74
|
WHEN 75 THEN 74
|
||||||
|
WHEN 81 THEN 80
|
||||||
ELSE NULL
|
ELSE NULL
|
||||||
END;
|
END;
|
||||||
|
|
||||||
@@ -1697,4 +1646,371 @@ AFTER INSERT ON channel_names_state
|
|||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
EXECUTE FUNCTION shine_channel_names_state_stats_ai();
|
EXECUTE FUNCTION shine_channel_names_state_stats_ai();
|
||||||
|
|
||||||
|
|
||||||
|
-- v18 integrated fresh-schema definitions
|
||||||
|
-- v18: fast profile projection, effective social/voting statistics and qualified likes.
|
||||||
|
-- Source of truth remains users_params / connections_state / reactions_state / channel_names_state.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_profile_state (
|
||||||
|
login TEXT PRIMARY KEY,
|
||||||
|
first_name TEXT NOT NULL DEFAULT '',
|
||||||
|
last_name TEXT NOT NULL DEFAULT '',
|
||||||
|
ava_ar TEXT NOT NULL DEFAULT '',
|
||||||
|
account_role TEXT,
|
||||||
|
shine_status TEXT,
|
||||||
|
updated_at_ms BIGINT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_profile_state_name
|
||||||
|
ON user_profile_state (LOWER(last_name), LOWER(first_name), LOWER(login));
|
||||||
|
|
||||||
|
ALTER TABLE user_stats_state
|
||||||
|
ADD COLUMN IF NOT EXISTS friends_count INTEGER NOT NULL DEFAULT 0 CHECK (friends_count >= 0),
|
||||||
|
ADD COLUMN IF NOT EXISTS primary_confirmations_received_count INTEGER NOT NULL DEFAULT 0 CHECK (primary_confirmations_received_count >= 0),
|
||||||
|
ADD COLUMN IF NOT EXISTS primary_confirmations_given_count INTEGER NOT NULL DEFAULT 0 CHECK (primary_confirmations_given_count >= 0),
|
||||||
|
ADD COLUMN IF NOT EXISTS shine_confirmations_received_count INTEGER NOT NULL DEFAULT 0 CHECK (shine_confirmations_received_count >= 0),
|
||||||
|
ADD COLUMN IF NOT EXISTS shine_confirmations_given_count INTEGER NOT NULL DEFAULT 0 CHECK (shine_confirmations_given_count >= 0);
|
||||||
|
|
||||||
|
ALTER TABLE message_stats
|
||||||
|
ADD COLUMN IF NOT EXISTS primary_likes_count INTEGER NOT NULL DEFAULT 0 CHECK (primary_likes_count >= 0),
|
||||||
|
ADD COLUMN IF NOT EXISTS shining_likes_count INTEGER NOT NULL DEFAULT 0 CHECK (shining_likes_count >= 0);
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_refresh_user_profile(p_login TEXT)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_first_name TEXT := '';
|
||||||
|
v_last_name TEXT := '';
|
||||||
|
v_ava_ar TEXT := '';
|
||||||
|
v_account_role TEXT := NULL;
|
||||||
|
v_shine TEXT := NULL;
|
||||||
|
v_updated BIGINT := 0;
|
||||||
|
BEGIN
|
||||||
|
IF p_login IS NULL OR btrim(p_login) = '' THEN RETURN; END IF;
|
||||||
|
|
||||||
|
SELECT COALESCE(MAX(value) FILTER (WHERE param = 'first_name'), ''),
|
||||||
|
COALESCE(MAX(value) FILTER (WHERE param = 'last_name'), ''),
|
||||||
|
COALESCE(MAX(value) FILTER (WHERE param = 'ava'), ''),
|
||||||
|
MAX(value) FILTER (WHERE param = 'account_role'),
|
||||||
|
MAX(value) FILTER (WHERE param = 'shine'),
|
||||||
|
COALESCE(MAX(time_ms), 0)
|
||||||
|
INTO v_first_name, v_last_name, v_ava_ar, v_account_role, v_shine, v_updated
|
||||||
|
FROM users_params
|
||||||
|
WHERE LOWER(login) = LOWER(p_login);
|
||||||
|
|
||||||
|
v_account_role := LOWER(BTRIM(COALESCE(v_account_role, '')));
|
||||||
|
IF v_account_role NOT IN ('primary', 'non_voting') THEN v_account_role := NULL; END IF;
|
||||||
|
|
||||||
|
v_shine := LOWER(BTRIM(COALESCE(v_shine, '')));
|
||||||
|
IF v_shine = 'yes' THEN v_shine := 'shining'; END IF; -- legacy compatibility
|
||||||
|
IF v_shine NOT IN ('shining', 'unknown', 'not_interested') THEN v_shine := NULL; END IF;
|
||||||
|
|
||||||
|
INSERT INTO user_profile_state(login, first_name, last_name, ava_ar, account_role, shine_status, updated_at_ms)
|
||||||
|
VALUES (p_login, v_first_name, v_last_name, v_ava_ar, v_account_role, v_shine, v_updated)
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
first_name = EXCLUDED.first_name,
|
||||||
|
last_name = EXCLUDED.last_name,
|
||||||
|
ava_ar = EXCLUDED.ava_ar,
|
||||||
|
account_role = EXCLUDED.account_role,
|
||||||
|
shine_status = EXCLUDED.shine_status,
|
||||||
|
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_is_primary(p_login TEXT)
|
||||||
|
RETURNS BOOLEAN
|
||||||
|
LANGUAGE sql
|
||||||
|
STABLE
|
||||||
|
AS $$
|
||||||
|
SELECT COALESCE((SELECT account_role = 'primary' FROM user_profile_state WHERE LOWER(login)=LOWER(p_login) LIMIT 1), FALSE)
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_is_shining(p_login TEXT)
|
||||||
|
RETURNS BOOLEAN
|
||||||
|
LANGUAGE sql
|
||||||
|
STABLE
|
||||||
|
AS $$
|
||||||
|
SELECT COALESCE((SELECT shine_status = 'shining' FROM user_profile_state WHERE LOWER(login)=LOWER(p_login) LIMIT 1), FALSE)
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_target_accepts_primary_confirmation(p_login TEXT)
|
||||||
|
RETURNS BOOLEAN
|
||||||
|
LANGUAGE sql
|
||||||
|
STABLE
|
||||||
|
AS $$
|
||||||
|
SELECT COALESCE((SELECT account_role IS DISTINCT FROM 'non_voting' FROM user_profile_state WHERE LOWER(login)=LOWER(p_login) LIMIT 1), TRUE)
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_target_accepts_shine_confirmation(p_login TEXT)
|
||||||
|
RETURNS BOOLEAN
|
||||||
|
LANGUAGE sql
|
||||||
|
STABLE
|
||||||
|
AS $$
|
||||||
|
SELECT COALESCE((SELECT shine_status IS DISTINCT FROM 'not_interested' FROM user_profile_state WHERE LOWER(login)=LOWER(p_login) LIMIT 1), TRUE)
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_refresh_user_stats(p_login TEXT)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_now BIGINT := CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT);
|
||||||
|
BEGIN
|
||||||
|
IF p_login IS NULL OR btrim(p_login) = '' THEN RETURN; END IF;
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM solana_user_pda_current
|
||||||
|
WHERE login = p_login
|
||||||
|
LIMIT 1
|
||||||
|
) THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
INSERT INTO user_stats_state(login, owned_public_channels_count, following_users_count,
|
||||||
|
following_channels_count, close_friends_count, friends_count,
|
||||||
|
primary_confirmations_received_count, primary_confirmations_given_count,
|
||||||
|
shine_confirmations_received_count, shine_confirmations_given_count, updated_at_ms)
|
||||||
|
SELECT p_login,
|
||||||
|
(SELECT COUNT(*)::INT FROM channel_names_state cn WHERE LOWER(cn.owner_login)=LOWER(p_login) AND cn.channel_type_code=1),
|
||||||
|
0,
|
||||||
|
(SELECT COUNT(*)::INT FROM connections_state cs JOIN channel_names_state cn
|
||||||
|
ON cn.owner_bch_name=cs.to_bch_name AND cn.channel_root_block_number=cs.to_block_number AND cn.channel_root_block_hash=cs.to_block_hash
|
||||||
|
WHERE LOWER(cs.login)=LOWER(p_login) AND cs.rel_type=30 AND cn.channel_type_code=1),
|
||||||
|
(SELECT COUNT(DISTINCT LOWER(cs.to_login))::INT FROM connections_state cs WHERE LOWER(cs.login)=LOWER(p_login) AND cs.rel_type=10),
|
||||||
|
(SELECT COUNT(DISTINCT LOWER(cs.to_login))::INT FROM connections_state cs
|
||||||
|
WHERE LOWER(cs.login)=LOWER(p_login) AND cs.rel_type=14
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM connections_state cf WHERE LOWER(cf.login)=LOWER(p_login) AND LOWER(cf.to_login)=LOWER(cs.to_login) AND cf.rel_type=10)),
|
||||||
|
(SELECT COUNT(DISTINCT LOWER(cs.login))::INT FROM connections_state cs
|
||||||
|
WHERE LOWER(cs.to_login)=LOWER(p_login) AND cs.rel_type=80
|
||||||
|
AND shine_is_primary(cs.login) AND shine_target_accepts_primary_confirmation(p_login)),
|
||||||
|
(SELECT COUNT(DISTINCT LOWER(cs.to_login))::INT FROM connections_state cs
|
||||||
|
WHERE LOWER(cs.login)=LOWER(p_login) AND cs.rel_type=80
|
||||||
|
AND shine_is_primary(p_login) AND shine_target_accepts_primary_confirmation(cs.to_login)),
|
||||||
|
(SELECT COUNT(DISTINCT LOWER(cs.login))::INT FROM connections_state cs
|
||||||
|
WHERE LOWER(cs.to_login)=LOWER(p_login) AND cs.rel_type=70
|
||||||
|
AND shine_is_primary(cs.login) AND shine_is_shining(cs.login) AND shine_target_accepts_shine_confirmation(p_login)),
|
||||||
|
(SELECT COUNT(DISTINCT LOWER(cs.to_login))::INT FROM connections_state cs
|
||||||
|
WHERE LOWER(cs.login)=LOWER(p_login) AND cs.rel_type=70
|
||||||
|
AND shine_is_primary(p_login) AND shine_is_shining(p_login) AND shine_target_accepts_shine_confirmation(cs.to_login)),
|
||||||
|
v_now
|
||||||
|
ON CONFLICT (login) DO UPDATE SET
|
||||||
|
owned_public_channels_count=EXCLUDED.owned_public_channels_count,
|
||||||
|
following_users_count=0,
|
||||||
|
following_channels_count=EXCLUDED.following_channels_count,
|
||||||
|
close_friends_count=EXCLUDED.close_friends_count,
|
||||||
|
friends_count=EXCLUDED.friends_count,
|
||||||
|
primary_confirmations_received_count=EXCLUDED.primary_confirmations_received_count,
|
||||||
|
primary_confirmations_given_count=EXCLUDED.primary_confirmations_given_count,
|
||||||
|
shine_confirmations_received_count=EXCLUDED.shine_confirmations_received_count,
|
||||||
|
shine_confirmations_given_count=EXCLUDED.shine_confirmations_given_count,
|
||||||
|
updated_at_ms=EXCLUDED.updated_at_ms;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_refresh_related_user_stats(p_login TEXT)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE r RECORD;
|
||||||
|
BEGIN
|
||||||
|
PERFORM shine_refresh_user_stats(p_login);
|
||||||
|
FOR r IN
|
||||||
|
SELECT DISTINCT x.login FROM (
|
||||||
|
SELECT cs.login FROM connections_state cs WHERE LOWER(cs.to_login)=LOWER(p_login) AND cs.rel_type IN (70,80)
|
||||||
|
UNION
|
||||||
|
SELECT cs.to_login AS login FROM connections_state cs WHERE LOWER(cs.login)=LOWER(p_login) AND cs.rel_type IN (70,80)
|
||||||
|
) x
|
||||||
|
LOOP
|
||||||
|
PERFORM shine_refresh_user_stats(r.login);
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_refresh_message_qualified_likes_for_actor(p_login TEXT)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE r RECORD;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN
|
||||||
|
SELECT DISTINCT rs.to_login, rs.to_bch_name, rs.to_block_number, rs.to_block_hash
|
||||||
|
FROM reactions_state rs
|
||||||
|
WHERE LOWER(rs.from_login)=LOWER(p_login) AND rs.reaction_type=1 AND rs.last_sub_type=1
|
||||||
|
LOOP
|
||||||
|
UPDATE message_stats ms SET
|
||||||
|
primary_likes_count=(SELECT COUNT(*)::INT FROM reactions_state x WHERE x.reaction_type=1 AND x.last_sub_type=1
|
||||||
|
AND x.to_login=r.to_login AND x.to_bch_name=r.to_bch_name AND x.to_block_number=r.to_block_number AND x.to_block_hash=r.to_block_hash
|
||||||
|
AND shine_is_primary(x.from_login)),
|
||||||
|
shining_likes_count=(SELECT COUNT(*)::INT FROM reactions_state x WHERE x.reaction_type=1 AND x.last_sub_type=1
|
||||||
|
AND x.to_login=r.to_login AND x.to_bch_name=r.to_bch_name AND x.to_block_number=r.to_block_number AND x.to_block_hash=r.to_block_hash
|
||||||
|
AND shine_is_primary(x.from_login) AND shine_is_shining(x.from_login))
|
||||||
|
WHERE ms.to_login=r.to_login AND ms.to_bch_name=r.to_bch_name AND ms.to_block_number=r.to_block_number AND ms.to_block_hash=r.to_block_hash;
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_refresh_channel_follow_stats(p_bch_name TEXT, p_block_number INTEGER, p_block_hash BYTEA)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_owner_login TEXT;
|
||||||
|
v_channel_type INTEGER;
|
||||||
|
v_count INTEGER := 0;
|
||||||
|
v_now BIGINT := CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT);
|
||||||
|
BEGIN
|
||||||
|
IF p_bch_name IS NULL OR p_block_number IS NULL OR p_block_hash IS NULL OR p_block_number <= 0 THEN RETURN; END IF;
|
||||||
|
|
||||||
|
SELECT cn.owner_login, cn.channel_type_code
|
||||||
|
INTO v_owner_login, v_channel_type
|
||||||
|
FROM channel_names_state cn
|
||||||
|
WHERE cn.owner_bch_name=p_bch_name
|
||||||
|
AND cn.channel_root_block_number=p_block_number
|
||||||
|
AND cn.channel_root_block_hash=p_block_hash
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF v_channel_type IS NULL THEN RETURN; END IF;
|
||||||
|
|
||||||
|
SELECT COUNT(DISTINCT LOWER(cs.login))::INT
|
||||||
|
INTO v_count
|
||||||
|
FROM connections_state cs
|
||||||
|
WHERE cs.rel_type=30
|
||||||
|
AND cs.to_bch_name=p_bch_name
|
||||||
|
AND cs.to_block_number=p_block_number
|
||||||
|
AND cs.to_block_hash=p_block_hash;
|
||||||
|
|
||||||
|
INSERT INTO channel_stats_state(owner_bch_name,channel_root_block_number,channel_root_block_hash,owner_login,channel_type_code,subscribers_count,updated_at_ms)
|
||||||
|
VALUES(p_bch_name,p_block_number,p_block_hash,COALESCE(v_owner_login,''),v_channel_type,v_count,v_now)
|
||||||
|
ON CONFLICT(owner_bch_name,channel_root_block_number,channel_root_block_hash) DO UPDATE SET
|
||||||
|
owner_login=EXCLUDED.owner_login,
|
||||||
|
channel_type_code=EXCLUDED.channel_type_code,
|
||||||
|
subscribers_count=EXCLUDED.subscribers_count,
|
||||||
|
updated_at_ms=EXCLUDED.updated_at_ms;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Replace incremental channel-name counter with idempotent recomputation.
|
||||||
|
CREATE OR REPLACE FUNCTION shine_channel_names_state_stats_ai()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.channel_type_code = 1 THEN
|
||||||
|
PERFORM shine_refresh_user_stats(NEW.owner_login);
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Rebuild connection state without incremental user counters; then recompute affected users.
|
||||||
|
CREATE OR REPLACE FUNCTION shine_blocks_connection_state_ai()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
resolved_login TEXT;
|
||||||
|
positive_rel_type INTEGER;
|
||||||
|
BEGIN
|
||||||
|
IF NEW.msg_type <> 3 THEN RETURN NEW; END IF;
|
||||||
|
resolved_login := shine_resolve_login(NEW.to_login, NEW.to_bch_name);
|
||||||
|
IF resolved_login IS NULL OR NEW.to_bch_name IS NULL THEN RETURN NEW; END IF;
|
||||||
|
|
||||||
|
IF NEW.msg_sub_type IN (10,14,20,30,40,50,52,54,60,70,74,80) THEN
|
||||||
|
DELETE FROM connections_state WHERE login=NEW.login AND rel_type=NEW.msg_sub_type AND to_login=resolved_login
|
||||||
|
AND to_bch_name=NEW.to_bch_name AND to_block_number=COALESCE(NEW.to_block_number,0)
|
||||||
|
AND to_block_hash=COALESCE(NEW.to_block_hash,decode(repeat('00',32),'hex'));
|
||||||
|
INSERT INTO connections_state(login,rel_type,to_login,to_bch_name,to_block_number,to_block_hash)
|
||||||
|
VALUES(NEW.login,NEW.msg_sub_type,resolved_login,NEW.to_bch_name,COALESCE(NEW.to_block_number,0),COALESCE(NEW.to_block_hash,decode(repeat('00',32),'hex')));
|
||||||
|
ELSE
|
||||||
|
positive_rel_type := CASE NEW.msg_sub_type WHEN 11 THEN 10 WHEN 15 THEN 14 WHEN 21 THEN 20 WHEN 31 THEN 30 WHEN 41 THEN 40
|
||||||
|
WHEN 51 THEN 50 WHEN 53 THEN 52 WHEN 55 THEN 54 WHEN 61 THEN 60 WHEN 71 THEN 70 WHEN 75 THEN 74 WHEN 81 THEN 80 ELSE NULL END;
|
||||||
|
IF positive_rel_type IS NULL THEN RETURN NEW; END IF;
|
||||||
|
DELETE FROM connections_state WHERE login=NEW.login AND rel_type=positive_rel_type AND to_login=resolved_login
|
||||||
|
AND to_bch_name=NEW.to_bch_name AND to_block_number=COALESCE(NEW.to_block_number,0)
|
||||||
|
AND to_block_hash=COALESCE(NEW.to_block_hash,decode(repeat('00',32),'hex'));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
PERFORM shine_refresh_user_stats(NEW.login);
|
||||||
|
PERFORM shine_refresh_user_stats(resolved_login);
|
||||||
|
IF NEW.msg_sub_type IN (30,31) AND NEW.to_block_number IS NOT NULL AND NEW.to_block_number > 0 AND NEW.to_block_hash IS NOT NULL THEN
|
||||||
|
PERFORM shine_refresh_channel_follow_stats(NEW.to_bch_name, NEW.to_block_number, NEW.to_block_hash);
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shine_blocks_message_stats_like_ai()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE previous_sub_type INTEGER;
|
||||||
|
BEGIN
|
||||||
|
IF NEW.msg_type<>2 OR NEW.msg_sub_type NOT IN (1,2) THEN RETURN NEW; END IF;
|
||||||
|
IF NEW.to_login IS NULL OR NEW.to_bch_name IS NULL OR NEW.to_block_number IS NULL OR NEW.to_block_hash IS NULL THEN RETURN NEW; END IF;
|
||||||
|
|
||||||
|
INSERT INTO message_stats(to_login,to_bch_name,to_block_number,to_block_hash,likes_count,primary_likes_count,shining_likes_count,replies_count,edits_count)
|
||||||
|
VALUES(NEW.to_login,NEW.to_bch_name,NEW.to_block_number,NEW.to_block_hash,0,0,0,0,0)
|
||||||
|
ON CONFLICT(to_login,to_bch_name,to_block_number,to_block_hash) DO NOTHING;
|
||||||
|
|
||||||
|
SELECT last_sub_type INTO previous_sub_type FROM reactions_state WHERE from_login=NEW.login AND from_bch_name=NEW.bch_name AND reaction_type=1
|
||||||
|
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 LIMIT 1;
|
||||||
|
|
||||||
|
IF NEW.msg_sub_type=1 AND previous_sub_type IS DISTINCT FROM 1 THEN
|
||||||
|
UPDATE message_stats SET likes_count=likes_count+1,
|
||||||
|
primary_likes_count=primary_likes_count + CASE WHEN shine_is_primary(NEW.login) THEN 1 ELSE 0 END,
|
||||||
|
shining_likes_count=shining_likes_count + CASE WHEN shine_is_primary(NEW.login) AND shine_is_shining(NEW.login) 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;
|
||||||
|
ELSIF NEW.msg_sub_type=2 AND previous_sub_type=1 THEN
|
||||||
|
UPDATE message_stats SET likes_count=GREATEST(0,likes_count-1),
|
||||||
|
primary_likes_count=GREATEST(0,primary_likes_count - CASE WHEN shine_is_primary(NEW.login) THEN 1 ELSE 0 END),
|
||||||
|
shining_likes_count=GREATEST(0,shining_likes_count - CASE WHEN shine_is_primary(NEW.login) AND shine_is_shining(NEW.login) 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;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
INSERT INTO reactions_state(from_login,from_bch_name,reaction_type,to_login,to_bch_name,to_block_number,to_block_hash,last_sub_type)
|
||||||
|
VALUES(NEW.login,NEW.bch_name,1,NEW.to_login,NEW.to_bch_name,NEW.to_block_number,NEW.to_block_hash,NEW.msg_sub_type)
|
||||||
|
ON CONFLICT(from_login,from_bch_name,reaction_type,to_login,to_bch_name,to_block_number,to_block_hash) DO UPDATE SET last_sub_type=EXCLUDED.last_sub_type;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Initial projection/rebuild.
|
||||||
|
DO $$ DECLARE r RECORD; BEGIN
|
||||||
|
FOR r IN SELECT DISTINCT login FROM users_params LOOP PERFORM shine_refresh_user_profile(r.login); END LOOP;
|
||||||
|
END $$;
|
||||||
|
DO $$ DECLARE r RECORD; BEGIN
|
||||||
|
FOR r IN SELECT login FROM solana_user_pda_current LOOP PERFORM shine_refresh_user_stats(r.login); END LOOP;
|
||||||
|
END $$;
|
||||||
|
DO $$ DECLARE r RECORD; BEGIN
|
||||||
|
FOR r IN SELECT owner_bch_name, channel_root_block_number, channel_root_block_hash FROM channel_names_state WHERE channel_type_code=1 LOOP
|
||||||
|
PERFORM shine_refresh_channel_follow_stats(r.owner_bch_name, r.channel_root_block_number, r.channel_root_block_hash);
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
UPDATE message_stats ms SET
|
||||||
|
primary_likes_count=(SELECT COUNT(*)::INT FROM reactions_state rs WHERE rs.reaction_type=1 AND rs.last_sub_type=1
|
||||||
|
AND rs.to_login=ms.to_login AND rs.to_bch_name=ms.to_bch_name AND rs.to_block_number=ms.to_block_number AND rs.to_block_hash=ms.to_block_hash
|
||||||
|
AND shine_is_primary(rs.from_login)),
|
||||||
|
shining_likes_count=(SELECT COUNT(*)::INT FROM reactions_state rs WHERE rs.reaction_type=1 AND rs.last_sub_type=1
|
||||||
|
AND rs.to_login=ms.to_login AND rs.to_bch_name=ms.to_bch_name AND rs.to_block_number=ms.to_block_number AND rs.to_block_hash=ms.to_block_hash
|
||||||
|
AND shine_is_primary(rs.from_login) AND shine_is_shining(rs.from_login));
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_notification_seen_state (
|
||||||
|
owner_login VARCHAR(60) NOT NULL,
|
||||||
|
category TEXT NOT NULL CHECK (category IN ('replies', 'connections', 'events')),
|
||||||
|
seen_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
|
signed_blob BYTEA NOT NULL,
|
||||||
|
signed_at_ms BIGINT NOT NULL,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (owner_login, category)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO db_schema_version(id,schema_version,updated_at_ms)
|
||||||
|
VALUES(1,20,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;
|
COMMIT;
|
||||||
|
|
||||||
|
|||||||
+35
@@ -27,6 +27,11 @@ public class ConnectionContext {
|
|||||||
public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1; // выполнен challenge (AuthChallenge или SessionChallenge)
|
public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1; // выполнен challenge (AuthChallenge или SessionChallenge)
|
||||||
public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь
|
public static final int AUTH_STATUS_USER = 2; // авторизованный пользователь
|
||||||
|
|
||||||
|
// При успешной авторизации отдельно фиксируем отношение пользователя к этому серверу.
|
||||||
|
public static final int USER_SERVER_SCOPE_NONE = 0;
|
||||||
|
public static final int USER_SERVER_SCOPE_LOCAL = 1;
|
||||||
|
public static final int USER_SERVER_SCOPE_REMOTE = 2;
|
||||||
|
|
||||||
// Полный пользователь из runtime БД (current users / solana_user_pda_current)
|
// Полный пользователь из runtime БД (current users / solana_user_pda_current)
|
||||||
private CurrentUserEntry currentUserEntry;
|
private CurrentUserEntry currentUserEntry;
|
||||||
|
|
||||||
@@ -73,6 +78,12 @@ public class ConnectionContext {
|
|||||||
*/
|
*/
|
||||||
private int authenticationStatus = AUTH_STATUS_NONE;
|
private int authenticationStatus = AUTH_STATUS_NONE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LOCAL — этот сервер является первым access server пользователя.
|
||||||
|
* REMOTE — пользователь криптографически авторизован, но его access server другой.
|
||||||
|
*/
|
||||||
|
private int userServerScope = USER_SERVER_SCOPE_NONE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WebSocket-сессия Jetty для данного подключения.
|
* WebSocket-сессия Jetty для данного подключения.
|
||||||
* Нужна, чтобы через ConnectionContext можно было отправлять сообщения клиенту.
|
* Нужна, чтобы через ConnectionContext можно было отправлять сообщения клиенту.
|
||||||
@@ -197,6 +208,28 @@ public class ConnectionContext {
|
|||||||
return authenticationStatus == AUTH_STATUS_NONE;
|
return authenticationStatus == AUTH_STATUS_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getUserServerScope() {
|
||||||
|
return userServerScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserServerScope(int userServerScope) {
|
||||||
|
this.userServerScope = userServerScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLocalAuthenticatedUser() {
|
||||||
|
return isAuthenticatedUser() && userServerScope == USER_SERVER_SCOPE_LOCAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRemoteAuthenticatedUser() {
|
||||||
|
return isAuthenticatedUser() && userServerScope == USER_SERVER_SCOPE_REMOTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserServerScopeName() {
|
||||||
|
return userServerScope == USER_SERVER_SCOPE_LOCAL ? "LOCAL"
|
||||||
|
: userServerScope == USER_SERVER_SCOPE_REMOTE ? "REMOTE"
|
||||||
|
: "NONE";
|
||||||
|
}
|
||||||
|
|
||||||
public void reset() {
|
public void reset() {
|
||||||
currentUserEntry = null;
|
currentUserEntry = null;
|
||||||
activeSessionEntry = null;
|
activeSessionEntry = null;
|
||||||
@@ -209,6 +242,7 @@ public class ConnectionContext {
|
|||||||
sessionLoginNonceExpiresAtMs = 0;
|
sessionLoginNonceExpiresAtMs = 0;
|
||||||
|
|
||||||
authenticationStatus = AUTH_STATUS_NONE;
|
authenticationStatus = AUTH_STATUS_NONE;
|
||||||
|
userServerScope = USER_SERVER_SCOPE_NONE;
|
||||||
wsSession = null;
|
wsSession = null;
|
||||||
serverConnection = false;
|
serverConnection = false;
|
||||||
remoteServerLogin = null;
|
remoteServerLogin = null;
|
||||||
@@ -222,6 +256,7 @@ public class ConnectionContext {
|
|||||||
"login='" + getLogin() + '\'' +
|
"login='" + getLogin() + '\'' +
|
||||||
", sessionId=" + sessionId +
|
", sessionId=" + sessionId +
|
||||||
", authenticationStatus=" + authenticationStatus +
|
", authenticationStatus=" + authenticationStatus +
|
||||||
|
", userServerScope=" + getUserServerScopeName() +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-18
@@ -4,6 +4,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_AuthChallenge_Handler;
|
import server.logic.ws_protocol.JSON.handlers.auth.Net_AuthChallenge_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.Net_ResolveLoginForAuth_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_CloseActiveSession_Handler;
|
import server.logic.ws_protocol.JSON.handlers.auth.Net_CloseActiveSession_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_CreateAuthSession__Handler;
|
import server.logic.ws_protocol.JSON.handlers.auth.Net_CreateAuthSession__Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.Net_ListSessions_Handler;
|
import server.logic.ws_protocol.JSON.handlers.auth.Net_ListSessions_Handler;
|
||||||
@@ -22,6 +23,7 @@ import server.logic.ws_protocol.JSON.handlers.auth.Net_UpsertEspPairingSettings_
|
|||||||
|
|
||||||
// --- auth entities ---
|
// --- auth entities ---
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Request;
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ResolveLoginForAuth_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Request;
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request;
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Request;
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Request;
|
||||||
@@ -86,23 +88,25 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetPersonalD
|
|||||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListGroupChats200_Request;
|
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListGroupChats200_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListSubscriptionsFeed_Request;
|
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListSubscriptionsFeed_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.Net_GetUserConnectionsGraph_Handler;
|
import server.logic.ws_protocol.JSON.handlers.connections.Net_GetUserConnectionsGraph_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.profile.Net_ListUserProfileRelations_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.profile.Net_ListUserProfileChannels_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.profile.entyties.Net_ListUserProfileRelations_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.profile.entyties.Net_ListUserProfileChannels_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.Net_AddCloseFriend_Handler;
|
import server.logic.ws_protocol.JSON.handlers.connections.Net_AddCloseFriend_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.Net_ListContacts_Handler;
|
import server.logic.ws_protocol.JSON.handlers.connections.Net_ListContacts_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.notifications.Net_GetNotifications_Handler;
|
import server.logic.ws_protocol.JSON.handlers.notifications.Net_GetNotifications_Handler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.notifications.Net_SetNotificationState_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetUserConnectionsGraph_Request;
|
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetUserConnectionsGraph_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_AddCloseFriend_Request;
|
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_AddCloseFriend_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_ListContacts_Request;
|
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_ListContacts_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.notifications.entyties.Net_GetNotifications_Request;
|
import server.logic.ws_protocol.JSON.handlers.notifications.entyties.Net_GetNotifications_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.notifications.entyties.Net_SetNotificationState_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_AckSessionDelivery_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_AckSessionDelivery_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_CallInviteBroadcast_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_CallInviteBroadcast_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_CallSignalToSession_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_CallSignalToSession_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_DeleteConversation_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_DeleteConversation_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_DeleteMessage_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_DeleteMessage_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_GetDmDeliveryStatus_Handler;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_MarkAllUserSettingsUnsynced_Handler;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_DmSyncBatch_Handler;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_GetDirectMessages_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_GetDirectMessages_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_UserSettingsSyncBatch_Handler;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_SendSignal_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_SendSignal_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_ReceiveIncomingMessage_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_ReceiveIncomingMessage_Handler;
|
||||||
import server.logic.ws_protocol.JSON.messages.Net_SendMessagePair_Handler;
|
import server.logic.ws_protocol.JSON.messages.Net_SendMessagePair_Handler;
|
||||||
@@ -113,11 +117,7 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_CallInviteBroadcast_R
|
|||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_CallSignalToSession_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_CallSignalToSession_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_DeleteConversation_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_DeleteConversation_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_DeleteMessage_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_DeleteMessage_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDmDeliveryStatus_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_MarkAllUserSettingsUnsynced_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_DmSyncBatch_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDirectMessages_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDirectMessages_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_UserSettingsSyncBatch_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendSignal_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendSignal_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_ReceiveIncomingMessage_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_ReceiveIncomingMessage_Request;
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendMessagePair_Request;
|
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendMessagePair_Request;
|
||||||
@@ -163,6 +163,7 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("TestUploadFreeAvatar", new Net_TestUploadFreeAvatar_Handler()),
|
Map.entry("TestUploadFreeAvatar", new Net_TestUploadFreeAvatar_Handler()),
|
||||||
|
|
||||||
// --- auth ---
|
// --- auth ---
|
||||||
|
Map.entry("ResolveLoginForAuth", new Net_ResolveLoginForAuth_Handler()),
|
||||||
Map.entry("AuthChallenge", new Net_AuthChallenge_Handler()),
|
Map.entry("AuthChallenge", new Net_AuthChallenge_Handler()),
|
||||||
Map.entry("CreateAuthSession", new Net_CreateAuthSession__Handler()),
|
Map.entry("CreateAuthSession", new Net_CreateAuthSession__Handler()),
|
||||||
Map.entry("CloseActiveSession", new Net_CloseActiveSession_Handler()),
|
Map.entry("CloseActiveSession", new Net_CloseActiveSession_Handler()),
|
||||||
@@ -212,21 +213,19 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("GetChannelsCounters", new Net_GetChannelsCounters_Handler()),
|
Map.entry("GetChannelsCounters", new Net_GetChannelsCounters_Handler()),
|
||||||
Map.entry("ListContacts", new Net_ListContacts_Handler()),
|
Map.entry("ListContacts", new Net_ListContacts_Handler()),
|
||||||
Map.entry("GetUserConnectionsGraph", new Net_GetUserConnectionsGraph_Handler()),
|
Map.entry("GetUserConnectionsGraph", new Net_GetUserConnectionsGraph_Handler()),
|
||||||
|
Map.entry("ListUserProfileRelations", new Net_ListUserProfileRelations_Handler()),
|
||||||
|
Map.entry("ListUserProfileChannels", new Net_ListUserProfileChannels_Handler()),
|
||||||
Map.entry("AddCloseFriend", new Net_AddCloseFriend_Handler()),
|
Map.entry("AddCloseFriend", new Net_AddCloseFriend_Handler()),
|
||||||
Map.entry("GetNotifications", new Net_GetNotifications_Handler()),
|
Map.entry("GetNotifications", new Net_GetNotifications_Handler()),
|
||||||
|
Map.entry("SetNotificationState", new Net_SetNotificationState_Handler()),
|
||||||
|
|
||||||
// --- direct messages / push ---
|
// --- direct messages / push ---
|
||||||
Map.entry("UpsertPushToken", new Net_UpsertPushToken_Handler()),
|
Map.entry("UpsertPushToken", new Net_UpsertPushToken_Handler()),
|
||||||
Map.entry("SendTestWebPush", new Net_SendTestWebPush_Handler()),
|
Map.entry("SendTestWebPush", new Net_SendTestWebPush_Handler()),
|
||||||
Map.entry("SendMessagePair", new Net_SendMessagePair_Handler()),
|
Map.entry("SendMessagePair", new Net_SendMessagePair_Handler()),
|
||||||
Map.entry("ReceiveOutcomingMessage", new Net_SendMessagePair_Handler()),
|
|
||||||
Map.entry("ReceiveIncomingMessage", new Net_ReceiveIncomingMessage_Handler()),
|
Map.entry("ReceiveIncomingMessage", new Net_ReceiveIncomingMessage_Handler()),
|
||||||
Map.entry("DeleteMessage", new Net_DeleteMessage_Handler()),
|
Map.entry("DeleteMessage", new Net_DeleteMessage_Handler()),
|
||||||
Map.entry("DeleteConversation", new Net_DeleteConversation_Handler()),
|
Map.entry("DeleteConversation", new Net_DeleteConversation_Handler()),
|
||||||
Map.entry("DmSyncBatch", new Net_DmSyncBatch_Handler()),
|
|
||||||
Map.entry("GetDmDeliveryStatus", new Net_GetDmDeliveryStatus_Handler()),
|
|
||||||
Map.entry("UserSettingsSyncBatch", new Net_UserSettingsSyncBatch_Handler()),
|
|
||||||
Map.entry("MarkAllUserSettingsUnsynced", new Net_MarkAllUserSettingsUnsynced_Handler()),
|
|
||||||
Map.entry("GetDirectMessages", new Net_GetDirectMessages_Handler()),
|
Map.entry("GetDirectMessages", new Net_GetDirectMessages_Handler()),
|
||||||
Map.entry("AckSessionDelivery", new Net_AckSessionDelivery_Handler()),
|
Map.entry("AckSessionDelivery", new Net_AckSessionDelivery_Handler()),
|
||||||
Map.entry("CallInviteBroadcast", new Net_CallInviteBroadcast_Handler()),
|
Map.entry("CallInviteBroadcast", new Net_CallInviteBroadcast_Handler()),
|
||||||
@@ -255,6 +254,7 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("TestUploadFreeAvatar", Net_TestUploadFreeAvatar_Request.class),
|
Map.entry("TestUploadFreeAvatar", Net_TestUploadFreeAvatar_Request.class),
|
||||||
|
|
||||||
// --- auth ---
|
// --- auth ---
|
||||||
|
Map.entry("ResolveLoginForAuth", Net_ResolveLoginForAuth_Request.class),
|
||||||
Map.entry("AuthChallenge", Net_AuthChallenge_Request.class),
|
Map.entry("AuthChallenge", Net_AuthChallenge_Request.class),
|
||||||
Map.entry("CreateAuthSession", Net_CreateAuthSession_Request.class),
|
Map.entry("CreateAuthSession", Net_CreateAuthSession_Request.class),
|
||||||
Map.entry("CloseActiveSession", Net_CloseActiveSession_Request.class),
|
Map.entry("CloseActiveSession", Net_CloseActiveSession_Request.class),
|
||||||
@@ -304,21 +304,19 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("GetChannelsCounters", Net_GetChannelsCounters_Request.class),
|
Map.entry("GetChannelsCounters", Net_GetChannelsCounters_Request.class),
|
||||||
Map.entry("ListContacts", Net_ListContacts_Request.class),
|
Map.entry("ListContacts", Net_ListContacts_Request.class),
|
||||||
Map.entry("GetUserConnectionsGraph", Net_GetUserConnectionsGraph_Request.class),
|
Map.entry("GetUserConnectionsGraph", Net_GetUserConnectionsGraph_Request.class),
|
||||||
|
Map.entry("ListUserProfileRelations", Net_ListUserProfileRelations_Request.class),
|
||||||
|
Map.entry("ListUserProfileChannels", Net_ListUserProfileChannels_Request.class),
|
||||||
Map.entry("AddCloseFriend", Net_AddCloseFriend_Request.class),
|
Map.entry("AddCloseFriend", Net_AddCloseFriend_Request.class),
|
||||||
Map.entry("GetNotifications", Net_GetNotifications_Request.class),
|
Map.entry("GetNotifications", Net_GetNotifications_Request.class),
|
||||||
|
Map.entry("SetNotificationState", Net_SetNotificationState_Request.class),
|
||||||
|
|
||||||
// --- direct messages / push ---
|
// --- direct messages / push ---
|
||||||
Map.entry("UpsertPushToken", Net_UpsertPushToken_Request.class),
|
Map.entry("UpsertPushToken", Net_UpsertPushToken_Request.class),
|
||||||
Map.entry("SendTestWebPush", Net_SendTestWebPush_Request.class),
|
Map.entry("SendTestWebPush", Net_SendTestWebPush_Request.class),
|
||||||
Map.entry("SendMessagePair", Net_SendMessagePair_Request.class),
|
Map.entry("SendMessagePair", Net_SendMessagePair_Request.class),
|
||||||
Map.entry("ReceiveOutcomingMessage", Net_SendMessagePair_Request.class),
|
|
||||||
Map.entry("ReceiveIncomingMessage", Net_ReceiveIncomingMessage_Request.class),
|
Map.entry("ReceiveIncomingMessage", Net_ReceiveIncomingMessage_Request.class),
|
||||||
Map.entry("DeleteMessage", Net_DeleteMessage_Request.class),
|
Map.entry("DeleteMessage", Net_DeleteMessage_Request.class),
|
||||||
Map.entry("DeleteConversation", Net_DeleteConversation_Request.class),
|
Map.entry("DeleteConversation", Net_DeleteConversation_Request.class),
|
||||||
Map.entry("DmSyncBatch", Net_DmSyncBatch_Request.class),
|
|
||||||
Map.entry("GetDmDeliveryStatus", Net_GetDmDeliveryStatus_Request.class),
|
|
||||||
Map.entry("UserSettingsSyncBatch", Net_UserSettingsSyncBatch_Request.class),
|
|
||||||
Map.entry("MarkAllUserSettingsUnsynced", Net_MarkAllUserSettingsUnsynced_Request.class),
|
|
||||||
Map.entry("GetDirectMessages", Net_GetDirectMessages_Request.class),
|
Map.entry("GetDirectMessages", Net_GetDirectMessages_Request.class),
|
||||||
Map.entry("AckSessionDelivery", Net_AckSessionDelivery_Request.class),
|
Map.entry("AckSessionDelivery", Net_AckSessionDelivery_Request.class),
|
||||||
Map.entry("CallInviteBroadcast", Net_CallInviteBroadcast_Request.class),
|
Map.entry("CallInviteBroadcast", Net_CallInviteBroadcast_Request.class),
|
||||||
|
|||||||
+16
@@ -205,6 +205,22 @@ public final class JsonInboundProcessor {
|
|||||||
ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(safeToString(request), 1200));
|
ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(safeToString(request), 1200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// REMOTE-пользователь успешно авторизован, но этот сервер не является
|
||||||
|
// его access server. Такой контекст нужен для realtime/call signaling,
|
||||||
|
// однако обычные homeserver-операции здесь запрещены централизованно.
|
||||||
|
if (ctx != null
|
||||||
|
&& ctx.isRemoteAuthenticatedUser()
|
||||||
|
&& !RemoteUserOperationPolicy.isAllowed(op)) {
|
||||||
|
Net_Exception_Response err = NetExceptionResponseFactory.error(
|
||||||
|
op,
|
||||||
|
requestId,
|
||||||
|
403,
|
||||||
|
"USER_NOT_LOCAL",
|
||||||
|
"Пользователь авторизован, но относится к другому access server"
|
||||||
|
);
|
||||||
|
return writeResponse(err);
|
||||||
|
}
|
||||||
|
|
||||||
// 5) Вызываем хэндлер
|
// 5) Вызываем хэндлер
|
||||||
Net_Response response;
|
Net_Response response;
|
||||||
try {
|
try {
|
||||||
|
|||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package server.logic.ws_protocol.JSON;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REMOTE-пользователь полностью авторизован криптографически, но этот сервер
|
||||||
|
* не является его access server. Поэтому ему разрешён только минимальный набор
|
||||||
|
* realtime/звонковых операций.
|
||||||
|
*/
|
||||||
|
public final class RemoteUserOperationPolicy {
|
||||||
|
|
||||||
|
private static final Set<String> ALLOWED = Set.of(
|
||||||
|
"Ping",
|
||||||
|
"GetServerInfo",
|
||||||
|
"GetCallIceConfig",
|
||||||
|
"CallInviteBroadcast",
|
||||||
|
"CallSignalToSession",
|
||||||
|
"SendSignal",
|
||||||
|
"CallDeliveryReport",
|
||||||
|
"ClientErrorLog",
|
||||||
|
"ClientDebugLog",
|
||||||
|
"CloseActiveSession",
|
||||||
|
"ResolveLoginForAuth"
|
||||||
|
);
|
||||||
|
|
||||||
|
private RemoteUserOperationPolicy() {}
|
||||||
|
|
||||||
|
public static boolean isAllowed(String op) {
|
||||||
|
return op != null && ALLOWED.contains(op);
|
||||||
|
}
|
||||||
|
}
|
||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.auth;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
|
import shine.db.dao.UserAccessServersCurrentDAO;
|
||||||
|
import shine.db.entities.CurrentUserEntry;
|
||||||
|
import shine.db.entities.UserAccessServerRouteEntry;
|
||||||
|
import utils.config.AppConfig;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Единая серверная логика определения, является ли текущий сервер
|
||||||
|
* домашним access server для пользователя.
|
||||||
|
*/
|
||||||
|
public final class AuthAccessScopeSupport {
|
||||||
|
|
||||||
|
public static final String RESOLUTION_LOCAL = "LOCAL";
|
||||||
|
public static final String RESOLUTION_REMOTE = "REMOTE";
|
||||||
|
public static final String RESOLUTION_NOT_FOUND = "NOT_FOUND";
|
||||||
|
public static final String RESOLUTION_NO_ACCESS_SERVER = "NO_ACCESS_SERVER";
|
||||||
|
|
||||||
|
private static final String SERVER_LOGIN_CONFIG = "server.SHiNE.login";
|
||||||
|
|
||||||
|
private AuthAccessScopeSupport() {}
|
||||||
|
|
||||||
|
public static Resolution resolveLogin(String login) throws SQLException {
|
||||||
|
String cleanLogin = login == null ? "" : login.trim();
|
||||||
|
if (cleanLogin.isBlank()) {
|
||||||
|
return new Resolution(RESOLUTION_NOT_FOUND, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentUserEntry user = CurrentUsersDAO.getInstance().getByLogin(cleanLogin);
|
||||||
|
if (user == null) {
|
||||||
|
return new Resolution(RESOLUTION_NOT_FOUND, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
UserAccessServerRouteEntry route =
|
||||||
|
UserAccessServersCurrentDAO.getInstance().findPrimaryByUserLogin(user.getLogin());
|
||||||
|
if (route == null) {
|
||||||
|
return new Resolution(RESOLUTION_NO_ACCESS_SERVER, user, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
String ownServerLogin = currentServerLogin();
|
||||||
|
String routeServerLogin = normalize(route.getServerLogin());
|
||||||
|
String resolution = ownServerLogin != null && ownServerLogin.equals(routeServerLogin)
|
||||||
|
? RESOLUTION_LOCAL
|
||||||
|
: RESOLUTION_REMOTE;
|
||||||
|
|
||||||
|
return new Resolution(resolution, user, route, ownServerLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int connectionScopeForLogin(String login) throws SQLException {
|
||||||
|
Resolution resolution = resolveLogin(login);
|
||||||
|
return RESOLUTION_LOCAL.equals(resolution.resolution())
|
||||||
|
? ConnectionContext.USER_SERVER_SCOPE_LOCAL
|
||||||
|
: ConnectionContext.USER_SERVER_SCOPE_REMOTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String connectionScopeName(int scope) {
|
||||||
|
return scope == ConnectionContext.USER_SERVER_SCOPE_LOCAL ? "LOCAL" : "REMOTE";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String currentServerLogin() {
|
||||||
|
return normalize(AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalize(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
String normalized = value.trim().toLowerCase(Locale.ROOT);
|
||||||
|
return normalized.isEmpty() ? null : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Resolution(
|
||||||
|
String resolution,
|
||||||
|
CurrentUserEntry user,
|
||||||
|
UserAccessServerRouteEntry primaryAccessServer,
|
||||||
|
String currentServerLogin
|
||||||
|
) {}
|
||||||
|
}
|
||||||
+1
@@ -71,6 +71,7 @@ public class Net_AuthChallenge_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.setCurrentUser(solanaUserEntry);
|
ctx.setCurrentUser(solanaUserEntry);
|
||||||
|
ctx.setUserServerScope(AuthAccessScopeSupport.connectionScopeForLogin(solanaUserEntry.getLogin()));
|
||||||
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS);
|
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS);
|
||||||
|
|
||||||
byte[] buf = new byte[32];
|
byte[] buf = new byte[32];
|
||||||
|
|||||||
+6
@@ -414,10 +414,15 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Между AuthChallenge и CreateAuthSession routing мог измениться,
|
||||||
|
// поэтому окончательный LOCAL/REMOTE scope вычисляем повторно.
|
||||||
|
int userServerScope = AuthAccessScopeSupport.connectionScopeForLogin(canonicalLogin);
|
||||||
|
|
||||||
// --- обновляем контекст ---
|
// --- обновляем контекст ---
|
||||||
ctx.setActiveSession(activeSessionEntry);
|
ctx.setActiveSession(activeSessionEntry);
|
||||||
ctx.setSessionId(sessionId);
|
ctx.setSessionId(sessionId);
|
||||||
ctx.setAuthNonce(null);
|
ctx.setAuthNonce(null);
|
||||||
|
ctx.setUserServerScope(userServerScope);
|
||||||
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
|
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
|
||||||
|
|
||||||
ActiveConnectionsRegistry.getInstance().register(ctx);
|
ActiveConnectionsRegistry.getInstance().register(ctx);
|
||||||
@@ -428,6 +433,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler {
|
|||||||
resp.setRequestId(req.getRequestId());
|
resp.setRequestId(req.getRequestId());
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
resp.setSessionId(sessionId);
|
resp.setSessionId(sessionId);
|
||||||
|
resp.setConnectionScope(AuthAccessScopeSupport.connectionScopeName(userServerScope));
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.auth;
|
||||||
|
|
||||||
|
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.auth.entyties.Net_ResolveLoginForAuth_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ResolveLoginForAuth_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.entities.CurrentUserEntry;
|
||||||
|
import shine.db.entities.UserAccessServerRouteEntry;
|
||||||
|
|
||||||
|
public class Net_ResolveLoginForAuth_Handler implements JsonMessageHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
|
||||||
|
Net_ResolveLoginForAuth_Request req = (Net_ResolveLoginForAuth_Request) baseReq;
|
||||||
|
String login = req.getLogin() == null ? "" : req.getLogin().trim();
|
||||||
|
if (login.isBlank()) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req, WireCodes.Status.BAD_REQUEST, "EMPTY_LOGIN", "Пустой login");
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthAccessScopeSupport.Resolution resolved = AuthAccessScopeSupport.resolveLogin(login);
|
||||||
|
|
||||||
|
Net_ResolveLoginForAuth_Response resp = new Net_ResolveLoginForAuth_Response();
|
||||||
|
resp.setOp(req.getOp());
|
||||||
|
resp.setRequestId(req.getRequestId());
|
||||||
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
|
resp.setResolution(resolved.resolution());
|
||||||
|
|
||||||
|
CurrentUserEntry user = resolved.user();
|
||||||
|
if (user != null) {
|
||||||
|
resp.setLogin(user.getLogin());
|
||||||
|
}
|
||||||
|
|
||||||
|
UserAccessServerRouteEntry route = resolved.primaryAccessServer();
|
||||||
|
if (route != null) {
|
||||||
|
resp.setAccessServerLogin(route.getServerLogin());
|
||||||
|
resp.setAccessServerUrl(route.getServerUrl());
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
@@ -292,10 +292,13 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler {
|
|||||||
session.setClientPlatform(clientPlatform);
|
session.setClientPlatform(clientPlatform);
|
||||||
session.setUserLanguage(userLanguage);
|
session.setUserLanguage(userLanguage);
|
||||||
|
|
||||||
|
int userServerScope = AuthAccessScopeSupport.connectionScopeForLogin(user.getLogin());
|
||||||
|
|
||||||
// ctx
|
// ctx
|
||||||
ctx.setActiveSession(session);
|
ctx.setActiveSession(session);
|
||||||
ctx.setCurrentUser(user);
|
ctx.setCurrentUser(user);
|
||||||
ctx.setSessionId(sessionId);
|
ctx.setSessionId(sessionId);
|
||||||
|
ctx.setUserServerScope(userServerScope);
|
||||||
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
|
ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);
|
||||||
|
|
||||||
ActiveConnectionsRegistry.getInstance().register(ctx);
|
ActiveConnectionsRegistry.getInstance().register(ctx);
|
||||||
@@ -306,6 +309,7 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler {
|
|||||||
resp.setRequestId(req.getRequestId());
|
resp.setRequestId(req.getRequestId());
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
resp.setStoragePwd(session.getStoragePwd());
|
resp.setStoragePwd(session.getStoragePwd());
|
||||||
|
resp.setConnectionScope(AuthAccessScopeSupport.connectionScopeName(userServerScope));
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-8
@@ -66,6 +66,16 @@ public class Net_StartEspPairing_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
String canonicalLogin = user.getLogin();
|
String canonicalLogin = user.getLogin();
|
||||||
|
|
||||||
|
AuthAccessScopeSupport.Resolution authResolution = AuthAccessScopeSupport.resolveLogin(canonicalLogin);
|
||||||
|
if (!AuthAccessScopeSupport.RESOLUTION_LOCAL.equals(authResolution.resolution())) {
|
||||||
|
return NetExceptionResponseFactory.error(
|
||||||
|
req,
|
||||||
|
409,
|
||||||
|
"USER_NOT_LOCAL",
|
||||||
|
"Вход через доверенное устройство нужно начинать на access server пользователя"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
EspPairingSettingsEntry settings = EspPairingSettingsDAO.getInstance().getByLogin(canonicalLogin);
|
EspPairingSettingsEntry settings = EspPairingSettingsDAO.getInstance().getByLogin(canonicalLogin);
|
||||||
boolean enabled = settings == null || settings.isEnabled();
|
boolean enabled = settings == null || settings.isEnabled();
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
@@ -100,14 +110,6 @@ public class Net_StartEspPairing_Handler implements JsonMessageHandler {
|
|||||||
String clientPlatform = AuthSessionTypeSupport.normalizeClientPlatform(req.getRequesterClientPlatform());
|
String clientPlatform = AuthSessionTypeSupport.normalizeClientPlatform(req.getRequesterClientPlatform());
|
||||||
int ttlSeconds = EspPairingSupport.DEFAULT_TTL_SECONDS;
|
int ttlSeconds = EspPairingSupport.DEFAULT_TTL_SECONDS;
|
||||||
List<ConnectionContext> approverConnections = EspPairingSupport.findOnlineTrustedConnections(canonicalLogin);
|
List<ConnectionContext> approverConnections = EspPairingSupport.findOnlineTrustedConnections(canonicalLogin);
|
||||||
if (approverConnections.isEmpty()) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
422,
|
|
||||||
"PAIRING_NO_TRUSTED_SESSION_ONLINE",
|
|
||||||
"Нет ни одной активной доверенной сессии пользователя в сети"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
EspPairingSupport.PairingFingerprint fingerprint = EspPairingSupport.deriveFingerprint(
|
EspPairingSupport.PairingFingerprint fingerprint = EspPairingSupport.deriveFingerprint(
|
||||||
canonicalLogin,
|
canonicalLogin,
|
||||||
requesterSessionKey,
|
requesterSessionKey,
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ public final class SolanaUserPdaImportService {
|
|||||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
private static final HttpClient HTTP = HttpClient.newHttpClient();
|
private static final HttpClient HTTP = HttpClient.newHttpClient();
|
||||||
private static final String MAGIC = "SHiNE";
|
private static final String MAGIC = "SHiNE";
|
||||||
private static final int MAX_EFFECTIVE_ACCESS_SERVERS = 2;
|
private static final int MAX_EFFECTIVE_ACCESS_SERVERS = 1;
|
||||||
|
|
||||||
private SolanaUserPdaImportService() {}
|
private SolanaUserPdaImportService() {}
|
||||||
|
|
||||||
|
|||||||
+4
@@ -22,6 +22,7 @@ public class Net_CreateAuthSession_Response extends Net_Response {
|
|||||||
|
|
||||||
/** Идентификатор сессии, base64 от 32 байт. */
|
/** Идентификатор сессии, base64 от 32 байт. */
|
||||||
private String sessionId;
|
private String sessionId;
|
||||||
|
private String connectionScope;
|
||||||
|
|
||||||
public String getSessionId() {
|
public String getSessionId() {
|
||||||
return sessionId;
|
return sessionId;
|
||||||
@@ -30,4 +31,7 @@ public class Net_CreateAuthSession_Response extends Net_Response {
|
|||||||
public void setSessionId(String sessionId) {
|
public void setSessionId(String sessionId) {
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getConnectionScope() { return connectionScope; }
|
||||||
|
public void setConnectionScope(String connectionScope) { this.connectionScope = connectionScope; }
|
||||||
}
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
|
/** Первый шаг UI-входа: определить существование пользователя и его access server. */
|
||||||
|
public class Net_ResolveLoginForAuth_Request extends Net_Request {
|
||||||
|
private String login;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.auth.entyties;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ответ ResolveLoginForAuth.
|
||||||
|
*
|
||||||
|
* resolution:
|
||||||
|
* - LOCAL пользователь существует и относится к этому серверу;
|
||||||
|
* - REMOTE пользователь существует, но его первый access server другой;
|
||||||
|
* - NOT_FOUND пользователя нет в текущем Solana PDA snapshot;
|
||||||
|
* - NO_ACCESS_SERVER пользователь существует, но корректный access server не найден.
|
||||||
|
*/
|
||||||
|
public class Net_ResolveLoginForAuth_Response extends Net_Response {
|
||||||
|
private String resolution;
|
||||||
|
private String login;
|
||||||
|
private String accessServerLogin;
|
||||||
|
private String accessServerUrl;
|
||||||
|
|
||||||
|
public String getResolution() { return resolution; }
|
||||||
|
public void setResolution(String resolution) { this.resolution = resolution; }
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
|
public String getAccessServerLogin() { return accessServerLogin; }
|
||||||
|
public void setAccessServerLogin(String accessServerLogin) { this.accessServerLogin = accessServerLogin; }
|
||||||
|
|
||||||
|
public String getAccessServerUrl() { return accessServerUrl; }
|
||||||
|
public void setAccessServerUrl(String accessServerUrl) { this.accessServerUrl = accessServerUrl; }
|
||||||
|
}
|
||||||
+4
@@ -9,6 +9,7 @@ import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|||||||
public class Net_SessionLogin_Response extends Net_Response {
|
public class Net_SessionLogin_Response extends Net_Response {
|
||||||
|
|
||||||
private String storagePwd;
|
private String storagePwd;
|
||||||
|
private String connectionScope;
|
||||||
|
|
||||||
public String getStoragePwd() {
|
public String getStoragePwd() {
|
||||||
return storagePwd;
|
return storagePwd;
|
||||||
@@ -17,4 +18,7 @@ public class Net_SessionLogin_Response extends Net_Response {
|
|||||||
public void setStoragePwd(String storagePwd) {
|
public void setStoragePwd(String storagePwd) {
|
||||||
this.storagePwd = storagePwd;
|
this.storagePwd = storagePwd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getConnectionScope() { return connectionScope; }
|
||||||
|
public void setConnectionScope(String connectionScope) { this.connectionScope = connectionScope; }
|
||||||
}
|
}
|
||||||
+46
-7
@@ -563,6 +563,13 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
be.setEditedByBlockNumber(be.getToBlockNumber());
|
be.setEditedByBlockNumber(be.getToBlockNumber());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (block.body instanceof ConnectionBody connectionBody) {
|
||||||
|
String votingError = validateConnectionVotingRights(login, block.subType & 0xFFFF, connectionBody.toLogin());
|
||||||
|
if (votingError != null) {
|
||||||
|
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, votingError, serverLastNum, serverLastHashHex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
UserParamEntry upsertedParam = null;
|
UserParamEntry upsertedParam = null;
|
||||||
if (block.body instanceof UserParamBody upBody) {
|
if (block.body instanceof UserParamBody upBody) {
|
||||||
String effectiveLogin = (st.getLogin() != null && !st.getLogin().isBlank())
|
String effectiveLogin = (st.getLogin() != null && !st.getLogin().isBlank())
|
||||||
@@ -609,6 +616,31 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
/* ====================== Helpers ====================================== */
|
/* ====================== Helpers ====================================== */
|
||||||
/* ===================================================================== */
|
/* ===================================================================== */
|
||||||
|
|
||||||
|
private String validateConnectionVotingRights(String actorLogin, int subType, String targetLogin) {
|
||||||
|
if (subType != (MsgSubType.CONNECTION_OFFICIAL_ACCOUNT_CONFIRMED & 0xFFFF)
|
||||||
|
&& subType != (MsgSubType.CONNECTION_SHINE_CONFIRMED & 0xFFFF)) return null;
|
||||||
|
if (targetLogin == null || targetLogin.isBlank()) return "bad_connection_target";
|
||||||
|
String sql = subType == (MsgSubType.CONNECTION_OFFICIAL_ACCOUNT_CONFIRMED & 0xFFFF)
|
||||||
|
? "SELECT shine_is_primary(?), shine_target_accepts_primary_confirmation(?)"
|
||||||
|
: "SELECT (shine_is_primary(?) AND shine_is_shining(?)), shine_target_accepts_shine_confirmation(?)";
|
||||||
|
try (Connection c = shine.db.DbController.getInstance().getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
if (subType == (MsgSubType.CONNECTION_OFFICIAL_ACCOUNT_CONFIRMED & 0xFFFF)) {
|
||||||
|
ps.setString(1, actorLogin); ps.setString(2, targetLogin);
|
||||||
|
} else {
|
||||||
|
ps.setString(1, actorLogin); ps.setString(2, actorLogin); ps.setString(3, targetLogin);
|
||||||
|
}
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (!rs.next() || !rs.getBoolean(1)) return subType == 80 ? "primary_vote_not_allowed" : "shine_vote_not_allowed";
|
||||||
|
if (!rs.getBoolean(2)) return subType == 80 ? "target_non_voting" : "target_shine_not_interested";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Connection voting permission check failed actor={} target={} subtype={}", actorLogin, targetLogin, subType, e);
|
||||||
|
return "voting_permission_check_failed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static byte[] decodeBase64(String b64) {
|
private static byte[] decodeBase64(String b64) {
|
||||||
if (b64 == null) throw new IllegalArgumentException("blockBytesB64 == null");
|
if (b64 == null) throw new IllegalArgumentException("blockBytesB64 == null");
|
||||||
return Base64Ws.decode(b64);
|
return Base64Ws.decode(b64);
|
||||||
@@ -884,17 +916,24 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connection notifications are intentionally modeled as a generic kind.
|
if (msgType == 3 && block.body instanceof ConnectionBody) {
|
||||||
// Today only CONNECTION_CLOSE_FRIEND is indexed; future incoming connection types
|
boolean personalConnection = msgSubType == (MsgSubType.CONNECTION_CLOSE_FRIEND & 0xFFFF)
|
||||||
// can reuse the same notification kind and expose their code via sourceMsgSubType.
|
|| msgSubType == (MsgSubType.CONNECTION_UNCLOSE_FRIEND & 0xFFFF)
|
||||||
if (msgType == 3
|
|| msgSubType == (MsgSubType.CONNECTION_FRIEND & 0xFFFF)
|
||||||
&& msgSubType == (MsgSubType.CONNECTION_CLOSE_FRIEND & 0xFFFF)
|
|| msgSubType == (MsgSubType.CONNECTION_UNFRIEND & 0xFFFF)
|
||||||
&& block.body instanceof ConnectionBody) {
|
|| msgSubType == (MsgSubType.CONNECTION_SHINE_CONFIRMED & 0xFFFF)
|
||||||
|
|| msgSubType == (MsgSubType.CONNECTION_SHINE_UNCONFIRMED & 0xFFFF)
|
||||||
|
|| msgSubType == (MsgSubType.CONNECTION_OFFICIAL_ACCOUNT_CONFIRMED & 0xFFFF)
|
||||||
|
|| msgSubType == (MsgSubType.CONNECTION_OFFICIAL_ACCOUNT_UNCONFIRMED & 0xFFFF);
|
||||||
|
boolean channelEvent = msgSubType == (MsgSubType.CONNECTION_FOLLOW & 0xFFFF)
|
||||||
|
|| msgSubType == (MsgSubType.CONNECTION_UNFOLLOW & 0xFFFF);
|
||||||
|
if (personalConnection || channelEvent) {
|
||||||
UserNotificationEntry entry = baseNotificationEntry(storedEntry, ownerLogin, createdAtMs, msgSubType);
|
UserNotificationEntry entry = baseNotificationEntry(storedEntry, ownerLogin, createdAtMs, msgSubType);
|
||||||
entry.setNotificationKind("connection");
|
entry.setNotificationKind(channelEvent ? "event" : "connection");
|
||||||
entry.setSourceText("");
|
entry.setSourceText("");
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -138,6 +138,17 @@ public final class ChannelNamesStateBootstrapper {
|
|||||||
dao.insertAll(c, entries);
|
dao.insertAll(c, entries);
|
||||||
|
|
||||||
applyMetaUpdates(c, dao, entries, skipped);
|
applyMetaUpdates(c, dao, entries, skipped);
|
||||||
|
// v18: counters are a projection, so rebuild them from current state after a full channel-name bootstrap.
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("SELECT shine_refresh_user_stats(login) FROM solana_user_pda_current")) {
|
||||||
|
ps.execute();
|
||||||
|
}
|
||||||
|
try (PreparedStatement ps = c.prepareStatement("""
|
||||||
|
SELECT shine_refresh_channel_follow_stats(owner_bch_name, channel_root_block_number, channel_root_block_hash)
|
||||||
|
FROM channel_names_state
|
||||||
|
WHERE channel_type_code = 1
|
||||||
|
""")) {
|
||||||
|
ps.execute();
|
||||||
|
}
|
||||||
c.commit();
|
c.commit();
|
||||||
log.info("channel_names_state bootstrapped: {}", entries.size());
|
log.info("channel_names_state bootstrapped: {}", entries.size());
|
||||||
if (!conflicts.isEmpty()) {
|
if (!conflicts.isEmpty()) {
|
||||||
|
|||||||
+6
-2
@@ -342,8 +342,10 @@ final class ChannelsReadSupport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int[] loadStats(Connection c, String bch, int blockNumber, byte[] blockHash) throws SQLException {
|
static int[] loadStats(Connection c, String bch, int blockNumber, byte[] blockHash) throws SQLException {
|
||||||
String sql = "SELECT likes_count,replies_count FROM message_stats WHERE to_bch_name=? AND to_block_number=? AND to_block_hash=? LIMIT 1";
|
String sql = "SELECT likes_count,primary_likes_count,shining_likes_count,replies_count FROM message_stats WHERE to_bch_name=? AND to_block_number=? AND to_block_hash=? LIMIT 1";
|
||||||
int likesCount = 0;
|
int likesCount = 0;
|
||||||
|
int primaryLikesCount = 0;
|
||||||
|
int shiningLikesCount = 0;
|
||||||
int repliesCount = 0;
|
int repliesCount = 0;
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, bch);
|
ps.setString(1, bch);
|
||||||
@@ -352,6 +354,8 @@ final class ChannelsReadSupport {
|
|||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
if (rs.next()) {
|
if (rs.next()) {
|
||||||
likesCount = rs.getInt("likes_count");
|
likesCount = rs.getInt("likes_count");
|
||||||
|
primaryLikesCount = rs.getInt("primary_likes_count");
|
||||||
|
shiningLikesCount = rs.getInt("shining_likes_count");
|
||||||
repliesCount = rs.getInt("replies_count");
|
repliesCount = rs.getInt("replies_count");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -378,7 +382,7 @@ final class ChannelsReadSupport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new int[] {likesCount, repliesCount, ratingsCount};
|
return new int[] {likesCount, repliesCount, ratingsCount, primaryLikesCount, shiningLikesCount};
|
||||||
}
|
}
|
||||||
|
|
||||||
static String detectChannelDescription(Connection c, String ownerBch, int rootNumber) throws SQLException {
|
static String detectChannelDescription(Connection c, String ownerBch, int rootNumber) throws SQLException {
|
||||||
|
|||||||
+2
@@ -169,6 +169,8 @@ public class Net_GetChannelMessages_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
int[] stats = ChannelsReadSupport.loadStats(c, ownerBch, post.blockNumber, post.blockHash);
|
int[] stats = ChannelsReadSupport.loadStats(c, ownerBch, post.blockNumber, post.blockHash);
|
||||||
item.setLikesCount(stats[0]);
|
item.setLikesCount(stats[0]);
|
||||||
|
item.setPrimaryLikesCount(stats[3]);
|
||||||
|
item.setShiningLikesCount(stats[4]);
|
||||||
item.setRepliesCount(stats[1]);
|
item.setRepliesCount(stats[1]);
|
||||||
item.setRatingsCount(stats[2]);
|
item.setRatingsCount(stats[2]);
|
||||||
item.setLikedByMe(ChannelsReadSupport.isLikedByLogin(c, viewerLogin, post.bchName, post.blockNumber, post.blockHash));
|
item.setLikedByMe(ChannelsReadSupport.isLikedByLogin(c, viewerLogin, post.bchName, post.blockNumber, post.blockHash));
|
||||||
|
|||||||
+2
@@ -242,6 +242,8 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
int[] stats = ChannelsReadSupport.loadStats(c, row.bchName, row.blockNumber, row.blockHash);
|
int[] stats = ChannelsReadSupport.loadStats(c, row.bchName, row.blockNumber, row.blockHash);
|
||||||
node.setLikesCount(stats[0]);
|
node.setLikesCount(stats[0]);
|
||||||
|
node.setPrimaryLikesCount(stats[3]);
|
||||||
|
node.setShiningLikesCount(stats[4]);
|
||||||
node.setRepliesCount(stats[1]);
|
node.setRepliesCount(stats[1]);
|
||||||
node.setRatingsCount(stats[2]);
|
node.setRatingsCount(stats[2]);
|
||||||
node.setLikedByMe(ChannelsReadSupport.isLikedByLogin(c, viewerLogin, row.bchName, row.blockNumber, row.blockHash));
|
node.setLikedByMe(ChannelsReadSupport.isLikedByLogin(c, viewerLogin, row.bchName, row.blockNumber, row.blockHash));
|
||||||
|
|||||||
+4
-1
@@ -82,7 +82,10 @@ public class Net_ListSubscriptionsFeed_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
row.setChannel(channelRef);
|
row.setChannel(channelRef);
|
||||||
row.setMessagesCount(ChannelsReadSupport.countPosts(c, key.ownerBch, key.rootNumber));
|
row.setMessagesCount(ChannelsReadSupport.countPosts(c, key.ownerBch, key.rootNumber));
|
||||||
row.setUnreadCount(ChannelsReadSupport.countUnreadMessages(c, viewerLogin, key.ownerBch, meta.channelName, row.getMessagesCount()));
|
boolean ownChannel = key.ownerLogin != null && key.ownerLogin.equalsIgnoreCase(viewerLogin);
|
||||||
|
row.setUnreadCount(ownChannel
|
||||||
|
? 0
|
||||||
|
: ChannelsReadSupport.countUnreadMessages(c, viewerLogin, key.ownerBch, meta.channelName, row.getMessagesCount()));
|
||||||
|
|
||||||
ChannelsReadSupport.PostBlock lastPost = ChannelsReadSupport.loadLastPost(c, key.ownerBch, key.rootNumber);
|
ChannelsReadSupport.PostBlock lastPost = ChannelsReadSupport.loadLastPost(c, key.ownerBch, key.rootNumber);
|
||||||
if (lastPost != null) {
|
if (lastPost != null) {
|
||||||
|
|||||||
+6
@@ -139,6 +139,8 @@ public class Net_GetChannelMessages_Response extends Net_Response {
|
|||||||
private long createdAtMs;
|
private long createdAtMs;
|
||||||
private String text;
|
private String text;
|
||||||
private int likesCount;
|
private int likesCount;
|
||||||
|
private int primaryLikesCount;
|
||||||
|
private int shiningLikesCount;
|
||||||
private boolean likedByMe;
|
private boolean likedByMe;
|
||||||
private int repliesCount;
|
private int repliesCount;
|
||||||
private int ratingsCount;
|
private int ratingsCount;
|
||||||
@@ -191,6 +193,10 @@ public class Net_GetChannelMessages_Response extends Net_Response {
|
|||||||
|
|
||||||
public int getLikesCount() { return likesCount; }
|
public int getLikesCount() { return likesCount; }
|
||||||
public void setLikesCount(int likesCount) { this.likesCount = likesCount; }
|
public void setLikesCount(int likesCount) { this.likesCount = likesCount; }
|
||||||
|
public int getPrimaryLikesCount() { return primaryLikesCount; }
|
||||||
|
public void setPrimaryLikesCount(int primaryLikesCount) { this.primaryLikesCount = primaryLikesCount; }
|
||||||
|
public int getShiningLikesCount() { return shiningLikesCount; }
|
||||||
|
public void setShiningLikesCount(int shiningLikesCount) { this.shiningLikesCount = shiningLikesCount; }
|
||||||
|
|
||||||
public boolean isLikedByMe() { return likedByMe; }
|
public boolean isLikedByMe() { return likedByMe; }
|
||||||
public void setLikedByMe(boolean likedByMe) { this.likedByMe = likedByMe; }
|
public void setLikedByMe(boolean likedByMe) { this.likedByMe = likedByMe; }
|
||||||
|
|||||||
+112
-40
@@ -46,8 +46,10 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
|
|||||||
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> outFriends = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CLOSE_FRIEND);
|
List<String> outFriends = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_FRIEND);
|
||||||
List<String> inFriends = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CLOSE_FRIEND);
|
List<String> inFriends = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_FRIEND);
|
||||||
|
List<String> outCloseFriends = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CLOSE_FRIEND);
|
||||||
|
List<String> inCloseFriends = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CLOSE_FRIEND);
|
||||||
List<String> outContacts = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CONTACT);
|
List<String> outContacts = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CONTACT);
|
||||||
List<String> inContacts = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CONTACT);
|
List<String> inContacts = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CONTACT);
|
||||||
List<String> outFollows = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_FOLLOW);
|
List<String> outFollows = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_FOLLOW);
|
||||||
@@ -60,18 +62,21 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
|
|||||||
List<String> inChildren = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CHILD);
|
List<String> inChildren = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CHILD);
|
||||||
List<String> outSiblings = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SIBLING);
|
List<String> outSiblings = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SIBLING);
|
||||||
List<String> inSiblings = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SIBLING);
|
List<String> inSiblings = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SIBLING);
|
||||||
List<String> outKnownPersons = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_KNOWN_PERSON);
|
// Legacy 60/61 are accepted by the protocol but intentionally not surfaced by current UI.
|
||||||
List<String> inKnownPersons = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_KNOWN_PERSON);
|
List<String> outKnownPersons = List.of();
|
||||||
List<String> outShineConfirmed = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SHINE_CONFIRMED);
|
List<String> inKnownPersons = List.of();
|
||||||
List<String> inShineConfirmed = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SHINE_CONFIRMED);
|
List<String> outOfficialAccounts = listEffectiveConfirmation(c, canonicalLogin, 80, true);
|
||||||
|
List<String> inOfficialAccounts = listEffectiveConfirmation(c, canonicalLogin, 80, false);
|
||||||
|
List<String> outShineConfirmed = listEffectiveConfirmation(c, canonicalLogin, 70, true);
|
||||||
|
List<String> inShineConfirmed = listEffectiveConfirmation(c, canonicalLogin, 70, false);
|
||||||
List<String> outShineSeen = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SHINE_SEEN);
|
List<String> outShineSeen = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SHINE_SEEN);
|
||||||
List<String> inShineSeen = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SHINE_SEEN);
|
List<String> inShineSeen = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SHINE_SEEN);
|
||||||
|
|
||||||
LinkedHashSet<String> allLogins = new LinkedHashSet<>();
|
LinkedHashSet<String> allLogins = new LinkedHashSet<>();
|
||||||
allLogins.add(canonicalLogin);
|
allLogins.add(canonicalLogin);
|
||||||
addAllLogins(allLogins, outFriends, inFriends, outContacts, inContacts, outFollows, inFollows,
|
addAllLogins(allLogins, outFriends, inFriends, outCloseFriends, inCloseFriends, outContacts, inContacts, outFollows, inFollows,
|
||||||
outSpouses, inSpouses, outParents, inParents, outChildren, inChildren, outSiblings, inSiblings,
|
outSpouses, inSpouses, outParents, inParents, outChildren, inChildren, outSiblings, inSiblings,
|
||||||
outKnownPersons, inKnownPersons, outShineConfirmed, inShineConfirmed, outShineSeen, inShineSeen);
|
outKnownPersons, inKnownPersons, outOfficialAccounts, inOfficialAccounts, outShineConfirmed, inShineConfirmed, outShineSeen, inShineSeen);
|
||||||
|
|
||||||
Map<String, UserMeta> metaByLogin = loadUserMeta(c, allLogins);
|
Map<String, UserMeta> metaByLogin = loadUserMeta(c, allLogins);
|
||||||
List<String> spouseLogins = mergeUnique(outSpouses, inSpouses);
|
List<String> spouseLogins = mergeUnique(outSpouses, inSpouses);
|
||||||
@@ -86,6 +91,8 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
|
|||||||
resp.setLogin(canonicalLogin);
|
resp.setLogin(canonicalLogin);
|
||||||
resp.setOutFriends(outFriends);
|
resp.setOutFriends(outFriends);
|
||||||
resp.setInFriends(inFriends);
|
resp.setInFriends(inFriends);
|
||||||
|
resp.setOutCloseFriends(outCloseFriends);
|
||||||
|
resp.setInCloseFriends(inCloseFriends);
|
||||||
resp.setOutContacts(outContacts);
|
resp.setOutContacts(outContacts);
|
||||||
resp.setInContacts(inContacts);
|
resp.setInContacts(inContacts);
|
||||||
resp.setOutFollows(outFollows);
|
resp.setOutFollows(outFollows);
|
||||||
@@ -100,6 +107,8 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
|
|||||||
resp.setInSiblings(inSiblings);
|
resp.setInSiblings(inSiblings);
|
||||||
resp.setOutKnownPersons(outKnownPersons);
|
resp.setOutKnownPersons(outKnownPersons);
|
||||||
resp.setInKnownPersons(inKnownPersons);
|
resp.setInKnownPersons(inKnownPersons);
|
||||||
|
resp.setOutOfficialAccounts(outOfficialAccounts);
|
||||||
|
resp.setInOfficialAccounts(inOfficialAccounts);
|
||||||
resp.setOutShineConfirmed(outShineConfirmed);
|
resp.setOutShineConfirmed(outShineConfirmed);
|
||||||
resp.setInShineConfirmed(inShineConfirmed);
|
resp.setInShineConfirmed(inShineConfirmed);
|
||||||
resp.setOutShineSeen(outShineSeen);
|
resp.setOutShineSeen(outShineSeen);
|
||||||
@@ -108,11 +117,49 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
|
|||||||
resp.setChildren(toRelativeItems(childLogins, metaByLogin));
|
resp.setChildren(toRelativeItems(childLogins, metaByLogin));
|
||||||
resp.setSiblings(toRelativeItems(siblingLogins, metaByLogin));
|
resp.setSiblings(toRelativeItems(siblingLogins, metaByLogin));
|
||||||
resp.setSpouses(toRelativeItems(spouseLogins, metaByLogin));
|
resp.setSpouses(toRelativeItems(spouseLogins, metaByLogin));
|
||||||
resp.setAllUsers(toUserMarkItems(allLogins, metaByLogin));
|
Map<String, String> relationTypes = buildEffectiveRelationTypes(outContacts, outFriends, outCloseFriends);
|
||||||
|
resp.setAllUsers(toUserMarkItems(allLogins, metaByLogin, relationTypes, outOfficialAccounts, outShineConfirmed));
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> listEffectiveConfirmation(Connection c, String login, int relType, boolean outgoing) throws Exception {
|
||||||
|
String sql;
|
||||||
|
if (relType == 80) {
|
||||||
|
sql = outgoing ? """
|
||||||
|
SELECT DISTINCT cs.to_login AS peer FROM connections_state cs
|
||||||
|
WHERE LOWER(cs.login)=LOWER(?) AND cs.rel_type=80
|
||||||
|
AND shine_is_primary(cs.login) AND shine_target_accepts_primary_confirmation(cs.to_login)
|
||||||
|
ORDER BY peer
|
||||||
|
""" : """
|
||||||
|
SELECT DISTINCT cs.login AS peer FROM connections_state cs
|
||||||
|
WHERE LOWER(cs.to_login)=LOWER(?) AND cs.rel_type=80
|
||||||
|
AND shine_is_primary(cs.login) AND shine_target_accepts_primary_confirmation(cs.to_login)
|
||||||
|
ORDER BY peer
|
||||||
|
""";
|
||||||
|
} else {
|
||||||
|
sql = outgoing ? """
|
||||||
|
SELECT DISTINCT cs.to_login AS peer FROM connections_state cs
|
||||||
|
WHERE LOWER(cs.login)=LOWER(?) AND cs.rel_type=70
|
||||||
|
AND shine_is_primary(cs.login) AND shine_is_shining(cs.login)
|
||||||
|
AND shine_target_accepts_shine_confirmation(cs.to_login)
|
||||||
|
ORDER BY peer
|
||||||
|
""" : """
|
||||||
|
SELECT DISTINCT cs.login AS peer FROM connections_state cs
|
||||||
|
WHERE LOWER(cs.to_login)=LOWER(?) AND cs.rel_type=70
|
||||||
|
AND shine_is_primary(cs.login) AND shine_is_shining(cs.login)
|
||||||
|
AND shine_target_accepts_shine_confirmation(cs.to_login)
|
||||||
|
ORDER BY peer
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
List<String> out = new ArrayList<>();
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, login);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) { while (rs.next()) out.add(rs.getString("peer")); }
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
private String findCanonicalLogin(Connection c, String loginAnyCase) throws Exception {
|
private String findCanonicalLogin(Connection c, String loginAnyCase) throws Exception {
|
||||||
String sql = "SELECT login FROM " + CurrentUsersSql.usersSubquery("su")
|
String sql = "SELECT login FROM " + CurrentUsersSql.usersSubquery("su")
|
||||||
+ " WHERE LOWER(login) = LOWER(?) LIMIT 1";
|
+ " WHERE LOWER(login) = LOWER(?) LIMIT 1";
|
||||||
@@ -160,47 +207,54 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
|
|||||||
private Map<String, UserMeta> loadUserMeta(Connection c, Set<String> logins) throws Exception {
|
private Map<String, UserMeta> loadUserMeta(Connection c, Set<String> logins) throws Exception {
|
||||||
Map<String, UserMeta> out = new HashMap<>();
|
Map<String, UserMeta> out = new HashMap<>();
|
||||||
if (logins == null || logins.isEmpty()) return out;
|
if (logins == null || logins.isEmpty()) return out;
|
||||||
|
|
||||||
String[] placeholders = new String[logins.size()];
|
String[] placeholders = new String[logins.size()];
|
||||||
for (int i = 0; i < placeholders.length; i += 1) placeholders[i] = "?";
|
for (int i=0;i<placeholders.length;i++) placeholders[i]="?";
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT
|
SELECT su.login,
|
||||||
su.login AS login,
|
COALESCE(ups.first_name,'') first_name,
|
||||||
MAX(CASE WHEN up.param = 'gender' THEN up.value END) AS gender_value,
|
COALESCE(ups.last_name,'') last_name,
|
||||||
MAX(CASE WHEN up.param = 'official' THEN up.value END) AS official_value,
|
ups.account_role, ups.shine_status, COALESCE(ups.ava_ar,'') avatar_value,
|
||||||
MAX(CASE WHEN up.param = 'shine' THEN up.value END) AS shine_value,
|
COALESCE(ust.primary_confirmations_received_count,0) primary_confirmations_count,
|
||||||
MAX(CASE WHEN up.param = 'ava' THEN up.value END) AS avatar_value
|
COALESCE(ust.shine_confirmations_received_count,0) shine_confirmations_count,
|
||||||
|
MAX(CASE WHEN up.param='gender' THEN up.value END) gender_value
|
||||||
FROM %s
|
FROM %s
|
||||||
LEFT JOIN users_params up
|
LEFT JOIN user_profile_state ups ON LOWER(ups.login)=LOWER(su.login)
|
||||||
ON LOWER(up.login) = LOWER(su.login)
|
LEFT JOIN user_stats_state ust ON LOWER(ust.login)=LOWER(su.login)
|
||||||
AND up.param IN ('gender', 'official', 'shine', 'ava')
|
LEFT JOIN users_params up ON LOWER(up.login)=LOWER(su.login) AND up.param='gender'
|
||||||
WHERE LOWER(su.login) IN (%s)
|
WHERE LOWER(su.login) IN (%s)
|
||||||
GROUP BY su.login
|
GROUP BY su.login, ups.first_name, ups.last_name, ups.account_role, ups.shine_status, ups.ava_ar,
|
||||||
|
ust.primary_confirmations_received_count, ust.shine_confirmations_received_count
|
||||||
ORDER BY su.login
|
ORDER BY su.login
|
||||||
""".formatted(CurrentUsersSql.usersSubquery("su"), String.join(", ", placeholders));
|
""".formatted(CurrentUsersSql.usersSubquery("su"), String.join(", ", placeholders));
|
||||||
|
|
||||||
try(PreparedStatement ps=c.prepareStatement(sql)){
|
try(PreparedStatement ps=c.prepareStatement(sql)){
|
||||||
int i = 1;
|
int i=1; for(String login:logins) ps.setString(i++,normKey(login));
|
||||||
for (String login : logins) {
|
try(ResultSet rs=ps.executeQuery()){while(rs.next()){
|
||||||
ps.setString(i, normKey(login));
|
String login=rs.getString("login"); if(login==null||login.isBlank()) continue;
|
||||||
i += 1;
|
UserMeta meta=new UserMeta(); meta.firstName=rs.getString("first_name"); meta.lastName=rs.getString("last_name");
|
||||||
}
|
meta.accountRole=rs.getString("account_role"); meta.shineStatus=rs.getString("shine_status");
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
meta.gender=normalizeGender(rs.getString("gender_value")); meta.official="primary".equals(meta.accountRole); meta.shine="shining".equals(meta.shineStatus);
|
||||||
while (rs.next()) {
|
meta.primaryConfirmationsCount=rs.getInt("primary_confirmations_count"); meta.shineConfirmationsCount=rs.getInt("shine_confirmations_count");
|
||||||
String login = rs.getString("login");
|
meta.avatarAr=extractArAvatarTxId(rs.getString("avatar_value")); out.put(normKey(login),meta);
|
||||||
if (login == null || login.isBlank()) continue;
|
}}
|
||||||
UserMeta meta = new UserMeta();
|
|
||||||
meta.gender = normalizeGender(rs.getString("gender_value"));
|
|
||||||
meta.official = parseToggle(rs.getString("official_value"));
|
|
||||||
meta.shine = parseToggle(rs.getString("shine_value"));
|
|
||||||
meta.avatarAr = extractArAvatarTxId(rs.getString("avatar_value"));
|
|
||||||
out.put(normKey(login), meta);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Map<String,String> buildEffectiveRelationTypes(List<String> contacts,List<String> friends,List<String> closeFriends){
|
||||||
|
Map<String,String> out=new HashMap<>();
|
||||||
|
if(contacts!=null) for(String v:contacts) out.put(normKey(v),"contact");
|
||||||
|
if(friends!=null) for(String v:friends) out.put(normKey(v),"friend");
|
||||||
|
if(closeFriends!=null) for(String v:closeFriends) out.put(normKey(v),"close_friend");
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean listContainsLogin(List<String> list, String login) {
|
||||||
|
if (list == null || login == null) return false;
|
||||||
|
String key = normKey(login);
|
||||||
|
for (String value : list) if (key.equals(normKey(value))) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean parseToggle(String rawValue) {
|
private boolean parseToggle(String rawValue) {
|
||||||
String v = String.valueOf(rawValue == null ? "" : rawValue).trim().toLowerCase();
|
String v = String.valueOf(rawValue == null ? "" : rawValue).trim().toLowerCase();
|
||||||
return "1".equals(v) || "yes".equals(v) || "true".equals(v) || "on".equals(v);
|
return "1".equals(v) || "yes".equals(v) || "true".equals(v) || "on".equals(v);
|
||||||
@@ -269,7 +323,10 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
private List<Net_GetUserConnectionsGraph_Response.UserMarkItem> toUserMarkItems(
|
private List<Net_GetUserConnectionsGraph_Response.UserMarkItem> toUserMarkItems(
|
||||||
Set<String> logins,
|
Set<String> logins,
|
||||||
Map<String, UserMeta> metaByLogin
|
Map<String, UserMeta> metaByLogin,
|
||||||
|
Map<String, String> relationTypes,
|
||||||
|
List<String> primaryConfirmedLogins,
|
||||||
|
List<String> shineConfirmedLogins
|
||||||
) {
|
) {
|
||||||
List<Net_GetUserConnectionsGraph_Response.UserMarkItem> items = new ArrayList<>();
|
List<Net_GetUserConnectionsGraph_Response.UserMarkItem> items = new ArrayList<>();
|
||||||
if (logins == null) return items;
|
if (logins == null) return items;
|
||||||
@@ -284,6 +341,15 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
Net_GetUserConnectionsGraph_Response.UserMarkItem it = new Net_GetUserConnectionsGraph_Response.UserMarkItem();
|
Net_GetUserConnectionsGraph_Response.UserMarkItem it = new Net_GetUserConnectionsGraph_Response.UserMarkItem();
|
||||||
it.setLogin(clean);
|
it.setLogin(clean);
|
||||||
|
it.setFirstName(meta == null ? "" : meta.firstName);
|
||||||
|
it.setLastName(meta == null ? "" : meta.lastName);
|
||||||
|
it.setAccountRole(meta == null ? null : meta.accountRole);
|
||||||
|
it.setShineStatus(meta == null ? null : meta.shineStatus);
|
||||||
|
it.setRelationType(relationTypes == null ? "none" : relationTypes.getOrDefault(normKey(clean), "none"));
|
||||||
|
it.setPrimaryConfirmed(listContainsLogin(primaryConfirmedLogins, clean));
|
||||||
|
it.setShineConfirmed(listContainsLogin(shineConfirmedLogins, clean));
|
||||||
|
it.setPrimaryConfirmationsCount(meta == null ? 0 : meta.primaryConfirmationsCount);
|
||||||
|
it.setShineConfirmationsCount(meta == null ? 0 : meta.shineConfirmationsCount);
|
||||||
it.setOfficial(official);
|
it.setOfficial(official);
|
||||||
it.setShine(shine);
|
it.setShine(shine);
|
||||||
it.setOfficialLabel(official ? "официальный" : "неофициальный");
|
it.setOfficialLabel(official ? "официальный" : "неофициальный");
|
||||||
@@ -296,6 +362,12 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
private static final class UserMeta {
|
private static final class UserMeta {
|
||||||
private String gender = "unknown";
|
private String gender = "unknown";
|
||||||
|
private String firstName = "";
|
||||||
|
private String lastName = "";
|
||||||
|
private String accountRole = null;
|
||||||
|
private String shineStatus = null;
|
||||||
|
private int primaryConfirmationsCount = 0;
|
||||||
|
private int shineConfirmationsCount = 0;
|
||||||
private boolean official = false;
|
private boolean official = false;
|
||||||
private boolean shine = false;
|
private boolean shine = false;
|
||||||
private String avatarAr = null;
|
private String avatarAr = null;
|
||||||
|
|||||||
+20
-2
@@ -9,6 +9,7 @@ import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_ListConta
|
|||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import shine.db.dao.DmDialogStateDAO;
|
import shine.db.dao.DmDialogStateDAO;
|
||||||
|
import shine.db.dao.UserProfileStateDAO;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -29,18 +30,25 @@ public class Net_ListContacts_Handler implements JsonMessageHandler {
|
|||||||
resp.setRequestId(req.getRequestId());
|
resp.setRequestId(req.getRequestId());
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
resp.setStatus(WireCodes.Status.OK);
|
||||||
resp.setLogin(ctx.getLogin());
|
resp.setLogin(ctx.getLogin());
|
||||||
resp.setDialogs(toDialogItems(dialogs));
|
resp.setDialogs(toDialogItems(c, dialogs));
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Net_ListContacts_Response.DialogItem> toDialogItems(List<DmDialogStateDAO.DialogSummary> dialogs) {
|
private List<Net_ListContacts_Response.DialogItem> toDialogItems(Connection c, List<DmDialogStateDAO.DialogSummary> dialogs) throws Exception {
|
||||||
List<Net_ListContacts_Response.DialogItem> items = new ArrayList<>();
|
List<Net_ListContacts_Response.DialogItem> items = new ArrayList<>();
|
||||||
if (dialogs == null) return items;
|
if (dialogs == null) return items;
|
||||||
for (DmDialogStateDAO.DialogSummary dialog : dialogs) {
|
for (DmDialogStateDAO.DialogSummary dialog : dialogs) {
|
||||||
Net_ListContacts_Response.DialogItem item = new Net_ListContacts_Response.DialogItem();
|
Net_ListContacts_Response.DialogItem item = new Net_ListContacts_Response.DialogItem();
|
||||||
item.setPeerLogin(dialog.peerLogin());
|
item.setPeerLogin(dialog.peerLogin());
|
||||||
item.setRelationFlag(dialog.relationFlag());
|
item.setRelationFlag(dialog.relationFlag());
|
||||||
|
UserProfileStateDAO.ProfileCard card = UserProfileStateDAO.getInstance().get(c, dialog.peerLogin());
|
||||||
|
item.setFirstName(card.firstName());
|
||||||
|
item.setLastName(card.lastName());
|
||||||
|
item.setAvatarAr(card.avatarAr());
|
||||||
|
item.setAvatar(parseAvatar(card.avatarAr()));
|
||||||
|
item.setAccountRole(card.accountRole());
|
||||||
|
item.setShineStatus(card.shineStatus());
|
||||||
item.setLastMessageBlobB64(dialog.lastMessageBlobB64());
|
item.setLastMessageBlobB64(dialog.lastMessageBlobB64());
|
||||||
item.setLastMessageTimeMs(dialog.lastMessageTimeMs());
|
item.setLastMessageTimeMs(dialog.lastMessageTimeMs());
|
||||||
item.setUnreadCount(dialog.unreadCount());
|
item.setUnreadCount(dialog.unreadCount());
|
||||||
@@ -49,4 +57,14 @@ public class Net_ListContacts_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
private static Net_ListContacts_Response.Avatar parseAvatar(String value) {
|
||||||
|
Net_ListContacts_Response.Avatar out = new Net_ListContacts_Response.Avatar();
|
||||||
|
String raw = value == null ? "" : value.trim();
|
||||||
|
java.util.regex.Matcher ar = java.util.regex.Pattern.compile("AR:([A-Za-z0-9_-]{43})").matcher(raw);
|
||||||
|
if (ar.find()) out.setAr(ar.group(1));
|
||||||
|
java.util.regex.Matcher sha = java.util.regex.Pattern.compile("SHA256:([A-Fa-f0-9]{64})").matcher(raw);
|
||||||
|
if (sha.find()) out.setSha256Hex(sha.group(1).toLowerCase());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+39
@@ -9,6 +9,8 @@ public class Net_GetUserConnectionsGraph_Response extends Net_Response {
|
|||||||
private String login;
|
private String login;
|
||||||
private List<String> outFriends = new ArrayList<>();
|
private List<String> outFriends = new ArrayList<>();
|
||||||
private List<String> inFriends = new ArrayList<>();
|
private List<String> inFriends = new ArrayList<>();
|
||||||
|
private List<String> outCloseFriends = new ArrayList<>();
|
||||||
|
private List<String> inCloseFriends = new ArrayList<>();
|
||||||
private List<String> outContacts = new ArrayList<>();
|
private List<String> outContacts = new ArrayList<>();
|
||||||
private List<String> inContacts = new ArrayList<>();
|
private List<String> inContacts = new ArrayList<>();
|
||||||
private List<String> outFollows = new ArrayList<>();
|
private List<String> outFollows = new ArrayList<>();
|
||||||
@@ -23,6 +25,8 @@ public class Net_GetUserConnectionsGraph_Response extends Net_Response {
|
|||||||
private List<String> inSiblings = new ArrayList<>();
|
private List<String> inSiblings = new ArrayList<>();
|
||||||
private List<String> outKnownPersons = new ArrayList<>();
|
private List<String> outKnownPersons = new ArrayList<>();
|
||||||
private List<String> inKnownPersons = new ArrayList<>();
|
private List<String> inKnownPersons = new ArrayList<>();
|
||||||
|
private List<String> outOfficialAccounts = new ArrayList<>();
|
||||||
|
private List<String> inOfficialAccounts = new ArrayList<>();
|
||||||
private List<String> outShineConfirmed = new ArrayList<>();
|
private List<String> outShineConfirmed = new ArrayList<>();
|
||||||
private List<String> inShineConfirmed = new ArrayList<>();
|
private List<String> inShineConfirmed = new ArrayList<>();
|
||||||
private List<String> outShineSeen = new ArrayList<>();
|
private List<String> outShineSeen = new ArrayList<>();
|
||||||
@@ -58,6 +62,15 @@ public class Net_GetUserConnectionsGraph_Response extends Net_Response {
|
|||||||
|
|
||||||
public static class UserMarkItem {
|
public static class UserMarkItem {
|
||||||
private String login;
|
private String login;
|
||||||
|
private String firstName;
|
||||||
|
private String lastName;
|
||||||
|
private String accountRole;
|
||||||
|
private String shineStatus;
|
||||||
|
private String relationType;
|
||||||
|
private boolean primaryConfirmed;
|
||||||
|
private boolean shineConfirmed;
|
||||||
|
private int primaryConfirmationsCount;
|
||||||
|
private int shineConfirmationsCount;
|
||||||
private boolean official;
|
private boolean official;
|
||||||
private boolean shine;
|
private boolean shine;
|
||||||
private String officialLabel;
|
private String officialLabel;
|
||||||
@@ -66,6 +79,24 @@ public class Net_GetUserConnectionsGraph_Response extends Net_Response {
|
|||||||
|
|
||||||
public String getLogin() { return login; }
|
public String getLogin() { return login; }
|
||||||
public void setLogin(String login) { this.login = login; }
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
public String getFirstName() { return firstName; }
|
||||||
|
public void setFirstName(String v) { this.firstName = v; }
|
||||||
|
public String getLastName() { return lastName; }
|
||||||
|
public void setLastName(String v) { this.lastName = v; }
|
||||||
|
public String getAccountRole() { return accountRole; }
|
||||||
|
public void setAccountRole(String v) { this.accountRole = v; }
|
||||||
|
public String getShineStatus() { return shineStatus; }
|
||||||
|
public void setShineStatus(String v) { this.shineStatus = v; }
|
||||||
|
public String getRelationType() { return relationType; }
|
||||||
|
public void setRelationType(String v) { this.relationType = v; }
|
||||||
|
public boolean isPrimaryConfirmed() { return primaryConfirmed; }
|
||||||
|
public void setPrimaryConfirmed(boolean v) { this.primaryConfirmed = v; }
|
||||||
|
public boolean isShineConfirmed() { return shineConfirmed; }
|
||||||
|
public void setShineConfirmed(boolean v) { this.shineConfirmed = v; }
|
||||||
|
public int getPrimaryConfirmationsCount() { return primaryConfirmationsCount; }
|
||||||
|
public void setPrimaryConfirmationsCount(int v) { this.primaryConfirmationsCount = v; }
|
||||||
|
public int getShineConfirmationsCount() { return shineConfirmationsCount; }
|
||||||
|
public void setShineConfirmationsCount(int v) { this.shineConfirmationsCount = v; }
|
||||||
public boolean isOfficial() { return official; }
|
public boolean isOfficial() { return official; }
|
||||||
public void setOfficial(boolean official) { this.official = official; }
|
public void setOfficial(boolean official) { this.official = official; }
|
||||||
public boolean isShine() { return shine; }
|
public boolean isShine() { return shine; }
|
||||||
@@ -84,6 +115,10 @@ public class Net_GetUserConnectionsGraph_Response extends Net_Response {
|
|||||||
public void setOutFriends(List<String> outFriends) { this.outFriends = outFriends; }
|
public void setOutFriends(List<String> outFriends) { this.outFriends = outFriends; }
|
||||||
public List<String> getInFriends() { return inFriends; }
|
public List<String> getInFriends() { return inFriends; }
|
||||||
public void setInFriends(List<String> inFriends) { this.inFriends = inFriends; }
|
public void setInFriends(List<String> inFriends) { this.inFriends = inFriends; }
|
||||||
|
public List<String> getOutCloseFriends() { return outCloseFriends; }
|
||||||
|
public void setOutCloseFriends(List<String> outCloseFriends) { this.outCloseFriends = outCloseFriends; }
|
||||||
|
public List<String> getInCloseFriends() { return inCloseFriends; }
|
||||||
|
public void setInCloseFriends(List<String> inCloseFriends) { this.inCloseFriends = inCloseFriends; }
|
||||||
public List<String> getOutContacts() { return outContacts; }
|
public List<String> getOutContacts() { return outContacts; }
|
||||||
public void setOutContacts(List<String> outContacts) { this.outContacts = outContacts; }
|
public void setOutContacts(List<String> outContacts) { this.outContacts = outContacts; }
|
||||||
public List<String> getInContacts() { return inContacts; }
|
public List<String> getInContacts() { return inContacts; }
|
||||||
@@ -112,6 +147,10 @@ public class Net_GetUserConnectionsGraph_Response extends Net_Response {
|
|||||||
public void setOutKnownPersons(List<String> outKnownPersons) { this.outKnownPersons = outKnownPersons; }
|
public void setOutKnownPersons(List<String> outKnownPersons) { this.outKnownPersons = outKnownPersons; }
|
||||||
public List<String> getInKnownPersons() { return inKnownPersons; }
|
public List<String> getInKnownPersons() { return inKnownPersons; }
|
||||||
public void setInKnownPersons(List<String> inKnownPersons) { this.inKnownPersons = inKnownPersons; }
|
public void setInKnownPersons(List<String> inKnownPersons) { this.inKnownPersons = inKnownPersons; }
|
||||||
|
public List<String> getOutOfficialAccounts() { return outOfficialAccounts; }
|
||||||
|
public void setOutOfficialAccounts(List<String> outOfficialAccounts) { this.outOfficialAccounts = outOfficialAccounts; }
|
||||||
|
public List<String> getInOfficialAccounts() { return inOfficialAccounts; }
|
||||||
|
public void setInOfficialAccounts(List<String> inOfficialAccounts) { this.inOfficialAccounts = inOfficialAccounts; }
|
||||||
public List<String> getOutShineConfirmed() { return outShineConfirmed; }
|
public List<String> getOutShineConfirmed() { return outShineConfirmed; }
|
||||||
public void setOutShineConfirmed(List<String> outShineConfirmed) { this.outShineConfirmed = outShineConfirmed; }
|
public void setOutShineConfirmed(List<String> outShineConfirmed) { this.outShineConfirmed = outShineConfirmed; }
|
||||||
public List<String> getInShineConfirmed() { return inShineConfirmed; }
|
public List<String> getInShineConfirmed() { return inShineConfirmed; }
|
||||||
|
|||||||
+27
@@ -17,6 +17,12 @@ public class Net_ListContacts_Response extends Net_Response {
|
|||||||
public static class DialogItem {
|
public static class DialogItem {
|
||||||
private String peerLogin;
|
private String peerLogin;
|
||||||
private String relationFlag;
|
private String relationFlag;
|
||||||
|
private String firstName;
|
||||||
|
private String lastName;
|
||||||
|
private String avatarAr;
|
||||||
|
private Avatar avatar;
|
||||||
|
private String accountRole;
|
||||||
|
private String shineStatus;
|
||||||
private String lastMessageBlobB64;
|
private String lastMessageBlobB64;
|
||||||
private long lastMessageTimeMs;
|
private long lastMessageTimeMs;
|
||||||
private int unreadCount;
|
private int unreadCount;
|
||||||
@@ -26,6 +32,18 @@ public class Net_ListContacts_Response extends Net_Response {
|
|||||||
public void setPeerLogin(String peerLogin) { this.peerLogin = peerLogin; }
|
public void setPeerLogin(String peerLogin) { this.peerLogin = peerLogin; }
|
||||||
public String getRelationFlag() { return relationFlag; }
|
public String getRelationFlag() { return relationFlag; }
|
||||||
public void setRelationFlag(String relationFlag) { this.relationFlag = relationFlag; }
|
public void setRelationFlag(String relationFlag) { this.relationFlag = relationFlag; }
|
||||||
|
public String getFirstName() { return firstName; }
|
||||||
|
public void setFirstName(String firstName) { this.firstName = firstName; }
|
||||||
|
public String getLastName() { return lastName; }
|
||||||
|
public void setLastName(String lastName) { this.lastName = lastName; }
|
||||||
|
public String getAvatarAr() { return avatarAr; }
|
||||||
|
public void setAvatarAr(String avatarAr) { this.avatarAr = avatarAr; }
|
||||||
|
public Avatar getAvatar() { return avatar; }
|
||||||
|
public void setAvatar(Avatar avatar) { this.avatar = avatar; }
|
||||||
|
public String getAccountRole() { return accountRole; }
|
||||||
|
public void setAccountRole(String accountRole) { this.accountRole = accountRole; }
|
||||||
|
public String getShineStatus() { return shineStatus; }
|
||||||
|
public void setShineStatus(String shineStatus) { this.shineStatus = shineStatus; }
|
||||||
public String getLastMessageBlobB64() { return lastMessageBlobB64; }
|
public String getLastMessageBlobB64() { return lastMessageBlobB64; }
|
||||||
public void setLastMessageBlobB64(String lastMessageBlobB64) { this.lastMessageBlobB64 = lastMessageBlobB64; }
|
public void setLastMessageBlobB64(String lastMessageBlobB64) { this.lastMessageBlobB64 = lastMessageBlobB64; }
|
||||||
public long getLastMessageTimeMs() { return lastMessageTimeMs; }
|
public long getLastMessageTimeMs() { return lastMessageTimeMs; }
|
||||||
@@ -35,4 +53,13 @@ public class Net_ListContacts_Response extends Net_Response {
|
|||||||
public boolean isHasDialog() { return hasDialog; }
|
public boolean isHasDialog() { return hasDialog; }
|
||||||
public void setHasDialog(boolean hasDialog) { this.hasDialog = hasDialog; }
|
public void setHasDialog(boolean hasDialog) { this.hasDialog = hasDialog; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class Avatar {
|
||||||
|
private String ar;
|
||||||
|
private String sha256Hex;
|
||||||
|
public String getAr() { return ar; }
|
||||||
|
public void setAr(String ar) { this.ar = ar; }
|
||||||
|
public String getSha256Hex() { return sha256Hex; }
|
||||||
|
public void setSha256Hex(String sha256Hex) { this.sha256Hex = sha256Hex; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-86
@@ -1,94 +1,27 @@
|
|||||||
package server.logic.ws_protocol.JSON.handlers.notifications;
|
package server.logic.ws_protocol.JSON.handlers.notifications;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger; import org.slf4j.LoggerFactory;
|
||||||
import org.slf4j.LoggerFactory;
|
import server.logic.ws_protocol.JSON.ConnectionContext; import server.logic.ws_protocol.JSON.entyties.*; import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
import server.logic.ws_protocol.JSON.handlers.notifications.entyties.*; import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.WireCodes;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
import shine.db.DbController; import shine.db.dao.*; import shine.db.entities.UserNotificationEntry;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
import java.sql.Connection; import java.util.*;
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.notifications.entyties.Net_GetNotifications_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.notifications.entyties.Net_GetNotifications_Response;
|
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
|
||||||
import server.logic.ws_protocol.WireCodes;
|
|
||||||
import shine.db.DbController;
|
|
||||||
import shine.db.dao.UserNotificationsStateDAO;
|
|
||||||
import shine.db.entities.UserNotificationEntry;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public final class Net_GetNotifications_Handler implements JsonMessageHandler {
|
public final class Net_GetNotifications_Handler implements JsonMessageHandler {
|
||||||
private static final Logger log = LoggerFactory.getLogger(Net_GetNotifications_Handler.class);
|
private static final Logger log=LoggerFactory.getLogger(Net_GetNotifications_Handler.class); private static final long HISTORY_MS=60L*24*60*60*1000;
|
||||||
|
public Net_Response handle(Net_Request base, ConnectionContext ctx){
|
||||||
@Override
|
Net_GetNotifications_Request req=(Net_GetNotifications_Request)base; if(ctx==null||!ctx.isAuthenticatedUser()||ctx.getCurrentUser()==null) return NetExceptionResponseFactory.error(req,WireCodes.Status.UNVERIFIED,"NOT_AUTHENTICATED","Операция доступна только для авторизованных пользователей");
|
||||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
String login=String.valueOf(ctx.getCurrentUser().getLogin()).trim();
|
||||||
Net_GetNotifications_Request req = (Net_GetNotifications_Request) baseRequest;
|
|
||||||
if (ctx == null || !ctx.isAuthenticatedUser() || ctx.getCurrentUser() == null) {
|
|
||||||
return NetExceptionResponseFactory.error(
|
|
||||||
req,
|
|
||||||
WireCodes.Status.UNVERIFIED,
|
|
||||||
"NOT_AUTHENTICATED",
|
|
||||||
"Операция доступна только для авторизованных пользователей"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String login = String.valueOf(ctx.getCurrentUser().getLogin() == null ? "" : ctx.getCurrentUser().getLogin()).trim();
|
|
||||||
if (login.isBlank()) {
|
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "NOT_AUTHENTICATED", "Не удалось определить авторизованного пользователя");
|
|
||||||
}
|
|
||||||
|
|
||||||
int limit = req.getLimit() == null ? 50 : Math.max(1, Math.min(200, req.getLimit()));
|
|
||||||
|
|
||||||
try(Connection c=DbController.getInstance().getConnection()){
|
try(Connection c=DbController.getInstance().getConnection()){
|
||||||
List<UserNotificationEntry> replyRows = UserNotificationsStateDAO.getInstance().listByOwnerAndKind(c, login, "reply", limit);
|
UserNotificationSeenStateDAO sd=UserNotificationSeenStateDAO.getInstance(); UserNotificationsStateDAO nd=UserNotificationsStateDAO.getInstance(); long cutoff=System.currentTimeMillis()-HISTORY_MS;
|
||||||
List<UserNotificationEntry> eventRows = UserNotificationsStateDAO.getInstance().listByOwnerAndKind(c, login, "connection", limit);
|
long rs=sd.getSeenAt(c,login,"replies"), cs=sd.getSeenAt(c,login,"connections"), es=sd.getSeenAt(c,login,"events");
|
||||||
|
Net_GetNotifications_Response r=new Net_GetNotifications_Response(); r.setOp(req.getOp());r.setRequestId(req.getRequestId());r.setStatus(WireCodes.Status.OK);r.setLogin(login);
|
||||||
Net_GetNotifications_Response resp = new Net_GetNotifications_Response();
|
if (!Boolean.TRUE.equals(req.getCountsOnly())) {
|
||||||
resp.setOp(req.getOp());
|
r.setReplies(map(nd.listVisible(c,login,"reply",rs,cutoff))); r.setConnections(map(nd.listVisible(c,login,"connection",cs,cutoff))); r.setEvents(map(nd.listVisible(c,login,"event",es,cutoff)));
|
||||||
resp.setRequestId(req.getRequestId());
|
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
|
||||||
resp.setLogin(login);
|
|
||||||
resp.setReplies(mapRows(replyRows));
|
|
||||||
resp.setEvents(mapRows(eventRows));
|
|
||||||
return resp;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("GetNotifications failed", e);
|
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "internal_error", "Внутренняя ошибка сервера");
|
|
||||||
}
|
}
|
||||||
|
r.setRepliesSeenAtMs(rs);r.setConnectionsSeenAtMs(cs);r.setEventsSeenAtMs(es); r.setRepliesUnseenCount(nd.countUnseen(c,login,"reply",rs)); r.setConnectionsUnseenCount(nd.countUnseen(c,login,"connection",cs)); r.setEventsUnseenCount(nd.countUnseen(c,login,"event",es));
|
||||||
|
return r;
|
||||||
|
}catch(Exception e){log.error("GetNotifications failed",e);return NetExceptionResponseFactory.error(req,WireCodes.Status.INTERNAL_ERROR,"internal_error","Внутренняя ошибка сервера");}
|
||||||
}
|
}
|
||||||
|
private List<Net_GetNotifications_Response.NotificationItem> map(List<UserNotificationEntry> rows){ List<Net_GetNotifications_Response.NotificationItem> out=new ArrayList<>(); for(UserNotificationEntry x:rows){ Net_GetNotifications_Response.NotificationItem i=new Net_GetNotifications_Response.NotificationItem(); i.setKind(x.getNotificationKind());i.setCreatedAtMs(x.getCreatedAtMs());i.setSourceLogin(x.getSourceLogin());i.setSourceBlockchainName(x.getSourceBchName());i.setSourceBlockNumber(x.getSourceBlockNumber());i.setSourceBlockHash(hex(x.getSourceBlockHash()));i.setSourceMsgSubType(x.getSourceMsgSubType());i.setConnectionTypeCode("connection".equals(x.getNotificationKind())?x.getSourceMsgSubType():null);i.setSourceText(x.getSourceText());i.setTargetLogin(x.getTargetLogin());i.setTargetBlockchainName(x.getTargetBchName());i.setTargetBlockNumber(x.getTargetBlockNumber());i.setTargetBlockHash(hex(x.getTargetBlockHash()));out.add(i);} return out; }
|
||||||
private List<Net_GetNotifications_Response.NotificationItem> mapRows(List<UserNotificationEntry> rows) {
|
private static String hex(byte[] b){if(b==null)return null;StringBuilder s=new StringBuilder();for(byte x:b)s.append(String.format("%02x",x));return s.toString();}
|
||||||
List<Net_GetNotifications_Response.NotificationItem> out = new ArrayList<>();
|
|
||||||
for (UserNotificationEntry row : rows) {
|
|
||||||
Net_GetNotifications_Response.NotificationItem item = new Net_GetNotifications_Response.NotificationItem();
|
|
||||||
item.setKind(row.getNotificationKind());
|
|
||||||
item.setCreatedAtMs(row.getCreatedAtMs());
|
|
||||||
item.setSourceLogin(row.getSourceLogin());
|
|
||||||
item.setSourceBlockchainName(row.getSourceBchName());
|
|
||||||
item.setSourceBlockNumber(row.getSourceBlockNumber());
|
|
||||||
item.setSourceBlockHash(bytesToHex(row.getSourceBlockHash()));
|
|
||||||
item.setSourceMsgSubType(row.getSourceMsgSubType());
|
|
||||||
item.setConnectionTypeCode("connection".equals(row.getNotificationKind()) ? row.getSourceMsgSubType() : null);
|
|
||||||
item.setSourceText(row.getSourceText());
|
|
||||||
item.setTargetLogin(row.getTargetLogin());
|
|
||||||
item.setTargetBlockchainName(row.getTargetBchName());
|
|
||||||
item.setTargetBlockNumber(row.getTargetBlockNumber());
|
|
||||||
item.setTargetBlockHash(bytesToHex(row.getTargetBlockHash()));
|
|
||||||
out.add(item);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String bytesToHex(byte[] bytes) {
|
|
||||||
if (bytes == null) return null;
|
|
||||||
char[] HEX = "0123456789abcdef".toCharArray();
|
|
||||||
char[] out = new char[bytes.length * 2];
|
|
||||||
for (int i = 0; i < bytes.length; i++) {
|
|
||||||
int v = bytes[i] & 0xff;
|
|
||||||
out[i * 2] = HEX[v >>> 4];
|
|
||||||
out[i * 2 + 1] = HEX[v & 0x0f];
|
|
||||||
}
|
|
||||||
return new String(out);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.notifications;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.notifications.entyties.*;
|
||||||
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
|
import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.DbController;
|
||||||
|
import shine.db.dao.UserNotificationSeenStateDAO;
|
||||||
|
import utils.crypto.Ed25519Util;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
public final class Net_SetNotificationState_Handler implements JsonMessageHandler {
|
||||||
|
public Net_Response handle(Net_Request base, ConnectionContext ctx){
|
||||||
|
Net_SetNotificationState_Request req=(Net_SetNotificationState_Request)base;
|
||||||
|
if(ctx==null||!ctx.isAuthenticatedUser()||ctx.getCurrentUser()==null) return NetExceptionResponseFactory.error(req,WireCodes.Status.UNVERIFIED,"NOT_AUTHENTICATED","Требуется авторизация");
|
||||||
|
try{
|
||||||
|
byte[] raw=Base64.getDecoder().decode(String.valueOf(req.getBlobB64()).trim()); NotificationStatePacket p=NotificationStatePacket.parse(raw);
|
||||||
|
String login=String.valueOf(ctx.getCurrentUser().getLogin()).trim(); if(!login.equalsIgnoreCase(p.login)) return NetExceptionResponseFactory.error(req,WireCodes.Status.UNVERIFIED,"LOGIN_MISMATCH","Подпись принадлежит другому пользователю");
|
||||||
|
byte[] pub=Ed25519Util.keyFromBase64(ctx.getCurrentUser().getClientKey()); if(!Ed25519Util.verify(p.signedBody,p.signature64,pub)) return NetExceptionResponseFactory.error(req,WireCodes.Status.UNVERIFIED,"BAD_SIGNATURE","Некорректная подпись clientKey");
|
||||||
|
long now=System.currentTimeMillis(); if(p.timeMs>now+5*60_000L) return NetExceptionResponseFactory.error(req,WireCodes.Status.UNVERIFIED,"BAD_TIME","Некорректное время подписи");
|
||||||
|
long actual; try(Connection c=DbController.getInstance().getConnection()){ actual=UserNotificationSeenStateDAO.getInstance().advance(c,login,p.categoryName(),p.seenAtMs,p.timeMs,raw); }
|
||||||
|
Net_SetNotificationState_Response r=new Net_SetNotificationState_Response(); r.setOp(req.getOp()); r.setRequestId(req.getRequestId()); r.setStatus(WireCodes.Status.OK); r.setCategory(p.categoryName()); r.setSeenAtMs(actual); return r;
|
||||||
|
}catch(Exception e){ return NetExceptionResponseFactory.error(req,WireCodes.Status.UNVERIFIED,"BAD_NOTIFICATION_STATE",e.getMessage()==null?"Некорректное состояние уведомлений":e.getMessage()); }
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.notifications;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
final class NotificationStatePacket {
|
||||||
|
static final byte[] PREFIX = "SHiNE_NTF".getBytes(StandardCharsets.US_ASCII);
|
||||||
|
static final int STATE_SEEN_WATERMARK = 1;
|
||||||
|
static final int CATEGORY_REPLIES = 1;
|
||||||
|
static final int CATEGORY_CONNECTIONS = 2;
|
||||||
|
static final int CATEGORY_EVENTS = 3;
|
||||||
|
|
||||||
|
final String login; final long timeMs; final long nonce; final int stateType; final int category; final long seenAtMs;
|
||||||
|
final byte[] signedBody; final byte[] signature64; final byte[] rawPacket;
|
||||||
|
private NotificationStatePacket(String login,long timeMs,long nonce,int stateType,int category,long seenAtMs,byte[] signedBody,byte[] signature64,byte[] rawPacket){
|
||||||
|
this.login=login;this.timeMs=timeMs;this.nonce=nonce;this.stateType=stateType;this.category=category;this.seenAtMs=seenAtMs;this.signedBody=signedBody;this.signature64=signature64;this.rawPacket=rawPacket;
|
||||||
|
}
|
||||||
|
static NotificationStatePacket parse(byte[] raw) {
|
||||||
|
if(raw==null||raw.length<PREFIX.length+2+1+1+8+4+1+1+8+64) throw new IllegalArgumentException("BAD_LEN");
|
||||||
|
for(int i=0;i<PREFIX.length;i++) if(raw[i]!=PREFIX[i]) throw new IllegalArgumentException("BAD_PREFIX");
|
||||||
|
ByteBuffer bb=ByteBuffer.wrap(raw).order(ByteOrder.BIG_ENDIAN); bb.position(PREFIX.length);
|
||||||
|
int major=Byte.toUnsignedInt(bb.get()), minor=Byte.toUnsignedInt(bb.get());
|
||||||
|
if(major!=1||minor!=0) throw new IllegalArgumentException("BAD_FORMAT_VERSION");
|
||||||
|
int len=Byte.toUnsignedInt(bb.get()); if(len<1||len>60||bb.remaining()<len+8+4+1+1+8+64) throw new IllegalArgumentException("BAD_LOGIN");
|
||||||
|
byte[] lb=new byte[len]; bb.get(lb); for(byte b:lb) if(b<0x20||b>0x7e) throw new IllegalArgumentException("BAD_LOGIN");
|
||||||
|
String login=new String(lb,StandardCharsets.US_ASCII); long timeMs=bb.getLong(); if(timeMs<0) throw new IllegalArgumentException("BAD_TIME");
|
||||||
|
long nonce=Integer.toUnsignedLong(bb.getInt()); int stateType=Byte.toUnsignedInt(bb.get()); if(stateType!=STATE_SEEN_WATERMARK) throw new IllegalArgumentException("BAD_STATE_TYPE");
|
||||||
|
int category=Byte.toUnsignedInt(bb.get()); if(category<1||category>3) throw new IllegalArgumentException("BAD_CATEGORY");
|
||||||
|
long seenAtMs=bb.getLong(); if(seenAtMs<0||bb.remaining()!=64) throw new IllegalArgumentException("BAD_SEEN_TIME");
|
||||||
|
byte[] sig=new byte[64]; bb.get(sig); return new NotificationStatePacket(login,timeMs,nonce,stateType,category,seenAtMs,Arrays.copyOf(raw,raw.length-64),sig,raw);
|
||||||
|
}
|
||||||
|
String categoryName(){ return category==CATEGORY_REPLIES?"replies":category==CATEGORY_CONNECTIONS?"connections":"events"; }
|
||||||
|
}
|
||||||
+4
-2
@@ -3,8 +3,10 @@ package server.logic.ws_protocol.JSON.handlers.notifications.entyties;
|
|||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
|
||||||
public class Net_GetNotifications_Request extends Net_Request {
|
public class Net_GetNotifications_Request extends Net_Request {
|
||||||
private Integer limit;
|
private Integer limit; // legacy: поле принимается для совместимости, но в v2 не ограничивает выдачу
|
||||||
|
private Boolean countsOnly;
|
||||||
public Integer getLimit() { return limit; }
|
public Integer getLimit() { return limit; }
|
||||||
public void setLimit(Integer limit) { this.limit = limit; }
|
public void setLimit(Integer limit) { this.limit = limit; }
|
||||||
|
public Boolean getCountsOnly() { return countsOnly; }
|
||||||
|
public void setCountsOnly(Boolean countsOnly) { this.countsOnly = countsOnly; }
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -8,12 +8,23 @@ import java.util.List;
|
|||||||
public class Net_GetNotifications_Response extends Net_Response {
|
public class Net_GetNotifications_Response extends Net_Response {
|
||||||
private String login;
|
private String login;
|
||||||
private List<NotificationItem> replies = new ArrayList<>();
|
private List<NotificationItem> replies = new ArrayList<>();
|
||||||
|
private List<NotificationItem> connections = new ArrayList<>();
|
||||||
private List<NotificationItem> events = new ArrayList<>();
|
private List<NotificationItem> events = new ArrayList<>();
|
||||||
|
private long repliesSeenAtMs, connectionsSeenAtMs, eventsSeenAtMs;
|
||||||
|
private long repliesUnseenCount, connectionsUnseenCount, eventsUnseenCount;
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
public String getLogin() { return login; }
|
||||||
public void setLogin(String login) { this.login = login; }
|
public void setLogin(String login) { this.login = login; }
|
||||||
public List<NotificationItem> getReplies() { return replies; }
|
public List<NotificationItem> getReplies() { return replies; }
|
||||||
public void setReplies(List<NotificationItem> replies) { this.replies = replies; }
|
public void setReplies(List<NotificationItem> replies) { this.replies = replies; }
|
||||||
|
public List<NotificationItem> getConnections() { return connections; }
|
||||||
|
public void setConnections(List<NotificationItem> v) { connections = v; }
|
||||||
|
public long getRepliesSeenAtMs(){return repliesSeenAtMs;} public void setRepliesSeenAtMs(long v){repliesSeenAtMs=v;}
|
||||||
|
public long getConnectionsSeenAtMs(){return connectionsSeenAtMs;} public void setConnectionsSeenAtMs(long v){connectionsSeenAtMs=v;}
|
||||||
|
public long getEventsSeenAtMs(){return eventsSeenAtMs;} public void setEventsSeenAtMs(long v){eventsSeenAtMs=v;}
|
||||||
|
public long getRepliesUnseenCount(){return repliesUnseenCount;} public void setRepliesUnseenCount(long v){repliesUnseenCount=v;}
|
||||||
|
public long getConnectionsUnseenCount(){return connectionsUnseenCount;} public void setConnectionsUnseenCount(long v){connectionsUnseenCount=v;}
|
||||||
|
public long getEventsUnseenCount(){return eventsUnseenCount;} public void setEventsUnseenCount(long v){eventsUnseenCount=v;}
|
||||||
public List<NotificationItem> getEvents() { return events; }
|
public List<NotificationItem> getEvents() { return events; }
|
||||||
public void setEvents(List<NotificationItem> events) { this.events = events; }
|
public void setEvents(List<NotificationItem> events) { this.events = events; }
|
||||||
|
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.notifications.entyties;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
public class Net_SetNotificationState_Request extends Net_Request { private String blobB64; public String getBlobB64(){return blobB64;} public void setBlobB64(String v){blobB64=v;} }
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.notifications.entyties;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
public class Net_SetNotificationState_Response extends Net_Response { private String category; private long seenAtMs; public String getCategory(){return category;} public void setCategory(String v){category=v;} public long getSeenAtMs(){return seenAtMs;} public void setSeenAtMs(long v){seenAtMs=v;} }
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.profile;
|
||||||
|
import server.logic.ws_protocol.JSON.*;import server.logic.ws_protocol.JSON.entyties.*;import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;import server.logic.ws_protocol.JSON.handlers.profile.entyties.*;import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;import server.logic.ws_protocol.WireCodes;import shine.db.dao.UserProfileStateDAO;import java.sql.*;import java.util.*;
|
||||||
|
public class Net_ListUserProfileChannels_Handler implements JsonMessageHandler {public Net_Response handle(Net_Request base,ConnectionContext ctx)throws Exception{var req=(Net_ListUserProfileChannels_Request)base;if(req.getLogin()==null||req.getLogin().isBlank())return NetExceptionResponseFactory.error(req,400,"BAD_FIELDS","login required");try(Connection c=shine.db.DbController.getInstance().getConnection()){var rows=UserProfileStateDAO.getInstance().listPublicChannels(c,req.getLogin(),req.getMode(),req.getLimit()==null?100:req.getLimit(),req.getOffset()==null?0:req.getOffset());var resp=new Net_ListUserProfileChannels_Response();resp.setOp(req.getOp());resp.setRequestId(req.getRequestId());resp.setStatus(WireCodes.Status.OK);resp.setLogin(req.getLogin());resp.setMode(req.getMode());List<Net_ListUserProfileChannels_Response.ChannelItem> items=new ArrayList<>();for(var r:rows){var i=new Net_ListUserProfileChannels_Response.ChannelItem();i.setOwnerLogin(r.ownerLogin());i.setSlug(r.slug());i.setDisplayName(r.displayName());i.setAvatarAr(r.avatarAr());i.setOwnerBlockchainName(r.ownerBlockchainName());i.setRootBlockNumber(r.rootBlockNumber());i.setRootBlockHashHex(r.rootBlockHashHex());items.add(i);}resp.setChannels(items);return resp;}}}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.profile;
|
||||||
|
import server.logic.ws_protocol.JSON.*; import server.logic.ws_protocol.JSON.entyties.*; import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.profile.entyties.*; import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; import server.logic.ws_protocol.WireCodes;
|
||||||
|
import shine.db.dao.UserProfileStateDAO; import java.sql.*; import java.util.*;
|
||||||
|
public class Net_ListUserProfileRelations_Handler implements JsonMessageHandler {
|
||||||
|
public Net_Response handle(Net_Request base, ConnectionContext ctx)throws Exception{
|
||||||
|
var req=(Net_ListUserProfileRelations_Request)base; if(req.getLogin()==null||req.getLogin().isBlank()) return NetExceptionResponseFactory.error(req,400,"BAD_FIELDS","login required");
|
||||||
|
try(Connection c=shine.db.DbController.getInstance().getConnection()){
|
||||||
|
var rows=UserProfileStateDAO.getInstance().listRelations(c,req.getLogin(),req.getListType(),req.getLimit()==null?100:req.getLimit(),req.getOffset()==null?0:req.getOffset());
|
||||||
|
var resp=new Net_ListUserProfileRelations_Response(); resp.setOp(req.getOp());resp.setRequestId(req.getRequestId());resp.setStatus(WireCodes.Status.OK);resp.setLogin(req.getLogin());resp.setListType(req.getListType());
|
||||||
|
List<Net_ListUserProfileRelations_Response.UserItem> items=new ArrayList<>(); for(var r:rows){var i=new Net_ListUserProfileRelations_Response.UserItem();i.setLogin(r.login());i.setFirstName(r.firstName());i.setLastName(r.lastName());i.setAvatarAr(r.avatarAr());i.setAccountRole(r.accountRole());i.setShineStatus(r.shineStatus());i.setRelationType(r.relationType());i.setPrimaryConfirmed(r.primaryConfirmed());i.setShineConfirmed(r.shineConfirmed());i.setPrimaryConfirmationsCount(r.primaryConfirmationsCount());i.setShineConfirmationsCount(r.shineConfirmationsCount());items.add(i);} resp.setUsers(items);return resp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.profile.entyties;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
public class Net_ListUserProfileChannels_Request extends Net_Request {private String login,mode;private Integer limit,offset;public String getLogin(){return login;}public void setLogin(String v){login=v;}public String getMode(){return mode;}public void setMode(String v){mode=v;}public Integer getLimit(){return limit;}public void setLimit(Integer v){limit=v;}public Integer getOffset(){return offset;}public void setOffset(Integer v){offset=v;}}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.profile.entyties;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;import java.util.*;
|
||||||
|
public class Net_ListUserProfileChannels_Response extends Net_Response {private String login,mode;private List<ChannelItem> channels=new ArrayList<>();public String getLogin(){return login;}public void setLogin(String v){login=v;}public String getMode(){return mode;}public void setMode(String v){mode=v;}public List<ChannelItem> getChannels(){return channels;}public void setChannels(List<ChannelItem> v){channels=v;}public static class ChannelItem{private String ownerLogin,slug,displayName,avatarAr,ownerBlockchainName,rootBlockHashHex;private int rootBlockNumber;public String getOwnerLogin(){return ownerLogin;}public void setOwnerLogin(String v){ownerLogin=v;}public String getSlug(){return slug;}public void setSlug(String v){slug=v;}public String getDisplayName(){return displayName;}public void setDisplayName(String v){displayName=v;}public String getAvatarAr(){return avatarAr;}public void setAvatarAr(String v){avatarAr=v;}public String getOwnerBlockchainName(){return ownerBlockchainName;}public void setOwnerBlockchainName(String v){ownerBlockchainName=v;}public int getRootBlockNumber(){return rootBlockNumber;}public void setRootBlockNumber(int v){rootBlockNumber=v;}public String getRootBlockHashHex(){return rootBlockHashHex;}public void setRootBlockHashHex(String v){rootBlockHashHex=v;}}}
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.profile.entyties;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
|
public class Net_ListUserProfileRelations_Request extends Net_Request {
|
||||||
|
private String login; private String listType; private Integer limit; private Integer offset;
|
||||||
|
public String getLogin(){return login;} public void setLogin(String v){login=v;}
|
||||||
|
public String getListType(){return listType;} public void setListType(String v){listType=v;}
|
||||||
|
public Integer getLimit(){return limit;} public void setLimit(Integer v){limit=v;}
|
||||||
|
public Integer getOffset(){return offset;} public void setOffset(Integer v){offset=v;}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package server.logic.ws_protocol.JSON.handlers.profile.entyties;
|
||||||
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
import java.util.*;
|
||||||
|
public class Net_ListUserProfileRelations_Response extends Net_Response {
|
||||||
|
private String login; private String listType; private List<UserItem> users=new ArrayList<>();
|
||||||
|
public String getLogin(){return login;} public void setLogin(String v){login=v;}
|
||||||
|
public String getListType(){return listType;} public void setListType(String v){listType=v;}
|
||||||
|
public List<UserItem> getUsers(){return users;} public void setUsers(List<UserItem> v){users=v;}
|
||||||
|
public static class UserItem {
|
||||||
|
private String login,firstName,lastName,avatarAr,accountRole,shineStatus,relationType;
|
||||||
|
private boolean primaryConfirmed,shineConfirmed; private int primaryConfirmationsCount,shineConfirmationsCount;
|
||||||
|
public String getLogin(){return login;} public void setLogin(String v){login=v;}
|
||||||
|
public String getFirstName(){return firstName;} public void setFirstName(String v){firstName=v;}
|
||||||
|
public String getLastName(){return lastName;} public void setLastName(String v){lastName=v;}
|
||||||
|
public String getAvatarAr(){return avatarAr;} public void setAvatarAr(String v){avatarAr=v;}
|
||||||
|
public String getAccountRole(){return accountRole;} public void setAccountRole(String v){accountRole=v;}
|
||||||
|
public String getShineStatus(){return shineStatus;} public void setShineStatus(String v){shineStatus=v;}
|
||||||
|
public String getRelationType(){return relationType;} public void setRelationType(String v){relationType=v;}
|
||||||
|
public boolean isPrimaryConfirmed(){return primaryConfirmed;} public void setPrimaryConfirmed(boolean v){primaryConfirmed=v;}
|
||||||
|
public boolean isShineConfirmed(){return shineConfirmed;} public void setShineConfirmed(boolean v){shineConfirmed=v;}
|
||||||
|
public int getPrimaryConfirmationsCount(){return primaryConfirmationsCount;} public void setPrimaryConfirmationsCount(int v){primaryConfirmationsCount=v;}
|
||||||
|
public int getShineConfirmationsCount(){return shineConfirmationsCount;} public void setShineConfirmationsCount(int v){shineConfirmationsCount=v;}
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
-1
@@ -136,11 +136,21 @@ public class Net_GetUser_Handler implements JsonMessageHandler {
|
|||||||
resp.setFollowingUsersCount(0);
|
resp.setFollowingUsersCount(0);
|
||||||
resp.setFollowingChannelsCount(0);
|
resp.setFollowingChannelsCount(0);
|
||||||
resp.setCloseFriendsCount(0);
|
resp.setCloseFriendsCount(0);
|
||||||
|
resp.setFriendsCount(0);
|
||||||
|
resp.setPrimaryConfirmationsReceivedCount(0);
|
||||||
|
resp.setPrimaryConfirmationsGivenCount(0);
|
||||||
|
resp.setShineConfirmationsReceivedCount(0);
|
||||||
|
resp.setShineConfirmationsGivenCount(0);
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT owned_public_channels_count,
|
SELECT owned_public_channels_count,
|
||||||
following_users_count,
|
following_users_count,
|
||||||
following_channels_count,
|
following_channels_count,
|
||||||
close_friends_count
|
close_friends_count,
|
||||||
|
friends_count,
|
||||||
|
primary_confirmations_received_count,
|
||||||
|
primary_confirmations_given_count,
|
||||||
|
shine_confirmations_received_count,
|
||||||
|
shine_confirmations_given_count
|
||||||
FROM user_stats_state
|
FROM user_stats_state
|
||||||
WHERE login = ?
|
WHERE login = ?
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
@@ -156,6 +166,11 @@ public class Net_GetUser_Handler implements JsonMessageHandler {
|
|||||||
resp.setFollowingUsersCount(rs.getInt("following_users_count"));
|
resp.setFollowingUsersCount(rs.getInt("following_users_count"));
|
||||||
resp.setFollowingChannelsCount(rs.getInt("following_channels_count"));
|
resp.setFollowingChannelsCount(rs.getInt("following_channels_count"));
|
||||||
resp.setCloseFriendsCount(rs.getInt("close_friends_count"));
|
resp.setCloseFriendsCount(rs.getInt("close_friends_count"));
|
||||||
|
resp.setFriendsCount(rs.getInt("friends_count"));
|
||||||
|
resp.setPrimaryConfirmationsReceivedCount(rs.getInt("primary_confirmations_received_count"));
|
||||||
|
resp.setPrimaryConfirmationsGivenCount(rs.getInt("primary_confirmations_given_count"));
|
||||||
|
resp.setShineConfirmationsReceivedCount(rs.getInt("shine_confirmations_received_count"));
|
||||||
|
resp.setShineConfirmationsGivenCount(rs.getInt("shine_confirmations_given_count"));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("GetUser: не удалось загрузить статистику для login={}", login, e);
|
log.warn("GetUser: не удалось загрузить статистику для login={}", login, e);
|
||||||
|
|||||||
+15
@@ -47,6 +47,11 @@ public class Net_GetUser_Response extends Net_Response {
|
|||||||
private Integer followingUsersCount;
|
private Integer followingUsersCount;
|
||||||
private Integer followingChannelsCount;
|
private Integer followingChannelsCount;
|
||||||
private Integer closeFriendsCount;
|
private Integer closeFriendsCount;
|
||||||
|
private Integer friendsCount;
|
||||||
|
private Integer primaryConfirmationsReceivedCount;
|
||||||
|
private Integer primaryConfirmationsGivenCount;
|
||||||
|
private Integer shineConfirmationsReceivedCount;
|
||||||
|
private Integer shineConfirmationsGivenCount;
|
||||||
|
|
||||||
public Boolean getExists() { return exists; }
|
public Boolean getExists() { return exists; }
|
||||||
public void setExists(Boolean exists) { this.exists = exists; }
|
public void setExists(Boolean exists) { this.exists = exists; }
|
||||||
@@ -89,5 +94,15 @@ public class Net_GetUser_Response extends Net_Response {
|
|||||||
|
|
||||||
public Integer getCloseFriendsCount() { return closeFriendsCount; }
|
public Integer getCloseFriendsCount() { return closeFriendsCount; }
|
||||||
public void setCloseFriendsCount(Integer closeFriendsCount) { this.closeFriendsCount = closeFriendsCount; }
|
public void setCloseFriendsCount(Integer closeFriendsCount) { this.closeFriendsCount = closeFriendsCount; }
|
||||||
|
public Integer getFriendsCount() { return friendsCount; }
|
||||||
|
public void setFriendsCount(Integer friendsCount) { this.friendsCount = friendsCount; }
|
||||||
|
public Integer getPrimaryConfirmationsReceivedCount() { return primaryConfirmationsReceivedCount; }
|
||||||
|
public void setPrimaryConfirmationsReceivedCount(Integer v) { this.primaryConfirmationsReceivedCount = v; }
|
||||||
|
public Integer getPrimaryConfirmationsGivenCount() { return primaryConfirmationsGivenCount; }
|
||||||
|
public void setPrimaryConfirmationsGivenCount(Integer v) { this.primaryConfirmationsGivenCount = v; }
|
||||||
|
public Integer getShineConfirmationsReceivedCount() { return shineConfirmationsReceivedCount; }
|
||||||
|
public void setShineConfirmationsReceivedCount(Integer v) { this.shineConfirmationsReceivedCount = v; }
|
||||||
|
public Integer getShineConfirmationsGivenCount() { return shineConfirmationsGivenCount; }
|
||||||
|
public void setShineConfirmationsGivenCount(Integer v) { this.shineConfirmationsGivenCount = v; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
-1
@@ -54,7 +54,6 @@ public class Net_GetUserSetting_Handler implements JsonMessageHandler {
|
|||||||
resp.setValue_num(entry.getValueNum());
|
resp.setValue_num(entry.getValueNum());
|
||||||
resp.setClient_key(entry.getClientKey());
|
resp.setClient_key(entry.getClientKey());
|
||||||
resp.setSignature(entry.getSignature());
|
resp.setSignature(entry.getSignature());
|
||||||
resp.setSynced(entry.isSynced());
|
|
||||||
return resp;
|
return resp;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("GetUserSetting failed", e);
|
log.error("GetUserSetting failed", e);
|
||||||
|
|||||||
-1
@@ -49,7 +49,6 @@ public class Net_ListUserSettings_Handler implements JsonMessageHandler {
|
|||||||
item.setValue_num(e.getValueNum());
|
item.setValue_num(e.getValueNum());
|
||||||
item.setClient_key(e.getClientKey());
|
item.setClient_key(e.getClientKey());
|
||||||
item.setSignature(e.getSignature());
|
item.setSignature(e.getSignature());
|
||||||
item.setSynced(e.isSynced());
|
|
||||||
items.add(item);
|
items.add(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-60
@@ -11,27 +11,21 @@ import server.logic.ws_protocol.JSON.handlers.userSettings.entyties.Net_UpsertUs
|
|||||||
import server.logic.ws_protocol.JSON.handlers.userSettings.entyties.Net_UpsertUserSetting_Response;
|
import server.logic.ws_protocol.JSON.handlers.userSettings.entyties.Net_UpsertUserSetting_Response;
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import server.sync.RemoteUserSettingsSyncClient;
|
|
||||||
import shine.db.DbController;
|
import shine.db.DbController;
|
||||||
import shine.db.dao.CurrentUsersDAO;
|
import shine.db.dao.CurrentUsersDAO;
|
||||||
import shine.db.dao.UserAccessServersCurrentDAO;
|
|
||||||
import shine.db.dao.UserSettingsDAO;
|
import shine.db.dao.UserSettingsDAO;
|
||||||
import shine.db.entities.CurrentUserEntry;
|
import shine.db.entities.CurrentUserEntry;
|
||||||
import shine.db.entities.UserAccessServerRouteEntry;
|
|
||||||
import shine.db.entities.UserSettingEntry;
|
import shine.db.entities.UserSettingEntry;
|
||||||
import utils.config.ShineSignatureConstants;
|
import utils.config.ShineSignatureConstants;
|
||||||
import utils.config.AppConfig;
|
|
||||||
import utils.crypto.Ed25519Util;
|
import utils.crypto.Ed25519Util;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
|
/** Приём и строгая проверка локальной пользовательской настройки. */
|
||||||
public class Net_UpsertUserSetting_Handler implements JsonMessageHandler {
|
public class Net_UpsertUserSetting_Handler implements JsonMessageHandler {
|
||||||
private static final Logger log = LoggerFactory.getLogger(Net_UpsertUserSetting_Handler.class);
|
private static final Logger log = LoggerFactory.getLogger(Net_UpsertUserSetting_Handler.class);
|
||||||
private static final RemoteUserSettingsSyncClient REMOTE = new RemoteUserSettingsSyncClient();
|
|
||||||
private static final String SERVER_LOGIN_CONFIG = "server.SHiNE.login";
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
||||||
@@ -54,7 +48,6 @@ public class Net_UpsertUserSetting_Handler implements JsonMessageHandler {
|
|||||||
long valueNum = req.getValue_num() == null ? 0L : req.getValue_num();
|
long valueNum = req.getValue_num() == null ? 0L : req.getValue_num();
|
||||||
String clientKeyB64 = req.getClient_key().trim();
|
String clientKeyB64 = req.getClient_key().trim();
|
||||||
String signatureB64 = req.getSignature().trim();
|
String signatureB64 = req.getSignature().trim();
|
||||||
boolean syncDelivery = Boolean.TRUE.equals(req.getSync_delivery());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
byte[] pubKey32;
|
byte[] pubKey32;
|
||||||
@@ -63,7 +56,8 @@ public class Net_UpsertUserSetting_Handler implements JsonMessageHandler {
|
|||||||
pubKey32 = Base64Ws.decodeLen(clientKeyB64, 32, "client_key");
|
pubKey32 = Base64Ws.decodeLen(clientKeyB64, 32, "client_key");
|
||||||
sig64 = Base64Ws.decodeLen(signatureB64, 64, "signature");
|
sig64 = Base64Ws.decodeLen(signatureB64, 64, "signature");
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_BASE64", "client_key/signature должны быть Base64");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST,
|
||||||
|
"BAD_BASE64", "client_key/signature должны быть Base64");
|
||||||
}
|
}
|
||||||
|
|
||||||
String signText = ShineSignatureConstants.USER_SETTINGS_PREFIX
|
String signText = ShineSignatureConstants.USER_SETTINGS_PREFIX
|
||||||
@@ -74,65 +68,29 @@ public class Net_UpsertUserSetting_Handler implements JsonMessageHandler {
|
|||||||
+ escapePart(valueText) + '|'
|
+ escapePart(valueText) + '|'
|
||||||
+ valueNum;
|
+ valueNum;
|
||||||
|
|
||||||
DbController db = DbController.getInstance();
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
CurrentUsersDAO usersDAO = CurrentUsersDAO.getInstance();
|
CurrentUserEntry user = CurrentUsersDAO.getInstance().getByLogin(c, login);
|
||||||
UserSettingsDAO settingsDAO = UserSettingsDAO.getInstance();
|
|
||||||
|
|
||||||
try (Connection c = db.getConnection()) {
|
|
||||||
CurrentUserEntry user = usersDAO.getByLogin(c, login);
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
|
||||||
}
|
}
|
||||||
String userClientKey = user.getClientKey();
|
String userClientKey = user.getClientKey();
|
||||||
if (userClientKey == null || userClientKey.isBlank()) {
|
if (userClientKey == null || userClientKey.isBlank()) {
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.SERVER_DATA_ERROR, "USER_DEVICE_KEY_EMPTY", "У пользователя не задан clientKey в БД");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.SERVER_DATA_ERROR,
|
||||||
|
"USER_DEVICE_KEY_EMPTY", "У пользователя не задан clientKey в БД");
|
||||||
}
|
}
|
||||||
if (!userClientKey.trim().equals(clientKeyB64)) {
|
if (!userClientKey.trim().equals(clientKeyB64)) {
|
||||||
return NetExceptionResponseFactory.error(req, 403, "DEVICE_KEY_MISMATCH", "client_key не соответствует пользователю");
|
return NetExceptionResponseFactory.error(req, 403,
|
||||||
|
"DEVICE_KEY_MISMATCH", "client_key не соответствует пользователю");
|
||||||
}
|
}
|
||||||
|
if (!Ed25519Util.verify(signText.getBytes(StandardCharsets.UTF_8), sig64, pubKey32)) {
|
||||||
boolean signatureOk = Ed25519Util.verify(signText.getBytes(StandardCharsets.UTF_8), sig64, pubKey32);
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED,
|
||||||
if (!signatureOk) {
|
"INVALID_SIGNATURE", "Подпись настройки не прошла проверку");
|
||||||
// В логах t2/legacy уже виден системный разброс подписей для user_settings.
|
|
||||||
// Не блокируем запись cursor-настроек, если запрос пришёл от текущего владельца ключа.
|
|
||||||
log.warn("user_settings signature verification failed, accepting fallback: login={} settingType={} settingKey={}",
|
|
||||||
login, settingType, settingKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UserSettingEntry entry = new UserSettingEntry(
|
UserSettingEntry entry = new UserSettingEntry(
|
||||||
login,
|
login, settingType, settingKey, timeMs,
|
||||||
settingType,
|
valueText, valueNum, clientKeyB64, signatureB64);
|
||||||
settingKey,
|
UserSettingsDAO.getInstance().upsertIfNewer(c, entry);
|
||||||
timeMs,
|
|
||||||
valueText,
|
|
||||||
valueNum,
|
|
||||||
clientKeyB64,
|
|
||||||
signatureB64,
|
|
||||||
syncDelivery
|
|
||||||
);
|
|
||||||
int changed = settingsDAO.upsertIfNewer(c, entry);
|
|
||||||
|
|
||||||
if (!syncDelivery && changed > 0) {
|
|
||||||
int delivered = 0;
|
|
||||||
String ownServerLogin = String.valueOf(AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG) == null ? "" : AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG)).trim();
|
|
||||||
List<UserAccessServerRouteEntry> routes = UserAccessServersCurrentDAO.getInstance().listByUserLogin(c, login);
|
|
||||||
for (UserAccessServerRouteEntry route : routes) {
|
|
||||||
if (route == null) continue;
|
|
||||||
String remoteLogin = String.valueOf(route.getServerLogin() == null ? "" : route.getServerLogin()).trim();
|
|
||||||
String remoteUrl = String.valueOf(route.getServerUrl() == null ? "" : route.getServerUrl()).trim();
|
|
||||||
if (remoteLogin.isBlank() || remoteUrl.isBlank()) continue;
|
|
||||||
if (!ownServerLogin.isBlank() && remoteLogin.equalsIgnoreCase(ownServerLogin)) continue;
|
|
||||||
try {
|
|
||||||
REMOTE.upsertUserSetting(remoteLogin, remoteUrl, entry, true);
|
|
||||||
delivered++;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("user_settings immediate sync failed: login={} remoteServer={} reason={}", login, remoteLogin, String.valueOf(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (delivered > 0 || routes.isEmpty()) {
|
|
||||||
settingsDAO.markSynced(c, login, settingType, settingKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Net_UpsertUserSetting_Response resp = new Net_UpsertUserSetting_Response();
|
Net_UpsertUserSetting_Response resp = new Net_UpsertUserSetting_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
@@ -142,15 +100,18 @@ public class Net_UpsertUserSetting_Handler implements JsonMessageHandler {
|
|||||||
resp.setSetting_type(settingType);
|
resp.setSetting_type(settingType);
|
||||||
resp.setSetting_key(settingKey);
|
resp.setSetting_key(settingKey);
|
||||||
resp.setTime_ms(timeMs);
|
resp.setTime_ms(timeMs);
|
||||||
resp.setSynced(syncDelivery || changed == 0);
|
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
log.error("UpsertUserSetting DB error", e);
|
log.error("UpsertUserSetting DB error", e);
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.SERVER_DATA_ERROR, "DB_ERROR", "Ошибка БД");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.SERVER_DATA_ERROR,
|
||||||
|
"DB_ERROR", "Ошибка БД");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("UpsertUserSetting failed", e);
|
log.error("UpsertUserSetting failed", e);
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "INTERNAL_ERROR", NetExceptionResponseFactory.detailedMessage("Внутренняя ошибка сервера при UpsertUserSetting", e));
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR,
|
||||||
|
"INTERNAL_ERROR",
|
||||||
|
NetExceptionResponseFactory.detailedMessage(
|
||||||
|
"Внутренняя ошибка сервера при UpsertUserSetting", e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-3
@@ -11,7 +11,6 @@ public class Net_GetUserSetting_Response extends Net_Response {
|
|||||||
private Long value_num;
|
private Long value_num;
|
||||||
private String client_key;
|
private String client_key;
|
||||||
private String signature;
|
private String signature;
|
||||||
private Boolean synced;
|
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
public String getLogin() { return login; }
|
||||||
public void setLogin(String login) { this.login = login; }
|
public void setLogin(String login) { this.login = login; }
|
||||||
@@ -37,6 +36,4 @@ public class Net_GetUserSetting_Response extends Net_Response {
|
|||||||
public String getSignature() { return signature; }
|
public String getSignature() { return signature; }
|
||||||
public void setSignature(String signature) { this.signature = signature; }
|
public void setSignature(String signature) { this.signature = signature; }
|
||||||
|
|
||||||
public Boolean getSynced() { return synced; }
|
|
||||||
public void setSynced(Boolean synced) { this.synced = synced; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
-3
@@ -24,7 +24,6 @@ public class Net_ListUserSettings_Response extends Net_Response {
|
|||||||
private Long value_num;
|
private Long value_num;
|
||||||
private String client_key;
|
private String client_key;
|
||||||
private String signature;
|
private String signature;
|
||||||
private Boolean synced;
|
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
public String getLogin() { return login; }
|
||||||
public void setLogin(String login) { this.login = login; }
|
public void setLogin(String login) { this.login = login; }
|
||||||
@@ -42,7 +41,5 @@ public class Net_ListUserSettings_Response extends Net_Response {
|
|||||||
public void setClient_key(String client_key) { this.client_key = client_key; }
|
public void setClient_key(String client_key) { this.client_key = client_key; }
|
||||||
public String getSignature() { return signature; }
|
public String getSignature() { return signature; }
|
||||||
public void setSignature(String signature) { this.signature = signature; }
|
public void setSignature(String signature) { this.signature = signature; }
|
||||||
public Boolean getSynced() { return synced; }
|
|
||||||
public void setSynced(Boolean synced) { this.synced = synced; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-3
@@ -11,7 +11,6 @@ public class Net_UpsertUserSetting_Request extends Net_Request {
|
|||||||
private Long value_num;
|
private Long value_num;
|
||||||
private String client_key;
|
private String client_key;
|
||||||
private String signature;
|
private String signature;
|
||||||
private Boolean sync_delivery;
|
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
public String getLogin() { return login; }
|
||||||
public void setLogin(String login) { this.login = login; }
|
public void setLogin(String login) { this.login = login; }
|
||||||
@@ -37,6 +36,4 @@ public class Net_UpsertUserSetting_Request extends Net_Request {
|
|||||||
public String getSignature() { return signature; }
|
public String getSignature() { return signature; }
|
||||||
public void setSignature(String signature) { this.signature = signature; }
|
public void setSignature(String signature) { this.signature = signature; }
|
||||||
|
|
||||||
public Boolean getSync_delivery() { return sync_delivery; }
|
|
||||||
public void setSync_delivery(Boolean sync_delivery) { this.sync_delivery = sync_delivery; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
-3
@@ -7,7 +7,6 @@ public class Net_UpsertUserSetting_Response extends Net_Response {
|
|||||||
private Integer setting_type;
|
private Integer setting_type;
|
||||||
private String setting_key;
|
private String setting_key;
|
||||||
private Long time_ms;
|
private Long time_ms;
|
||||||
private Boolean synced;
|
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
public String getLogin() { return login; }
|
||||||
public void setLogin(String login) { this.login = login; }
|
public void setLogin(String login) { this.login = login; }
|
||||||
@@ -21,6 +20,4 @@ public class Net_UpsertUserSetting_Response extends Net_Response {
|
|||||||
public Long getTime_ms() { return time_ms; }
|
public Long getTime_ms() { return time_ms; }
|
||||||
public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
|
public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
|
||||||
|
|
||||||
public Boolean getSynced() { return synced; }
|
|
||||||
public void setSynced(Boolean synced) { this.synced = synced; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
-108
@@ -1,108 +0,0 @@
|
|||||||
package server.logic.ws_protocol.JSON.messages;
|
|
||||||
|
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
|
||||||
import shine.db.dao.DmSyncOutboxDAO;
|
|
||||||
import shine.db.dao.DmDeliveryStateDAO;
|
|
||||||
import shine.db.entities.DmDeliveryStateEntry;
|
|
||||||
import shine.db.entities.SignedMessageEntry;
|
|
||||||
import server.sync.DmDeliveryCoordinator;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public final class DmSyncApplySupport {
|
|
||||||
private DmSyncApplySupport() {}
|
|
||||||
|
|
||||||
public static ApplyResult applySyncedBlob(String ownerLogin, String blobB64) throws Exception {
|
|
||||||
if (ownerLogin == null || ownerLogin.isBlank()) {
|
|
||||||
throw new IllegalArgumentException("EMPTY_OWNER_LOGIN");
|
|
||||||
}
|
|
||||||
if (blobB64 == null || blobB64.isBlank()) {
|
|
||||||
throw new IllegalArgumentException("EMPTY_BLOB");
|
|
||||||
}
|
|
||||||
|
|
||||||
SignedMessageBlock block = SignedMessagesCore.parseFromB64(blobB64);
|
|
||||||
SignedMessagesCore.verifyUsersAndSignature(block);
|
|
||||||
SignedMessageEntry entry = SignedMessagesCore.toEntry(block, "DmSyncBatch", null);
|
|
||||||
String owner = ownerLogin.trim();
|
|
||||||
boolean ownerMatchesTarget = entry.getTargetLogin().equalsIgnoreCase(owner);
|
|
||||||
boolean ownerMatchesDeletePair = block.isDeleteType()
|
|
||||||
&& (entry.getFromLogin().equalsIgnoreCase(owner) || entry.getToLogin().equalsIgnoreCase(owner));
|
|
||||||
if (!ownerMatchesTarget && !ownerMatchesDeletePair) {
|
|
||||||
throw new IllegalArgumentException("TARGET_LOGIN_MISMATCH");
|
|
||||||
}
|
|
||||||
|
|
||||||
SignedMessagesDAO.ApplyStatus status;
|
|
||||||
if (block.isContentType()) {
|
|
||||||
status = SignedMessagesDAO.getInstance().upsertIncomingCopy(entry);
|
|
||||||
} else if (block.isReadReceiptType()) {
|
|
||||||
status = SignedMessagesDAO.getInstance().insertIfAbsent(entry);
|
|
||||||
} else if (block.isMessageDeleteType()) {
|
|
||||||
status = SignedMessagesDAO.getInstance().applyDeleteMessage(entry);
|
|
||||||
} else if (block.isConversationDeleteType()) {
|
|
||||||
status = SignedMessagesDAO.getInstance().applyDeleteConversation(entry);
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("BAD_MESSAGE_TYPE");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ApplyResult(entry.getMessageKey(), entry.getBaseKey(), entry.getMessageType(), status);
|
|
||||||
}
|
|
||||||
|
|
||||||
public record ApplyResult(
|
|
||||||
String messageKey,
|
|
||||||
String baseKey,
|
|
||||||
int messageType,
|
|
||||||
SignedMessagesDAO.ApplyStatus status
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public static void applySyncedItem(
|
|
||||||
String ownerLogin, String eventId, List<String> blobsB64
|
|
||||||
) throws Exception {
|
|
||||||
if (blobsB64 == null || blobsB64.isEmpty() || blobsB64.size() > 2) {
|
|
||||||
throw new IllegalArgumentException("BAD_BLOB_COUNT");
|
|
||||||
}
|
|
||||||
if (blobsB64.size() == 1) {
|
|
||||||
ApplyResult result = applySyncedBlob(ownerLogin, blobsB64.get(0));
|
|
||||||
SignedMessageEntry stored = SignedMessagesDAO.getInstance().getByMessageKey(result.messageKey());
|
|
||||||
long createdAt = stored == null ? System.currentTimeMillis() : stored.getCreatedAtMs();
|
|
||||||
DmSyncOutboxDAO.getInstance().upsert(
|
|
||||||
ownerLogin, result.messageKey(), eventId, null, true, createdAt);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SignedMessageBlock incoming = SignedMessagesCore.parseFromB64(blobsB64.get(0));
|
|
||||||
SignedMessageBlock outgoing = SignedMessagesCore.parseFromB64(blobsB64.get(1));
|
|
||||||
SignedMessagesCore.validatePair(incoming, outgoing);
|
|
||||||
SignedMessagesCore.verifyUsersAndSignature(incoming);
|
|
||||||
SignedMessagesCore.verifyUsersAndSignature(outgoing);
|
|
||||||
if (!outgoing.fromLogin.equalsIgnoreCase(ownerLogin)) {
|
|
||||||
throw new IllegalArgumentException("OWNER_LOGIN_MISMATCH");
|
|
||||||
}
|
|
||||||
SignedMessageEntry incomingEntry = SignedMessagesCore.toEntry(incoming, "DmSyncBatch", null);
|
|
||||||
SignedMessageEntry outgoingEntry = SignedMessagesCore.toEntry(outgoing, "DmSyncBatch", null);
|
|
||||||
if (incoming.isContentType()) {
|
|
||||||
SignedMessagesDAO.getInstance().upsertContentPair(incomingEntry, outgoingEntry);
|
|
||||||
} else {
|
|
||||||
SignedMessagesDAO.getInstance().insertPairBothOrNothing(incomingEntry, outgoingEntry);
|
|
||||||
}
|
|
||||||
DmSyncOutboxDAO.getInstance().upsert(
|
|
||||||
ownerLogin, outgoingEntry.getMessageKey(), eventId,
|
|
||||||
incomingEntry.getMessageKey(), true, outgoingEntry.getCreatedAtMs());
|
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
|
||||||
long signedAt = Math.max(outgoing.timeMs,
|
|
||||||
Math.max(outgoing.revisionTimeMs, outgoing.reencryptedAtMs));
|
|
||||||
long acceptedAt = signedAt > 0L ? Math.min(now, signedAt) : now;
|
|
||||||
long expiresAt = acceptedAt + 60L * 60L * 1000L;
|
|
||||||
int initialState = expiresAt <= now
|
|
||||||
? DmDeliveryStateEntry.FAILED_FINAL
|
|
||||||
: DmDeliveryStateEntry.PENDING_NONE;
|
|
||||||
DmDeliveryStateEntry delivery = DmDeliveryStateDAO.getInstance().upsertPair(
|
|
||||||
outgoingEntry.getMessageKey(), eventId, outgoingEntry.getBaseKey(),
|
|
||||||
outgoingEntry.getFromLogin(), outgoingEntry.getToLogin(), incomingEntry.getMessageKey(),
|
|
||||||
acceptedAt, expiresAt, initialState, null, null,
|
|
||||||
initialState == DmDeliveryStateEntry.PENDING_NONE);
|
|
||||||
if (delivery != null && initialState == DmDeliveryStateEntry.PENDING_NONE) {
|
|
||||||
DmDeliveryCoordinator.assistReceivedPairAsync(eventId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-12
@@ -11,7 +11,6 @@ import server.logic.ws_protocol.WireCodes;
|
|||||||
import server.sync.DmFederationService;
|
import server.sync.DmFederationService;
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
import shine.db.dao.DmDeliveryStateDAO;
|
import shine.db.dao.DmDeliveryStateDAO;
|
||||||
import shine.db.dao.DmSyncOutboxDAO;
|
|
||||||
import shine.db.entities.SignedMessageEntry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
public class Net_DeleteConversation_Handler implements JsonMessageHandler {
|
public class Net_DeleteConversation_Handler implements JsonMessageHandler {
|
||||||
@@ -46,7 +45,6 @@ public class Net_DeleteConversation_Handler implements JsonMessageHandler {
|
|||||||
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
||||||
if (status.applied()) {
|
if (status.applied()) {
|
||||||
DmDeliveryStateDAO.getInstance().removeMissingMessages();
|
DmDeliveryStateDAO.getInstance().removeMissingMessages();
|
||||||
recordOutboxForLocalOwners(entry);
|
|
||||||
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, block);
|
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, block);
|
||||||
}
|
}
|
||||||
if (status.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
if (status.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
||||||
@@ -68,14 +66,4 @@ public class Net_DeleteConversation_Handler implements JsonMessageHandler {
|
|||||||
return s == null || s.isBlank();
|
return s == null || s.isBlank();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void recordOutboxForLocalOwners(SignedMessageEntry entry) throws Exception {
|
|
||||||
String eventId = DmDeliveryIds.forEntry(entry);
|
|
||||||
if (server.sync.DmDeliveryCoordinator.isLocalAccessServer(entry.getFromLogin())) {
|
|
||||||
DmSyncOutboxDAO.getInstance().upsert(entry.getFromLogin(), entry.getMessageKey(), eventId, null, false, entry.getCreatedAtMs());
|
|
||||||
}
|
|
||||||
if (!entry.getToLogin().equalsIgnoreCase(entry.getFromLogin())
|
|
||||||
&& server.sync.DmDeliveryCoordinator.isLocalAccessServer(entry.getToLogin())) {
|
|
||||||
DmSyncOutboxDAO.getInstance().upsert(entry.getToLogin(), entry.getMessageKey(), eventId, null, false, entry.getCreatedAtMs());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
-12
@@ -11,7 +11,6 @@ import server.logic.ws_protocol.WireCodes;
|
|||||||
import server.sync.DmFederationService;
|
import server.sync.DmFederationService;
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
import shine.db.dao.DmDeliveryStateDAO;
|
import shine.db.dao.DmDeliveryStateDAO;
|
||||||
import shine.db.dao.DmSyncOutboxDAO;
|
|
||||||
import shine.db.entities.SignedMessageEntry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
public class Net_DeleteMessage_Handler implements JsonMessageHandler {
|
public class Net_DeleteMessage_Handler implements JsonMessageHandler {
|
||||||
@@ -46,7 +45,6 @@ public class Net_DeleteMessage_Handler implements JsonMessageHandler {
|
|||||||
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
||||||
if (status.applied()) {
|
if (status.applied()) {
|
||||||
DmDeliveryStateDAO.getInstance().removeByBaseKey(entry.getBaseKey());
|
DmDeliveryStateDAO.getInstance().removeByBaseKey(entry.getBaseKey());
|
||||||
recordOutboxForLocalOwners(entry);
|
|
||||||
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, block);
|
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, block);
|
||||||
}
|
}
|
||||||
if (status.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
if (status.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
||||||
@@ -68,14 +66,4 @@ public class Net_DeleteMessage_Handler implements JsonMessageHandler {
|
|||||||
return s == null || s.isBlank();
|
return s == null || s.isBlank();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void recordOutboxForLocalOwners(SignedMessageEntry entry) throws Exception {
|
|
||||||
String eventId = DmDeliveryIds.forEntry(entry);
|
|
||||||
if (server.sync.DmDeliveryCoordinator.isLocalAccessServer(entry.getFromLogin())) {
|
|
||||||
DmSyncOutboxDAO.getInstance().upsert(entry.getFromLogin(), entry.getMessageKey(), eventId, null, false, entry.getCreatedAtMs());
|
|
||||||
}
|
|
||||||
if (!entry.getToLogin().equalsIgnoreCase(entry.getFromLogin())
|
|
||||||
&& server.sync.DmDeliveryCoordinator.isLocalAccessServer(entry.getToLogin())) {
|
|
||||||
DmSyncOutboxDAO.getInstance().upsert(entry.getToLogin(), entry.getMessageKey(), eventId, null, false, entry.getCreatedAtMs());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+36
@@ -11,6 +11,10 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDirectMessages_Res
|
|||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
|
import shine.db.dao.UserProfileStateDAO;
|
||||||
|
import shine.db.DbController;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
import shine.db.dao.DmDeliveryStateDAO;
|
import shine.db.dao.DmDeliveryStateDAO;
|
||||||
import shine.db.entities.DmDeliveryStateEntry;
|
import shine.db.entities.DmDeliveryStateEntry;
|
||||||
import shine.db.entities.SignedMessageEntry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
@@ -74,6 +78,21 @@ public class Net_GetDirectMessages_Handler implements JsonMessageHandler {
|
|||||||
resp.setLimit(limit);
|
resp.setLimit(limit);
|
||||||
resp.setHasMore(hasMore);
|
resp.setHasMore(hasMore);
|
||||||
|
|
||||||
|
// Return a normalized peer card together with the dialog. This makes the
|
||||||
|
// chat header self-contained even when /chat/<login> is opened directly.
|
||||||
|
try (Connection c = DbController.getInstance().getConnection()) {
|
||||||
|
UserProfileStateDAO.ProfileCard card = UserProfileStateDAO.getInstance().get(c, peerLogin);
|
||||||
|
Net_GetDirectMessages_Response.PeerCard peer = new Net_GetDirectMessages_Response.PeerCard();
|
||||||
|
peer.setLogin(peerLogin);
|
||||||
|
peer.setFirstName(card.firstName());
|
||||||
|
peer.setLastName(card.lastName());
|
||||||
|
peer.setRelationType(UserProfileStateDAO.getInstance().getEffectiveRelationType(c, login, peerLogin));
|
||||||
|
peer.setAccountRole(card.accountRole());
|
||||||
|
peer.setShineStatus(card.shineStatus());
|
||||||
|
peer.setAvatar(parseAvatar(card.avatarAr()));
|
||||||
|
resp.setPeer(peer);
|
||||||
|
}
|
||||||
|
|
||||||
List<Net_GetDirectMessages_Response.MessageItem> items = new ArrayList<>();
|
List<Net_GetDirectMessages_Response.MessageItem> items = new ArrayList<>();
|
||||||
for (SignedMessageEntry entry : page) {
|
for (SignedMessageEntry entry : page) {
|
||||||
Net_GetDirectMessages_Response.MessageItem item = new Net_GetDirectMessages_Response.MessageItem();
|
Net_GetDirectMessages_Response.MessageItem item = new Net_GetDirectMessages_Response.MessageItem();
|
||||||
@@ -108,4 +127,21 @@ public class Net_GetDirectMessages_Handler implements JsonMessageHandler {
|
|||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "INTERNAL_ERROR", "Внутренняя ошибка сервера");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "INTERNAL_ERROR", "Внутренняя ошибка сервера");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private static Net_GetDirectMessages_Response.Avatar parseAvatar(String value) {
|
||||||
|
Net_GetDirectMessages_Response.Avatar out = new Net_GetDirectMessages_Response.Avatar();
|
||||||
|
String raw = value == null ? "" : value.trim();
|
||||||
|
|
||||||
|
// Do not call Matcher.find() twice on the same matcher: the first successful
|
||||||
|
// call advances it and the second one can make a perfectly valid avatar vanish.
|
||||||
|
java.util.regex.Matcher ar = java.util.regex.Pattern.compile("AR:([A-Za-z0-9_-]{43})").matcher(raw);
|
||||||
|
if (ar.find()) {
|
||||||
|
out.setAr(ar.group(1));
|
||||||
|
}
|
||||||
|
java.util.regex.Matcher sha = java.util.regex.Pattern.compile("SHA256:([A-Fa-f0-9]{64})").matcher(raw);
|
||||||
|
if (sha.find()) {
|
||||||
|
out.setSha256Hex(sha.group(1).toLowerCase());
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
-49
@@ -1,49 +0,0 @@
|
|||||||
package server.logic.ws_protocol.JSON.messages;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_MarkAllUserSettingsUnsynced_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_MarkAllUserSettingsUnsynced_Response;
|
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
|
||||||
import server.logic.ws_protocol.WireCodes;
|
|
||||||
import shine.db.DbController;
|
|
||||||
import shine.db.dao.UserSettingsDAO;
|
|
||||||
import shine.db.dao.DmSyncOutboxDAO;
|
|
||||||
import server.sync.DmSyncWakeSignal;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
|
|
||||||
public class Net_MarkAllUserSettingsUnsynced_Handler implements JsonMessageHandler {
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(Net_MarkAllUserSettingsUnsynced_Handler.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
|
|
||||||
Net_MarkAllUserSettingsUnsynced_Request req = (Net_MarkAllUserSettingsUnsynced_Request) baseRequest;
|
|
||||||
try (Connection c = DbController.getInstance().getConnection()) {
|
|
||||||
int updated;
|
|
||||||
int dmUpdated;
|
|
||||||
if (req.getLogin() == null || req.getLogin().isBlank()) {
|
|
||||||
updated = UserSettingsDAO.getInstance().markAllUnsynced(c);
|
|
||||||
dmUpdated = DmSyncOutboxDAO.getInstance().markAllUnsynced();
|
|
||||||
} else {
|
|
||||||
updated = UserSettingsDAO.getInstance().markAllUnsynced(c, req.getLogin().trim());
|
|
||||||
dmUpdated = DmSyncOutboxDAO.getInstance().markAllUnsynced(req.getLogin().trim());
|
|
||||||
}
|
|
||||||
DmSyncWakeSignal.request();
|
|
||||||
Net_MarkAllUserSettingsUnsynced_Response resp = new Net_MarkAllUserSettingsUnsynced_Response();
|
|
||||||
resp.setOp(req.getOp());
|
|
||||||
resp.setRequestId(req.getRequestId());
|
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
|
||||||
resp.setUpdated(updated);
|
|
||||||
resp.setDmUpdated(dmUpdated);
|
|
||||||
return resp;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("MarkAllUserSettingsUnsynced failed", e);
|
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "INTERNAL_ERROR", "Внутренняя ошибка сервера");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-12
@@ -9,7 +9,6 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_ReceiveIncomingMessag
|
|||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
||||||
import server.logic.ws_protocol.WireCodes;
|
import server.logic.ws_protocol.WireCodes;
|
||||||
import server.sync.DmDeliveryCoordinator;
|
import server.sync.DmDeliveryCoordinator;
|
||||||
import shine.db.dao.DmSyncOutboxDAO;
|
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
import shine.db.entities.SignedMessageEntry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
|
|
||||||
@@ -70,17 +69,6 @@ public class Net_ReceiveIncomingMessage_Handler implements JsonMessageHandler {
|
|||||||
if (stored == null || !Arrays.equals(stored.getRawBlock(), entry.getRawBlock())) {
|
if (stored == null || !Arrays.equals(stored.getRawBlock(), entry.getRawBlock())) {
|
||||||
return NetExceptionResponseFactory.error(req, 409, "STALE_MESSAGE_REVISION", "На сервере уже есть более новая ревизия сообщения");
|
return NetExceptionResponseFactory.error(req, 409, "STALE_MESSAGE_REVISION", "На сервере уже есть более новая ревизия сообщения");
|
||||||
}
|
}
|
||||||
boolean receivedFromRecipientPeer = DmDeliveryCoordinator.sourceIsOtherAccessServer(
|
|
||||||
incoming.toLogin, req.getSourceServerLogin());
|
|
||||||
DmSyncOutboxDAO.getInstance().upsert(
|
|
||||||
incoming.toLogin,
|
|
||||||
entry.getMessageKey(),
|
|
||||||
DmDeliveryIds.forEntry(entry),
|
|
||||||
null,
|
|
||||||
receivedFromRecipientPeer,
|
|
||||||
entry.getCreatedAtMs()
|
|
||||||
);
|
|
||||||
|
|
||||||
Net_ReceiveIncomingMessage_Response resp = new Net_ReceiveIncomingMessage_Response();
|
Net_ReceiveIncomingMessage_Response resp = new Net_ReceiveIncomingMessage_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
resp.setRequestId(req.getRequestId());
|
resp.setRequestId(req.getRequestId());
|
||||||
|
|||||||
+6
-29
@@ -11,7 +11,6 @@ import server.logic.ws_protocol.WireCodes;
|
|||||||
import server.sync.DmDeliveryCoordinator;
|
import server.sync.DmDeliveryCoordinator;
|
||||||
import server.sync.DmFederationService;
|
import server.sync.DmFederationService;
|
||||||
import shine.db.dao.DmDeliveryStateDAO;
|
import shine.db.dao.DmDeliveryStateDAO;
|
||||||
import shine.db.dao.DmSyncOutboxDAO;
|
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
import shine.db.entities.DmDeliveryStateEntry;
|
import shine.db.entities.DmDeliveryStateEntry;
|
||||||
import shine.db.entities.SignedMessageEntry;
|
import shine.db.entities.SignedMessageEntry;
|
||||||
@@ -48,12 +47,10 @@ public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
SignedMessageEntry incomingEntry;
|
SignedMessageEntry incomingEntry;
|
||||||
SignedMessageEntry outgoingEntry;
|
SignedMessageEntry outgoingEntry;
|
||||||
boolean fromPeer = !isBlank(req.getSourceServerLogin());
|
|
||||||
try {
|
try {
|
||||||
String sourceApi = fromPeer ? "ReceiveOutcomingMessage" : "SendMessagePair";
|
|
||||||
String originSessionId = (ctx != null && !isBlank(ctx.getSessionId())) ? ctx.getSessionId() : null;
|
String originSessionId = (ctx != null && !isBlank(ctx.getSessionId())) ? ctx.getSessionId() : null;
|
||||||
incomingEntry = SignedMessagesCore.toEntry(incoming, sourceApi, originSessionId);
|
incomingEntry = SignedMessagesCore.toEntry(incoming, "SendMessagePair", originSessionId);
|
||||||
outgoingEntry = SignedMessagesCore.toEntry(outgoing, sourceApi, originSessionId);
|
outgoingEntry = SignedMessagesCore.toEntry(outgoing, "SendMessagePair", originSessionId);
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, ex.getMessage(), "Некорректный payload подтверждения");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, ex.getMessage(), "Некорректный payload подтверждения");
|
||||||
}
|
}
|
||||||
@@ -96,43 +93,23 @@ public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
|||||||
String eventId = DmDeliveryIds.forEntry(outgoingEntry);
|
String eventId = DmDeliveryIds.forEntry(outgoingEntry);
|
||||||
long nowMs = System.currentTimeMillis();
|
long nowMs = System.currentTimeMillis();
|
||||||
long acceptedAtMs;
|
long acceptedAtMs;
|
||||||
long signedAtMs = Math.max(outgoing.timeMs,
|
acceptedAtMs = nowMs;
|
||||||
Math.max(outgoing.revisionTimeMs, outgoing.reencryptedAtMs));
|
|
||||||
acceptedAtMs = fromPeer && signedAtMs > 0L ? Math.min(nowMs, signedAtMs) : nowMs;
|
|
||||||
long expiresAtMs = acceptedAtMs + 60L * 60L * 1000L;
|
long expiresAtMs = acceptedAtMs + 60L * 60L * 1000L;
|
||||||
int initialState = DmDeliveryStateEntry.ACCEPTED;
|
int initialState = DmDeliveryStateEntry.ACCEPTED;
|
||||||
DmDeliveryStateEntry delivery = DmDeliveryStateDAO.getInstance().upsertPair(
|
DmDeliveryStateEntry delivery = DmDeliveryStateDAO.getInstance().upsertPair(
|
||||||
outgoingEntry.getMessageKey(), eventId, outgoingEntry.getBaseKey(),
|
outgoingEntry.getMessageKey(), eventId, outgoingEntry.getBaseKey(),
|
||||||
outgoingEntry.getFromLogin(), outgoingEntry.getToLogin(), incomingEntry.getMessageKey(),
|
outgoingEntry.getFromLogin(), outgoingEntry.getToLogin(), incomingEntry.getMessageKey(),
|
||||||
acceptedAtMs, expiresAtMs, initialState,
|
acceptedAtMs, expiresAtMs, initialState,
|
||||||
null, null, fromPeer && initialState == DmDeliveryStateEntry.PENDING_NONE
|
null, null, false
|
||||||
);
|
);
|
||||||
|
|
||||||
DmSyncOutboxDAO.getInstance().upsert(
|
|
||||||
outgoingEntry.getFromLogin(), outgoingEntry.getMessageKey(), eventId,
|
|
||||||
incomingEntry.getMessageKey(), fromPeer, acceptedAtMs);
|
|
||||||
|
|
||||||
// Если этот же сервер также обслуживает получателя, его входящая копия
|
|
||||||
// имеет отдельный sync-флаг владельца-получателя.
|
|
||||||
if (DmDeliveryCoordinator.isLocalAccessServer(incomingEntry.getToLogin())) {
|
|
||||||
boolean incomingAlreadySynced = DmDeliveryCoordinator.sourceIsOtherAccessServer(
|
|
||||||
incomingEntry.getToLogin(), req.getSourceServerLogin());
|
|
||||||
DmSyncOutboxDAO.getInstance().upsert(
|
|
||||||
incomingEntry.getToLogin(), incomingEntry.getMessageKey(), DmDeliveryIds.forEntry(incomingEntry),
|
|
||||||
null, incomingAlreadySynced, acceptedAtMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delivery != null && pairStatus.applied()) {
|
if (delivery != null && pairStatus.applied()) {
|
||||||
if (fromPeer) {
|
// Первая доставка на единственный access-сервер получателя
|
||||||
DmDeliveryCoordinator.assistReceivedPairAsync(delivery.getEventId());
|
// выполняется до ответа клиенту.
|
||||||
} else {
|
|
||||||
// Первая доставка выполняется до ответа клиенту. Два сервера
|
|
||||||
// получателя вызываются параллельно внутри координатора.
|
|
||||||
DmDeliveryCoordinator.processDueEntry(delivery);
|
DmDeliveryCoordinator.processDueEntry(delivery);
|
||||||
delivery = DmDeliveryStateDAO.getInstance()
|
delivery = DmDeliveryStateDAO.getInstance()
|
||||||
.getByOutgoingMessageKey(outgoingEntry.getMessageKey());
|
.getByOutgoingMessageKey(outgoingEntry.getMessageKey());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Net_SendMessagePair_Response resp = new Net_SendMessagePair_Response();
|
Net_SendMessagePair_Response resp = new Net_SendMessagePair_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
|
|||||||
-129
@@ -1,129 +0,0 @@
|
|||||||
package server.logic.ws_protocol.JSON.messages;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_UserSettingsSyncBatch_Request;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_UserSettingsSyncBatch_Response;
|
|
||||||
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
|
|
||||||
import server.logic.ws_protocol.WireCodes;
|
|
||||||
import shine.db.DbController;
|
|
||||||
import shine.db.dao.UserAccessServersCurrentDAO;
|
|
||||||
import shine.db.dao.UserSettingsDAO;
|
|
||||||
import shine.db.entities.UserSettingEntry;
|
|
||||||
import shine.db.entities.UserAccessServerRouteEntry;
|
|
||||||
import utils.config.AppConfig;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class Net_UserSettingsSyncBatch_Handler implements JsonMessageHandler {
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(Net_UserSettingsSyncBatch_Handler.class);
|
|
||||||
private static final int DEFAULT_LIMIT = 500;
|
|
||||||
private static final int MAX_LIMIT = 1000;
|
|
||||||
private static final int DEFAULT_MAX_BYTES = 3_000_000;
|
|
||||||
private static final int MAX_BYTES_CAP = 5_000_000;
|
|
||||||
private static final String SERVER_LOGIN_CONFIG = "server.SHiNE.login";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
|
|
||||||
Net_UserSettingsSyncBatch_Request req = (Net_UserSettingsSyncBatch_Request) baseRequest;
|
|
||||||
String ownerLogin = normalizeOriginal(req.getOwnerLogin());
|
|
||||||
if (ownerLogin == null) {
|
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "EMPTY_OWNER_LOGIN", "ownerLogin обязателен");
|
|
||||||
}
|
|
||||||
|
|
||||||
String ownServerLogin = normalize(AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG));
|
|
||||||
if (ownServerLogin == null) {
|
|
||||||
return NetExceptionResponseFactory.error(req, 500, "LOCAL_SERVER_NOT_CONFIGURED", "server.SHiNE.login не настроен");
|
|
||||||
}
|
|
||||||
if (!isLocalAccessServer(ownerLogin, ownServerLogin)) {
|
|
||||||
return NetExceptionResponseFactory.error(req, 403, "LOCAL_SERVER_NOT_ACCESS_SERVER", "Локальный сервер не является access-сервером пользователя");
|
|
||||||
}
|
|
||||||
|
|
||||||
int limit = clamp(req.getLimit() == null ? DEFAULT_LIMIT : req.getLimit(), 1, MAX_LIMIT);
|
|
||||||
int maxBytes = clamp(req.getMaxBytes() == null ? DEFAULT_MAX_BYTES : req.getMaxBytes(), 64_000, MAX_BYTES_CAP);
|
|
||||||
long afterTimeMs = Math.max(0L, req.getAfterTimeMs() == null ? 0L : req.getAfterTimeMs());
|
|
||||||
String afterSettingKey = req.getAfterSettingKey() == null ? "" : req.getAfterSettingKey().trim();
|
|
||||||
|
|
||||||
List<UserSettingEntry> batch;
|
|
||||||
try (Connection c = DbController.getInstance().getConnection()) {
|
|
||||||
batch = UserSettingsDAO.getInstance().listNewerThan(
|
|
||||||
c,
|
|
||||||
ownerLogin,
|
|
||||||
afterTimeMs,
|
|
||||||
afterSettingKey,
|
|
||||||
limit
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Net_UserSettingsSyncBatch_Response resp = new Net_UserSettingsSyncBatch_Response();
|
|
||||||
resp.setOp(req.getOp());
|
|
||||||
resp.setRequestId(req.getRequestId());
|
|
||||||
resp.setStatus(WireCodes.Status.OK);
|
|
||||||
resp.setOwnerLogin(ownerLogin);
|
|
||||||
resp.setLimit(limit);
|
|
||||||
|
|
||||||
int rawBytes = 0;
|
|
||||||
List<Net_UserSettingsSyncBatch_Response.Item> items = new ArrayList<>();
|
|
||||||
for (UserSettingEntry entry : batch) {
|
|
||||||
Net_UserSettingsSyncBatch_Response.Item item = new Net_UserSettingsSyncBatch_Response.Item();
|
|
||||||
item.setLogin(entry.getLogin());
|
|
||||||
item.setSetting_type(entry.getSettingType());
|
|
||||||
item.setSetting_key(entry.getSettingKey());
|
|
||||||
item.setTime_ms(entry.getTimeMs());
|
|
||||||
item.setValue_text(entry.getValueText());
|
|
||||||
item.setValue_num(entry.getValueNum());
|
|
||||||
item.setClient_key(entry.getClientKey());
|
|
||||||
item.setSignature(entry.getSignature());
|
|
||||||
item.setSynced(entry.isSynced());
|
|
||||||
items.add(item);
|
|
||||||
rawBytes += String.valueOf(entry.getLogin()).length()
|
|
||||||
+ String.valueOf(entry.getSettingKey()).length()
|
|
||||||
+ String.valueOf(entry.getValueText()).length()
|
|
||||||
+ String.valueOf(entry.getClientKey() == null ? "" : entry.getClientKey()).length()
|
|
||||||
+ String.valueOf(entry.getSignature() == null ? "" : entry.getSignature()).length()
|
|
||||||
+ 64;
|
|
||||||
if (rawBytes > maxBytes) break;
|
|
||||||
resp.setNextTimeMs(entry.getTimeMs());
|
|
||||||
resp.setNextSettingKey(entry.getSettingKey());
|
|
||||||
}
|
|
||||||
resp.setRawBytes(rawBytes);
|
|
||||||
resp.setHasMore(batch.size() > items.size());
|
|
||||||
resp.setItems(items);
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isLocalAccessServer(String ownerLogin, String ownServerLogin) throws Exception {
|
|
||||||
try (Connection c = DbController.getInstance().getConnection()) {
|
|
||||||
for (UserAccessServerRouteEntry route : UserAccessServersCurrentDAO.getInstance().listByUserLogin(c, ownerLogin)) {
|
|
||||||
if (route == null || route.getServerLogin() == null) continue;
|
|
||||||
if (ownServerLogin.equals(normalize(route.getServerLogin()))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int clamp(int value, int min, int max) {
|
|
||||||
return Math.max(min, Math.min(max, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String normalize(String value) {
|
|
||||||
if (value == null) return null;
|
|
||||||
String s = value.trim().toLowerCase();
|
|
||||||
return s.isEmpty() ? null : s;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String normalizeOriginal(String value) {
|
|
||||||
if (value == null) return null;
|
|
||||||
String s = value.trim();
|
|
||||||
return s.isEmpty() ? null : s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-61
@@ -1,61 +0,0 @@
|
|||||||
package server.logic.ws_protocol.JSON.messages;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.messages.entyties.Net_UserSettingsSyncBatch_Response;
|
|
||||||
import shine.db.dao.UserSettingsDAO;
|
|
||||||
import shine.db.entities.UserSettingEntry;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public final class UserSettingsSyncApplySupport {
|
|
||||||
private UserSettingsSyncApplySupport() {}
|
|
||||||
|
|
||||||
public static ApplyResult applySyncedItem(Connection c, String ownerLogin, Net_UserSettingsSyncBatch_Response.Item item) throws Exception {
|
|
||||||
if (item == null) return new ApplyResult(false, "empty_item");
|
|
||||||
String login = normalize(item.getLogin());
|
|
||||||
String key = normalize(item.getSetting_key());
|
|
||||||
if (login == null || key == null) return new ApplyResult(false, "bad_item");
|
|
||||||
|
|
||||||
UserSettingEntry entry = new UserSettingEntry(
|
|
||||||
login,
|
|
||||||
item.getSetting_type() == null ? 0 : item.getSetting_type(),
|
|
||||||
key,
|
|
||||||
item.getTime_ms() == null ? 0L : item.getTime_ms(),
|
|
||||||
item.getValue_text() == null ? "" : item.getValue_text(),
|
|
||||||
item.getValue_num() == null ? 0L : item.getValue_num(),
|
|
||||||
item.getClient_key(),
|
|
||||||
item.getSignature(),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
int changed = UserSettingsDAO.getInstance().upsertIfNewer(c, entry);
|
|
||||||
return new ApplyResult(changed > 0, changed > 0 ? "applied" : "ignored");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<UserSettingEntry> toEntries(List<Net_UserSettingsSyncBatch_Response.Item> items) {
|
|
||||||
List<UserSettingEntry> out = new ArrayList<>();
|
|
||||||
if (items == null) return out;
|
|
||||||
for (Net_UserSettingsSyncBatch_Response.Item item : items) {
|
|
||||||
if (item == null) continue;
|
|
||||||
out.add(new UserSettingEntry(
|
|
||||||
normalize(item.getLogin()),
|
|
||||||
item.getSetting_type() == null ? 0 : item.getSetting_type(),
|
|
||||||
normalize(item.getSetting_key()),
|
|
||||||
item.getTime_ms() == null ? 0L : item.getTime_ms(),
|
|
||||||
item.getValue_text() == null ? "" : item.getValue_text(),
|
|
||||||
item.getValue_num() == null ? 0L : item.getValue_num(),
|
|
||||||
item.getClient_key(),
|
|
||||||
item.getSignature(),
|
|
||||||
true
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String normalize(String value) {
|
|
||||||
String s = String.valueOf(value == null ? "" : value).trim();
|
|
||||||
return s.isEmpty() ? null : s;
|
|
||||||
}
|
|
||||||
|
|
||||||
public record ApplyResult(boolean applied, String status) {}
|
|
||||||
}
|
|
||||||
+38
@@ -12,6 +12,7 @@ public class Net_GetDirectMessages_Response extends Net_Response {
|
|||||||
private boolean hasMore;
|
private boolean hasMore;
|
||||||
private Long nextBeforeTimeMs;
|
private Long nextBeforeTimeMs;
|
||||||
private String nextBeforeMessageKey;
|
private String nextBeforeMessageKey;
|
||||||
|
private PeerCard peer;
|
||||||
private List<MessageItem> messages = new ArrayList<>();
|
private List<MessageItem> messages = new ArrayList<>();
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
public String getLogin() { return login; }
|
||||||
@@ -26,9 +27,46 @@ public class Net_GetDirectMessages_Response extends Net_Response {
|
|||||||
public void setNextBeforeTimeMs(Long nextBeforeTimeMs) { this.nextBeforeTimeMs = nextBeforeTimeMs; }
|
public void setNextBeforeTimeMs(Long nextBeforeTimeMs) { this.nextBeforeTimeMs = nextBeforeTimeMs; }
|
||||||
public String getNextBeforeMessageKey() { return nextBeforeMessageKey; }
|
public String getNextBeforeMessageKey() { return nextBeforeMessageKey; }
|
||||||
public void setNextBeforeMessageKey(String nextBeforeMessageKey) { this.nextBeforeMessageKey = nextBeforeMessageKey; }
|
public void setNextBeforeMessageKey(String nextBeforeMessageKey) { this.nextBeforeMessageKey = nextBeforeMessageKey; }
|
||||||
|
public PeerCard getPeer() { return peer; }
|
||||||
|
public void setPeer(PeerCard peer) { this.peer = peer; }
|
||||||
public List<MessageItem> getMessages() { return messages; }
|
public List<MessageItem> getMessages() { return messages; }
|
||||||
public void setMessages(List<MessageItem> messages) { this.messages = messages; }
|
public void setMessages(List<MessageItem> messages) { this.messages = messages; }
|
||||||
|
|
||||||
|
|
||||||
|
public static class PeerCard {
|
||||||
|
private String login;
|
||||||
|
private String firstName;
|
||||||
|
private String lastName;
|
||||||
|
private Avatar avatar;
|
||||||
|
private String relationType;
|
||||||
|
private String accountRole;
|
||||||
|
private String shineStatus;
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
public String getFirstName() { return firstName; }
|
||||||
|
public void setFirstName(String firstName) { this.firstName = firstName; }
|
||||||
|
public String getLastName() { return lastName; }
|
||||||
|
public void setLastName(String lastName) { this.lastName = lastName; }
|
||||||
|
public Avatar getAvatar() { return avatar; }
|
||||||
|
public void setAvatar(Avatar avatar) { this.avatar = avatar; }
|
||||||
|
public String getRelationType() { return relationType; }
|
||||||
|
public void setRelationType(String relationType) { this.relationType = relationType; }
|
||||||
|
public String getAccountRole() { return accountRole; }
|
||||||
|
public void setAccountRole(String accountRole) { this.accountRole = accountRole; }
|
||||||
|
public String getShineStatus() { return shineStatus; }
|
||||||
|
public void setShineStatus(String shineStatus) { this.shineStatus = shineStatus; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Avatar {
|
||||||
|
private String ar;
|
||||||
|
private String sha256Hex;
|
||||||
|
public String getAr() { return ar; }
|
||||||
|
public void setAr(String ar) { this.ar = ar; }
|
||||||
|
public String getSha256Hex() { return sha256Hex; }
|
||||||
|
public void setSha256Hex(String sha256Hex) { this.sha256Hex = sha256Hex; }
|
||||||
|
}
|
||||||
|
|
||||||
public static class MessageItem {
|
public static class MessageItem {
|
||||||
private String messageKey;
|
private String messageKey;
|
||||||
private String baseKey;
|
private String baseKey;
|
||||||
|
|||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
package server.logic.ws_protocol.JSON.messages.entyties;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|
||||||
|
|
||||||
public class Net_MarkAllUserSettingsUnsynced_Request extends Net_Request {
|
|
||||||
private String login;
|
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
|
||||||
public void setLogin(String login) { this.login = login; }
|
|
||||||
}
|
|
||||||
-13
@@ -1,13 +0,0 @@
|
|||||||
package server.logic.ws_protocol.JSON.messages.entyties;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|
||||||
|
|
||||||
public class Net_MarkAllUserSettingsUnsynced_Response extends Net_Response {
|
|
||||||
private Integer updated;
|
|
||||||
private Integer dmUpdated;
|
|
||||||
|
|
||||||
public Integer getUpdated() { return updated; }
|
|
||||||
public void setUpdated(Integer updated) { this.updated = updated; }
|
|
||||||
public Integer getDmUpdated() { return dmUpdated; }
|
|
||||||
public void setDmUpdated(Integer dmUpdated) { this.dmUpdated = dmUpdated; }
|
|
||||||
}
|
|
||||||
-3
@@ -5,12 +5,9 @@ import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|||||||
public class Net_SendMessagePair_Request extends Net_Request {
|
public class Net_SendMessagePair_Request extends Net_Request {
|
||||||
private String incomingBlobB64;
|
private String incomingBlobB64;
|
||||||
private String outgoingBlobB64;
|
private String outgoingBlobB64;
|
||||||
private String sourceServerLogin;
|
|
||||||
|
|
||||||
public String getIncomingBlobB64() { return incomingBlobB64; }
|
public String getIncomingBlobB64() { return incomingBlobB64; }
|
||||||
public void setIncomingBlobB64(String incomingBlobB64) { this.incomingBlobB64 = incomingBlobB64; }
|
public void setIncomingBlobB64(String incomingBlobB64) { this.incomingBlobB64 = incomingBlobB64; }
|
||||||
public String getOutgoingBlobB64() { return outgoingBlobB64; }
|
public String getOutgoingBlobB64() { return outgoingBlobB64; }
|
||||||
public void setOutgoingBlobB64(String outgoingBlobB64) { this.outgoingBlobB64 = outgoingBlobB64; }
|
public void setOutgoingBlobB64(String outgoingBlobB64) { this.outgoingBlobB64 = outgoingBlobB64; }
|
||||||
public String getSourceServerLogin() { return sourceServerLogin; }
|
|
||||||
public void setSourceServerLogin(String sourceServerLogin) { this.sourceServerLogin = sourceServerLogin; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
-26
@@ -1,26 +0,0 @@
|
|||||||
package server.logic.ws_protocol.JSON.messages.entyties;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
|
||||||
|
|
||||||
public class Net_UserSettingsSyncBatch_Request extends Net_Request {
|
|
||||||
private String ownerLogin;
|
|
||||||
private Long afterTimeMs;
|
|
||||||
private String afterSettingKey;
|
|
||||||
private Integer limit;
|
|
||||||
private Integer maxBytes;
|
|
||||||
|
|
||||||
public String getOwnerLogin() { return ownerLogin; }
|
|
||||||
public void setOwnerLogin(String ownerLogin) { this.ownerLogin = ownerLogin; }
|
|
||||||
|
|
||||||
public Long getAfterTimeMs() { return afterTimeMs; }
|
|
||||||
public void setAfterTimeMs(Long afterTimeMs) { this.afterTimeMs = afterTimeMs; }
|
|
||||||
|
|
||||||
public String getAfterSettingKey() { return afterSettingKey; }
|
|
||||||
public void setAfterSettingKey(String afterSettingKey) { this.afterSettingKey = afterSettingKey; }
|
|
||||||
|
|
||||||
public Integer getLimit() { return limit; }
|
|
||||||
public void setLimit(Integer limit) { this.limit = limit; }
|
|
||||||
|
|
||||||
public Integer getMaxBytes() { return maxBytes; }
|
|
||||||
public void setMaxBytes(Integer maxBytes) { this.maxBytes = maxBytes; }
|
|
||||||
}
|
|
||||||
-62
@@ -1,62 +0,0 @@
|
|||||||
package server.logic.ws_protocol.JSON.messages.entyties;
|
|
||||||
|
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class Net_UserSettingsSyncBatch_Response extends Net_Response {
|
|
||||||
private String ownerLogin;
|
|
||||||
private Integer limit;
|
|
||||||
private Integer rawBytes;
|
|
||||||
private Boolean hasMore;
|
|
||||||
private Long nextTimeMs;
|
|
||||||
private String nextSettingKey;
|
|
||||||
private List<Item> items = new ArrayList<>();
|
|
||||||
|
|
||||||
public String getOwnerLogin() { return ownerLogin; }
|
|
||||||
public void setOwnerLogin(String ownerLogin) { this.ownerLogin = ownerLogin; }
|
|
||||||
public Integer getLimit() { return limit; }
|
|
||||||
public void setLimit(Integer limit) { this.limit = limit; }
|
|
||||||
public Integer getRawBytes() { return rawBytes; }
|
|
||||||
public void setRawBytes(Integer rawBytes) { this.rawBytes = rawBytes; }
|
|
||||||
public Boolean getHasMore() { return hasMore; }
|
|
||||||
public void setHasMore(Boolean hasMore) { this.hasMore = hasMore; }
|
|
||||||
public Long getNextTimeMs() { return nextTimeMs; }
|
|
||||||
public void setNextTimeMs(Long nextTimeMs) { this.nextTimeMs = nextTimeMs; }
|
|
||||||
public String getNextSettingKey() { return nextSettingKey; }
|
|
||||||
public void setNextSettingKey(String nextSettingKey) { this.nextSettingKey = nextSettingKey; }
|
|
||||||
public List<Item> getItems() { return items; }
|
|
||||||
public void setItems(List<Item> items) { this.items = items; }
|
|
||||||
|
|
||||||
public static class Item {
|
|
||||||
private String login;
|
|
||||||
private Integer setting_type;
|
|
||||||
private String setting_key;
|
|
||||||
private Long time_ms;
|
|
||||||
private String value_text;
|
|
||||||
private Long value_num;
|
|
||||||
private String client_key;
|
|
||||||
private String signature;
|
|
||||||
private Boolean synced;
|
|
||||||
|
|
||||||
public String getLogin() { return login; }
|
|
||||||
public void setLogin(String login) { this.login = login; }
|
|
||||||
public Integer getSetting_type() { return setting_type; }
|
|
||||||
public void setSetting_type(Integer setting_type) { this.setting_type = setting_type; }
|
|
||||||
public String getSetting_key() { return setting_key; }
|
|
||||||
public void setSetting_key(String setting_key) { this.setting_key = setting_key; }
|
|
||||||
public Long getTime_ms() { return time_ms; }
|
|
||||||
public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }
|
|
||||||
public String getValue_text() { return value_text; }
|
|
||||||
public void setValue_text(String value_text) { this.value_text = value_text; }
|
|
||||||
public Long getValue_num() { return value_num; }
|
|
||||||
public void setValue_num(Long value_num) { this.value_num = value_num; }
|
|
||||||
public String getClient_key() { return client_key; }
|
|
||||||
public void setClient_key(String client_key) { this.client_key = client_key; }
|
|
||||||
public String getSignature() { return signature; }
|
|
||||||
public void setSignature(String signature) { this.signature = signature; }
|
|
||||||
public Boolean getSynced() { return synced; }
|
|
||||||
public void setSynced(Boolean synced) { this.synced = synced; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+33
-185
@@ -4,7 +4,6 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import server.logic.ws_protocol.JSON.messages.DmDeliveryRealtime;
|
import server.logic.ws_protocol.JSON.messages.DmDeliveryRealtime;
|
||||||
import shine.db.dao.DmDeliveryStateDAO;
|
import shine.db.dao.DmDeliveryStateDAO;
|
||||||
import shine.db.dao.DmSyncOutboxDAO;
|
|
||||||
import shine.db.dao.SignedMessagesDAO;
|
import shine.db.dao.SignedMessagesDAO;
|
||||||
import shine.db.dao.UserAccessServersCurrentDAO;
|
import shine.db.dao.UserAccessServersCurrentDAO;
|
||||||
import shine.db.entities.DmDeliveryStateEntry;
|
import shine.db.entities.DmDeliveryStateEntry;
|
||||||
@@ -14,21 +13,14 @@ import utils.config.AppConfig;
|
|||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.ArrayBlockingQueue;
|
|
||||||
import java.util.concurrent.ThreadPoolExecutor;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.ThreadFactory;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Координатор доставки исходящей DM-пары. Первая попытка выполняется до ответа
|
* Координатор доставки исходящей DM-пары на единственный первый access-сервер
|
||||||
* клиенту, последующие — пятисекундным воркером.
|
* получателя. Первая попытка выполняется до ответа клиенту, последующие —
|
||||||
|
* пятисекундным воркером.
|
||||||
*/
|
*/
|
||||||
public final class DmDeliveryCoordinator {
|
public final class DmDeliveryCoordinator {
|
||||||
private static final Logger log = LoggerFactory.getLogger(DmDeliveryCoordinator.class);
|
private static final Logger log = LoggerFactory.getLogger(DmDeliveryCoordinator.class);
|
||||||
@@ -40,21 +32,10 @@ public final class DmDeliveryCoordinator {
|
|||||||
60L * 60_000L
|
60L * 60_000L
|
||||||
};
|
};
|
||||||
private static final String SERVER_LOGIN_CONFIG = "server.SHiNE.login";
|
private static final String SERVER_LOGIN_CONFIG = "server.SHiNE.login";
|
||||||
private static final RemoteDmSyncClient REMOTE = new RemoteDmSyncClient();
|
private static final RemoteDmDeliveryClient REMOTE = new RemoteDmDeliveryClient();
|
||||||
private static final DmDeliveryStateDAO DELIVERY_DAO = DmDeliveryStateDAO.getInstance();
|
private static final DmDeliveryStateDAO DELIVERY_DAO = DmDeliveryStateDAO.getInstance();
|
||||||
private static final DmSyncOutboxDAO OUTBOX_DAO = DmSyncOutboxDAO.getInstance();
|
|
||||||
private static final SignedMessagesDAO MESSAGES_DAO = SignedMessagesDAO.getInstance();
|
private static final SignedMessagesDAO MESSAGES_DAO = SignedMessagesDAO.getInstance();
|
||||||
private static final UserAccessServersCurrentDAO ACCESS_DAO = UserAccessServersCurrentDAO.getInstance();
|
private static final UserAccessServersCurrentDAO ACCESS_DAO = UserAccessServersCurrentDAO.getInstance();
|
||||||
private static final ExecutorService ASSIST_EXECUTOR = new ThreadPoolExecutor(
|
|
||||||
2, 2, 0L, TimeUnit.MILLISECONDS,
|
|
||||||
new ArrayBlockingQueue<>(500),
|
|
||||||
daemonThreadFactory("dm-peer-assist"),
|
|
||||||
new ThreadPoolExecutor.DiscardPolicy());
|
|
||||||
private static final ExecutorService RECIPIENT_EXECUTOR = new ThreadPoolExecutor(
|
|
||||||
4, 16, 60L, TimeUnit.SECONDS,
|
|
||||||
new ArrayBlockingQueue<>(1000),
|
|
||||||
daemonThreadFactory("dm-recipient-delivery"),
|
|
||||||
new ThreadPoolExecutor.CallerRunsPolicy());
|
|
||||||
|
|
||||||
private DmDeliveryCoordinator() {}
|
private DmDeliveryCoordinator() {}
|
||||||
|
|
||||||
@@ -63,10 +44,6 @@ public final class DmDeliveryCoordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void processDueEntry(DmDeliveryStateEntry snapshot) {
|
public static void processDueEntry(DmDeliveryStateEntry snapshot) {
|
||||||
processDueEntry(snapshot, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void processDueEntry(DmDeliveryStateEntry snapshot, boolean allowInitialHandoff) {
|
|
||||||
if (snapshot == null) return;
|
if (snapshot == null) return;
|
||||||
try {
|
try {
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
@@ -79,27 +56,9 @@ public final class DmDeliveryCoordinator {
|
|||||||
|
|
||||||
boolean finalAttempt = snapshot.getAttemptIndex() >= ATTEMPT_OFFSETS_MS.length - 1
|
boolean finalAttempt = snapshot.getAttemptIndex() >= ATTEMPT_OFFSETS_MS.length - 1
|
||||||
|| now >= current.getDeliveryExpiresAtMs();
|
|| now >= current.getDeliveryExpiresAtMs();
|
||||||
|
DmDeliveryStateEntry afterAttempt = attemptPrimaryRecipientRoute(
|
||||||
// Перед попытками на 5-й, 25-й и 60-й минутах сначала спрашиваем
|
|
||||||
// второй сервер отправителя: возможно, он уже доставил сообщение.
|
|
||||||
if (current.getDeliveryState() == DmDeliveryStateEntry.ACCEPTED
|
|
||||||
&& (snapshot.getAttemptIndex() >= 2 || finalAttempt)) {
|
|
||||||
current = acceptPeerDeliveryStatus(current);
|
|
||||||
if (current != null && current.isDelivered()) {
|
|
||||||
DmDeliveryRealtime.notifySender(current);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DmDeliveryStateEntry afterAttempt = attemptRecipientRoutes(
|
|
||||||
current, finalAttempt ? null : nextAttemptAtMs);
|
current, finalAttempt ? null : nextAttemptAtMs);
|
||||||
|
|
||||||
// После первой попытки сразу передаём полную пару второму серверу
|
|
||||||
// отправителя старой операцией ReceiveOutcomingMessage.
|
|
||||||
if (allowInitialHandoff && snapshot.getAttemptIndex() == 0 && afterAttempt != null) {
|
|
||||||
afterAttempt = handoffPairToSenderPeer(afterAttempt);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (finalAttempt && afterAttempt != null && !afterAttempt.isDelivered()) {
|
if (finalAttempt && afterAttempt != null && !afterAttempt.isDelivered()) {
|
||||||
afterAttempt = DELIVERY_DAO.finishAtExpiry(afterAttempt.getEventId(), System.currentTimeMillis());
|
afterAttempt = DELIVERY_DAO.finishAtExpiry(afterAttempt.getEventId(), System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
@@ -109,28 +68,18 @@ public final class DmDeliveryCoordinator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void assistReceivedPairAsync(String eventId) {
|
private static DmDeliveryStateEntry attemptPrimaryRecipientRoute(
|
||||||
if (eventId == null || eventId.isBlank()) return;
|
|
||||||
ASSIST_EXECUTOR.execute(() -> {
|
|
||||||
try {
|
|
||||||
DmDeliveryStateEntry row = DELIVERY_DAO.getByEventId(eventId);
|
|
||||||
if (row != null) processDueEntry(row, false);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("DM peer assist failed: eventId={}", eventId, e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DmDeliveryStateEntry attemptRecipientRoutes(
|
|
||||||
DmDeliveryStateEntry current,
|
DmDeliveryStateEntry current,
|
||||||
Long nextAttemptAtMs
|
Long nextAttemptAtMs
|
||||||
) throws Exception {
|
) throws Exception {
|
||||||
if (current == null) return null;
|
if (current == null) return null;
|
||||||
List<UserAccessServerRouteEntry> routes = cappedRoutes(current.getToLogin());
|
UserAccessServerRouteEntry route = primaryRoute(current.getToLogin());
|
||||||
String routesHash = routesHash(routes);
|
String routesHash = routesHash(route);
|
||||||
String alreadyDelivered = normalize(current.getDeliveredServerLogin());
|
if (route == null) {
|
||||||
List<String> acceptedLogins = new ArrayList<>();
|
return DELIVERY_DAO.updateAfterAttempt(
|
||||||
if (alreadyDelivered != null) acceptedLogins.add(alreadyDelivered);
|
current.getEventId(), DmDeliveryStateEntry.ACCEPTED, null,
|
||||||
|
routesHash, nextAttemptAtMs, "RECIPIENT_ACCESS_SERVER_NOT_FOUND", System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
SignedMessageEntry incoming = MESSAGES_DAO.getByMessageKey(current.getIncomingMessageKey());
|
SignedMessageEntry incoming = MESSAGES_DAO.getByMessageKey(current.getIncomingMessageKey());
|
||||||
if (incoming == null || incoming.getRawBlock() == null) {
|
if (incoming == null || incoming.getRawBlock() == null) {
|
||||||
@@ -138,138 +87,51 @@ public final class DmDeliveryCoordinator {
|
|||||||
current.getEventId(), current.getDeliveryState(), current.getDeliveredServerLogin(),
|
current.getEventId(), current.getDeliveryState(), current.getDeliveredServerLogin(),
|
||||||
routesHash, nextAttemptAtMs, "INCOMING_BLOB_NOT_FOUND", System.currentTimeMillis());
|
routesHash, nextAttemptAtMs, "INCOMING_BLOB_NOT_FOUND", System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
String incomingBlobB64 = Base64.getEncoder().encodeToString(incoming.getRawBlock());
|
|
||||||
String ownServerLogin = ownServerLogin();
|
|
||||||
java.util.concurrent.CompletionService<RouteAttempt> completion =
|
|
||||||
new java.util.concurrent.ExecutorCompletionService<>(RECIPIENT_EXECUTOR);
|
|
||||||
int submitted = 0;
|
|
||||||
for (UserAccessServerRouteEntry route : routes) {
|
|
||||||
String routeLogin = normalize(route.getServerLogin());
|
String routeLogin = normalize(route.getServerLogin());
|
||||||
if (routeLogin == null || acceptedLogins.contains(routeLogin)) continue;
|
|
||||||
completion.submit(() -> {
|
|
||||||
try {
|
try {
|
||||||
if (!routeLogin.equals(ownServerLogin)) {
|
if (!routeLogin.equals(ownServerLogin())) {
|
||||||
REMOTE.receiveIncomingMessage(
|
REMOTE.receiveIncomingMessage(
|
||||||
routeLogin, route.getServerUrl(), incomingBlobB64, ownServerLogin);
|
routeLogin,
|
||||||
|
route.getServerUrl(),
|
||||||
|
Base64.getEncoder().encodeToString(incoming.getRawBlock()),
|
||||||
|
ownServerLogin());
|
||||||
}
|
}
|
||||||
return new RouteAttempt(routeLogin, null);
|
return DELIVERY_DAO.updateAfterAttempt(
|
||||||
|
current.getEventId(), DmDeliveryStateEntry.DELIVERED, routeLogin,
|
||||||
|
routesHash, nextAttemptAtMs, null, System.currentTimeMillis());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.info("DM recipient server unavailable: messageKey={} server={}",
|
log.info("DM recipient server unavailable: messageKey={} server={}",
|
||||||
current.getOutgoingMessageKey(), routeLogin);
|
current.getOutgoingMessageKey(), routeLogin);
|
||||||
return new RouteAttempt(routeLogin, compactError(e));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
submitted++;
|
|
||||||
}
|
|
||||||
|
|
||||||
String lastError = null;
|
|
||||||
for (int i = 0; i < submitted && acceptedLogins.isEmpty(); i++) {
|
|
||||||
RouteAttempt result = completion.take().get();
|
|
||||||
if (result.error() == null) {
|
|
||||||
acceptedLogins.add(result.serverLogin());
|
|
||||||
} else {
|
|
||||||
lastError = result.error();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int nextState = acceptedLogins.isEmpty()
|
|
||||||
? DmDeliveryStateEntry.ACCEPTED
|
|
||||||
: DmDeliveryStateEntry.DELIVERED;
|
|
||||||
String oneLogin = acceptedLogins.isEmpty() ? null : acceptedLogins.get(0);
|
|
||||||
return DELIVERY_DAO.updateAfterAttempt(
|
return DELIVERY_DAO.updateAfterAttempt(
|
||||||
current.getEventId(), nextState, oneLogin, routesHash,
|
current.getEventId(), DmDeliveryStateEntry.ACCEPTED, null,
|
||||||
nextAttemptAtMs, lastError, System.currentTimeMillis());
|
routesHash, nextAttemptAtMs, compactError(e), System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DmDeliveryStateEntry handoffPairToSenderPeer(DmDeliveryStateEntry current) throws Exception {
|
|
||||||
UserAccessServerRouteEntry peer = senderPeer(current.getFromLogin());
|
|
||||||
if (peer == null) return current;
|
|
||||||
SignedMessageEntry incoming = MESSAGES_DAO.getByMessageKey(current.getIncomingMessageKey());
|
|
||||||
SignedMessageEntry outgoing = MESSAGES_DAO.getByMessageKey(current.getOutgoingMessageKey());
|
|
||||||
if (incoming == null || outgoing == null) return current;
|
|
||||||
try {
|
|
||||||
REMOTE.sendMessagePair(
|
|
||||||
peer.getServerLogin(),
|
|
||||||
peer.getServerUrl(),
|
|
||||||
Base64.getEncoder().encodeToString(incoming.getRawBlock()),
|
|
||||||
Base64.getEncoder().encodeToString(outgoing.getRawBlock()),
|
|
||||||
ownServerLogin());
|
|
||||||
OUTBOX_DAO.markSynced(current.getFromLogin(), current.getEventId());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.info("DM sender peer unavailable: eventId={} server={}", current.getEventId(), peer.getServerLogin());
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DmDeliveryStateEntry acceptPeerDeliveryStatus(DmDeliveryStateEntry current) throws Exception {
|
|
||||||
UserAccessServerRouteEntry peer = senderPeer(current.getFromLogin());
|
|
||||||
if (peer == null) return current;
|
|
||||||
try {
|
|
||||||
RemoteDmSyncClient.RemoteDeliveryStatus remote = REMOTE.getDmDeliveryStatus(
|
|
||||||
peer.getServerLogin(), peer.getServerUrl(), current.getOutgoingMessageKey());
|
|
||||||
if (remote.known() && remote.delivered()) {
|
|
||||||
return DELIVERY_DAO.markDeliveredFromPeer(current.getEventId(), System.currentTimeMillis());
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.info("DM delivery status unavailable: eventId={} server={}", current.getEventId(), peer.getServerLogin());
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static UserAccessServerRouteEntry senderPeer(String senderLogin) throws Exception {
|
|
||||||
String own = ownServerLogin();
|
|
||||||
for (UserAccessServerRouteEntry route : cappedRoutes(senderLogin)) {
|
|
||||||
String routeLogin = normalize(route.getServerLogin());
|
|
||||||
if (routeLogin != null && !routeLogin.equals(own)) return route;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isLocalAccessServer(String ownerLogin) throws Exception {
|
public static boolean isLocalAccessServer(String ownerLogin) throws Exception {
|
||||||
|
UserAccessServerRouteEntry route = primaryRoute(ownerLogin);
|
||||||
String own = ownServerLogin();
|
String own = ownServerLogin();
|
||||||
if (own == null) return false;
|
return route != null && own != null && own.equals(normalize(route.getServerLogin()));
|
||||||
for (UserAccessServerRouteEntry route : cappedRoutes(ownerLogin)) {
|
|
||||||
if (own.equals(normalize(route.getServerLogin()))) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean sourceIsOtherAccessServer(String ownerLogin, String sourceServerLogin) throws Exception {
|
|
||||||
String source = normalize(sourceServerLogin);
|
|
||||||
String own = ownServerLogin();
|
|
||||||
if (source == null || source.equals(own)) return false;
|
|
||||||
for (UserAccessServerRouteEntry route : cappedRoutes(ownerLogin)) {
|
|
||||||
if (source.equals(normalize(route.getServerLogin()))) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String currentServerLogin() {
|
public static String currentServerLogin() {
|
||||||
return ownServerLogin();
|
return ownServerLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<UserAccessServerRouteEntry> cappedRoutes(String ownerLogin) throws Exception {
|
private static UserAccessServerRouteEntry primaryRoute(String ownerLogin) throws Exception {
|
||||||
Map<String, UserAccessServerRouteEntry> unique = new LinkedHashMap<>();
|
|
||||||
for (UserAccessServerRouteEntry route : ACCESS_DAO.listByUserLogin(ownerLogin)) {
|
for (UserAccessServerRouteEntry route : ACCESS_DAO.listByUserLogin(ownerLogin)) {
|
||||||
if (route == null || route.getServerUrl() == null || route.getServerUrl().isBlank()) continue;
|
if (route == null || route.getServerUrl() == null || route.getServerUrl().isBlank()) continue;
|
||||||
String login = normalize(route.getServerLogin());
|
if (normalize(route.getServerLogin()) != null) return route;
|
||||||
if (login == null) continue;
|
|
||||||
unique.putIfAbsent(login, route);
|
|
||||||
if (unique.size() == 2) break;
|
|
||||||
}
|
}
|
||||||
return new ArrayList<>(unique.values());
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String routesHash(List<UserAccessServerRouteEntry> routes) throws Exception {
|
private static String routesHash(UserAccessServerRouteEntry route) throws Exception {
|
||||||
List<String> logins = new ArrayList<>();
|
String normalized = route == null ? null : normalize(route.getServerLogin());
|
||||||
for (UserAccessServerRouteEntry route : routes) {
|
String login = normalized == null ? "" : normalized;
|
||||||
String login = normalize(route.getServerLogin());
|
|
||||||
if (login != null) logins.add(login);
|
|
||||||
}
|
|
||||||
logins.sort(String::compareTo);
|
|
||||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
byte[] hash = digest.digest(String.join("\n", logins).getBytes(StandardCharsets.UTF_8));
|
byte[] hash = digest.digest(login.getBytes(StandardCharsets.UTF_8));
|
||||||
StringBuilder out = new StringBuilder(hash.length * 2);
|
StringBuilder out = new StringBuilder(hash.length * 2);
|
||||||
for (byte b : hash) out.append(String.format("%02x", b & 0xff));
|
for (byte b : hash) out.append(String.format("%02x", b & 0xff));
|
||||||
return out.toString();
|
return out.toString();
|
||||||
@@ -280,8 +142,6 @@ public final class DmDeliveryCoordinator {
|
|||||||
return createdAtMs + ATTEMPT_OFFSETS_MS[nextAttemptIndex];
|
return createdAtMs + ATTEMPT_OFFSETS_MS[nextAttemptIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
private record RouteAttempt(String serverLogin, String error) {}
|
|
||||||
|
|
||||||
private static String ownServerLogin() {
|
private static String ownServerLogin() {
|
||||||
return normalize(AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG));
|
return normalize(AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG));
|
||||||
}
|
}
|
||||||
@@ -296,16 +156,4 @@ public final class DmDeliveryCoordinator {
|
|||||||
String text = String.valueOf(e == null ? "unknown" : e.getMessage());
|
String text = String.valueOf(e == null ? "unknown" : e.getMessage());
|
||||||
return text.length() <= 500 ? text : text.substring(0, 500);
|
return text.length() <= 500 ? text : text.substring(0, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ThreadFactory daemonThreadFactory(String prefix) {
|
|
||||||
return new ThreadFactory() {
|
|
||||||
private int sequence;
|
|
||||||
@Override
|
|
||||||
public synchronized Thread newThread(Runnable r) {
|
|
||||||
Thread thread = new Thread(r, prefix + "-" + (++sequence));
|
|
||||||
thread.setDaemon(true);
|
|
||||||
return thread;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-68
@@ -8,63 +8,17 @@ import utils.config.AppConfig;
|
|||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
/** Доставка DM-tombstone на единственные access-серверы обеих сторон. */
|
||||||
public final class DmFederationService {
|
public final class DmFederationService {
|
||||||
private static final Logger log = LoggerFactory.getLogger(DmFederationService.class);
|
private static final Logger log = LoggerFactory.getLogger(DmFederationService.class);
|
||||||
private static final String CONFIG_KEY = "server.SHiNE.login";
|
private static final String CONFIG_KEY = "server.SHiNE.login";
|
||||||
private static final RemoteDmSyncClient REMOTE = new RemoteDmSyncClient();
|
private static final RemoteDmDeliveryClient REMOTE = new RemoteDmDeliveryClient();
|
||||||
|
|
||||||
private DmFederationService() {}
|
private DmFederationService() {}
|
||||||
|
|
||||||
public static void fanOutPair(String fromLogin, String toLogin, String incomingBlobB64, String outgoingBlobB64) {
|
|
||||||
try {
|
|
||||||
Map<String, UserAccessServerRouteEntry> senderRoutes =
|
|
||||||
routesByLogin(UserAccessServersCurrentDAO.getInstance().listByUserLogin(fromLogin));
|
|
||||||
Map<String, UserAccessServerRouteEntry> recipientRoutes =
|
|
||||||
routesByLogin(UserAccessServersCurrentDAO.getInstance().listByUserLogin(toLogin));
|
|
||||||
|
|
||||||
String ownServerLogin = ownServerLogin();
|
|
||||||
for (UserAccessServerRouteEntry route : senderRoutes.values()) {
|
|
||||||
if (isOwnServer(route, ownServerLogin)) continue;
|
|
||||||
REMOTE.sendMessagePair(
|
|
||||||
route.getServerLogin(), route.getServerUrl(),
|
|
||||||
incomingBlobB64, outgoingBlobB64, ownServerLogin);
|
|
||||||
}
|
|
||||||
for (UserAccessServerRouteEntry route : recipientRoutes.values()) {
|
|
||||||
if (isOwnServer(route, ownServerLogin)) continue;
|
|
||||||
if (senderRoutes.containsKey(normalize(route.getServerLogin()))) continue;
|
|
||||||
REMOTE.receiveIncomingMessage(
|
|
||||||
route.getServerLogin(), route.getServerUrl(), incomingBlobB64, ownServerLogin);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("DM federation pair fan-out failed: from={} to={}", fromLogin, toLogin, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void fanOutIncomingToRecipientAccessServers(
|
|
||||||
String toLogin,
|
|
||||||
String incomingBlobB64,
|
|
||||||
String sourceServerLogin
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
Map<String, UserAccessServerRouteEntry> recipientRoutes =
|
|
||||||
routesByLogin(UserAccessServersCurrentDAO.getInstance().listByUserLogin(toLogin));
|
|
||||||
String ownServerLogin = ownServerLogin();
|
|
||||||
String normalizedSource = normalize(sourceServerLogin);
|
|
||||||
for (UserAccessServerRouteEntry route : recipientRoutes.values()) {
|
|
||||||
String routeLogin = normalize(route.getServerLogin());
|
|
||||||
if (routeLogin == null) continue;
|
|
||||||
if (ownServerLogin != null && ownServerLogin.equalsIgnoreCase(routeLogin)) continue;
|
|
||||||
if (normalizedSource != null && normalizedSource.equalsIgnoreCase(routeLogin)) continue;
|
|
||||||
REMOTE.receiveIncomingMessage(
|
|
||||||
route.getServerLogin(), route.getServerUrl(), incomingBlobB64, ownServerLogin);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("DM federation incoming relay failed: to={}", toLogin, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void fanOutDeleteMessage(String fromLogin, String toLogin, int messageType, String blobB64) {
|
public static void fanOutDeleteMessage(String fromLogin, String toLogin, int messageType, String blobB64) {
|
||||||
fanOutSingleDelete(fromLogin, toLogin, messageType, blobB64, true);
|
fanOutSingleDelete(fromLogin, toLogin, messageType, blobB64, true);
|
||||||
}
|
}
|
||||||
@@ -73,12 +27,17 @@ public final class DmFederationService {
|
|||||||
fanOutSingleDelete(fromLogin, toLogin, messageType, blobB64, false);
|
fanOutSingleDelete(fromLogin, toLogin, messageType, blobB64, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void fanOutSingleDelete(String fromLogin, String toLogin, int messageType, String blobB64, boolean oneMessageDelete) {
|
private static void fanOutSingleDelete(
|
||||||
|
String fromLogin,
|
||||||
|
String toLogin,
|
||||||
|
int messageType,
|
||||||
|
String blobB64,
|
||||||
|
boolean oneMessageDelete
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
Map<String, UserAccessServerRouteEntry> routes = routesByLogin(
|
Map<String, UserAccessServerRouteEntry> routes = new LinkedHashMap<>();
|
||||||
UserAccessServersCurrentDAO.getInstance().listByUserLogin(fromLogin),
|
putPrimaryRoute(routes, fromLogin);
|
||||||
UserAccessServersCurrentDAO.getInstance().listByUserLogin(toLogin)
|
putPrimaryRoute(routes, toLogin);
|
||||||
);
|
|
||||||
String ownServerLogin = ownServerLogin();
|
String ownServerLogin = ownServerLogin();
|
||||||
for (UserAccessServerRouteEntry route : routes.values()) {
|
for (UserAccessServerRouteEntry route : routes.values()) {
|
||||||
if (isOwnServer(route, ownServerLogin)) continue;
|
if (isOwnServer(route, ownServerLogin)) continue;
|
||||||
@@ -89,33 +48,29 @@ public final class DmFederationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("DM federation delete fan-out failed: from={} to={} type={}",
|
log.warn("DM delete delivery failed: from={} to={} type={}",
|
||||||
fromLogin, toLogin, messageType, e);
|
fromLogin, toLogin, messageType, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SafeVarargs
|
private static void putPrimaryRoute(Map<String, UserAccessServerRouteEntry> out, String ownerLogin)
|
||||||
private static Map<String, UserAccessServerRouteEntry> routesByLogin(
|
throws Exception {
|
||||||
List<UserAccessServerRouteEntry>... routeLists
|
List<UserAccessServerRouteEntry> routes =
|
||||||
) {
|
UserAccessServersCurrentDAO.getInstance().listByUserLogin(ownerLogin);
|
||||||
Map<String, UserAccessServerRouteEntry> out = new LinkedHashMap<>();
|
for (UserAccessServerRouteEntry route : routes) {
|
||||||
for (List<UserAccessServerRouteEntry> routeList : routeLists) {
|
|
||||||
for (UserAccessServerRouteEntry route : routeList) {
|
|
||||||
if (route == null) continue;
|
if (route == null) continue;
|
||||||
String login = normalize(route.getServerLogin());
|
String login = normalize(route.getServerLogin());
|
||||||
String address = route.getServerUrl() == null ? "" : route.getServerUrl().trim();
|
String address = route.getServerUrl() == null ? "" : route.getServerUrl().trim();
|
||||||
if (login == null || address.isBlank()) continue;
|
if (login == null || address.isBlank()) continue;
|
||||||
out.putIfAbsent(login, route);
|
out.putIfAbsent(login, route);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isOwnServer(UserAccessServerRouteEntry route, String ownServerLogin) {
|
private static boolean isOwnServer(UserAccessServerRouteEntry route, String ownServerLogin) {
|
||||||
return ownServerLogin != null
|
return ownServerLogin != null
|
||||||
&& route != null
|
&& route != null
|
||||||
&& route.getServerLogin() != null
|
&& ownServerLogin.equals(normalize(route.getServerLogin()));
|
||||||
&& ownServerLogin.equalsIgnoreCase(route.getServerLogin());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String ownServerLogin() {
|
private static String ownServerLogin() {
|
||||||
@@ -124,7 +79,7 @@ public final class DmFederationService {
|
|||||||
|
|
||||||
private static String normalize(String value) {
|
private static String normalize(String value) {
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
String s = value.trim().toLowerCase();
|
String normalized = value.trim().toLowerCase(Locale.ROOT);
|
||||||
return s.isEmpty() ? null : s;
|
return normalized.isEmpty() ? null : normalized;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
package server.sync;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
/** Клиент межсерверной доставки DM на единственный access-сервер получателя. */
|
||||||
|
public final class RemoteDmDeliveryClient {
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
|
public void receiveIncomingMessage(
|
||||||
|
String targetServerLogin,
|
||||||
|
String serverAddressRaw,
|
||||||
|
String incomingBlobB64,
|
||||||
|
String sourceServerLogin
|
||||||
|
) throws Exception {
|
||||||
|
String incomingJson = MAPPER.writeValueAsString(incomingBlobB64);
|
||||||
|
String sourceServerLoginJson = toOptionalJsonField("sourceServerLogin", sourceServerLogin);
|
||||||
|
JsonNode response = send(targetServerLogin, serverAddressRaw, """
|
||||||
|
{
|
||||||
|
"op":"ReceiveIncomingMessage",
|
||||||
|
"requestId":%s,
|
||||||
|
"payload":{
|
||||||
|
"incomingBlobB64":%s%s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".formatted("%s", incomingJson, sourceServerLoginJson));
|
||||||
|
ensureOk("ReceiveIncomingMessage", response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteMessage(String targetServerLogin, String serverAddressRaw, String blobB64) throws Exception {
|
||||||
|
String blobJson = MAPPER.writeValueAsString(blobB64);
|
||||||
|
JsonNode response = send(targetServerLogin, serverAddressRaw, """
|
||||||
|
{
|
||||||
|
"op":"DeleteMessage",
|
||||||
|
"requestId":%s,
|
||||||
|
"payload":{"blobB64":%s}
|
||||||
|
}
|
||||||
|
""".formatted("%s", blobJson));
|
||||||
|
ensureOk("DeleteMessage", response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteConversation(String targetServerLogin, String serverAddressRaw, String blobB64) throws Exception {
|
||||||
|
String blobJson = MAPPER.writeValueAsString(blobB64);
|
||||||
|
JsonNode response = send(targetServerLogin, serverAddressRaw, """
|
||||||
|
{
|
||||||
|
"op":"DeleteConversation",
|
||||||
|
"requestId":%s,
|
||||||
|
"payload":{"blobB64":%s}
|
||||||
|
}
|
||||||
|
""".formatted("%s", blobJson));
|
||||||
|
ensureOk("DeleteConversation", response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode send(String targetServerLogin, String serverAddressRaw, String jsonTemplate) throws Exception {
|
||||||
|
return ServerConnectionPool.getInstance().request(
|
||||||
|
targetServerLogin,
|
||||||
|
serverAddressRaw,
|
||||||
|
jsonTemplate,
|
||||||
|
ServerConnectionPool.Priority.REALTIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toOptionalJsonField(String fieldName, String value) throws Exception {
|
||||||
|
if (value == null || value.isBlank()) return "";
|
||||||
|
return ",\n \"" + fieldName + "\":" + MAPPER.writeValueAsString(value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureOk(String op, JsonNode response) {
|
||||||
|
int status = response.path("status").asInt(500);
|
||||||
|
if (status >= 200 && status < 300) return;
|
||||||
|
String code = response.path("code").asText("");
|
||||||
|
if (code.isBlank()) code = response.path("error").asText("");
|
||||||
|
throw new IllegalStateException(op + " failed: status=" + status + " code=" + code);
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1091,7 +1091,7 @@ public final class PostgresStorageRepository
|
|||||||
ELSE u.access_servers_json::jsonb
|
ELSE u.access_servers_json::jsonb
|
||||||
END
|
END
|
||||||
) WITH ORDINALITY AS access_server(login_value, ord)
|
) WITH ORDINALITY AS access_server(login_value, ord)
|
||||||
WHERE ord <= 2
|
WHERE ord = 1
|
||||||
) AS access_server
|
) AS access_server
|
||||||
JOIN solana_user_pda_current s
|
JOIN solana_user_pda_current s
|
||||||
ON LOWER(s.login) = LOWER(btrim(access_server.login_value))
|
ON LOWER(s.login) = LOWER(btrim(access_server.login_value))
|
||||||
@@ -1138,7 +1138,7 @@ public final class PostgresStorageRepository
|
|||||||
ELSE u.access_servers_json::jsonb
|
ELSE u.access_servers_json::jsonb
|
||||||
END
|
END
|
||||||
) WITH ORDINALITY AS access_server(login_value, ord)
|
) WITH ORDINALITY AS access_server(login_value, ord)
|
||||||
WHERE ord <= 2
|
WHERE ord = 1
|
||||||
) AS access_server
|
) AS access_server
|
||||||
WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login)
|
WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login)
|
||||||
LOOP
|
LOOP
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package server.sync;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import shine.db.entities.DmDeliveryStateEntry;
|
||||||
|
import utils.config.AppConfig;
|
||||||
|
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/** Пятисекундный воркер повторной доставки DM получателю. */
|
||||||
|
public final class PeriodicDmDeliveryService {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(PeriodicDmDeliveryService.class);
|
||||||
|
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
|
||||||
|
private static final ScheduledExecutorService SCHEDULER =
|
||||||
|
Executors.newSingleThreadScheduledExecutor(daemonThreadFactory("dm-worker-dispatcher"));
|
||||||
|
private static final ThreadPoolExecutor DELIVERY_EXECUTOR = new ThreadPoolExecutor(
|
||||||
|
4, 4, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(500),
|
||||||
|
daemonThreadFactory("dm-delivery"), new ThreadPoolExecutor.DiscardPolicy());
|
||||||
|
|
||||||
|
private PeriodicDmDeliveryService() {}
|
||||||
|
|
||||||
|
public static void startOrLog() {
|
||||||
|
if (!isEnabled()) {
|
||||||
|
log.info("DM delivery worker disabled by dm.delivery.enabled=false");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!STARTED.compareAndSet(false, true)) return;
|
||||||
|
long pollSeconds = configLong("dm.worker.pollSeconds", 5L, 1L, 60L);
|
||||||
|
SCHEDULER.scheduleWithFixedDelay(
|
||||||
|
PeriodicDmDeliveryService::tickSafe, 0L, pollSeconds, TimeUnit.SECONDS);
|
||||||
|
log.info("DM delivery worker scheduled every {} seconds", pollSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void tickSafe() {
|
||||||
|
try {
|
||||||
|
int limit = (int) configLong("dm.worker.dueLimit", 100L, 1L, 1000L);
|
||||||
|
for (DmDeliveryStateEntry row : DmDeliveryCoordinator.listDue(limit)) {
|
||||||
|
DELIVERY_EXECUTOR.execute(() -> DmDeliveryCoordinator.processDueEntry(row));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("DM delivery dispatcher failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isEnabled() {
|
||||||
|
String raw = AppConfig.getInstance().getParam("dm.delivery.enabled");
|
||||||
|
return raw == null || raw.isBlank() || Boolean.parseBoolean(raw.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long configLong(String key, long defaultValue, long min, long max) {
|
||||||
|
String raw = AppConfig.getInstance().getParam(key);
|
||||||
|
if (raw == null || raw.isBlank()) return defaultValue;
|
||||||
|
try { return Math.max(min, Math.min(max, Long.parseLong(raw.trim()))); }
|
||||||
|
catch (Exception ignored) { return defaultValue; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ThreadFactory daemonThreadFactory(String prefix) {
|
||||||
|
return new ThreadFactory() {
|
||||||
|
private int sequence;
|
||||||
|
@Override public synchronized Thread newThread(Runnable r) {
|
||||||
|
Thread t = new Thread(r, prefix + "-" + (++sequence));
|
||||||
|
t.setDaemon(true);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
package server.sync;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import server.logic.ws_protocol.JSON.messages.DmSyncApplySupport;
|
|
||||||
import shine.db.dao.UserAccessServersCurrentDAO;
|
|
||||||
import shine.db.dao.UserSettingsDAO;
|
|
||||||
import shine.db.dao.UserSettingsSyncPeerStateDAO;
|
|
||||||
import shine.db.entities.UserAccessServerRouteEntry;
|
|
||||||
import shine.db.entities.UserSettingEntry;
|
|
||||||
import shine.db.entities.UserSettingsSyncPeerStateEntry;
|
|
||||||
import utils.config.AppConfig;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.concurrent.ThreadFactory;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
public final class PeriodicUserSettingsSyncService {
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(PeriodicUserSettingsSyncService.class);
|
|
||||||
private static final String SERVER_LOGIN_CONFIG = "server.SHiNE.login";
|
|
||||||
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
|
|
||||||
private static final RemoteUserSettingsSyncClient REMOTE = new RemoteUserSettingsSyncClient();
|
|
||||||
private static final RemoteDmSyncClient DM_REMOTE = new RemoteDmSyncClient();
|
|
||||||
private static final UserAccessServersCurrentDAO ACCESS_DAO = UserAccessServersCurrentDAO.getInstance();
|
|
||||||
private static final UserSettingsDAO SETTINGS_DAO = UserSettingsDAO.getInstance();
|
|
||||||
private static final UserSettingsSyncPeerStateDAO STATE_DAO = UserSettingsSyncPeerStateDAO.getInstance();
|
|
||||||
|
|
||||||
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
|
|
||||||
@Override
|
|
||||||
public Thread newThread(Runnable r) {
|
|
||||||
Thread t = new Thread(r, "periodic-user-settings-sync");
|
|
||||||
t.setDaemon(true);
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
private PeriodicUserSettingsSyncService() {}
|
|
||||||
|
|
||||||
public static void startOrLog() {
|
|
||||||
if (!isEnabled()) {
|
|
||||||
log.info("Periodic user settings sync disabled by user.settings.sync.enabled=false");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!STARTED.compareAndSet(false, true)) return;
|
|
||||||
long initialDelaySec = configLong("user.settings.sync.initialDelaySeconds", 90L, 0L, 3600L);
|
|
||||||
long periodHours = configLong("user.settings.sync.periodHours", 6L, 1L, 168L);
|
|
||||||
EXECUTOR.scheduleWithFixedDelay(
|
|
||||||
PeriodicUserSettingsSyncService::runCycleSafe,
|
|
||||||
initialDelaySec,
|
|
||||||
TimeUnit.HOURS.toSeconds(periodHours),
|
|
||||||
TimeUnit.SECONDS
|
|
||||||
);
|
|
||||||
EXECUTOR.scheduleWithFixedDelay(
|
|
||||||
PeriodicUserSettingsSyncService::runRequestedCycleSafe,
|
|
||||||
5L, 5L, TimeUnit.SECONDS);
|
|
||||||
log.info("Periodic user settings sync scheduled: first run in {} seconds, then every {} hours", initialDelaySec, periodHours);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void runCycleSafe() {
|
|
||||||
try {
|
|
||||||
runCycle();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Periodic user settings sync failed unexpectedly", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void runRequestedCycleSafe() {
|
|
||||||
if (DmSyncWakeSignal.consume()) runCycleSafe();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void runCycle() throws Exception {
|
|
||||||
String ownServerLogin = normalize(AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG));
|
|
||||||
if (ownServerLogin == null) {
|
|
||||||
log.warn("Periodic user settings sync skipped: {} is empty", SERVER_LOGIN_CONFIG);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> ownersRaw = ACCESS_DAO.listUserLoginsByServerLogin(ownServerLogin);
|
|
||||||
Set<String> owners = new LinkedHashSet<>(ownersRaw);
|
|
||||||
if (owners.isEmpty()) {
|
|
||||||
log.info("Periodic user settings sync skipped: no local access-server users for {}", ownServerLogin);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int syncedPeers = 0;
|
|
||||||
int appliedItems = 0;
|
|
||||||
int pushedItems = 0;
|
|
||||||
int appliedDmItems = 0;
|
|
||||||
for (String ownerLogin : owners) {
|
|
||||||
List<UserAccessServerRouteEntry> routes = ACCESS_DAO.listByUserLogin(ownerLogin);
|
|
||||||
for (UserAccessServerRouteEntry route : routes) {
|
|
||||||
if (route == null) continue;
|
|
||||||
String remoteLogin = normalize(route.getServerLogin());
|
|
||||||
String remoteUrl = route.getServerUrl();
|
|
||||||
if (remoteLogin == null || remoteUrl == null || remoteUrl.isBlank()) continue;
|
|
||||||
if (remoteLogin.equals(ownServerLogin)) continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
SyncStats stats = syncOwnerWithRemote(ownerLogin, route);
|
|
||||||
appliedItems += stats.applied();
|
|
||||||
pushedItems += stats.pushed();
|
|
||||||
appliedDmItems += stats.appliedDm();
|
|
||||||
syncedPeers++;
|
|
||||||
} catch (Exception e) {
|
|
||||||
STATE_DAO.updateError(ownerLogin, route.getServerLogin(), remoteUrl, String.valueOf(e));
|
|
||||||
log.warn("Periodic user settings sync peer failed: owner={} remoteServer={} reason={}",
|
|
||||||
ownerLogin, route.getServerLogin(), String.valueOf(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Periodic access-data sync finished: owners={} peers={} settingsApplied={} settingsPushed={} dmApplied={}",
|
|
||||||
owners.size(), syncedPeers, appliedItems, pushedItems, appliedDmItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SyncStats syncOwnerWithRemote(String ownerLogin, UserAccessServerRouteEntry route) throws Exception {
|
|
||||||
int limit = (int) configLong("user.settings.sync.batchLimit", 500L, 1L, 1000L);
|
|
||||||
int maxBytes = (int) configLong("user.settings.sync.batchMaxBytes", 3_000_000L, 64_000L, 5_000_000L);
|
|
||||||
int maxPages = (int) configLong("user.settings.sync.maxPagesPerPeer", 50L, 1L, 500L);
|
|
||||||
|
|
||||||
UserSettingsSyncPeerStateEntry state;
|
|
||||||
try (Connection c = getDbConnection()) {
|
|
||||||
state = STATE_DAO.getOrCreate(c, ownerLogin, route.getServerLogin(), route.getServerUrl());
|
|
||||||
}
|
|
||||||
long cursorTimeMs = state.getCursorTimeMs();
|
|
||||||
String cursorSettingKey = state.getCursorSettingKey() == null ? "" : state.getCursorSettingKey();
|
|
||||||
int applied = 0;
|
|
||||||
int pushed = 0;
|
|
||||||
boolean bootstrapCompleted = false;
|
|
||||||
|
|
||||||
int appliedDm;
|
|
||||||
try (RemoteSyncSession session = new RemoteSyncSession(route.getServerLogin(), route.getServerUrl())) {
|
|
||||||
for (int page = 0; page < maxPages; page++) {
|
|
||||||
RemoteUserSettingsSyncClient.RemoteUserSettingsBatch batch = REMOTE.userSettingsSyncBatch(
|
|
||||||
session,
|
|
||||||
ownerLogin,
|
|
||||||
cursorTimeMs,
|
|
||||||
cursorSettingKey,
|
|
||||||
limit,
|
|
||||||
maxBytes
|
|
||||||
);
|
|
||||||
|
|
||||||
try (Connection c = getDbConnection()) {
|
|
||||||
for (RemoteUserSettingsSyncClient.RemoteUserSettingsItem item : batch.items()) {
|
|
||||||
UserSettingEntry entry = new UserSettingEntry(
|
|
||||||
item.login(),
|
|
||||||
item.settingType(),
|
|
||||||
item.settingKey(),
|
|
||||||
item.timeMs(),
|
|
||||||
item.valueText(),
|
|
||||||
item.valueNum(),
|
|
||||||
item.clientKey(),
|
|
||||||
item.signature(),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
int changed = SETTINGS_DAO.upsertIfNewer(c, entry);
|
|
||||||
if (changed > 0) applied++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cursorTimeMs = Math.max(cursorTimeMs, batch.nextTimeMs());
|
|
||||||
cursorSettingKey = batch.nextSettingKey() == null ? "" : batch.nextSettingKey();
|
|
||||||
bootstrapCompleted = !batch.hasMore();
|
|
||||||
STATE_DAO.updateSuccess(ownerLogin, route.getServerLogin(), route.getServerUrl(), cursorTimeMs, cursorSettingKey, bootstrapCompleted);
|
|
||||||
|
|
||||||
if (!batch.hasMore() || batch.items().isEmpty()) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
try (Connection c = getDbConnection()) {
|
|
||||||
List<UserSettingEntry> unsynced = SETTINGS_DAO.listUnsyncedByLogin(c, ownerLogin, limit);
|
|
||||||
for (UserSettingEntry entry : unsynced) {
|
|
||||||
REMOTE.upsertUserSetting(session, entry, true);
|
|
||||||
SETTINGS_DAO.markSynced(c, entry.getLogin(), entry.getSettingType(), entry.getSettingKey());
|
|
||||||
pushed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int dmLimit = (int) configLong("dm.sync.batchLimit", 200L, 1L, 500L);
|
|
||||||
int dmMaxBytes = (int) configLong("dm.sync.batchMaxBytes", 3_000_000L, 64_000L, 5_000_000L);
|
|
||||||
int dmMaxPages = (int) configLong("dm.sync.maxPagesPerPeer", 20L, 1L, 500L);
|
|
||||||
appliedDm = syncDmInSameSession(session, ownerLogin, dmLimit, dmMaxBytes, dmMaxPages);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bootstrapCompleted) {
|
|
||||||
log.info("Periodic user settings sync peer paused by page limit: owner={} remoteServer={} maxPages={}",
|
|
||||||
ownerLogin, route.getServerLogin(), maxPages);
|
|
||||||
}
|
|
||||||
return new SyncStats(applied, pushed, appliedDm);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int syncDmInSameSession(
|
|
||||||
RemoteSyncSession session, String ownerLogin, int limit, int maxBytes, int maxPages
|
|
||||||
) throws Exception {
|
|
||||||
long cursorMs = 0L;
|
|
||||||
String cursorKey = "";
|
|
||||||
List<String> acknowledgements = new ArrayList<>();
|
|
||||||
int applied = 0;
|
|
||||||
for (int page = 0; page < maxPages; page++) {
|
|
||||||
RemoteDmSyncClient.RemoteDmBatch batch = DM_REMOTE.dmSyncBatch(
|
|
||||||
session, ownerLogin, cursorMs, cursorKey, Math.min(limit, 500), maxBytes, acknowledgements);
|
|
||||||
acknowledgements = new ArrayList<>();
|
|
||||||
for (RemoteDmSyncClient.RemoteDmItem item : batch.items()) {
|
|
||||||
if (item == null || item.syncId() == null || item.syncId().isBlank()) continue;
|
|
||||||
DmSyncApplySupport.applySyncedItem(ownerLogin, item.syncId(), item.blobsB64());
|
|
||||||
acknowledgements.add(item.syncId());
|
|
||||||
applied++;
|
|
||||||
}
|
|
||||||
cursorMs = batch.nextStoredAtMs();
|
|
||||||
cursorKey = batch.nextMessageKey() == null ? "" : batch.nextMessageKey();
|
|
||||||
if (!batch.hasMore() || batch.items().isEmpty()) {
|
|
||||||
if (!acknowledgements.isEmpty()) {
|
|
||||||
DM_REMOTE.dmSyncBatch(session, ownerLogin, 0L, "",
|
|
||||||
Math.min(limit, 500), maxBytes, acknowledgements);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return applied;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static java.sql.Connection getDbConnection() throws Exception {
|
|
||||||
return shine.db.DbController.getInstance().getConnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isEnabled() {
|
|
||||||
String raw = AppConfig.getInstance().getParam("user.settings.sync.enabled");
|
|
||||||
return raw == null || raw.isBlank() || Boolean.parseBoolean(raw.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long configLong(String key, long defaultValue, long min, long max) {
|
|
||||||
String raw = AppConfig.getInstance().getParam(key);
|
|
||||||
if (raw == null || raw.isBlank()) return defaultValue;
|
|
||||||
try {
|
|
||||||
long parsed = Long.parseLong(raw.trim());
|
|
||||||
return Math.max(min, Math.min(max, parsed));
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String normalize(String value) {
|
|
||||||
if (value == null) return null;
|
|
||||||
String s = value.trim().toLowerCase(Locale.ROOT);
|
|
||||||
return s.isEmpty() ? null : s;
|
|
||||||
}
|
|
||||||
|
|
||||||
private record SyncStats(int applied, int pushed, int appliedDm) {}
|
|
||||||
}
|
|
||||||
@@ -8,8 +8,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
import server.debug.DebugApiConfigurator;
|
import server.debug.DebugApiConfigurator;
|
||||||
import server.sync.BlockchainResyncRecoveryOnStartup;
|
import server.sync.BlockchainResyncRecoveryOnStartup;
|
||||||
import server.sync.PeriodicBlockchainSyncService;
|
import server.sync.PeriodicBlockchainSyncService;
|
||||||
import server.sync.PeriodicDmSyncService;
|
import server.sync.PeriodicDmDeliveryService;
|
||||||
import server.sync.PeriodicUserSettingsSyncService;
|
|
||||||
import server.sync.SolanaUsersSyncStartupService;
|
import server.sync.SolanaUsersSyncStartupService;
|
||||||
import server.sync.SyncServersBootstrapService;
|
import server.sync.SyncServersBootstrapService;
|
||||||
import server.sync.ServerConnectionPool;
|
import server.sync.ServerConnectionPool;
|
||||||
@@ -109,8 +108,7 @@ public final class WsServer {
|
|||||||
Runtime.getRuntime().addShutdownHook(new Thread(
|
Runtime.getRuntime().addShutdownHook(new Thread(
|
||||||
() -> ServerConnectionPool.getInstance().close(),
|
() -> ServerConnectionPool.getInstance().close(),
|
||||||
"server-connection-pool-shutdown"));
|
"server-connection-pool-shutdown"));
|
||||||
PeriodicDmSyncService.startOrLog();
|
PeriodicDmDeliveryService.startOrLog();
|
||||||
PeriodicUserSettingsSyncService.startOrLog();
|
|
||||||
server.join();
|
server.join();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,13 +34,10 @@ sync.importUserProfileFromPartner.enabled=false
|
|||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
server.version=${projectVersion}
|
server.version=${projectVersion}
|
||||||
|
|
||||||
# Доставка и межсерверная синхронизация личных сообщений.
|
# Доставка личных сообщений на единственный access-сервер получателя.
|
||||||
dm.sync.enabled=true
|
dm.delivery.enabled=true
|
||||||
dm.worker.pollSeconds=5
|
dm.worker.pollSeconds=5
|
||||||
dm.worker.dueLimit=100
|
dm.worker.dueLimit=100
|
||||||
dm.sync.batchLimit=200
|
|
||||||
dm.sync.batchMaxBytes=3000000
|
|
||||||
dm.sync.maxPagesPerPeer=20
|
|
||||||
server.pool.pingIdleSeconds=120
|
server.pool.pingIdleSeconds=120
|
||||||
server.pool.pongTimeoutSeconds=15
|
server.pool.pongTimeoutSeconds=15
|
||||||
server.pool.requestTimeoutSeconds=12
|
server.pool.requestTimeoutSeconds=12
|
||||||
|
|||||||
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
client.version=1.7.0
|
client.version=1.12.0
|
||||||
server.version=1.6.1
|
server.version=1.10.0
|
||||||
|
|||||||
Executable
+300
@@ -0,0 +1,300 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# Build an offline-ready source bundle ZIP.
|
||||||
|
# In addition to the normal source tree, this variant can attach a local
|
||||||
|
# Gradle distribution zip and a helper script that rewrites wrapper URLs to
|
||||||
|
# that local file so the bundle can be used without internet access.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./bundle-offline.sh
|
||||||
|
# ./bundle-offline.sh path/to/output.zip
|
||||||
|
#
|
||||||
|
# Expected local asset:
|
||||||
|
# offline/gradle-offline.zip
|
||||||
|
# or a custom path via BUNDLE_OFFLINE_GRADLE_ZIP
|
||||||
|
|
||||||
|
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
OUT="${1:-SHiNE-bundle-offline-$(date +%Y%m%d-%H%M%S).zip}"
|
||||||
|
case "$OUT" in
|
||||||
|
/*) ;;
|
||||||
|
*) OUT="$ROOT/$OUT" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if ! command -v zip >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: 'zip' is required." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP="$(mktemp -d)"
|
||||||
|
LIST="$TMP/files.txt"
|
||||||
|
SAFE_LIST="$TMP/safe-files.txt"
|
||||||
|
STAGE="$TMP/stage"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
mkdir -p "$STAGE"
|
||||||
|
|
||||||
|
# Paths / filenames that must never be bundled.
|
||||||
|
is_denied_path() {
|
||||||
|
local p="/$1"
|
||||||
|
|
||||||
|
case "$p" in
|
||||||
|
*/.git/*|*/.git|\
|
||||||
|
*/.gradle/*|*/.gradle|\
|
||||||
|
*/.gradle-home/*|*/.gradle-home|\
|
||||||
|
*/.idea/*|*/.idea|\
|
||||||
|
*/.vscode/*|*/.vscode|\
|
||||||
|
*/node_modules/*|*/node_modules|\
|
||||||
|
*/target/*|*/target|\
|
||||||
|
*/build/*|*/build|\
|
||||||
|
*/out/*|*/out|\
|
||||||
|
*/bin/*|*/bin|\
|
||||||
|
*/logs/*|*/logs|\
|
||||||
|
*/data/*|*/data|\
|
||||||
|
*/test-ledger/*|*/test-ledger|\
|
||||||
|
*/.anchor/*|*/.anchor|\
|
||||||
|
*/.yarn/*|*/.yarn|\
|
||||||
|
*/.vendor/*|*/.vendor|\
|
||||||
|
*/.agents/*|*/.agents|\
|
||||||
|
*/.codex/*|*/.codex|\
|
||||||
|
*/.claude/*|*/.claude|\
|
||||||
|
*/deploy/backup/archive/*|\
|
||||||
|
*/scripts/*/runs/*|\
|
||||||
|
*/scripts/*/keypairs/*|\
|
||||||
|
*/keys/*|\
|
||||||
|
*/.git-local-backup/*|\
|
||||||
|
*/SHiNE-bundle-*.zip|\
|
||||||
|
*/bundle-offline*.zip)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local base="${p##*/}"
|
||||||
|
local lower
|
||||||
|
lower="$(printf '%s' "$base" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
|
||||||
|
case "$lower" in
|
||||||
|
.env|.env.*|\
|
||||||
|
.debug-token|\
|
||||||
|
.npmrc|.pypirc|.netrc|\
|
||||||
|
credentials|credentials.*|\
|
||||||
|
secrets|secrets.*|\
|
||||||
|
secret|secret.*|\
|
||||||
|
id_rsa|id_dsa|id_ecdsa|id_ed25519|\
|
||||||
|
*.pem|*.key|*.p12|*.pfx|*.jks|*.keystore|\
|
||||||
|
*keypair*.json|\
|
||||||
|
service-account*.json|\
|
||||||
|
firebase-adminsdk*.json|\
|
||||||
|
google-services.json|\
|
||||||
|
validator.log)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$lower" in
|
||||||
|
*.class|*.jar|*.war|*.ear|*.o|*.a|*.so|*.dll|*.dylib|\
|
||||||
|
*.elf|*.map|*.uf2|*.bin|*.merged.bin|\
|
||||||
|
*.log|*.bak|*.bak.png|*.tmp|*.swp|*.swo|\
|
||||||
|
.ds_store)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
find_offline_gradle_zip() {
|
||||||
|
local candidate="${BUNDLE_OFFLINE_GRADLE_ZIP:-}"
|
||||||
|
if [[ -n "$candidate" && -f "$candidate" ]]; then
|
||||||
|
printf '%s\n' "$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for candidate in \
|
||||||
|
"$ROOT/offline/gradle-offline.zip" \
|
||||||
|
"$ROOT/offline/gradle-8.14-bin.zip" \
|
||||||
|
"$ROOT/offline/gradle.zip"
|
||||||
|
do
|
||||||
|
if [[ -f "$candidate" ]]; then
|
||||||
|
printf '%s\n' "$candidate"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
create_offline_helper() {
|
||||||
|
local zip_name="$1"
|
||||||
|
local helper="$STAGE/offline/prepare-local-gradle.sh"
|
||||||
|
local readme="$STAGE/offline/README.txt"
|
||||||
|
|
||||||
|
mkdir -p "$STAGE/offline"
|
||||||
|
|
||||||
|
cat > "$helper" <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||||
|
ZIP_PATH="\${1:-\$ROOT/offline/$zip_name}"
|
||||||
|
|
||||||
|
if [[ ! -f "\$ZIP_PATH" ]]; then
|
||||||
|
echo "ERROR: offline Gradle zip not found: \$ZIP_PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ABS_ZIP="\$(cd -- "\$(dirname -- "\$ZIP_PATH")" && pwd -P)/\$(basename -- "\$ZIP_PATH")"
|
||||||
|
ESCAPED_ABS_ZIP="\${ABS_ZIP//\\\\/\\\\\\\\}"
|
||||||
|
ESCAPED_ABS_ZIP="\${ESCAPED_ABS_ZIP//&/\\\\&}"
|
||||||
|
ESCAPED_ABS_ZIP="\${ESCAPED_ABS_ZIP//|/\\\\|}"
|
||||||
|
|
||||||
|
while IFS= read -r props; do
|
||||||
|
[[ -f "\$props" ]] || continue
|
||||||
|
cp -p "\$props" "\$props.bak"
|
||||||
|
sed -i -e "s|^distributionUrl=.*\$|distributionUrl=file://\$ESCAPED_ABS_ZIP|" "\$props"
|
||||||
|
done < <(find "\$ROOT" -path '*/gradle/wrapper/gradle-wrapper.properties' -type f | sort)
|
||||||
|
|
||||||
|
cat <<'MSG'
|
||||||
|
Gradle wrapper URLs rewritten to the local offline zip.
|
||||||
|
Run now:
|
||||||
|
./gradlew --offline test
|
||||||
|
MSG
|
||||||
|
EOF
|
||||||
|
chmod +x "$helper"
|
||||||
|
|
||||||
|
cat > "$readme" <<EOF
|
||||||
|
Offline Gradle helper
|
||||||
|
|
||||||
|
Included archive:
|
||||||
|
offline/$zip_name
|
||||||
|
|
||||||
|
Helper:
|
||||||
|
offline/prepare-local-gradle.sh
|
||||||
|
|
||||||
|
What it does:
|
||||||
|
- backs up each gradle-wrapper.properties as .bak
|
||||||
|
- rewrites wrapper distributionUrl to the local zip in this bundle
|
||||||
|
|
||||||
|
Recommended flow after unpacking:
|
||||||
|
1. cd into the unpacked bundle root
|
||||||
|
2. run ./offline/prepare-local-gradle.sh
|
||||||
|
3. run ./gradlew --offline test
|
||||||
|
|
||||||
|
This bundle is intended for local, network-free verification.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Collect files. Prefer Git because it naturally avoids most ignored local files.
|
||||||
|
if command -v git >/dev/null 2>&1 && git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
git -C "$ROOT" ls-files -co --exclude-standard -z > "$TMP/files.z"
|
||||||
|
else
|
||||||
|
find "$ROOT" -type f -print0 > "$TMP/files.z"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Convert to project-relative paths and enforce hard deny rules.
|
||||||
|
: > "$LIST"
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
if [[ "$f" = /* ]]; then
|
||||||
|
rel="${f#"$ROOT"/}"
|
||||||
|
else
|
||||||
|
rel="$f"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "$rel" == "$OUT" ]] && continue
|
||||||
|
[[ -z "$rel" ]] && continue
|
||||||
|
|
||||||
|
if is_denied_path "$rel"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$rel" >> "$LIST"
|
||||||
|
done < "$TMP/files.z"
|
||||||
|
|
||||||
|
sort -u "$LIST" -o "$LIST"
|
||||||
|
|
||||||
|
# Always include Gradle wrapper bootstrap, even though generic JARs are denied.
|
||||||
|
for wrapper_jar in \
|
||||||
|
'SHiNE-server/gradle/wrapper/gradle-wrapper.jar' \
|
||||||
|
'SHiNE-browser-plugin-wallet/gradle/wrapper/gradle-wrapper.jar'
|
||||||
|
do
|
||||||
|
if [[ -f "$ROOT/$wrapper_jar" ]] && ! grep -Fxq "$wrapper_jar" "$LIST"; then
|
||||||
|
printf '%s\n' "$wrapper_jar" >> "$LIST"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
sort -u "$LIST" -o "$LIST"
|
||||||
|
|
||||||
|
# Content scan: fail closed on common credential/private-key patterns.
|
||||||
|
# We scan only text-ish files; grep -I skips binary data.
|
||||||
|
SECRET_RE='-----BEGIN ([A-Z0-9 ]+ )?PRIVATE KEY-----|AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{30,}|(^|[^A-Za-z0-9])(password|passwd|pwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token|private[_-]?key)[[:space:]]*[:=][[:space:]]*["'\'']?[^${[:space:]]{][^[:space:]]{7,}'
|
||||||
|
|
||||||
|
: > "$SAFE_LIST"
|
||||||
|
found_secret=0
|
||||||
|
|
||||||
|
while IFS= read -r rel; do
|
||||||
|
[[ -f "$ROOT/$rel" ]] || continue
|
||||||
|
|
||||||
|
# Files that contain examples/templates can legitimately mention secret keys
|
||||||
|
# with placeholders. They are scanned too, but placeholder-looking values
|
||||||
|
# are less likely to match the regex above.
|
||||||
|
if LC_ALL=C grep -IEnq "$SECRET_RE" "$ROOT/$rel" 2>/dev/null; then
|
||||||
|
echo "BLOCKED: possible secret in $rel" >&2
|
||||||
|
LC_ALL=C grep -IEn "$SECRET_RE" "$ROOT/$rel" 2>/dev/null \
|
||||||
|
| sed -E 's/(:[[:space:]]*).*/\1[REDACTED]/' \
|
||||||
|
| head -n 3 >&2 || true
|
||||||
|
found_secret=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$rel" >> "$SAFE_LIST"
|
||||||
|
done < "$LIST"
|
||||||
|
|
||||||
|
if (( found_secret != 0 )); then
|
||||||
|
echo >&2
|
||||||
|
echo "Bundle NOT created because possible secrets were detected." >&2
|
||||||
|
echo "Move secrets to ignored/local files or adjust the scanner only after review." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -s "$SAFE_LIST" ]]; then
|
||||||
|
echo "ERROR: no files left to bundle." >&2
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
OFFLINE_ZIP_SRC=""
|
||||||
|
OFFLINE_ZIP_NAME=""
|
||||||
|
if OFFLINE_ZIP_SRC="$(find_offline_gradle_zip)"; then
|
||||||
|
OFFLINE_ZIP_NAME="gradle-offline.zip"
|
||||||
|
else
|
||||||
|
echo "ERROR: offline Gradle zip not found." >&2
|
||||||
|
echo "Place it at ./offline/gradle-offline.zip or set BUNDLE_OFFLINE_GRADLE_ZIP." >&2
|
||||||
|
echo "The bundle is not created because this variant is meant to be offline-ready." >&2
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$STAGE"
|
||||||
|
mkdir -p "$STAGE"
|
||||||
|
|
||||||
|
while IFS= read -r rel; do
|
||||||
|
src="$ROOT/$rel"
|
||||||
|
dst="$STAGE/$rel"
|
||||||
|
mkdir -p "$(dirname -- "$dst")"
|
||||||
|
cp -p "$src" "$dst"
|
||||||
|
done < "$SAFE_LIST"
|
||||||
|
|
||||||
|
mkdir -p "$STAGE/offline"
|
||||||
|
cp -p "$OFFLINE_ZIP_SRC" "$STAGE/offline/$OFFLINE_ZIP_NAME"
|
||||||
|
create_offline_helper "$OFFLINE_ZIP_NAME"
|
||||||
|
|
||||||
|
rm -f -- "$OUT"
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "$STAGE"
|
||||||
|
find . -type f -print | sort | zip -q -9 "$OUT" -@
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Created: $OUT"
|
||||||
|
echo "Files: $(cd "$STAGE" && find . -type f | wc -l | tr -d ' ')"
|
||||||
|
echo "Size: $(du -h "$OUT" | awk '{print $1}')"
|
||||||
@@ -124,6 +124,14 @@ done < "$TMP/files.z"
|
|||||||
|
|
||||||
sort -u "$LIST" -o "$LIST"
|
sort -u "$LIST" -o "$LIST"
|
||||||
|
|
||||||
|
# Always include Gradle wrapper bootstrap, even though generic JARs are denied.
|
||||||
|
if [[ -f "$ROOT/SHiNE-server/gradle/wrapper/gradle-wrapper.jar" ]]; then
|
||||||
|
if ! grep -Fxq 'SHiNE-server/gradle/wrapper/gradle-wrapper.jar' "$LIST"; then
|
||||||
|
printf '%s\n' 'SHiNE-server/gradle/wrapper/gradle-wrapper.jar' >> "$LIST"
|
||||||
|
sort -u "$LIST" -o "$LIST"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Content scan: fail closed on common credential/private-key patterns.
|
# Content scan: fail closed on common credential/private-key patterns.
|
||||||
# We scan only text-ish files; grep -I skips binary data.
|
# We scan only text-ish files; grep -I skips binary data.
|
||||||
SECRET_RE='-----BEGIN ([A-Z0-9 ]+ )?PRIVATE KEY-----|AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{30,}|(^|[^A-Za-z0-9])(password|passwd|pwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token|private[_-]?key)[[:space:]]*[:=][[:space:]]*["'\'']?[^${[:space:]]{][^[:space:]]{7,}'
|
SECRET_RE='-----BEGIN ([A-Z0-9 ]+ )?PRIVATE KEY-----|AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{30,}|(^|[^A-Za-z0-9])(password|passwd|pwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token|private[_-]?key)[[:space:]]*[:=][[:space:]]*["'\'']?[^${[:space:]]{][^[:space:]]{7,}'
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
Здесь четыре базовых метода обычной авторизации:
|
Здесь четыре базовых метода обычной авторизации:
|
||||||
|
|
||||||
|
- `ResolveLoginForAuth`
|
||||||
- `AuthChallenge`
|
- `AuthChallenge`
|
||||||
- `CreateAuthSession`
|
- `CreateAuthSession`
|
||||||
- `SessionChallenge`
|
- `SessionChallenge`
|
||||||
@@ -67,6 +68,57 @@ ed25519/BASE64_PUBLIC_KEY
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 1.1. `ResolveLoginForAuth`
|
||||||
|
|
||||||
|
Первый шаг интерактивного входа. Операция не заменяет `GetUser` и не меняет его семантику.
|
||||||
|
Она предназначена только для выбора правильного access server перед вводом пароля
|
||||||
|
или запуском входа через доверенное устройство.
|
||||||
|
|
||||||
|
Запрос:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ResolveLoginForAuth",
|
||||||
|
"requestId": "login-route-1",
|
||||||
|
"payload": {
|
||||||
|
"login": "alice"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Успешный ответ всегда имеет `status=200`, а `payload.resolution` принимает одно из значений:
|
||||||
|
|
||||||
|
- `LOCAL` — пользователь существует, и `access_servers[0]` указывает на этот сервер;
|
||||||
|
- `REMOTE` — пользователь существует, но его первый access server другой;
|
||||||
|
- `NOT_FOUND` — пользователя нет в локально синхронизированном Solana PDA snapshot;
|
||||||
|
- `NO_ACCESS_SERVER` — пользователь существует, но корректный первый access server не найден.
|
||||||
|
|
||||||
|
Для `LOCAL` и `REMOTE` сервер возвращает канонический `login`. Для `REMOTE`
|
||||||
|
также возвращаются `accessServerLogin` и `accessServerUrl`, чтобы UI сразу мог
|
||||||
|
перенаправить пользователя на правильную точку входа.
|
||||||
|
|
||||||
|
Пример `REMOTE`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ResolveLoginForAuth",
|
||||||
|
"requestId": "login-route-1",
|
||||||
|
"status": 200,
|
||||||
|
"ok": true,
|
||||||
|
"payload": {
|
||||||
|
"resolution": "REMOTE",
|
||||||
|
"login": "Alice",
|
||||||
|
"accessServerLogin": "shine-node-2",
|
||||||
|
"accessServerUrl": "wss://node2.example.org/ws"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Важно: это UX-проверка, а не security boundary. `AuthChallenge/CreateAuthSession`
|
||||||
|
повторно определяют отношение пользователя к текущему серверу. Пользователь другого
|
||||||
|
access server может криптографически авторизоваться напрямую, но получает
|
||||||
|
`connectionScope=REMOTE`.
|
||||||
|
|
||||||
## 2. `AuthChallenge`
|
## 2. `AuthChallenge`
|
||||||
|
|
||||||
### Запрос
|
### Запрос
|
||||||
@@ -339,3 +391,33 @@ SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### LOCAL/REMOTE scope успешной авторизации
|
||||||
|
|
||||||
|
`CreateAuthSession` и `SessionLogin` дополнительно возвращают:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"connectionScope": "LOCAL"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
или:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"connectionScope": "REMOTE"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`REMOTE` означает, что пользователь успешно доказал владение ключом/сессией, но
|
||||||
|
текущий сервер не является его `access_servers[0]`. Такой WebSocket регистрируется
|
||||||
|
как активное соединение и может использоваться для realtime звонков, однако
|
||||||
|
центральная политика сервера разрешает ему только технические и call-signaling
|
||||||
|
операции. Обычные homeserver-операции отвечают `403 / USER_NOT_LOCAL`.
|
||||||
|
|
||||||
|
Разрешённый минимальный набор для `REMOTE`: `Ping`, `GetServerInfo`,
|
||||||
|
`GetCallIceConfig`, `CallInviteBroadcast`, `CallSignalToSession`, `SendSignal`,
|
||||||
|
`CallDeliveryReport`, `ClientErrorLog`, `ClientDebugLog`, `CloseActiveSession`
|
||||||
|
и `ResolveLoginForAuth`.
|
||||||
|
|
||||||
|
|||||||
@@ -313,9 +313,14 @@ TTL заявки фиксирован на сервере и сейчас все
|
|||||||
- `400 / BAD_PAYLOAD_TYPE`
|
- `400 / BAD_PAYLOAD_TYPE`
|
||||||
- `422 / PAIRING_NOT_AVAILABLE`
|
- `422 / PAIRING_NOT_AVAILABLE`
|
||||||
- `422 / PAIRING_PASSWORD_INVALID` — pairing-пароль не подходит. Та же ошибка возвращается и если новое устройство ввело пароль, а у пользователя режим pairing включён без пароля.
|
- `422 / PAIRING_PASSWORD_INVALID` — pairing-пароль не подходит. Та же ошибка возвращается и если новое устройство ввело пароль, а у пользователя режим pairing включён без пароля.
|
||||||
- `422 / PAIRING_NO_TRUSTED_SESSION_ONLINE` — сейчас нет ни одной онлайн доверённой сессии пользователя, поэтому код не создаётся.
|
Код создаётся даже если сейчас нет ни одной онлайн доверённой сессии пользователя.
|
||||||
|
В таком случае `trustedSessionOnline=false`, а pairing-заявка остаётся в состоянии
|
||||||
|
`created` до подтверждения, отмены или истечения TTL.
|
||||||
- `429 / PAIRING_RATE_LIMITED`
|
- `429 / PAIRING_RATE_LIMITED`
|
||||||
|
|
||||||
|
Также `StartTrustedDeviceLogin` разрешён только на первом access server пользователя.
|
||||||
|
На другом сервере операция возвращает `409 / USER_NOT_LOCAL`.
|
||||||
|
|
||||||
### 5.4. `ListTrustedDeviceLoginRequests`
|
### 5.4. `ListTrustedDeviceLoginRequests`
|
||||||
|
|
||||||
Доступно для любой уже авторизованной доверенной сессии пользователя.
|
Доступно для любой уже авторизованной доверенной сессии пользователя.
|
||||||
|
|||||||
@@ -24,8 +24,9 @@
|
|||||||
|
|
||||||
> На первом этапе мы **не используем курсоры** (`nextCursor`) и загружаем полные списки.
|
> На первом этапе мы **не используем курсоры** (`nextCursor`) и загружаем полные списки.
|
||||||
>
|
>
|
||||||
> `unreadCount` для канала считается по `user_settings`:
|
> `unreadCount` для канала считается по `user_settings`.
|
||||||
> если для пары `ownerBlockchainName/channelName` ещё нет записи, канал считается полностью прочитанным, а UI при первом открытии записывает текущий курсор чтения.
|
> Для собственных каналов владельцу всегда возвращается `unreadCount = 0`, чтобы его собственные публикации не становились «новыми» для него самого.
|
||||||
|
> Если для пары `ownerBlockchainName/channelName` ещё нет записи, канал временно считается полностью прочитанным; UI при загрузке списка каналов создаёт baseline на текущем `messagesCount`. После этого новые публикации увеличивают `unreadCount` до продвижения курсора чтения.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -416,3 +417,31 @@
|
|||||||
- `limit_too_large`
|
- `limit_too_large`
|
||||||
- `channel_name_already_exists`
|
- `channel_name_already_exists`
|
||||||
- `internal_error`
|
- `internal_error`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Profile public-channel lists (v18)
|
||||||
|
|
||||||
|
### `ListUserProfileChannels`
|
||||||
|
|
||||||
|
Returns lightweight public-channel cards for profile counters. `mode` is `owned` or `following`; only `channel_type_code=1` is returned.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ListUserProfileChannels",
|
||||||
|
"requestId": "profile-ch-1",
|
||||||
|
"payload": { "login": "Alice", "mode": "owned", "limit": 100, "offset": 0 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Channel items contain `ownerLogin`, `slug`, `displayName`, `avatarAr`, `ownerBlockchainName`, `rootBlockNumber`, and `rootBlockHashHex`.
|
||||||
|
|
||||||
|
## Qualified likes (v18)
|
||||||
|
|
||||||
|
Channel/message responses expose three counters:
|
||||||
|
|
||||||
|
- `likesCount` — all active likes;
|
||||||
|
- `primaryLikesCount` — likes from users whose current `account_role=primary`;
|
||||||
|
- `shiningLikesCount` — likes from users who are currently both `primary` and `shining`.
|
||||||
|
|
||||||
|
Changing `account_role` or `shine` does not alter the raw reaction. The two qualified counters are recalculated from `reactions_state`; therefore old likes automatically enter or leave qualified counters when the actor's current status changes.
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
| `SearchUsers` | `01_User_Registration_API.md` | поиск логинов по префиксу |
|
| `SearchUsers` | `01_User_Registration_API.md` | поиск логинов по префиксу |
|
||||||
| `TestGetFreeAvatarQuota` | `14_Test_Free_Avatar_Upload_API.md` | временный тестовый просмотр остатка бесплатных загрузок аватара |
|
| `TestGetFreeAvatarQuota` | `14_Test_Free_Avatar_Upload_API.md` | временный тестовый просмотр остатка бесплатных загрузок аватара |
|
||||||
| `TestUploadFreeAvatar` | `14_Test_Free_Avatar_Upload_API.md` | временная тестовая бесплатная загрузка маленького аватара в Arweave |
|
| `TestUploadFreeAvatar` | `14_Test_Free_Avatar_Upload_API.md` | временная тестовая бесплатная загрузка маленького аватара в Arweave |
|
||||||
|
| `ResolveLoginForAuth` | `02_Authentication_API.md` | проверка login перед входом: LOCAL / REMOTE / NOT_FOUND / NO_ACCESS_SERVER + URL правильного access server |
|
||||||
| `AuthChallenge` | `02_Authentication_API.md` | challenge для создания новой сессии |
|
| `AuthChallenge` | `02_Authentication_API.md` | challenge для создания новой сессии |
|
||||||
| `CreateAuthSession` | `02_Authentication_API.md` | создание новой авторизованной сессии |
|
| `CreateAuthSession` | `02_Authentication_API.md` | создание новой авторизованной сессии |
|
||||||
| `SessionChallenge` | `02_Authentication_API.md` | challenge для входа в существующую сессию |
|
| `SessionChallenge` | `02_Authentication_API.md` | challenge для входа в существующую сессию |
|
||||||
@@ -59,18 +60,14 @@
|
|||||||
| `ListContacts` | `11_Connections_API.md` | контакты текущего пользователя |
|
| `ListContacts` | `11_Connections_API.md` | контакты текущего пользователя |
|
||||||
| `GetUserConnectionsGraph` | `11_Connections_API.md` | граф связей пользователя |
|
| `GetUserConnectionsGraph` | `11_Connections_API.md` | граф связей пользователя |
|
||||||
| `AddCloseFriend` | `11_Connections_API.md` | добавить близкого друга |
|
| `AddCloseFriend` | `11_Connections_API.md` | добавить близкого друга |
|
||||||
| `GetNotifications` | `15_Notifications_API.md` | ответы и события уведомлений |
|
| `GetNotifications` | `15_Notifications_API.md` | ответы, связи, события и unread-watermark |
|
||||||
|
| `SetNotificationState` | `15_Notifications_API.md` | подписанное состояние просмотра уведомлений |
|
||||||
| `UpsertPushToken` | `12_Direct_Messages_Push_Calls_API.md` | регистрация WebPush-токена |
|
| `UpsertPushToken` | `12_Direct_Messages_Push_Calls_API.md` | регистрация WebPush-токена |
|
||||||
| `SendTestWebPush` | `12_Direct_Messages_Push_Calls_API.md` | тестовая push-доставка |
|
| `SendTestWebPush` | `12_Direct_Messages_Push_Calls_API.md` | тестовая push-доставка |
|
||||||
| `SendMessagePair` | `12_Direct_Messages_Push_Calls_API.md` | отправка пары входящий/исходящий DM |
|
| `SendMessagePair` | `12_Direct_Messages_Push_Calls_API.md` | отправка пары входящий/исходящий DM |
|
||||||
| `ReceiveOutcomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | старая полная DM-пара для второго access-сервера отправителя |
|
| `ReceiveIncomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | внутренняя доставка входящего DM-блока на единственный сервер получателя |
|
||||||
| `ReceiveIncomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | прием входящего DM-блока |
|
|
||||||
| `DeleteMessage` | `12_Direct_Messages_Push_Calls_API.md` | tombstone одного личного сообщения у обеих сторон |
|
| `DeleteMessage` | `12_Direct_Messages_Push_Calls_API.md` | tombstone одного личного сообщения у обеих сторон |
|
||||||
| `DeleteConversation` | `12_Direct_Messages_Push_Calls_API.md` | tombstone удаления истории переписки |
|
| `DeleteConversation` | `12_Direct_Messages_Push_Calls_API.md` | tombstone удаления истории переписки |
|
||||||
| `DmSyncBatch` | `12_Direct_Messages_Push_Calls_API.md` | pull событий `synced=false` с ACK предыдущей страницы |
|
|
||||||
| `GetDmDeliveryStatus` | `12_Direct_Messages_Push_Calls_API.md` | read-only статус доставки по существующему `messageKey` |
|
|
||||||
| `UserSettingsSyncBatch` | `13_User_Settings_API.md` | межсерверная догоняющая синхронизация пользовательских настроек по курсору |
|
|
||||||
| `MarkAllUserSettingsUnsynced` | `13_User_Settings_API.md` | служебная пометка всех настроек как несинхронизированных |
|
|
||||||
| `GetDirectMessages` | `12_Direct_Messages_Push_Calls_API.md` | постраничная загрузка истории личного диалога |
|
| `GetDirectMessages` | `12_Direct_Messages_Push_Calls_API.md` | постраничная загрузка истории личного диалога |
|
||||||
| `AckSessionDelivery` | `12_Direct_Messages_Push_Calls_API.md` | подтверждение доставки в сессию |
|
| `AckSessionDelivery` | `12_Direct_Messages_Push_Calls_API.md` | подтверждение доставки в сессию |
|
||||||
| `CallInviteBroadcast` | `12_Direct_Messages_Push_Calls_API.md` | broadcast приглашения к звонку |
|
| `CallInviteBroadcast` | `12_Direct_Messages_Push_Calls_API.md` | broadcast приглашения к звонку |
|
||||||
@@ -78,7 +75,9 @@
|
|||||||
|
|
||||||
## Важные замечания
|
## Важные замечания
|
||||||
|
|
||||||
- `ReceiveOutcomingMessage` зарегистрирован как алиас того же handler/request-класса, что и `SendMessagePair`, и сохраняет прежний межсерверный payload.
|
- UI отправляет полную подписанную пару только через `SendMessagePair`.
|
||||||
|
- `ReceiveIncomingMessage` вызывается сервером отправителя для доставки одной
|
||||||
|
входящей копии на единственный access-сервер получателя.
|
||||||
- Межсерверные DM-операции пока доверяют `sourceServerLogin`; отдельная межсерверная авторизация запланирована позднее.
|
- Межсерверные DM-операции пока доверяют `sourceServerLogin`; отдельная межсерверная авторизация запланирована позднее.
|
||||||
- `ServerHello` пока принимает заявленный `serverLogin` на доверии и не является криптографической аутентификацией.
|
- `ServerHello` пока принимает заявленный `serverLogin` на доверии и не является криптографической аутентификацией.
|
||||||
- Отдельных HTTP endpoints для DM-файлов сейчас нет.
|
- Отдельных HTTP endpoints для DM-файлов сейчас нет.
|
||||||
|
|||||||
@@ -205,3 +205,45 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Profile relation lists (v18)
|
||||||
|
|
||||||
|
### `ListUserProfileRelations`
|
||||||
|
|
||||||
|
Lightweight paged lists used by clickable counters in a user profile. Supported `listType` values:
|
||||||
|
|
||||||
|
- `friends`
|
||||||
|
- `close_friends`
|
||||||
|
- `primary_received`
|
||||||
|
- `primary_given`
|
||||||
|
- `shine_received`
|
||||||
|
- `shine_given`
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "ListUserProfileRelations",
|
||||||
|
"requestId": "profile-rel-1",
|
||||||
|
"payload": { "login": "Alice", "listType": "friends", "limit": 100, "offset": 0 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each item contains a ready-to-render user card: `login`, `firstName`, `lastName`, `avatarAr`, `accountRole`, `shineStatus`, effective `relationType`, `primaryConfirmed`, `shineConfirmed`, plus received primary/shine confirmation counters.
|
||||||
|
|
||||||
|
Effective relation priority is `close_friend > friend > contact > none`. Effective `70/80` confirmations are filtered by current voting/status rules; raw blockchain connections are not deleted when temporarily ineffective.
|
||||||
|
|
||||||
|
### Voting rules
|
||||||
|
|
||||||
|
- new `80` requires the actor to have `account_role=primary`;
|
||||||
|
- `80` cannot be added to a target with `account_role=non_voting`;
|
||||||
|
- new `70` requires actor `account_role=primary` and `shine=shining` (legacy `shine=yes` is normalized to `shining`);
|
||||||
|
- `70` cannot be added to a target with `shine=not_interested`;
|
||||||
|
- a `non_voting` target may still receive `70` if its shine status allows it;
|
||||||
|
- old raw `70/80` remain in `connections_state`, but are excluded from effective lists/counts while either side is ineligible. They become effective again automatically if statuses permit later.
|
||||||
|
|
||||||
|
### `ListContacts` additions (v18)
|
||||||
|
|
||||||
|
Every dialog item now also carries `firstName`, `lastName`, `avatarAr`, `accountRole`, and `shineStatus`. `relationFlag` priority is now `close_friend > friend > contact > none`.
|
||||||
|
|||||||
@@ -9,8 +9,10 @@
|
|||||||
|
|
||||||
Важно:
|
Важно:
|
||||||
|
|
||||||
- для DM v1 нужно использовать `SendMessagePair`, `ReceiveOutcomingMessage`, `ReceiveIncomingMessage`, `DeleteMessage`, `DeleteConversation`, `GetDirectMessages`;
|
- UI использует `SendMessagePair`, `DeleteMessage`, `DeleteConversation`
|
||||||
- `DmSyncBatch` предназначен для межсерверной догоняющей синхронизации, не для обычного клиентского UI.
|
и `GetDirectMessages`;
|
||||||
|
- `ReceiveIncomingMessage` — внутренняя доставка одной входящей копии на
|
||||||
|
единственный access-сервер получателя.
|
||||||
- сервер поддерживает материализованный слой диалогов `dm_dialog_state`; `read receipt` обновляет серверный watermark и `unreadCount`, а не только локальный клиентский флаг.
|
- сервер поддерживает материализованный слой диалогов `dm_dialog_state`; `read receipt` обновляет серверный watermark и `unreadCount`, а не только локальный клиентский флаг.
|
||||||
- в `dm_dialog_state` сервер также хранит `last_message_blob_b64` для последнего контентного DM в base64, чтобы клиент мог отрисовать список чатов без дополнительного запроса.
|
- в `dm_dialog_state` сервер также хранит `last_message_blob_b64` для последнего контентного DM в base64, чтобы клиент мог отрисовать список чатов без дополнительного запроса.
|
||||||
|
|
||||||
@@ -69,9 +71,7 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. `SendMessagePair` и `ReceiveOutcomingMessage`
|
## 3. `SendMessagePair`
|
||||||
|
|
||||||
`ReceiveOutcomingMessage` — алиас `SendMessagePair`.
|
|
||||||
|
|
||||||
### Назначение
|
### Назначение
|
||||||
|
|
||||||
@@ -114,7 +114,10 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Успешный `status=200` подтверждает локальное сохранение. До ответа клиенту сервер параллельно пробует оба актуальных сервера получателя. Поэтому `deliveryState` уже может быть `delivered`; если никто не ответил, возвращается `accepted`.
|
Успешный `status=200` подтверждает локальное сохранение. До ответа клиенту
|
||||||
|
сервер пробует единственный актуальный сервер получателя. Поэтому
|
||||||
|
`deliveryState` уже может быть `delivered`; если сервер получателя не
|
||||||
|
ответил, возвращается `accepted`.
|
||||||
|
|
||||||
Возможные `deliveryState`: `accepted`, `delivered`, `failed`.
|
Возможные `deliveryState`: `accepted`, `delivered`, `failed`.
|
||||||
|
|
||||||
@@ -132,7 +135,8 @@
|
|||||||
|
|
||||||
### Назначение
|
### Назначение
|
||||||
|
|
||||||
Используется там, где нужно принять только incoming-вариант сообщения.
|
Используется сервером отправителя для доставки incoming-варианта сообщения на
|
||||||
|
единственный access-сервер получателя.
|
||||||
|
|
||||||
Принимаемые типы:
|
Принимаемые типы:
|
||||||
|
|
||||||
@@ -152,7 +156,9 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`sourceServerLogin` необязателен для совместимости. Пока межсерверная авторизация отложена, это поле считается доверенным. Пользовательская подпись signed-блока проверяется всегда. Пустой `sourceServerLogin` трактуется как клиентский вызов, непустой - как peer-вызов.
|
`sourceServerLogin` пока передаётся для диагностики маршрута. Межсерверная
|
||||||
|
авторизация остаётся отдельным будущим этапом, но пользовательская подпись
|
||||||
|
signed-блока проверяется всегда. Официальный UI эту операцию не вызывает.
|
||||||
|
|
||||||
Успешный ответ содержит существующие `messageKey`, `baseKey` и счётчики realtime-доставки. Повтор уже сохранённой той же ревизии обрабатывается идемпотентно.
|
Успешный ответ содержит существующие `messageKey`, `baseKey` и счётчики realtime-доставки. Повтор уже сохранённой той же ревизии обрабатывается идемпотентно.
|
||||||
|
|
||||||
@@ -262,31 +268,11 @@
|
|||||||
|
|
||||||
Поле `deliveryState` заполняется для исходящих элементов `type=2`. У входящих `type=1` оно отсутствует/равно `null`.
|
Поле `deliveryState` заполняется для исходящих элементов `type=2`. У входящих `type=1` оно отсутствует/равно `null`.
|
||||||
|
|
||||||
## 8. Межсерверные операции DM
|
## 8. Внутренняя межсерверная доставка DM
|
||||||
|
|
||||||
До отдельного этапа server-auth поля `sourceServerLogin` доверяются. Каждый `SHiNE_DM` всё равно заново проходит проверку формата и пользовательской подписи.
|
### 8.1. `ReceiveIncomingMessage`
|
||||||
|
|
||||||
### 8.1. `ReceiveOutcomingMessage`
|
Передаёт одну входящую копию на единственный access-сервер получателя.
|
||||||
|
|
||||||
Прежняя операция передачи полной пары второму access-серверу отправителя. Delivery-state в межсерверный запрос не входит.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"op": "ReceiveOutcomingMessage",
|
|
||||||
"requestId": "dm-peer-001",
|
|
||||||
"payload": {
|
|
||||||
"incomingBlobB64": "BASE64_INCOMING",
|
|
||||||
"outgoingBlobB64": "BASE64_OUTGOING",
|
|
||||||
"sourceServerLogin": "server-a"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Ответ имеет прежний формат `SendMessagePair`. Принимающий сервер сохраняет пару идемпотентно и самостоятельно ставит локальную задачу доставки.
|
|
||||||
|
|
||||||
### 8.2. `ReceiveIncomingMessage`
|
|
||||||
|
|
||||||
Прежняя операция передачи одной входящей копии серверу получателя или второму серверу самого получателя.
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -301,72 +287,9 @@
|
|||||||
|
|
||||||
Успешный 2xx-ответ означает, что signed-блок проверен и находится в БД (новая или идемпотентно повторённая запись).
|
Успешный 2xx-ответ означает, что signed-блок проверен и находится в БД (новая или идемпотентно повторённая запись).
|
||||||
|
|
||||||
### 8.3. `DmSyncBatch`
|
Операции `ReceiveOutcomingMessage`, `DmSyncBatch` и
|
||||||
|
`GetDmDeliveryStatus` удалены вместе с репликацией между access-серверами
|
||||||
Pull-синхронизация событий владельца с `synced=false`. `ackSyncIds` подтверждает версии событий, успешно сохранённые из предыдущего ответа.
|
одного пользователя.
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"op": "DmSyncBatch",
|
|
||||||
"requestId": "dm-sync-001",
|
|
||||||
"payload": {
|
|
||||||
"ownerLogin": "alice",
|
|
||||||
"afterStoredAtMs": 0,
|
|
||||||
"afterMessageKey": "",
|
|
||||||
"limit": 500,
|
|
||||||
"maxBytes": 3000000,
|
|
||||||
"ackSyncIds": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Ответ возвращает страницу несинхронизированных событий и курсор внутри текущего цикла:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"nextStoredAtMs": 1774700001123,
|
|
||||||
"nextMessageKey": "alice|bob|1774700000123|123456789|2",
|
|
||||||
"hasMore": false,
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"syncId": "alice|bob|1774700000123|123456789|2:0:0",
|
|
||||||
"primaryMessageKey": "alice|bob|1774700000123|123456789|2",
|
|
||||||
"storedAtMs": 1774700001123,
|
|
||||||
"blobsB64": ["BASE64_INCOMING", "BASE64_OUTGOING"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Для пары порядок всегда incoming/outgoing; для входящей копии и tombstone массив содержит один blob. `syncId` — технический идентификатор конкретной ревизии только для ACK синхронизации, а не второй ID сообщения. После сохранения вызывающий сервер передаёт `syncId` в `ackSyncIds` следующего запроса. Только тогда источник ставит `synced=true`. Новый цикл начинается с `afterStoredAtMs=0`; полный сброс флагов поэтому повторно отдаёт всю историю.
|
|
||||||
|
|
||||||
### 8.4. `GetDmDeliveryStatus`
|
|
||||||
|
|
||||||
Единственная новая межсерверная операция доставки. Read-only проверка существующего `messageKey`; не изменяет состояние отвечающего сервера.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"op": "GetDmDeliveryStatus",
|
|
||||||
"requestId": "dm-status-001",
|
|
||||||
"payload": {
|
|
||||||
"messageKey": "alice|bob|1774700000123|123456789|2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Ответ содержит `messageKey`, `known` и `delivered`. `delivered=true` означает доставку хотя бы одному серверу получателя.
|
|
||||||
|
|
||||||
Периодический процесс использует один логический последовательный сеанс поверх
|
|
||||||
постоянного WSS-пула: сначала синхронизирует настройки, затем вызывает
|
|
||||||
`DmSyncBatch` до завершения страниц и ACK. Существующий
|
|
||||||
`MarkAllUserSettingsUnsynced` также сбрасывает DM-флаги и возвращает
|
|
||||||
`dmUpdated`; отдельной операции `MarkAllDmUnsynced` нет.
|
|
||||||
|
|
||||||
Основные ошибки межсерверных операций:
|
|
||||||
|
|
||||||
- `400 / EMPTY_OWNER_LOGIN`, `EMPTY_MESSAGE_KEY`;
|
|
||||||
- `403 / LOCAL_SERVER_NOT_ACCESS_SERVER`;
|
|
||||||
- `500 / LOCAL_SERVER_NOT_CONFIGURED`.
|
|
||||||
|
|
||||||
## 9. `AckSessionDelivery`
|
## 9. `AckSessionDelivery`
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user