diff --git a/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/MsgSubType.java b/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/MsgSubType.java index 3a0ae6e0..d2aa2e25 100644 --- a/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/MsgSubType.java +++ b/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/MsgSubType.java @@ -102,16 +102,23 @@ public final class MsgSubType { /** Удалить из близких друзей (close friend). */ 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_UNCONTACT = 21; + // Пока не используется в UI. Сохраняем для совместимости протокола/блокчейна. DO NOT REMOVE. /** Подписаться (follow). */ public static final short CONNECTION_FOLLOW = 30; /** Отписаться (unfollow). */ public static final short CONNECTION_UNFOLLOW = 31; + // Родственные связи пока не используются в UI. Сохраняем коды. DO NOT REMOVE. /** Добавить связь "жена/муж". */ public static final short CONNECTION_SPOUSE = 40; /** Удалить связь "жена/муж". */ @@ -132,9 +139,8 @@ public final class MsgSubType { /** Удалить связь "брат/сестра". */ 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_UNKNOWN_PERSON = 61; /** Точно уверен, что сияющий. */ @@ -142,11 +148,17 @@ public final class MsgSubType { /** Не подтверждаю, что сияющий. */ 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_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) ===================== */ /** Параметр профиля key/value (обе строки). */ diff --git a/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java b/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java index 53445aa8..1d8da91a 100644 --- a/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java +++ b/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java @@ -20,9 +20,11 @@ import java.util.Objects; * PARENT=50, UNPARENT=51 * CHILD=52, UNCHILD=53 * 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_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 НЕ ХРАНИМ): * [4] lineCode @@ -185,6 +187,8 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasL int v = st & 0xFFFF; return v == (MsgSubType.CONNECTION_CLOSE_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_UNCONTACT & 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_UNCONFIRMED & 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 diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java index 1f571cdd..8d5656c6 100644 --- a/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java +++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java @@ -33,6 +33,7 @@ public final class DatabaseInitializer { public static final int SCHEMA_VERSION_14 = 14; 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 String POSTGRES_SCHEMA_RESOURCE = "postgres/schema_v1.sql"; public static final String POSTGRES_MIGRATION_V2_RESOURCE = "postgres/migration_v2.sql"; public static final String POSTGRES_MIGRATION_V3_RESOURCE = "postgres/migration_v3.sql"; @@ -49,6 +50,7 @@ public final class DatabaseInitializer { 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_V16_RESOURCE = "postgres/migration_v16.sql"; + public static final String POSTGRES_MIGRATION_V17_RESOURCE = "postgres/migration_v17.sql"; private DatabaseInitializer() {} @@ -70,12 +72,17 @@ public final class DatabaseInitializer { public static final short CONNECTION_CLOSE_FRIEND = 10; 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_UNCONTACT = 21; + // Пока не используется в UI. DO NOT REMOVE. public static final short CONNECTION_FOLLOW = 30; public static final short CONNECTION_UNFOLLOW = 31; + // Родственные связи пока не используются в UI. DO NOT REMOVE. public static final short CONNECTION_SPOUSE = 40; public static final short CONNECTION_UNSPOUSE = 41; @@ -88,15 +95,20 @@ public final class DatabaseInitializer { public static final short CONNECTION_SIBLING = 54; 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_UNKNOWN_PERSON = 61; public static final short CONNECTION_SHINE_CONFIRMED = 70; 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_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, String user, String password) throws SQLException { @@ -172,6 +184,10 @@ public final class DatabaseInitializer { 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; + } } } diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/MsgSubType.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/MsgSubType.java index 3c8a1366..78e061d5 100644 --- a/SHiNE-server/shine-server-db/src/main/java/shine/db/MsgSubType.java +++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/MsgSubType.java @@ -58,10 +58,10 @@ public final class MsgSubType { /* ===================== CONNECTION (msg_type=3) ===================== */ /** * Совпадает с ConnectionBody: - * SET: CLOSE_FRIEND=10, CONTACT=20, FOLLOW=30, SPOUSE=40, PARENT=50, CHILD=52, SIBLING=54, - * KNOWN_PERSON=60, SHINE_CONFIRMED=70, SHINE_SEEN=74 - * UNSET: UNCLOSE_FRIEND=11, UNCONTACT=21, UNFOLLOW=31, UNSPOUSE=41, UNPARENT=51, UNCHILD=53, UNSIBLING=55, - * UNKNOWN_PERSON=61, SHINE_UNCONFIRMED=71, SHINE_UNSEEN=75 + * SET: CLOSE_FRIEND=10, FRIEND=14, CONTACT=20, FOLLOW=30, SPOUSE=40, PARENT=50, CHILD=52, SIBLING=54, + * KNOWN_PERSON(legacy)=60, SHINE_CONFIRMED=70, SHINE_SEEN=74, OFFICIAL_ACCOUNT_CONFIRMED=80 + * UNSET: UNCLOSE_FRIEND=11, UNFRIEND=15, UNCONTACT=21, UNFOLLOW=31, UNSPOUSE=41, UNPARENT=51, UNCHILD=53, UNSIBLING=55, + * UNKNOWN_PERSON(legacy)=61, SHINE_UNCONFIRMED=71, SHINE_UNSEEN=75, OFFICIAL_ACCOUNT_UNCONFIRMED=81 */ /** Добавить в близкие друзья (close friend). */ @@ -70,18 +70,26 @@ public final class MsgSubType { /** Удалить из близких друзей (close friend). */ 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_UNCONTACT = 21; + // Пока не используется в UI. Сохраняем для совместимости. DO NOT REMOVE. /** Подписаться (follow). */ public static final short CONNECTION_FOLLOW = 30; /** Отписаться (unfollow). */ public static final short CONNECTION_UNFOLLOW = 31; + // Родственные связи пока не используются в UI. Сохраняем коды. DO NOT REMOVE. /** Добавить связь "жена/муж". */ public static final short CONNECTION_SPOUSE = 40; @@ -106,10 +114,8 @@ public final class MsgSubType { /** Удалить связь "брат/сестра". */ 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_UNKNOWN_PERSON = 61; /** Точно уверен, что сияющий. */ @@ -118,12 +124,19 @@ public final class MsgSubType { /** Не подтверждаю, что сияющий. */ 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_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) ===================== */ /** Параметр профиля key/value (обе строки). */ diff --git a/SHiNE-server/shine-server-db/src/main/resources/postgres/migration_v17.sql b/SHiNE-server/shine-server-db/src/main/resources/postgres/migration_v17.sql new file mode 100644 index 00000000..9c3f38ae --- /dev/null +++ b/SHiNE-server/shine-server-db/src/main/resources/postgres/migration_v17.sql @@ -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; diff --git a/SHiNE-server/shine-server-db/src/main/resources/postgres/schema_v1.sql b/SHiNE-server/shine-server-db/src/main/resources/postgres/schema_v1.sql index 107c211a..4d0d9671 100644 --- a/SHiNE-server/shine-server-db/src/main/resources/postgres/schema_v1.sql +++ b/SHiNE-server/shine-server-db/src/main/resources/postgres/schema_v1.sql @@ -1113,7 +1113,7 @@ BEGIN 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 RETURN NEW; END IF; @@ -1288,6 +1288,7 @@ BEGIN 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 @@ -1297,6 +1298,7 @@ BEGIN WHEN 61 THEN 60 WHEN 71 THEN 70 WHEN 75 THEN 74 + WHEN 81 THEN 80 ELSE NULL END; diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java index b434eb5f..b0f3701d 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java @@ -885,10 +885,10 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler { } // Connection notifications are intentionally modeled as a generic kind. - // Today only CONNECTION_CLOSE_FRIEND is indexed; future incoming connection types - // can reuse the same notification kind and expose their code via sourceMsgSubType. + // Current UI surfaces FRIEND and CLOSE_FRIEND here; other reserved relation types stay silent. if (msgType == 3 - && msgSubType == (MsgSubType.CONNECTION_CLOSE_FRIEND & 0xFFFF) + && (msgSubType == (MsgSubType.CONNECTION_CLOSE_FRIEND & 0xFFFF) + || msgSubType == (MsgSubType.CONNECTION_FRIEND & 0xFFFF)) && block.body instanceof ConnectionBody) { UserNotificationEntry entry = baseNotificationEntry(storedEntry, ownerLogin, createdAtMs, msgSubType); entry.setNotificationKind("connection"); diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/Net_GetUserConnectionsGraph_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/Net_GetUserConnectionsGraph_Handler.java index ea83a791..88bce966 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/Net_GetUserConnectionsGraph_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/Net_GetUserConnectionsGraph_Handler.java @@ -46,8 +46,10 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler { return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден"); } - List outFriends = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CLOSE_FRIEND); - List inFriends = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CLOSE_FRIEND); + List outFriends = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_FRIEND); + List inFriends = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_FRIEND); + List outCloseFriends = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CLOSE_FRIEND); + List inCloseFriends = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CLOSE_FRIEND); List outContacts = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CONTACT); List inContacts = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CONTACT); List outFollows = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_FOLLOW); @@ -60,8 +62,11 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler { List inChildren = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CHILD); List outSiblings = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SIBLING); List inSiblings = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SIBLING); - List outKnownPersons = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_KNOWN_PERSON); - List inKnownPersons = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_KNOWN_PERSON); + // Legacy 60/61 are accepted by the protocol but intentionally not surfaced by current UI. + List outKnownPersons = List.of(); + List inKnownPersons = List.of(); + List outOfficialAccounts = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_OFFICIAL_ACCOUNT_CONFIRMED); + List inOfficialAccounts = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_OFFICIAL_ACCOUNT_CONFIRMED); List outShineConfirmed = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SHINE_CONFIRMED); List inShineConfirmed = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SHINE_CONFIRMED); List outShineSeen = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SHINE_SEEN); @@ -69,9 +74,9 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler { LinkedHashSet allLogins = new LinkedHashSet<>(); 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, - outKnownPersons, inKnownPersons, outShineConfirmed, inShineConfirmed, outShineSeen, inShineSeen); + outKnownPersons, inKnownPersons, outOfficialAccounts, inOfficialAccounts, outShineConfirmed, inShineConfirmed, outShineSeen, inShineSeen); Map metaByLogin = loadUserMeta(c, allLogins); List spouseLogins = mergeUnique(outSpouses, inSpouses); @@ -86,6 +91,8 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler { resp.setLogin(canonicalLogin); resp.setOutFriends(outFriends); resp.setInFriends(inFriends); + resp.setOutCloseFriends(outCloseFriends); + resp.setInCloseFriends(inCloseFriends); resp.setOutContacts(outContacts); resp.setInContacts(inContacts); resp.setOutFollows(outFollows); @@ -100,6 +107,8 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler { resp.setInSiblings(inSiblings); resp.setOutKnownPersons(outKnownPersons); resp.setInKnownPersons(inKnownPersons); + resp.setOutOfficialAccounts(outOfficialAccounts); + resp.setInOfficialAccounts(inOfficialAccounts); resp.setOutShineConfirmed(outShineConfirmed); resp.setInShineConfirmed(inShineConfirmed); resp.setOutShineSeen(outShineSeen); diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_GetUserConnectionsGraph_Response.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_GetUserConnectionsGraph_Response.java index feced17a..dbb41db1 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_GetUserConnectionsGraph_Response.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_GetUserConnectionsGraph_Response.java @@ -9,6 +9,8 @@ public class Net_GetUserConnectionsGraph_Response extends Net_Response { private String login; private List outFriends = new ArrayList<>(); private List inFriends = new ArrayList<>(); + private List outCloseFriends = new ArrayList<>(); + private List inCloseFriends = new ArrayList<>(); private List outContacts = new ArrayList<>(); private List inContacts = new ArrayList<>(); private List outFollows = new ArrayList<>(); @@ -23,6 +25,8 @@ public class Net_GetUserConnectionsGraph_Response extends Net_Response { private List inSiblings = new ArrayList<>(); private List outKnownPersons = new ArrayList<>(); private List inKnownPersons = new ArrayList<>(); + private List outOfficialAccounts = new ArrayList<>(); + private List inOfficialAccounts = new ArrayList<>(); private List outShineConfirmed = new ArrayList<>(); private List inShineConfirmed = new ArrayList<>(); private List outShineSeen = new ArrayList<>(); @@ -84,6 +88,10 @@ public class Net_GetUserConnectionsGraph_Response extends Net_Response { public void setOutFriends(List outFriends) { this.outFriends = outFriends; } public List getInFriends() { return inFriends; } public void setInFriends(List inFriends) { this.inFriends = inFriends; } + public List getOutCloseFriends() { return outCloseFriends; } + public void setOutCloseFriends(List outCloseFriends) { this.outCloseFriends = outCloseFriends; } + public List getInCloseFriends() { return inCloseFriends; } + public void setInCloseFriends(List inCloseFriends) { this.inCloseFriends = inCloseFriends; } public List getOutContacts() { return outContacts; } public void setOutContacts(List outContacts) { this.outContacts = outContacts; } public List getInContacts() { return inContacts; } @@ -112,6 +120,10 @@ public class Net_GetUserConnectionsGraph_Response extends Net_Response { public void setOutKnownPersons(List outKnownPersons) { this.outKnownPersons = outKnownPersons; } public List getInKnownPersons() { return inKnownPersons; } public void setInKnownPersons(List inKnownPersons) { this.inKnownPersons = inKnownPersons; } + public List getOutOfficialAccounts() { return outOfficialAccounts; } + public void setOutOfficialAccounts(List outOfficialAccounts) { this.outOfficialAccounts = outOfficialAccounts; } + public List getInOfficialAccounts() { return inOfficialAccounts; } + public void setInOfficialAccounts(List inOfficialAccounts) { this.inOfficialAccounts = inOfficialAccounts; } public List getOutShineConfirmed() { return outShineConfirmed; } public void setOutShineConfirmed(List outShineConfirmed) { this.outShineConfirmed = outShineConfirmed; } public List getInShineConfirmed() { return inShineConfirmed; } diff --git a/VERSION.properties b/VERSION.properties index 59887d7d..46011b76 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.10.0 -server.version=1.8.0 +client.version=1.10.1 +server.version=1.8.1 diff --git a/docs/API/11_Connections_API.md b/docs/API/11_Connections_API.md index c7acfa3e..b33c574a 100644 --- a/docs/API/11_Connections_API.md +++ b/docs/API/11_Connections_API.md @@ -132,6 +132,8 @@ "login": "Alice", "outFriends": ["Bob"], "inFriends": ["Kate"], + "outCloseFriends": [], + "inCloseFriends": [], "outContacts": [], "inContacts": [], "outFollows": [], @@ -146,6 +148,8 @@ "inSiblings": [], "outKnownPersons": [], "inKnownPersons": [], + "outOfficialAccounts": [], + "inOfficialAccounts": [], "outShineConfirmed": [], "inShineConfirmed": [], "outShineSeen": [], @@ -171,6 +175,7 @@ ### Примечание Поля `known_person`, `shine_confirmed`, `shine_seen` в UI считаются недопроверенной зоной проекта; при изменениях этой логики нужна ручная end-to-end проверка. +`outCloseFriends` / `inCloseFriends` возвращают близких друзей, а `outOfficialAccounts` / `inOfficialAccounts` - подтверждённые официальные аккаунты. --- diff --git a/shine-UI/js/pages/network-view.js b/shine-UI/js/pages/network-view.js index 91906643..32d7670a 100644 --- a/shine-UI/js/pages/network-view.js +++ b/shine-UI/js/pages/network-view.js @@ -104,10 +104,8 @@ function applyRelativeGender(map, rows) { function getRelativeGenderMap(graph) { const map = new Map(); - applyRelativeGender(map, graph?.parents); - applyRelativeGender(map, graph?.children); - applyRelativeGender(map, graph?.siblings); - applyRelativeGender(map, graph?.spouses); + // Родственные связи пока скрыты из UI, хотя сервер продолжает хранить их коды. + void graph; return map; } @@ -115,6 +113,8 @@ function buildGraphModel(graph, centerLogin) { const login = normalizeLogin(graph?.login || centerLogin || state.session.login); const outFriends = toSet(graph?.outFriends); const inFriends = toSet(graph?.inFriends); + const outCloseFriends = toSet(graph?.outCloseFriends); + const inCloseFriends = toSet(graph?.inCloseFriends); const outParents = toSet(graph?.outParents); const inParents = toSet(graph?.inParents); const outChildren = toSet(graph?.outChildren); @@ -128,8 +128,8 @@ function buildGraphModel(graph, centerLogin) { const inContacts = toSet(graph?.inContacts); const outFollows = toSet(graph?.outFollows); const inFollows = toSet(graph?.inFollows); - const outKnown = toSet(graph?.outKnownPersons); - const inKnown = toSet(graph?.inKnownPersons); + const outOfficial = toSet(graph?.outOfficialAccounts); + const inOfficial = toSet(graph?.inOfficialAccounts); const relativesGender = getRelativeGenderMap(graph); const allMarks = getMarkByLogin(graph?.allUsers); @@ -137,67 +137,35 @@ function buildGraphModel(graph, centerLogin) { const allLogins = uniqueLogins([ ...(graph?.outFriends || []), ...(graph?.inFriends || []), - ...(graph?.outParents || []), - ...(graph?.inParents || []), - ...(graph?.outChildren || []), - ...(graph?.inChildren || []), - ...(graph?.outSiblings || []), - ...(graph?.inSiblings || []), - ...(graph?.outSpouses || []), - ...(graph?.inSpouses || []), + ...(graph?.outCloseFriends || []), + ...(graph?.inCloseFriends || []), ...(graph?.outContacts || []), ...(graph?.inContacts || []), ...(graph?.outFollows || []), ...(graph?.inFollows || []), - ...(graph?.outKnownPersons || []), - ...(graph?.inKnownPersons || []), + ...(graph?.outOfficialAccounts || []), + ...(graph?.inOfficialAccounts || []), ]).filter((entry) => normKey(entry) !== normKey(login)); const relations = allLogins.map((targetLogin) => { - const parentOut = hasLogin(outParents, targetLogin); - const parentIn = hasLogin(inChildren, targetLogin); - const childOut = hasLogin(outChildren, targetLogin); - const childIn = hasLogin(inParents, targetLogin); - const siblingOut = hasLogin(outSiblings, targetLogin); - const siblingIn = hasLogin(inSiblings, targetLogin); - const spouseOut = hasLogin(outSpouses, targetLogin); - const spouseIn = hasLogin(inSpouses, targetLogin); const friendOut = hasLogin(outFriends, targetLogin); const friendIn = hasLogin(inFriends, targetLogin); - const contactOut = hasLogin(outContacts, targetLogin) || hasLogin(outFollows, targetLogin) || hasLogin(outKnown, targetLogin); - const contactIn = hasLogin(inContacts, targetLogin) || hasLogin(inFollows, targetLogin) || hasLogin(inKnown, targetLogin); + const closeFriendOut = hasLogin(outCloseFriends, targetLogin); + const closeFriendIn = hasLogin(inCloseFriends, targetLogin); + const contactOut = hasLogin(outContacts, targetLogin) || hasLogin(outFollows, targetLogin) || hasLogin(outOfficial, targetLogin); + const contactIn = hasLogin(inContacts, targetLogin) || hasLogin(inFollows, targetLogin) || hasLogin(inOfficial, targetLogin); let role = 'contact'; - if (parentOut || parentIn) role = 'parent'; - else if (childOut || childIn) role = 'child'; - else if (spouseOut || spouseIn) role = 'spouse'; - else if (siblingOut || siblingIn) role = 'sibling'; - else if (friendOut || friendIn) role = 'friend'; + if (closeFriendOut || closeFriendIn || friendOut || friendIn) role = 'friend'; - let forward = friendOut; - let backward = friendIn; - if (role === 'parent') { - forward = parentOut; - backward = parentIn; - } else if (role === 'child') { - forward = childOut; - backward = childIn; - } else if (role === 'spouse') { - forward = spouseOut; - backward = spouseIn; - } else if (role === 'sibling') { - forward = siblingOut; - backward = siblingIn; - } else if (role === 'contact') { - forward = contactOut; - backward = contactIn; - } + let forward = role === 'friend' ? (closeFriendOut || friendOut) : contactOut; + let backward = role === 'friend' ? (closeFriendIn || friendIn) : contactIn; return { login: targetLogin, key: normKey(targetLogin), role, - isRelative: role === 'parent' || role === 'child' || role === 'spouse' || role === 'sibling', + isRelative: false, gender: normalizeGender(relativesGender.get(normKey(targetLogin))), forward: Boolean(forward), backward: Boolean(backward), @@ -242,11 +210,10 @@ export function render({ navigate, route, chrome } = {}) { // Фильтры слоёв (Фаза 3). Фокус всегда виден; предикат применяется к периферийным узлам. const FILTERS = { all: { label: 'Все', pred: () => true }, - family: { label: 'Семья', pred: (n) => n.relationType === 'family' }, friends: { label: 'Друзья', pred: (n) => n.relationType === 'friend' }, shining: { label: 'Сияющие', pred: (n) => Boolean(n.shining) }, }; - const FILTER_ORDER = ['all', 'family', 'friends', 'shining']; + const FILTER_ORDER = ['all', 'friends', 'shining']; let activeFilter = 'all'; const filterChips = {}; diff --git a/shine-UI/js/pages/profile-edit-view.js b/shine-UI/js/pages/profile-edit-view.js index 54d5b9d8..26828c56 100644 --- a/shine-UI/js/pages/profile-edit-view.js +++ b/shine-UI/js/pages/profile-edit-view.js @@ -1,4 +1,4 @@ -import { renderHeader } from '../components/header.js'; +import { renderHeader } from '../components/header.js'; import { profile } from '../mock-data.js'; import { authService, state } from '../state.js'; import { @@ -38,11 +38,9 @@ const GENDER_OPTIONS = Object.freeze([ { value: PROFILE_GENDER_UNKNOWN, label: 'Не указан' }, ]); +// Родственные типы сохранены в протоколе, но пока намеренно скрыты из UI. const RELATIVE_RELATION_OPTIONS = Object.freeze([ - { value: 'parent', label: 'Родитель (мать/отец по полу)' }, - { value: 'child', label: 'Ребёнок (сын/дочь по полу)' }, - { value: 'spouse', label: 'Жена / Муж (по полу)' }, - { value: 'sibling', label: 'Брат или сестра (по полу)' }, + { value: 'friend', label: 'Друг' }, { value: 'close_friend', label: 'Близкий друг' }, ]); @@ -75,6 +73,7 @@ function relationAccusativeLabel(type, targetGender) { if (gender === PROFILE_GENDER_FEMALE) return 'жену'; return 'жену/мужа'; } + if (type === 'friend') return 'друга'; return 'близкого друга'; } @@ -134,12 +133,12 @@ export function render({ navigate, chrome }) { const relativesCard = document.createElement('div'); relativesCard.className = 'card stack'; relativesCard.innerHTML = ` -
Близкие родственники
+
Друзья
- Добавьте связь: родитель, ребёнок, жена/муж, брат/сестра или близкий друг. - Формулировка (мать/отец, брат/сестра) определяется по полу выбранного пользователя. + Добавьте пользователя в друзья или в близкие друзья. + Родственные типы связей сохранены в протоколе, но пока скрыты из интерфейса.
- + `; const reloadBtn = topRow.querySelector('[data-reload="true"]'); @@ -753,20 +752,16 @@ export function render({ navigate, chrome }) { status.textContent = 'Сохранение связи...'; try { - if (relationType === 'close_friend') { - await authService.addCloseFriend(targetLogin); - } else { - if (!state.session.storagePwdInMemory) { - throw new Error('Нет storagePwd в памяти сессии. Выполните вход заново.'); - } - await authService.setUserRelation({ - login: sessionLogin, - toLogin: targetLogin, - kind: relationType, - enabled: true, - storagePwd: state.session.storagePwdInMemory, - }); + if (!state.session.storagePwdInMemory) { + throw new Error('Нет storagePwd в памяти сессии. Выполните вход заново.'); } + await authService.setUserRelation({ + login: sessionLogin, + toLogin: targetLogin, + kind: relationType, + enabled: true, + storagePwd: state.session.storagePwdInMemory, + }); status.className = 'status-line is-available'; status.textContent = `Связь добавлена: ${targetLogin} как ${relationLabel}.`; } catch (error) { diff --git a/shine-UI/js/pages/user-profile-view.js b/shine-UI/js/pages/user-profile-view.js index 8e5ac022..d1c83744 100644 --- a/shine-UI/js/pages/user-profile-view.js +++ b/shine-UI/js/pages/user-profile-view.js @@ -64,20 +64,29 @@ function genderText(value) { function relationButtonLabel(kind, flags) { if (kind === 'contact') return flags.outContact ? 'Убрать из контактов' : 'Добавить в контакты'; - if (kind === 'friend') return flags.outFriend ? 'Убрать из близких друзей' : 'Добавить в близкие друзья'; - return flags.outFollow ? 'Отписаться' : 'Подписаться'; + if (kind === 'friend') return flags.outFriend ? 'Убрать из друзей' : 'Добавить в друзья'; + if (kind === 'close_friend') return flags.outCloseFriend ? 'Убрать из близких друзей' : 'Добавить в близкие друзья'; + if (kind === 'official_account') return flags.outOfficialAccount ? 'Снять подтверждение' : 'Подтвердить аккаунт'; + if (kind === 'shine_confirmed') return flags.outShineConfirmed ? 'Снять подтверждение' : 'Подтвердить сияющего'; + return ''; } function relationNextState(kind, flags) { if (kind === 'contact') return !flags.outContact; if (kind === 'friend') return !flags.outFriend; - return !flags.outFollow; + if (kind === 'close_friend') return !flags.outCloseFriend; + if (kind === 'official_account') return !flags.outOfficialAccount; + if (kind === 'shine_confirmed') return !flags.outShineConfirmed; + return false; } function relationConfirmLabel(kind) { if (kind === 'contact') return 'контакт'; - if (kind === 'friend') return 'статус близкого друга'; - return 'подписку'; + if (kind === 'friend') return 'статус друга'; + if (kind === 'close_friend') return 'статус близкого друга'; + if (kind === 'official_account') return 'подтверждение официального аккаунта'; + if (kind === 'shine_confirmed') return 'подтверждение сияющего'; + return 'связь'; } function relationStateText(kind, flags) { @@ -88,55 +97,49 @@ function relationStateText(kind, flags) { return ''; } if (kind === 'friend') { - if (flags.outFriend && flags.inFriend) return 'Вы взаимно близкие друзья.'; - if (flags.outFriend) return 'Вы считаете этот профиль близким другом.'; - if (flags.inFriend) return 'Этот профиль считает вас близким другом.'; + if (flags.outFriend && flags.inFriend) return 'Вы взаимно считаете друг друга друзьями.'; + if (flags.outFriend) return 'Вы считаете этого человека другом.'; + if (flags.inFriend) return 'Этот человек считает вас другом.'; return ''; } - if (flags.outFollow && flags.inFollow) return 'Вы взаимно подписаны.'; - if (flags.outFollow) return 'Вы подписаны на этот профиль.'; - if (flags.inFollow) return 'Этот профиль подписан на вас.'; + if (kind === 'close_friend') { + if (flags.outCloseFriend && flags.inCloseFriend) return 'Вы взаимно близкие друзья.'; + if (flags.outCloseFriend) return 'Вы считаете этого человека близким другом.'; + if (flags.inCloseFriend) return 'Этот человек считает вас близким другом.'; + return ''; + } + if (kind === 'official_account') { + return flags.outOfficialAccount + ? 'Вы подтверждаете, что это действительно официальный аккаунт этого человека.' + : ''; + } + if (kind === 'shine_confirmed') { + return flags.outShineConfirmed + ? 'Вы подтверждаете, что этот человек сияющий.' + : ''; + } return ''; } -function opinionItemsFromFlags(flags) { - const items = []; - if (flags.outShineSeen) { - items.push({ - kind: 'shine_seen', - text: 'вы утверждаете, что очень мало знаете этого человека, но вы видели его сияющим, и всё, что вы о нём знаете, подтверждает это', - label: 'видел сияющим', - }); - } - if (flags.outShineConfirmed) { - items.push({ - kind: 'shine_confirmed', - text: 'вы утверждаете, что достаточно хорошо знаете этого человека и точно уверены, что этот человек сияющий', - label: 'точно сияющий', - }); - } - if (flags.outKnownPerson) { - items.push({ - kind: 'known_person', - text: 'вы утверждаете, что просто знаете этого человека', - label: 'просто знаю', - }); - } - return items; -} +function renderRelations(flags) { + const rows = [ + { kind: 'contact', text: relationStateText('contact', flags), button: relationButtonLabel('contact', flags) }, + { kind: 'friend', text: relationStateText('friend', flags), button: relationButtonLabel('friend', flags) }, + { kind: 'close_friend', text: relationStateText('close_friend', flags), button: relationButtonLabel('close_friend', flags) }, + { kind: 'official_account', text: relationStateText('official_account', flags), button: relationButtonLabel('official_account', flags) }, + { kind: 'shine_confirmed', text: relationStateText('shine_confirmed', flags), button: relationButtonLabel('shine_confirmed', flags) }, + ]; -function resolveActiveOpinionKind(flags) { - if (flags.outShineSeen) return 'shine_seen'; - if (flags.outShineConfirmed) return 'shine_confirmed'; - if (flags.outKnownPerson) return 'known_person'; - return ''; -} - -function opinionLabelByKind(kind) { - if (kind === 'shine_seen') return 'мало знаком, но видел сияющим'; - if (kind === 'shine_confirmed') return 'точно уверен, что сияющий'; - if (kind === 'known_person') return 'просто знаю человека'; - return kind; + return ` +
+ ${rows.map((row) => ` +
+ ${escapeHtml(row.text)} + +
+ `).join('')} +
+ `; } function renderIdentity(card) { @@ -182,82 +185,6 @@ function renderReadOnlyBadges(card) { `; } -function renderRelations(flags) { - const rows = [ - { kind: 'contact', text: relationStateText('contact', flags), button: relationButtonLabel('contact', flags) }, - { kind: 'friend', text: relationStateText('friend', flags), button: relationButtonLabel('friend', flags) }, - { kind: 'follow', text: relationStateText('follow', flags), button: relationButtonLabel('follow', flags) }, - ]; - const opinionItems = opinionItemsFromFlags(flags); - const hasOpinion = opinionItems.length > 0; - - return ` -
- ${rows.map((row) => ` -
- ${escapeHtml(row.text)} - -
- `).join('')} -
-
- ${opinionItems.map((item) => ` -
${escapeHtml(item.text)}
- `).join('')} -
-
Добавьте одну из этих трёх формулировок.
-
-
- ${hasOpinion ? 'Мнение уже добавлено.' : 'Пока нет дополнительной связи.'} - -
-
- `; -} - -function openOpinionMenuModal({ flags, onApply }) { - const root = document.getElementById('modal-root'); - if (!root) return; - const activeKind = resolveActiveOpinionKind(flags); - const items = [ - { kind: 'known_person', title: 'просто знаю человека' }, - { kind: 'shine_confirmed', title: 'точно уверен, что сияющий' }, - { kind: 'shine_seen', title: 'мало знаком, но видел сияющим' }, - ]; - const rowsHtml = items - .filter((item) => item.kind !== activeKind) - .map((item) => ``) - .join(''); - const removeHtml = activeKind - ? `` - : ''; - - root.innerHTML = ` - - `; - - const close = () => { root.innerHTML = ''; }; - root.querySelector('#user-opinion-modal-close')?.addEventListener('click', close); - root.querySelector('#user-opinion-modal')?.addEventListener('click', (event) => { - if (event.target?.id === 'user-opinion-modal') close(); - }); - root.querySelectorAll('[data-opinion-mode]').forEach((btn) => { - btn.addEventListener('click', async () => { - const nextKind = String(btn.getAttribute('data-opinion-kind') || '').trim(); - const mode = String(btn.getAttribute('data-opinion-mode') || '').trim(); - close(); - if (!nextKind) return; - await onApply({ mode, nextKind, activeKind }); - }); - }); -} - function renderReadOnlyParams(card) { const rows = [ { label: 'Имя', value: card.firstName }, @@ -310,20 +237,15 @@ export function render({ navigate, route }) { let isBusy = false; function syncActionButtons() { - const followBtn = body.querySelector('[data-relation-action="follow"]'); - const friendBtn = body.querySelector('[data-relation-action="friend"]'); - const contactBtn = body.querySelector('[data-relation-action="contact"]'); - const opinionBtn = body.querySelector('[data-relation-action="opinion-menu"]'); - if (!followBtn || !friendBtn || !contactBtn || !opinionBtn || !currentFlags) return; + const kinds = ['contact', 'friend', 'close_friend', 'official_account', 'shine_confirmed']; + if (!currentFlags) return; const isSelf = currentCard && currentCard.login.toLowerCase() === sessionLogin.toLowerCase(); - contactBtn.textContent = relationButtonLabel('contact', currentFlags); - friendBtn.textContent = relationButtonLabel('friend', currentFlags); - followBtn.textContent = relationButtonLabel('follow', currentFlags); - contactBtn.disabled = Boolean(isSelf); - friendBtn.disabled = Boolean(isSelf); - followBtn.disabled = Boolean(isSelf); - opinionBtn.textContent = opinionItemsFromFlags(currentFlags).length ? 'Изменить мнение' : 'Добавить мнение'; - opinionBtn.disabled = Boolean(isSelf); + kinds.forEach((kind) => { + const btn = body.querySelector(`[data-relation-action="${kind}"]`); + if (!btn) return; + btn.textContent = relationButtonLabel(kind, currentFlags); + btn.disabled = Boolean(isSelf); + }); } async function refresh() { @@ -384,14 +306,6 @@ export function render({ navigate, route }) { return; } - if (kind === 'opinion-menu') { - openOpinionMenuModal({ - flags: currentFlags, - onApply: onOpinionApply, - }); - return; - } - const nextEnabled = relationNextState(kind, currentFlags); const confirmed = window.confirm( `Изменить ${relationConfirmLabel(kind)} с пользователем ${currentCard.login}?\n` + @@ -420,64 +334,6 @@ export function render({ navigate, route }) { } } - async function onOpinionApply({ mode, nextKind, activeKind }) { - if (isBusy || !currentCard || !currentFlags) return; - if (!sessionLogin) { - window.alert('Для изменения связей нужен активный вход.'); - return; - } - if (!state.session.storagePwdInMemory) { - window.alert('Нет storagePwd в памяти сессии. Выполните вход заново.'); - return; - } - - const confirmed = window.confirm(`Изменить мнение о пользователе ${currentCard.login}?`); - if (!confirmed) return; - - isBusy = true; - status.className = 'status-line'; - status.textContent = 'Сохранение отношения в блокчейн...'; - - try { - if (activeKind) { - await authService.setUserRelation({ - login: sessionLogin, - toLogin: currentCard.login, - kind: activeKind, - enabled: false, - storagePwd: state.session.storagePwdInMemory, - }); - } - - if (mode === 'set') { - await authService.setUserRelation({ - login: sessionLogin, - toLogin: currentCard.login, - kind: nextKind, - enabled: true, - storagePwd: state.session.storagePwdInMemory, - }); - } - await refresh(); - if (mode === 'set') { - const opinionVisible = Boolean( - currentFlags?.outKnownPerson - || currentFlags?.outShineConfirmed - || currentFlags?.outShineSeen, - ); - if (!opinionVisible) { - await new Promise((resolve) => window.setTimeout(resolve, 350)); - await refresh(); - } - } - } catch (error) { - status.className = 'status-line is-unavailable'; - status.textContent = `Ошибка изменения связи: ${error.message || 'unknown'}`; - window.alert(`Не удалось изменить связь: ${error.message || 'unknown'}`); - isBusy = false; - } - } - body.addEventListener('click', (event) => { const target = event.target; if (!(target instanceof HTMLElement)) return; diff --git a/shine-UI/js/services/auth-service.js b/shine-UI/js/services/auth-service.js index 3ad41385..38937f09 100644 --- a/shine-UI/js/services/auth-service.js +++ b/shine-UI/js/services/auth-service.js @@ -90,18 +90,21 @@ const SIGNAL_TYPE_REMOTE_ADDBLOCK_REQUEST = 'remote_addblock_request'; const SIGNAL_TYPE_REMOTE_ADDBLOCK_RESULT = 'remote_addblock_result'; const CONNECTION_SUBTYPES = Object.freeze({ - // Legacy alias: friend == close_friend. Оба ключа ведут на один и тот же код 10/11. close_friend: { on: 10, off: 11 }, - friend: { on: 10, off: 11 }, + friend: { on: 14, off: 15 }, contact: { on: 20, off: 21 }, + // Reserved: пока не используется UI. follow: { on: 30, off: 31 }, + // Reserved: семейные связи пока скрыты из UI. spouse: { on: 40, off: 41 }, parent: { on: 50, off: 51 }, child: { on: 52, off: 53 }, sibling: { on: 54, off: 55 }, - known_person: { on: 60, off: 61 }, + // Legacy 60/61 intentionally absent: старые блоки сервер принимает, новый UI их не создаёт. shine_confirmed: { on: 70, off: 71 }, + // Reserved: пока не используется UI. shine_seen: { on: 74, off: 75 }, + official_account: { on: 80, off: 81 }, }); function normalizeServerUrl(url) { diff --git a/shine-UI/js/services/user-connections.js b/shine-UI/js/services/user-connections.js index 67bbf071..d28155d1 100644 --- a/shine-UI/js/services/user-connections.js +++ b/shine-UI/js/services/user-connections.js @@ -50,6 +50,8 @@ function readArray(payload, key) { const aliases = { outKnownPersons: ['outKnownPersons', 'outKnownPerson', 'out_known_persons'], inKnownPersons: ['inKnownPersons', 'inKnownPerson', 'in_known_persons'], + outOfficialAccounts: ['outOfficialAccounts', 'outOfficialAccount', 'out_official_accounts'], + inOfficialAccounts: ['inOfficialAccounts', 'inOfficialAccount', 'in_official_accounts'], outShineConfirmed: ['outShineConfirmed', 'outShineConfident', 'out_shine_confirmed'], inShineConfirmed: ['inShineConfirmed', 'inShineConfident', 'in_shine_confirmed'], outShineSeen: ['outShineSeen', 'out_shine_seen'], @@ -81,6 +83,8 @@ async function buildRelationsModel(login) { return { outFriends: [], inFriends: [], + outCloseFriends: [], + inCloseFriends: [], outContacts: [], inContacts: [], outFollows: [], @@ -93,6 +97,8 @@ async function buildRelationsModel(login) { inSiblings: [], outKnownPersons: [], inKnownPersons: [], + outOfficialAccounts: [], + inOfficialAccounts: [], outShineConfirmed: [], inShineConfirmed: [], outShineSeen: [], @@ -129,6 +135,8 @@ async function buildRelationsModel(login) { return { outFriends: readArray(graph, 'outFriends') || [], inFriends: readArray(graph, 'inFriends') || [], + outCloseFriends: readArray(graph, 'outCloseFriends') || [], + inCloseFriends: readArray(graph, 'inCloseFriends') || [], outContacts, inContacts: readArray(graph, 'inContacts') || [], outFollows, @@ -141,6 +149,8 @@ async function buildRelationsModel(login) { inSiblings: readArray(graph, 'inSiblings') || [], outKnownPersons: readArray(graph, 'outKnownPersons') || [], inKnownPersons: readArray(graph, 'inKnownPersons') || [], + outOfficialAccounts: readArray(graph, 'outOfficialAccounts') || [], + inOfficialAccounts: readArray(graph, 'inOfficialAccounts') || [], outShineConfirmed: readArray(graph, 'outShineConfirmed') || [], inShineConfirmed: readArray(graph, 'inShineConfirmed') || [], outShineSeen: readArray(graph, 'outShineSeen') || [], @@ -169,6 +179,8 @@ export async function loadCurrentRelations() { return { outFriends: [], inFriends: [], + outCloseFriends: [], + inCloseFriends: [], outContacts: [], inContacts: [], outFollows: [], @@ -181,6 +193,8 @@ export async function loadCurrentRelations() { inSiblings: [], outKnownPersons: [], inKnownPersons: [], + outOfficialAccounts: [], + inOfficialAccounts: [], outShineConfirmed: [], inShineConfirmed: [], outShineSeen: [], @@ -194,6 +208,8 @@ export function relationFlagsForTarget(relations, targetLogin) { return { outFriend: listContainsLogin(relations?.outFriends, targetLogin), inFriend: listContainsLogin(relations?.inFriends, targetLogin), + outCloseFriend: listContainsLogin(relations?.outCloseFriends, targetLogin), + inCloseFriend: listContainsLogin(relations?.inCloseFriends, targetLogin), outContact: listContainsLogin(relations?.outContacts, targetLogin), inContact: listContainsLogin(relations?.inContacts, targetLogin), outFollow: listContainsLogin(relations?.outFollows, targetLogin), @@ -206,6 +222,8 @@ export function relationFlagsForTarget(relations, targetLogin) { inSibling: listContainsLogin(relations?.inSiblings, targetLogin), outKnownPerson: listContainsLogin(relations?.outKnownPersons, targetLogin), inKnownPerson: listContainsLogin(relations?.inKnownPersons, targetLogin), + outOfficialAccount: listContainsLogin(relations?.outOfficialAccounts, targetLogin), + inOfficialAccount: listContainsLogin(relations?.inOfficialAccounts, targetLogin), outShineConfirmed: listContainsLogin(relations?.outShineConfirmed, targetLogin), inShineConfirmed: listContainsLogin(relations?.inShineConfirmed, targetLogin), outShineSeen: listContainsLogin(relations?.outShineSeen, targetLogin),