diff --git a/ChatExport_2026-04-06/css/style.css b/ChatExport_2026-04-06/css/style.css new file mode 100644 index 0000000..3189390 --- /dev/null +++ b/ChatExport_2026-04-06/css/style.css @@ -0,0 +1,752 @@ +body { + margin: 0; + font: 12px/18px 'Open Sans',"Lucida Grande","Lucida Sans Unicode",Arial,Helvetica,Verdana,sans-serif; +} +strong { + font-weight: 700; +} +code, kbd, pre, samp { + font-family: Menlo,Monaco,Consolas,"Courier New",monospace; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} +pre { + display: block; + margin: 0; + line-height: 1.42857143; + word-break: break-all; + word-wrap: break-word; + color: #333; + background-color: #f5f5f5; + border-radius: 4px; + overflow: auto; + padding: 3px; + border: 1px solid #eee; + max-height: none; + font-size: inherit; +} +.clearfix:after { + content: " "; + visibility: hidden; + display: block; + height: 0; + clear: both; +} +.pull_left { + float: left; +} +.pull_right { + float: right; +} +.page_wrap { + background-color: #ffffff; + color: #000000; +} +.page_wrap a { + color: #168acd; + text-decoration: none; +} +.page_wrap a:hover { + text-decoration: underline; +} +.page_header { + position: fixed; + z-index: 10; + background-color: #ffffff; + width: 100%; + border-bottom: 1px solid #e3e6e8; +} +.page_header .content { + width: 480px; + margin: 0 auto; + border-radius: 0 !important; +} +.page_header a.content { + background-repeat: no-repeat; + background-position: 24px 21px; + background-size: 24px 24px; +} +.bold { + color: #212121; + font-weight: 700; +} +.details { + color: #70777b; +} +.page_header .content .text { + padding: 24px 24px 22px 24px; + font-size: 22px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.page_header a.content .text { + padding: 24px 24px 22px 82px; +} +.page_body { + padding-top: 64px; + width: 480px; + margin: 0 auto; +} +.page_about { + padding: 24px 24px; +} +.with_divider { + border-top: 1px solid #e3e6e8; +} +.userpic_link { + display: block; + text-decoration: none; +} +.userpic_link:hover { + text-decoration: none; +} +.userpic { + display: block; + border-radius: 50%; + overflow: hidden; +} +.story { + display: block; + border-radius: 4px; + overflow: hidden; +} +.userpic .initials { + display: block; + color: #fff; + text-align: center; + text-transform: uppercase; + user-select: none; +} +.color_red, +.userpic1, +.media_call .fill, +.media_file .fill, +.media_live_location .fill { + background-color: #ff5555; +} +.color_green, +.userpic2, +.media_call.success .fill, +.media_photo .fill { + background-color: #64bf47; +} +.color_yellow, +.userpic3, +.media_venue .fill { + background-color: #ffab00; +} +.color_blue, +.userpic4, +.media_audio_file .fill, +.media_voice_message .fill { + background-color: #4f9cd9; +} +.color_purple, +.userpic5, +.media_game .fill { + background-color: #9884e8; +} +.color_pink, +.userpic6, +.media_invoice .fill { + background-color: #e671a5; +} +.color_sea, +.userpic7, +.media_location .fill, +.media_video .fill { + background-color: #47bcd1; +} +.color_orange, +.userpic8, +.media_contact .fill { + background-color: #ff8c44; +} +.personal_info { + padding: 24px; +} +.personal_info .userpic .initials { + font-size: 30px; +} +.personal_info .rows { + float: left; + padding-right: 24px; +} +.personal_info .names { + width: 164px; +} +.personal_info .info { + width: 124px; +} +.personal_info .bio { + width: 400px; +} +.personal_info .row { + padding-bottom: 16px; +} +a.block_link { + display: block; + text-decoration: none !important; + border-radius: 4px; +} +a.block_link:hover { + text-decoration: none !important; + background-color: #f5f7f8; +} +a.expanded { + padding: 2px 8px; + margin: -2px -8px; +} +.sections { + padding: 11px 0; +} +.section { + height: 48px; + background-position: 24px 12px; + background-repeat: no-repeat; + background-size: 24px 24px; +} +.section .counter { + float: right; + padding: 14px 24px 0; + font-size: 15px; +} +.section .label { + padding: 15px 0 0 82px; + font-size: 15px; +} +.list_page .page_about { + padding: 16px 24px 0; + font-size: 11px; +} +.list_page .entry_list { + padding: 16px 0; +} +.list_page .entry { + padding: 10px 16px; +} +.list_page .entry .userpic .initials { + font-size: 18px; +} +.list_page .entry .body { + margin-left: 66px; +} +.list_page .entry .name { + padding: 4px 0 2px; + font-size: 14px; +} +.list_page .entry .subname { + padding-top: 4px; +} +.list_page .entry .details_entry { + padding-top: 4px; +} +.list_page .entry .info { + font-size: 11px; + padding-top: 5px; +} +.history { + padding: 16px 0; +} +.message { + margin: 0 -10px; + transition: background-color 2.0s ease; +} +div.selected { + background-color: rgba(242,246,250,255); + transition: background-color 0.5s ease; +} +.service { + padding: 10px 24px; +} +.service .body { + text-align: center; +} +.service .userpic_wrap { + padding-top: 10px; +} +.service .userpic { + margin: 0 auto; +} +.service .userpic .initials { + font-size: 24px; +} +.message .userpic .initials { + font-size: 16px; +} +.default { + padding: 10px; +} +.default.joined { + margin-top: -10px; +} +.default .from_name { + color: #3892db; + font-weight: 700; + padding-bottom: 5px; +} +.default .from_name .details { + font-weight: normal; +} +.default .body { + margin-left: 60px; +} +.default .text { + word-wrap: break-word; + line-height: 150%; + unicode-bidi: plaintext; + text-align: start; +} +.default .reply_to, +.default .media_wrap { + padding-bottom: 5px; +} +.default .media { + margin: 0 -10px; + padding: 5px 10px; +} +.default .media .fill, +.default .media .thumb { + width: 48px; + height: 48px; + border-radius: 50%; +} +.default .media .fill { + background-repeat: no-repeat; + background-position: 12px 12px; + background-size: 24px 24px; +} +.default .media .title, +.default .media_poll .question { + padding-top: 4px; + font-size: 14px; +} +.default .media .description { + color: #000000; + padding-top: 4px; + font-size: 13px; +} +.default .media .status { + padding-top: 4px; + font-size: 13px; +} +.default .video_file_wrap, +.default .animated_wrap { + position: relative; +} +.default .video_file, +.default .animated, +.default .photo, +.default .sticker { + display: block; +} +.video_duration { + background: rgba(0, 0, 0, .4); + padding: 0px 5px; + position: absolute; + z-index: 2; + border-radius: 2px; + right: 3px; + bottom: 3px; + color: #ffffff; + font-size: 11px; +} +.video_play_bg { + background: rgba(0, 0, 0, .4); + width: 40px; + height: 40px; + line-height: 0; + position: absolute; + z-index: 2; + border-radius: 50%; + overflow: hidden; + margin: -20px auto 0 -20px; + top: 50%; + left: 50%; + pointer-events: none; +} +.video_play { + position: absolute; + display: inline-block; + top: 50%; + left: 50%; + margin-left: -5px; + margin-top: -9px; + z-index: 1; + width: 0; + height: 0; + border-style: solid; + border-width: 9px 0 9px 14px; + border-color: transparent transparent transparent #fff; +} +.gif_play { + font-weight: 700; + color: #FFF; + display: block; + line-height: 40px; + font-size: 13px; + text-align: center; +} +.pagination { + text-align: center; + padding: 20px; + font-size: 16px; +} + +.toast_container { + position: fixed; + left: 50%; + top: 50%; + opacity: 0; + transition: opacity 3.0s ease; +} +.toast_body { + margin: 0 -50%; + float: left; + border-radius: 15px; + padding: 10px 20px; + background: rgba(0, 0, 0, 0.7); + color: #ffffff; +} +div.toast_shown { + opacity: 1; + transition: opacity 0.4s ease; +} + +.section.calls { + background-image: url(../images/section_calls.png); +} +.section.chats { + background-image: url(../images/section_chats.png); +} +.section.contacts { + background-image: url(../images/section_contacts.png); +} +.section.frequent { + background-image: url(../images/section_frequent.png); +} +.section.photos { + background-image: url(../images/section_photos.png); +} +.section.sessions { + background-image: url(../images/section_sessions.png); +} +.section.stories { + background-image: url(../images/section_stories.png); +} +.section.music { + background-image: url(../images/section_music.png); +} +.section.web { + background-image: url(../images/section_web.png); +} +.section.other { + background-image: url(../images/section_other.png) +} +.page_header a.content { + background-image: url(../images/back.png); +} +.media_call .fill { + background-image: url(../images/media_call.png) +} +.media_contact .fill { + background-image: url(../images/media_contact.png) +} +.media_file .fill { + background-image: url(../images/media_file.png) +} +.media_game .fill { + background-image: url(../images/media_game.png) +} +.media_live_location .fill, +.media_location .fill, +.media_venue .fill { + background-image: url(../images/media_location.png) +} +.media_audio_file .fill { + background-image: url(../images/media_music.png) +} +.media_invoice .fill { + background-image: url(../images/media_shop.png) +} +.media_voice_message .fill { + background-image: url(../images/media_voice.png) +} +.media_photo .fill { + background-image: url(../images/media_photo.png) +} +.media_video .fill { + background-image: url(../images/media_video.png) +} +.audio_icon { + width: 48px; + height: 48px; + border-radius: 50%; + background-color: #4f9cd9; + background-image: url(../images/media_music.png); + background-repeat: no-repeat; + background-position: 12px 12px; + background-size: 24px 24px; +} + +@media only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2) { +.section.calls { + background-image: url(../images/section_calls@2x.png); +} +.section.chats { + background-image: url(../images/section_chats@2x.png); +} +.section.contacts { + background-image: url(../images/section_contacts@2x.png); +} +.section.frequent { + background-image: url(../images/section_frequent@2x.png); +} +.section.photos { + background-image: url(../images/section_photos@2x.png); +} +.section.sessions { + background-image: url(../images/section_sessions@2x.png); +} +.section.stories { + background-image: url(../images/section_stories@2x.png); +} +.section.music { + background-image: url(../images/section_music@2x.png); +} +.section.web { + background-image: url(../images/section_web@2x.png); +} +.section.other { + background-image: url(../images/section_other@2x.png); +} +.page_header a.content { + background-image: url(../images/back@2x.png); +} +.media_call .fill { + background-image: url(../images/media_call@2x.png) +} +.media_contact .fill { + background-image: url(../images/media_contact@2x.png) +} +.media_file .fill { + background-image: url(../images/media_file@2x.png) +} +.media_game .fill { + background-image: url(../images/media_game@2x.png) +} +.media_live_location .fill, +.media_location .fill, +.media_venue .fill { + background-image: url(../images/media_location@2x.png) +} +.media_audio_file .fill { + background-image: url(../images/media_music@2x.png) +} +.media_invoice .fill { + background-image: url(../images/media_shop@2x.png) +} +.media_voice_message .fill { + background-image: url(../images/media_voice@2x.png) +} +.media_photo .fill { + background-image: url(../images/media_photo@2x.png) +} +.media_video .fill { + background-image: url(../images/media_video@2x.png) +} +.audio_icon { + background-image: url(../images/media_music@2x.png); +} +} + +.spoiler { + background: #e8e8e8; +} +.spoiler.hidden { + background: #a9a9a9; + cursor: pointer; + border-radius: 3px; +} +.spoiler.hidden span { + opacity: 0; + user-select: none; +} + +.bot_buttons_table { + border-spacing: 0px 2px; + width: 100%; +} +.bot_button { + border-radius: 8px; + text-align: center; + vertical-align: middle; + background-color: #168acd40; +} +.bot_button_row { + display: table; + table-layout: fixed; + padding: 0px; + width:100%; +} +.bot_button_row div { + display: table-cell; +} +.bot_button_column_separator { + width: 2px +} + +.reactions { + margin: 5px 0; +} + +.reactions .reaction { + display: inline-flex; + height: 20px; + border-radius: 15px; + background-color: #e8f5fc; + color: #168acd; + font-weight: bold; + margin-bottom: 5px; +} + +.reactions .reaction.active { + background-color: #40a6e2; + color: #fff; +} + +.reactions .reaction.paid { + background-color: #fdf6e1; + color: #c58523; +} + +.reactions .reaction.active.paid { + background-color: #ecae0a; + color: #fdf6e1; +} + +.reactions .reaction .emoji { + line-height: 20px; + margin: 0 5px; + font-size: 15px; +} + +.reactions .reaction .userpic:not(:first-child) { + margin-left: -8px; +} + +.reactions .reaction .userpic { + display: inline-block; +} + +.reactions .reaction .userpic .initials { + font-size: 8px; +} + +.reactions .reaction .count { + margin-right: 8px; + line-height: 20px; +} + +@media (prefers-color-scheme: dark) { +html, body { + background-color: #1a2026; /* groupCallBg */ + margin: 0; + padding: 0; +} +.page_wrap { + background-color: #1a2026; /* groupCallBg */ + color: #ffffff; /* groupCallMembersFg */ + min-height: 100vh; +} +.page_wrap a { + color: #4db8ff; /* groupCallActiveFg */ +} +.page_header { + background-color: #1a2026; /* groupCallBg */ + border-bottom: 1px solid #2c333d; /* groupCallMembersBg */ +} +.bold { + color: #ffffff; /* groupCallMembersFg */ +} +.details { + color: #91979e; /* groupCallMemberNotJoinedStatus */ +} +.page_body { + background-color: #1a2026; /* groupCallBg */ +} +code { + color: #ff8aac; /* historyPeer6UserpicBg */ + background-color: #2c333d; /* groupCallMembersBg */ +} +pre { + color: #ffffff; /* groupCallMembersFg */ + background-color: #2c333d; /* groupCallMembersBg */ + border: 1px solid #323a45; /* groupCallMembersBgOver */ +} +.with_divider { + border-top: 1px solid #2c333d; /* groupCallMembersBg */ +} +a.block_link:hover { + background-color: #323a45; /* groupCallMembersBgOver */ +} +.list_page .entry { + color: #ffffff; /* groupCallMembersFg */ +} +.message { + color: #ffffff; /* groupCallMembersFg */ +} +div.selected { + background-color: #323a45; /* groupCallMembersBgOver */ +} +.default .from_name { + color: #4db8ff; /* groupCallActiveFg */ +} +.default .media .description { + color: #ffffff; /* groupCallMembersFg */ +} +msgInBg, +.historyComposeAreaBg { + background-color: #2c333d; /* groupCallMembersBg */ +} +msgOutBg { + background-color: #323a45; /* groupCallMembersBgOver */ +} +msgInBgSelected { + background-color: #39424f; /* groupCallMembersBgRipple */ +} +msgOutBgSelected { + background-color: #39424f; /* groupCallMembersBgRipple */ +} +.spoiler { + background: #323a45; /* groupCallMembersBgOver */ +} +.spoiler.hidden { + background: #61c0ff; /* groupCallMemberInactiveStatus */ +} +.bot_button { + background-color: #4db8ff40; /* groupCallActiveFg with opacity */ +} +.reactions .reaction { + background-color: #2c333d; /* groupCallMembersBg */ + color: #4db8ff; /* groupCallActiveFg */ +} +.reactions .reaction.active { + background-color: #4db8ff; /* groupCallActiveFg */ + color: #1a2026; /* groupCallBg */ +} +.reactions .reaction.paid { + background-color: #323a45; /* groupCallMembersBgOver */ + color: #febb5b; /* historyPeer8UserpicBg */ +} +.reactions .reaction.active.paid { + background-color: #febb5b; /* historyPeer8UserpicBg */ + color: #1a2026; /* groupCallBg */ +} +} \ No newline at end of file diff --git a/ChatExport_2026-04-06/files/Лейсянке на день рождения РЭП.mp3 b/ChatExport_2026-04-06/files/Лейсянке на день рождения РЭП.mp3 new file mode 100644 index 0000000..0020bae Binary files /dev/null and b/ChatExport_2026-04-06/files/Лейсянке на день рождения РЭП.mp3 differ diff --git a/ChatExport_2026-04-06/files/Лейсянке на день рождения.mp3 b/ChatExport_2026-04-06/files/Лейсянке на день рождения.mp3 new file mode 100644 index 0000000..7ced967 Binary files /dev/null and b/ChatExport_2026-04-06/files/Лейсянке на день рождения.mp3 differ diff --git a/ChatExport_2026-04-06/images/back.png b/ChatExport_2026-04-06/images/back.png new file mode 100644 index 0000000..846479f Binary files /dev/null and b/ChatExport_2026-04-06/images/back.png differ diff --git a/ChatExport_2026-04-06/images/back@2x.png b/ChatExport_2026-04-06/images/back@2x.png new file mode 100644 index 0000000..03e3e58 Binary files /dev/null and b/ChatExport_2026-04-06/images/back@2x.png differ diff --git a/ChatExport_2026-04-06/images/media_call.png b/ChatExport_2026-04-06/images/media_call.png new file mode 100644 index 0000000..9614b95 Binary files /dev/null and b/ChatExport_2026-04-06/images/media_call.png differ diff --git a/ChatExport_2026-04-06/images/media_call@2x.png b/ChatExport_2026-04-06/images/media_call@2x.png new file mode 100644 index 0000000..92cdbe6 Binary files /dev/null and b/ChatExport_2026-04-06/images/media_call@2x.png differ diff --git a/ChatExport_2026-04-06/images/media_contact.png b/ChatExport_2026-04-06/images/media_contact.png new file mode 100644 index 0000000..be79172 Binary files /dev/null and b/ChatExport_2026-04-06/images/media_contact.png differ diff --git a/ChatExport_2026-04-06/images/media_contact@2x.png b/ChatExport_2026-04-06/images/media_contact@2x.png new file mode 100644 index 0000000..e4be8b0 Binary files /dev/null and b/ChatExport_2026-04-06/images/media_contact@2x.png differ diff --git a/ChatExport_2026-04-06/images/media_file.png b/ChatExport_2026-04-06/images/media_file.png new file mode 100644 index 0000000..3a5bde6 Binary files /dev/null and b/ChatExport_2026-04-06/images/media_file.png differ diff --git a/ChatExport_2026-04-06/images/media_file@2x.png b/ChatExport_2026-04-06/images/media_file@2x.png new file mode 100644 index 0000000..ad2c6cc Binary files /dev/null and b/ChatExport_2026-04-06/images/media_file@2x.png differ diff --git a/ChatExport_2026-04-06/images/media_game.png b/ChatExport_2026-04-06/images/media_game.png new file mode 100644 index 0000000..cad8ab8 Binary files /dev/null and b/ChatExport_2026-04-06/images/media_game.png differ diff --git a/ChatExport_2026-04-06/images/media_game@2x.png b/ChatExport_2026-04-06/images/media_game@2x.png new file mode 100644 index 0000000..09c2eb3 Binary files /dev/null and b/ChatExport_2026-04-06/images/media_game@2x.png differ diff --git a/ChatExport_2026-04-06/images/media_location.png b/ChatExport_2026-04-06/images/media_location.png new file mode 100644 index 0000000..ab8080b Binary files /dev/null and b/ChatExport_2026-04-06/images/media_location.png differ diff --git a/ChatExport_2026-04-06/images/media_location@2x.png b/ChatExport_2026-04-06/images/media_location@2x.png new file mode 100644 index 0000000..11d88d5 Binary files /dev/null and b/ChatExport_2026-04-06/images/media_location@2x.png differ diff --git a/ChatExport_2026-04-06/images/media_music.png b/ChatExport_2026-04-06/images/media_music.png new file mode 100644 index 0000000..057a694 Binary files /dev/null and b/ChatExport_2026-04-06/images/media_music.png differ diff --git a/ChatExport_2026-04-06/images/media_music@2x.png b/ChatExport_2026-04-06/images/media_music@2x.png new file mode 100644 index 0000000..20c8056 Binary files /dev/null and b/ChatExport_2026-04-06/images/media_music@2x.png differ diff --git a/ChatExport_2026-04-06/images/media_photo.png b/ChatExport_2026-04-06/images/media_photo.png new file mode 100644 index 0000000..d229387 Binary files /dev/null and b/ChatExport_2026-04-06/images/media_photo.png differ diff --git a/ChatExport_2026-04-06/images/media_photo@2x.png b/ChatExport_2026-04-06/images/media_photo@2x.png new file mode 100644 index 0000000..e46794f Binary files /dev/null and b/ChatExport_2026-04-06/images/media_photo@2x.png differ diff --git a/ChatExport_2026-04-06/images/media_shop.png b/ChatExport_2026-04-06/images/media_shop.png new file mode 100644 index 0000000..4a92ce8 Binary files /dev/null and b/ChatExport_2026-04-06/images/media_shop.png differ diff --git a/ChatExport_2026-04-06/images/media_shop@2x.png b/ChatExport_2026-04-06/images/media_shop@2x.png new file mode 100644 index 0000000..9cfe551 Binary files /dev/null and b/ChatExport_2026-04-06/images/media_shop@2x.png differ diff --git a/ChatExport_2026-04-06/images/media_video.png b/ChatExport_2026-04-06/images/media_video.png new file mode 100644 index 0000000..b0a132a Binary files /dev/null and b/ChatExport_2026-04-06/images/media_video.png differ diff --git a/ChatExport_2026-04-06/images/media_video@2x.png b/ChatExport_2026-04-06/images/media_video@2x.png new file mode 100644 index 0000000..2b554b0 Binary files /dev/null and b/ChatExport_2026-04-06/images/media_video@2x.png differ diff --git a/ChatExport_2026-04-06/images/media_voice.png b/ChatExport_2026-04-06/images/media_voice.png new file mode 100644 index 0000000..af33c2e Binary files /dev/null and b/ChatExport_2026-04-06/images/media_voice.png differ diff --git a/ChatExport_2026-04-06/images/media_voice@2x.png b/ChatExport_2026-04-06/images/media_voice@2x.png new file mode 100644 index 0000000..5356a37 Binary files /dev/null and b/ChatExport_2026-04-06/images/media_voice@2x.png differ diff --git a/ChatExport_2026-04-06/images/section_calls.png b/ChatExport_2026-04-06/images/section_calls.png new file mode 100644 index 0000000..adb42e2 Binary files /dev/null and b/ChatExport_2026-04-06/images/section_calls.png differ diff --git a/ChatExport_2026-04-06/images/section_calls@2x.png b/ChatExport_2026-04-06/images/section_calls@2x.png new file mode 100644 index 0000000..eb2de58 Binary files /dev/null and b/ChatExport_2026-04-06/images/section_calls@2x.png differ diff --git a/ChatExport_2026-04-06/images/section_chats.png b/ChatExport_2026-04-06/images/section_chats.png new file mode 100644 index 0000000..354ea9e Binary files /dev/null and b/ChatExport_2026-04-06/images/section_chats.png differ diff --git a/ChatExport_2026-04-06/images/section_chats@2x.png b/ChatExport_2026-04-06/images/section_chats@2x.png new file mode 100644 index 0000000..b7252a8 Binary files /dev/null and b/ChatExport_2026-04-06/images/section_chats@2x.png differ diff --git a/ChatExport_2026-04-06/images/section_contacts.png b/ChatExport_2026-04-06/images/section_contacts.png new file mode 100644 index 0000000..1b94b8e Binary files /dev/null and b/ChatExport_2026-04-06/images/section_contacts.png differ diff --git a/ChatExport_2026-04-06/images/section_contacts@2x.png b/ChatExport_2026-04-06/images/section_contacts@2x.png new file mode 100644 index 0000000..64a213d Binary files /dev/null and b/ChatExport_2026-04-06/images/section_contacts@2x.png differ diff --git a/ChatExport_2026-04-06/images/section_frequent.png b/ChatExport_2026-04-06/images/section_frequent.png new file mode 100644 index 0000000..1656019 Binary files /dev/null and b/ChatExport_2026-04-06/images/section_frequent.png differ diff --git a/ChatExport_2026-04-06/images/section_frequent@2x.png b/ChatExport_2026-04-06/images/section_frequent@2x.png new file mode 100644 index 0000000..ca9c3c8 Binary files /dev/null and b/ChatExport_2026-04-06/images/section_frequent@2x.png differ diff --git a/ChatExport_2026-04-06/images/section_music.png b/ChatExport_2026-04-06/images/section_music.png new file mode 100644 index 0000000..a76a244 Binary files /dev/null and b/ChatExport_2026-04-06/images/section_music.png differ diff --git a/ChatExport_2026-04-06/images/section_music@2x.png b/ChatExport_2026-04-06/images/section_music@2x.png new file mode 100644 index 0000000..53dc702 Binary files /dev/null and b/ChatExport_2026-04-06/images/section_music@2x.png differ diff --git a/ChatExport_2026-04-06/images/section_other.png b/ChatExport_2026-04-06/images/section_other.png new file mode 100644 index 0000000..a60ed7a Binary files /dev/null and b/ChatExport_2026-04-06/images/section_other.png differ diff --git a/ChatExport_2026-04-06/images/section_other@2x.png b/ChatExport_2026-04-06/images/section_other@2x.png new file mode 100644 index 0000000..497fb33 Binary files /dev/null and b/ChatExport_2026-04-06/images/section_other@2x.png differ diff --git a/ChatExport_2026-04-06/images/section_photos.png b/ChatExport_2026-04-06/images/section_photos.png new file mode 100644 index 0000000..3b511ae Binary files /dev/null and b/ChatExport_2026-04-06/images/section_photos.png differ diff --git a/ChatExport_2026-04-06/images/section_photos@2x.png b/ChatExport_2026-04-06/images/section_photos@2x.png new file mode 100644 index 0000000..587b4ff Binary files /dev/null and b/ChatExport_2026-04-06/images/section_photos@2x.png differ diff --git a/ChatExport_2026-04-06/images/section_sessions.png b/ChatExport_2026-04-06/images/section_sessions.png new file mode 100644 index 0000000..0354af4 Binary files /dev/null and b/ChatExport_2026-04-06/images/section_sessions.png differ diff --git a/ChatExport_2026-04-06/images/section_sessions@2x.png b/ChatExport_2026-04-06/images/section_sessions@2x.png new file mode 100644 index 0000000..58b213e Binary files /dev/null and b/ChatExport_2026-04-06/images/section_sessions@2x.png differ diff --git a/ChatExport_2026-04-06/images/section_stories.png b/ChatExport_2026-04-06/images/section_stories.png new file mode 100644 index 0000000..650c69c Binary files /dev/null and b/ChatExport_2026-04-06/images/section_stories.png differ diff --git a/ChatExport_2026-04-06/images/section_stories@2x.png b/ChatExport_2026-04-06/images/section_stories@2x.png new file mode 100644 index 0000000..4292451 Binary files /dev/null and b/ChatExport_2026-04-06/images/section_stories@2x.png differ diff --git a/ChatExport_2026-04-06/images/section_web.png b/ChatExport_2026-04-06/images/section_web.png new file mode 100644 index 0000000..d82e7fb Binary files /dev/null and b/ChatExport_2026-04-06/images/section_web.png differ diff --git a/ChatExport_2026-04-06/images/section_web@2x.png b/ChatExport_2026-04-06/images/section_web@2x.png new file mode 100644 index 0000000..b436884 Binary files /dev/null and b/ChatExport_2026-04-06/images/section_web@2x.png differ diff --git a/ChatExport_2026-04-06/js/script.js b/ChatExport_2026-04-06/js/script.js new file mode 100644 index 0000000..2842322 --- /dev/null +++ b/ChatExport_2026-04-06/js/script.js @@ -0,0 +1,211 @@ +"use strict"; + +window.AllowBackFromHistory = false; +function CheckLocation() { + var start = "#go_to_message"; + var hash = location.hash; + if (hash.substr(0, start.length) == start) { + var messageId = parseInt(hash.substr(start.length)); + if (messageId) { + GoToMessage(messageId); + } + } else if (hash == "#allow_back") { + window.AllowBackFromHistory = true; + } +} + +function ShowToast(text) { + var container = document.createElement("div"); + container.className = "toast_container"; + var inner = container.appendChild(document.createElement("div")); + inner.className = "toast_body"; + inner.appendChild(document.createTextNode(text)); + var appended = document.body.appendChild(container); + setTimeout(function () { + AddClass(appended, "toast_shown"); + setTimeout(function () { + RemoveClass(appended, "toast_shown"); + setTimeout(function () { + document.body.removeChild(appended); + }, 3000); + }, 3000); + }, 0); +} + +function ShowHashtag(tag) { + ShowToast("This is a hashtag '#" + tag + "' link."); + return false; +} + +function ShowCashtag(tag) { + ShowToast("This is a cashtag '$" + tag + "' link."); + return false; +} + +function ShowBotCommand(command) { + ShowToast("This is a bot command '/" + command + "' link."); + return false; +} + +function ShowMentionName() { + ShowToast("This is a link to a user mentioned by name."); + return false; +} + +function ShowNotLoadedEmoji() { + ShowToast("This custom emoji is not included, change data exporting settings to download."); + return false; +} + +function ShowNotAvailableEmoji() { + ShowToast("This custom emoji is not available."); + return false; +} + +function ShowTextCopied(content) { + navigator.clipboard.writeText(content); + ShowToast("Text copied to clipboard."); + return false; +} + +function ShowSpoiler(target) { + if (target.classList.contains("hidden")) { + target.classList.toggle("hidden"); + } +} + +function AddClass(element, name) { + var current = element.className; + var expression = new RegExp('(^|\\s)' + name + '(\\s|$)', 'g'); + if (expression.test(current)) { + return; + } + element.className = current + ' ' + name; +} + +function RemoveClass(element, name) { + var current = element.className; + var expression = new RegExp('(^|\\s)' + name + '(\\s|$)', ''); + var match = expression.exec(current); + while ((match = expression.exec(current)) != null) { + if (match[1].length > 0 && match[2].length > 0) { + current = current.substr(0, match.index + match[1].length) + + current.substr(match.index + match[0].length); + } else { + current = current.substr(0, match.index) + + current.substr(match.index + match[0].length); + } + } + element.className = current; +} + +function EaseOutQuad(t) { + return t * t; +} + +function EaseInOutQuad(t) { + return (t < 0.5) ? (2 * t * t) : ((4 - 2 * t) * t - 1); +} + +function ScrollHeight() { + if ("innerHeight" in window) { + return window.innerHeight; + } else if (document.documentElement) { + return document.documentElement.clientHeight; + } + return document.body.clientHeight; +} + +function ScrollTo(top, callback) { + var html = document.documentElement; + var current = html.scrollTop; + var delta = top - current; + var finish = function () { + html.scrollTop = top; + if (callback) { + callback(); + } + }; + if (!window.performance.now || delta == 0) { + finish(); + return; + } + var transition = EaseOutQuad; + var max = 300; + if (delta < -max) { + current = top + max; + delta = -max; + } else if (delta > max) { + current = top - max; + delta = max; + } else { + transition = EaseInOutQuad; + } + var duration = 150; + var interval = 7; + var time = window.performance.now(); + var animate = function () { + var now = window.performance.now(); + if (now >= time + duration) { + finish(); + return; + } + var dt = (now - time) / duration; + html.scrollTop = Math.round(current + delta * transition(dt)); + setTimeout(animate, interval); + }; + setTimeout(animate, interval); +} + +function ScrollToElement(element, callback) { + var header = document.getElementsByClassName("page_header")[0]; + var headerHeight = header.offsetHeight; + var html = document.documentElement; + var scrollHeight = ScrollHeight(); + var available = scrollHeight - headerHeight; + var padding = 10; + var top = element.offsetTop; + var height = element.offsetHeight; + var desired = top + - Math.max((available - height) / 2, padding) + - headerHeight; + var scrollTopMax = html.offsetHeight - scrollHeight; + ScrollTo(Math.min(desired, scrollTopMax), callback); +} + +function GoToMessage(messageId) { + var element = document.getElementById("message" + messageId); + if (element) { + var hash = "#go_to_message" + messageId; + if (location.hash != hash) { + location.hash = hash; + } + ScrollToElement(element, function () { + AddClass(element, "selected"); + setTimeout(function () { + RemoveClass(element, "selected"); + }, 1000); + }); + } else { + ShowToast("This message was not exported. Maybe it was deleted."); + } + return false; +} + +function GoBack(anchor) { + if (!window.AllowBackFromHistory) { + return true; + } + history.back(); + if (!anchor || !anchor.getAttribute) { + return true; + } + var destination = anchor.getAttribute("href"); + if (!destination) { + return true; + } + setTimeout(function () { + location.href = destination; + }, 100); + return false; +} diff --git a/ChatExport_2026-04-06/messages.html b/ChatExport_2026-04-06/messages.html new file mode 100644 index 0000000..2e5fb2e --- /dev/null +++ b/ChatExport_2026-04-06/messages.html @@ -0,0 +1,15558 @@ + + + + + + +Exported Data + + + + + + + + + + +
+ + + +
+ +
+ +
+ +
+1 March 2026 +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:08 +
+ +
+Рустам Брат Леськи +
+ +
+Привет +
+ +
+ +
+ +
+ +
+ +
+13:08 +
+ +
+Я в сервисе +
+ +
+ +
+ +
+ +
+ +
+13:08 +
+ +
+Диагностику сделали двигателя +
+ +
+ +
+ +
+ +
+ +
+13:08 +
+ +
+Задиры на всех +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:10 +
+ +
+Rustam Khalimov +
+ +
+Привет, ну это печально, но не смертельно, кстати фотки проси, наебать могут +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:12 +
+ +
+Рустам Брат Леськи +
+ +
+ +
+ +
+М +
+ +
+ +
+ +
+ +
+Марс 01.03.2026 12:48:20 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+13:12 +
+ +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+13:12 +
+ +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+13:12 +
+ +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+13:12 +
+ +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+13:12 +
+ +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+13:12 +
+ +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+13:12 +
+ +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+13:12 +
+ +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:12 +
+ +
+Рустам Брат Леськи +
+ +
+250-300к( +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:14 +
+ +
+Rustam Khalimov +
+ +
+На вид не страшно вроде +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+22:10 +
+ +
+Rustam Khalimov +
+ +
+ + + + + + + +
+ +
+Созвон с руководителем сейчас прошел. +
+ +
+ +
+ +
+ +
+ +
+22:10 +
+ +
+Ваши ставки, уволили или нет? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+22:11 +
+ +
+Рустам Брат Леськи +
+ +
+Нет конечно +
+ + + + + + +🤝 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+22:11 +
+ +
+Rustam Khalimov +
+ +
+Повысили, пока только на словах +
+ + + + + + +🔥 + + + + +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+22:11 +
+ +
+Рустам Брат Леськи +
+ +
+Сказал наверно дальше дома сиди +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+22:12 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+Он просил, что будет, если ЗП не дадут, я сказал, что похуй мне и буду дома сидеть +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+22:14 +
+ +
+Рустам Брат Леськи +
+ +
+У меня только что просто так пару раз телефон перезагрузился +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+22:15 +
+ +
+Rustam Khalimov +
+ +
+Да ёпрст, проблемы навалили +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+22:19 +
+ +
+Рустам Брат Леськи +
+ +
+Да что-то совсем +
+ +
+ +
+ +
+ +
+2 March 2026 +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+01:09 +
+ +
+Рустам Брат Леськи +
+ +
+Новый двигатель думаю буду лучше уж покупать. Пока недели 2-3 поезжу на этом если смогу обдумаю +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+01:09 +
+ +
+Rustam Khalimov +
+ +
+Тебе решать +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+01:10 +
+ +
+Рустам Брат Леськи +
+ + + +
+ +
+ +
+ +
+ +
+01:11 +
+ +
+Ну да. Много уж подумал, и драйв весь перечитал. +
+ +
+ +
+ +
+ +
+ +
+01:11 +
+ +
+Меняют, нормально ездят +
+ +
+ +
+ +
+ +
+ +
+01:11 +
+ +
+Новый двигатель получается. Но без навесного +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+01:11 +
+ +
+Rustam Khalimov +
+ +
+Завтра гляну, сегодня уже спать лен, завтра рано вставать +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+01:12 +
+ +
+Рустам Брат Леськи +
+ +
+Понял. Спокойной ночи +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+01:12 +
+ +
+Rustam Khalimov +
+ +
+Сладких! +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+08:44 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+Да проще часть нового купить, чем со старого ставить, за то в новом куча дерьма не вылезет как со старым +
+ +
+ +
+ +
+ +
+ +
+08:45 +
+ +
+Считай меньше спецов надо, меньше шансов накосячить +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+08:45 +
+ +
+Рустам Брат Леськи +
+ +
+Да, навесное не сильно и дорого. Да новое все +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+08:45 +
+ +
+Rustam Khalimov +
+ +
+И если замена двигателя, то коробку тоже снимать будут и прокладки менять или герметик, получается и масло в ней +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+08:46 +
+ +
+Рустам Брат Леськи +
+ +
+Свечи, модуль зажигания, термостат, помпа, ремень ГРМ, генератора +
+ +
+ +
+ +
+ +
+ +
+08:46 +
+ +
+In reply to this message +
+ +
+Вот это незнаю. Если коробку снимают, то как минимум масло поменять бы конечно +
+ +
+ +
+ +
+ +
+ +
+08:47 +
+ +
+Но коробку новую не буду уж пока +
+ +
+ +
+ +
+ +
+ +
+08:47 +
+ +
+Хотя смотрел вчера на Авито, за 80к можно вроде найти +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+08:47 +
+ +
+Rustam Khalimov +
+ +
+Лучше сразу, там где течь поменять прокладку или герметик +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+08:48 +
+ +
+Рустам Брат Леськи +
+ +
+Да, сцепление вроде можно поменять на новое +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+08:48 +
+ +
+Rustam Khalimov +
+ +
+Главное в тачке кузов, благо с ним всё збс +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+08:49 +
+ +
+Рустам Брат Леськи +
+ +
+Ну да +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+08:49 +
+ +
+Rustam Khalimov +
+ +
+Подушки тоже под замену будут +
+ +
+ +
+ +
+ +
+ +
+08:49 +
+ +
+Двигателя +
+ +
+ +
+ +
+ +
+ +
+08:51 +
+ +
+Если ЗП всю получу, то наверное смогу перезанять, что бы всё разом сделать +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+08:51 +
+ +
+Рустам Брат Леськи +
+ +
+Точно +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+08:57 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+А там разве ГРМ не стоит уже новый? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+09:05 +
+ +
+Рустам Брат Леськи +
+ +
+In reply to this message +
+ +
+Спасибо. Буду стараться своими силами уж. Пока я и время взял до месяца на обдумывание, остальное с кредитки, там главное в течение 100 дней вернуть, то не будет процентов +
+ +
+ +
+ +
+ +
+ +
+09:05 +
+ +
+In reply to this message +
+ +
+Вчера с сервисником общался он сказал вроде нет +
+ +
+ +
+ +
+ +
+ +
+09:06 +
+ +
+На Авито продавцу вопрос задал, пока не отвечает +
+ +
+ +
+ +
+ +
+ +
+09:07 +
+ +
+Ремни ГРМ и модуль зажигания есть на новом, посмотрел оказывается ответил +
+ + + + + + +🔥 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+11:39 +
+ +
+Rustam Khalimov +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+11:39 +
+ +
+ + + + + + + +
+ +
+Вот что вышло +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+11:40 +
+ +
+Рустам Брат Леськи +
+ +
+😄 +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+11:56 +
+ +
+Rustam Khalimov +
+ + + +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+11:57 +
+ +
+Рустам Брат Леськи +
+ +
+Фигасе +
+ +
+ +
+ +
+ +
+ +
+11:58 +
+ +
+Страшилка +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+11:58 +
+ +
+Rustam Khalimov +
+ +
+Да, четко выходит! +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+23:45 +
+ +
+Рустам Брат Леськи +
+ +
+ +
+ +
+ +
+ +
+ +
+Sticker +
+ +
+Not included, change data exporting settings to download. +
+ +
+37.4 KB +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+23:47 +
+ +
+Rustam Khalimov +
+ +
+Ого, а другие есть? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+23:48 +
+ +
+Рустам Брат Леськи +
+ +
+Не +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+23:49 +
+ +
+Rustam Khalimov +
+ + + +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+23:50 +
+ +
+Рустам Брат Леськи +
+ + + +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+23:58 +
+ +
+Rustam Khalimov +
+ +
+Ща сделаем +
+ +
+ +
+ +
+ +
+3 March 2026 +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:00 +
+ +
+Рустам Брат Леськи +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+00:00 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+00:00 +
+ +
+Сегодня обнаружили что вот эта фигня не застегнутая была +
+ +
+ +
+ +
+ +
+ +
+00:01 +
+ +
+Что-то машина подозрительно лучше начала работать +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:01 +
+ +
+Rustam Khalimov +
+ +
+Хм, интересно уже +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:01 +
+ +
+Рустам Брат Леськи +
+ +
+Но может кажется +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:01 +
+ +
+Rustam Khalimov +
+ +
+И это никто не заметил +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:01 +
+ +
+Рустам Брат Леськи +
+ +
+Завтра утром узнаю +
+ +
+ +
+ +
+ +
+ +
+00:02 +
+ +
+Сегодня еле с утра завели +
+ +
+ +
+ +
+ +
+ +
+00:02 +
+ +
+In reply to this message +
+ +
+Да она сзади же +
+ +
+ +
+ +
+ +
+ +
+00:02 +
+ +
+Форсунки промыли ещё +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:03 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+Компрессию мерили? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:03 +
+ +
+Рустам Брат Леськи +
+ +
+Не +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:03 +
+ +
+Rustam Khalimov +
+ +
+Ебать они дауны тогда, я бы не доверял им +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:04 +
+ +
+Рустам Брат Леськи +
+ +
+Почему-то все сказали что это ничего не даст +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:04 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+Да как так то, если есть задиры. должна и декомпрессия быть +
+ +
+ +
+ +
+ +
+ +
+00:05 +
+ +
+Хотя я не прав +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:05 +
+ +
+Рустам Брат Леськи +
+ +
+Ну вот они и сказали что там полюбому декомпрессия и не мерили. Я что-то сам не подумал понастоять +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:07 +
+ +
+Rustam Khalimov +
+ +
+Но проверить стоило, у меня на четырке была компрессия 12-12-8-12 по цилиндрам.
В итоге там где 8 прогарел выпускной клапан, а так бы вместо замены клапана капиталка всего двигателя была бы +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:09 +
+ +
+Рустам Брат Леськи +
+ +
+Понял. Но у меня то все равно задиры +
+ +
+ +
+ +
+ +
+ +
+00:09 +
+ +
+In reply to this message +
+ +
+Пока я пытаюсь понять что это +
+ +
+ +
+ +
+ +
+ +
+00:10 +
+ +
+У меня расход же ещё 13-14 +
+ +
+ +
+ +
+ +
+ +
+00:10 +
+ +
+Может сказали как раз изза этого +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:11 +
+ +
+Rustam Khalimov +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+00:13 +
+ +
+Зря ты gemini не используешь +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:13 +
+ +
+Рустам Брат Леськи +
+ +
+Фигасе это что так нейросеть работает +
+ + + + + + +💯 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+00:13 +
+ +
+In reply to this message +
+ +
+Походу да очень +
+ +
+ +
+ +
+ +
+ +
+00:14 +
+ +
+Я очень удивлен +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:15 +
+ +
+Rustam Khalimov +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+00:16 +
+ +
+Что то похоже не в двигателе проблема +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:17 +
+ +
+Рустам Брат Леськи +
+ +
+Походу да +
+ +
+ +
+ +
+ +
+ +
+00:17 +
+ +
+Жду очень утра +
+ +
+ +
+ +
+ +
+ +
+00:18 +
+ +
+Крутяк очень оказывается гемини +
+ +
+ +
+ +
+ +
+ +
+00:18 +
+ +
+Но с телефона бы конечно ещё +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:20 +
+ +
+Rustam Khalimov +
+ +
+С телефона так себе, с компа норм +
+ +
+ +
+ +
+ +
+ +
+00:21 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+00:21 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+00:23 +
+ +
+Короче выдыхай, двигатель живой и это норма +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:24 +
+ +
+Рустам Брат Леськи +
+ +
+Пиздец ии очень умный +
+ +
+ +
+ +
+ +
+ +
+00:24 +
+ +
+И все так разлагает +
+ +
+ +
+ +
+ +
+ +
+00:24 +
+ +
+In reply to this message +
+ +
+Да, я выдыхнул +
+ +
+ +
+ +
+ +
+ +
+00:25 +
+ +
+А эти черные нагары на клапане не надо ли убрать? +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:25 +
+ +
+Rustam Khalimov +
+ +
+Пора сервис открывать, я трактор через него чинил, он мне говорил как полуось сварить, что бы ее снова не сломало.
Карбюратор через нее настроил по фото.
Навес все расчеты по нагрузке тоже +
+ +
+ +
+ +
+ +
+ +
+00:25 +
+ +
+In reply to this message +
+ +
+Сейчас узнаем +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:25 +
+ +
+Рустам Брат Леськи +
+ +
+In reply to this message +
+ +
+И не говори. Все можно +
+ +
+ +
+ +
+ +
+ +
+00:26 +
+ +
+У меня к гемини потом ещё один вопрос есть. Не по машине +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:28 +
+ +
+Rustam Khalimov +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+00:28 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+00:28 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:31 +
+ +
+Рустам Брат Леськи +
+ +
+Итальянскую прожарку надо сделать +
+ +
+ +
+ +
+ +
+ +
+00:32 +
+ +
+У меня эндостоп есть же ещё свой. Потом как нибудь проверить +
+ +
+ +
+ +
+ +
+ +
+00:32 +
+ +
+На выходных в Челны собираемся съездить, потом посмотрю +
+ +
+ +
+ +
+ +
+ +
+00:32 +
+ +
+Может реально и уйдет +
+ +
+ +
+ +
+ +
+ +
+00:32 +
+ +
+Это может вот в последние дни собралось +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:33 +
+ +
+Rustam Khalimov +
+ +
+Благо на опелях/шевроле это легко снять свечи, у меня и на шкоде без спец инструмента невозможно +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:33 +
+ +
+Рустам Брат Леськи +
+ +
+А то и свечи которые 4 дня назад меняли все в грязи уже +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:33 +
+ +
+Rustam Khalimov +
+ +
+Трубку поменяй, она не дорогая и сам можешь или ко мне приедь, сменим +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:33 +
+ +
+Рустам Брат Леськи +
+ +
+Да у меня просто там очень +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:34 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+Так конечно смесь бедная судя по всему, вот и свечи заливает и нагар от этого +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:34 +
+ +
+Рустам Брат Леськи +
+ +
+In reply to this message +
+ +
+Ну да. Но я думаю она целая наверно. Просто незастегнутая была +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:34 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+Лучше поменяй +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:34 +
+ +
+Рустам Брат Леськи +
+ +
+Мастер как полез ща за форсунками сразу сам удивился +
+ +
+ +
+ +
+ +
+ +
+00:34 +
+ +
+In reply to this message +
+ +
+Хорошо +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:36 +
+ +
+Rustam Khalimov +
+ +
+General Motors (GM): 55559239 (это самый распространенный оригинальный номер).

Opel: 5607496 или 05607496.

На что обратить внимание при покупке
Оригинал против аналогов: Оригинальная трубка пластиковая и довольно дорогая для обычного куска пластика. Существуют аналоги (например, фирмы Dominant, ZZVF или Patron), они дешевле, но пластик там бывает еще более хрупким.

Уплотнительные кольца: Внутри защелок на концах трубки должны стоять резиновые колечки. Если берете дешевый аналог, проверьте их наличие, иначе подсос воздуха останется даже с новой трубкой.

Альтернатива (колхозинг): Многие владельцы вместо покупки дорогого пластика покупают обычный маслобензостойкий шланг (внутренний диаметр обычно 10-12 мм, нужно уточнить по месту) и два хороших винтовых хомута. Это надежнее, так как резина не треснет от вибраций и жара, как штатный пластик.

Как ее заменить самостоятельно
Там стоят быстросъемные соединения. Чтобы снять старую трубку, нужно:

Сжать стопорное кольцо (ушки) на пластиковом наконечнике.

Аккуратно потянуть на себя.

Осторожно: Если трубка старая, она, скорее всего, просто рассыплется у вас в руках — будьте готовы к тому, что осколки пластика могут упасть за двигатель.

Учитывая, что вы из Казани, эту деталь легко найти в наличии в крупных магазинах запчастей или заказать с доставкой за 1-2 дня. +
+ +
+ +
+ +
+ +
+ +
+00:36 +
+ +
+In reply to this message +
+ +
+Что там? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:37 +
+ +
+Рустам Брат Леськи +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+00:37 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+00:37 +
+ +
+Что за красная кнопка? +
+ +
+ +
+ +
+ +
+ +
+00:37 +
+ +
+Фотка конечно с плохого ракурса +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:38 +
+ +
+Rustam Khalimov +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:38 +
+ +
+Рустам Брат Леськи +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+00:39 +
+ +
+In reply to this message +
+ +
+Пиздец +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:39 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+Ищи по другим номерам +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:39 +
+ +
+Рустам Брат Леськи +
+ +
+Мы эту информацию несколько часов искали +
+ + + + + + +😁 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+00:40 +
+ +
+Ну я знал что это перевод на ручной. Но именно принцип хотел точный узнать, а он нигде не расписан +
+ +
+ +
+ +
+ +
+ +
+00:40 +
+ +
+А проверить никак. Там пломбы стоят +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:40 +
+ +
+Rustam Khalimov +
+ +
+Я сейчас всякую хуйню, в которой сомневаюсь или не пониаю, через ИИ прогоняю и сразу всё ясно +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:41 +
+ +
+Рустам Брат Леськи +
+ +
+Тоже надо начинать уже +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:41 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+Вот, пора использовать все знания нейронок +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:41 +
+ +
+Рустам Брат Леськи +
+ +
+Ну да +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:42 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+Главное логически хотя бы перепроверять, а то не всегда там то, что нужно +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:45 +
+ +
+Рустам Брат Леськи +
+ +
+Можно к Ильдару автоподбор на оживление идти помогать +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:46 +
+ +
+Rustam Khalimov +
+ +
+Можно, но зачем?
Я думаю, он сам уже юзает что то подобное +
+ +
+ +
+ +
+ +
+ +
+00:47 +
+ +
+Я смотрел на ютубе как древний уазик с ии запускают, который 7 лет стоял +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:48 +
+ +
+Рустам Брат Леськи +
+ +
+In reply to this message +
+ +
+Ничего себе +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:49 +
+ +
+Rustam Khalimov +
+ +
+Ладно, спать пора!
Я на покой ушел!
Заранее сладких! +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:49 +
+ +
+Рустам Брат Леськи +
+ +
+Сладких снов +
+ +
+ +
+ +
+ +
+ +
+00:49 +
+ +
+Я очень утра жду +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:49 +
+ +
+Rustam Khalimov +
+ +
+Зачем утра, что бы патрубок воткнуть? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:49 +
+ +
+Рустам Брат Леськи +
+ +
+Патрубок то воткнули уже +
+ +
+ +
+ +
+ +
+ +
+00:50 +
+ +
+Завести попробовать на холодную +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:50 +
+ +
+Rustam Khalimov +
+ +
+А, ты после не пробывал? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:50 +
+ +
+Рустам Брат Леськи +
+ +
+Пробовал. Но обычно только по утрам проблема +
+ +
+ +
+ +
+ +
+ +
+00:51 +
+ +
+4-5 часов обычно не влияют +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:51 +
+ +
+Rustam Khalimov +
+ +
+Ну значит завтра момент истины, хотя, если свечи залиты, то хз, повлияет ли +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:51 +
+ +
+Рустам Брат Леськи +
+ +
+А вот поэтому то мне и говорили. Что когда прогревается цилиндры расширяются и уже лучше становится +
+ +
+ +
+ +
+ +
+ +
+00:52 +
+ +
+А на холодном из-за задиров не может запуститься +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:52 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+Ну если так, то там зазор в цилиндрах должен быть 0,1 мм и больше +
+ +
+ +
+ +
+ +
+ +
+00:53 +
+ +
+In reply to this message +
+ +
+А по фото это не так +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:53 +
+ +
+Рустам Брат Леськи +
+ +
+Буду надеяться из-за патрубка +
+ +
+ +
+ +
+ +
+ +
+00:53 +
+ +
+Жду утра +
+ +
+ +
+ +
+ +
+ +
+00:53 +
+ +
+Споки +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:53 +
+ +
+Rustam Khalimov +
+ +
+Там бы стук был явный на холодную, даже не стук, а вибрация +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:54 +
+ +
+Рустам Брат Леськи +
+ +
+In reply to this message +
+ +
+Наверно +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:54 +
+ +
+Rustam Khalimov +
+ +
+Давай, до утра, отпишись. Но я завтра в 12 только встану. +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:54 +
+ +
+Рустам Брат Леськи +
+ +
+Хорошо +
+ +
+ +
+ +
+ +
+ +
+00:55 +
+ +
+И сервис просил написать с утра. Тоже ждут информацию +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+08:37 +
+ +
+Рустам Брат Леськи +
+ +
+Еле завел машину +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+09:13 +
+ +
+Рустам Брат Леськи +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+12:38 +
+ +
+Рустам Брат Леськи +
+ +
+ +
+ +
+А +
+ +
+ +
+ +
+ +
+АвтоСеть ГАРАЖ АртСити 03.03.2026 09:42:23 +
+ + + +
+ +
+ +
+ +
+ +
+ +
+12:38 +
+ + + +
+ +
+ +
+ +
+ +
+12:38 +
+ +
+ +
+ +
+А +
+ +
+ +
+ +
+ +
+АвтоСеть ГАРАЖ АртСити 03.03.2026 10:06:04 +
+ +
+150000руб мотор с документами для регистрации. Доставка 2-3 дня. 100% предоплата +
+ +
+ +
+ +
+ +
+ +
+ +
+12:38 +
+ +
+ +
+Замена + мотор + расходники выйдет около 220000руб. Капитальный ремонт выйдет около 2500000, но точная сумма появится только после деффектовки вашего мотора (разборки) +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:45 +
+ +
+Rustam Khalimov +
+ +
+Да уж, в итоге ничего так и не понятно, что делать, они тебе говорят всё менять, но логики в этом нет +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:45 +
+ +
+Рустам Брат Леськи +
+ +
+Да вот именно +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:46 +
+ +
+Rustam Khalimov +
+ +
+Леське так же говорили в гараже, что капитала нужна, оказалось, что турбина +
+ +
+ +
+ +
+ +
+ +
+13:46 +
+ +
+Так бы встряли на 250к и проблема бы осталась +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:47 +
+ +
+Рустам Брат Леськи +
+ +
+Да я и вот думаю вдруг проблема останется +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:48 +
+ +
+Rustam Khalimov +
+ +
+Делема +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+14:03 +
+ +
+Рустам Брат Леськи +
+ + + +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+14:09 +
+ +
+Rustam Khalimov +
+ +
+Диагностика нормальная нужна, каждый датчик прозванивать и прочее +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+14:11 +
+ +
+Рустам Брат Леськи +
+ +
+Да. К другим надо съездить показать +
+ +
+ +
+ +
+ +
+ +
+14:11 +
+ +
+Сегодня хочу вот этот датчик температуры на термостате поменять сам +
+ +
+ +
+ +
+ +
+ +
+14:12 +
+ +
+Быстро там оказывается. Просто фишка +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+14:12 +
+ +
+Rustam Khalimov +
+ +
+Можно конечно +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+14:12 +
+ +
+Рустам Брат Леськи +
+ +
+Он мастер и говорил что в термостате у меня проблема +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+14:14 +
+ +
+Rustam Khalimov +
+ +
+С этим ладно, а капиталка тут причем, из-за задиров и всё, которые не влияет ни на что в твоем случае? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+14:19 +
+ +
+Рустам Брат Леськи +
+ +
+Ну да очень странно. Тут просто завести нужно же. В основном поршневая группа рабочая же +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+14:37 +
+ +
+Рустам Брат Леськи +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+14:39 +
+ +
+Rustam Khalimov +
+ +
+Заезжай ближе к вечеру, глянем что там, промоем, датчик поменяем сразу. Свечи проверим. +
+ +
+ +
+ +
+ +
+ +
+14:39 +
+ +
+Только обезжиривателя у меня нет +
+ +
+ +
+ +
+ +
+ +
+14:40 +
+ +
+Купи в балоне, как спрей +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+14:42 +
+ +
+Рустам Брат Леськи +
+ +
+Хорошо. Датчик и обезжириватель куплю +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+15:14 +
+ +
+Рустам Брат Леськи +
+ +
+Походу точно в датчике охлаждающей жидкости проблема +
+ +
+ +
+ +
+ +
+ +
+15:15 +
+ +
+Что-то до этого Ютуб не открывал, а тут решил. Все подряд на этот датчик валят +
+ +
+ +
+ +
+ +
+ +
+15:15 +
+ +
+И симптомы очень схожие с моими +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+15:17 +
+ +
+Rustam Khalimov +
+ +
+Возможно +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+16:47 +
+ +
+Рустам Брат Леськи +
+ +
+Еду за датчиком в росско +
+ +
+ +
+ +
+ +
+ +
+16:47 +
+ +
+В Биби не оказалось +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+16:47 +
+ +
+Rustam Khalimov +
+ +
+Ок +
+ +
+ +
+ +
+ +
+ +
+16:47 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+16:47 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+16:47 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+16:48 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+16:49 +
+ +
+Рустам Брат Леськи +
+ +
+Много снега очень +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+16:58 +
+ +
+Rustam Khalimov +
+ +
+Да +
+ +
+ +
+ +
+ +
+4 March 2026 +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+09:51 +
+ +
+Рустам Брат Леськи +
+ +
+Ураааа +
+ + + + + + +🔥 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+12:35 +
+ +
+Рустам Брат Леськи +
+ +
+С пол пинка прям завелся +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+12:39 +
+ +
+Rustam Khalimov +
+ +
+Это же супер +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+12:41 +
+ +
+Рустам Брат Леськи +
+ +
+Очень супер +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:26 +
+ +
+Rustam Khalimov +
+ +
+ +
+ +
+Г +
+ +
+ +
+ +
+ +
+ГЕНЕРАЛЬНЫЙ ШТАБ 04.03.2026 13:25:10 +
+ + + +
+Израиль опубликовал кадры "уничтожения иранского военного вертолёта", но это было просто изображение, нарисованное на земле😂 +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:27 +
+ +
+Рустам Брат Леськи +
+ +
+Зато точно в цель +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:28 +
+ +
+Rustam Khalimov +
+ +
+Капиталку будешь делать, ну хотя бы просто так, что бы 200к просрать? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:29 +
+ +
+Рустам Брат Леськи +
+ +
+Конечно +
+ +
+ +
+ +
+ +
+ +
+13:30 +
+ +
+Чтобы не просто несколько в тревоге быть, надо уж до конца довести +
+ +
+ +
+ +
+ +
+ +
+13:30 +
+ +
+Сегодня масло поеду менять +
+ +
+ +
+ +
+ +
+ +
+13:30 +
+ +
+К ним же +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:30 +
+ +
+Rustam Khalimov +
+ +
+Как они отреагировали то? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:30 +
+ +
+Рустам Брат Леськи +
+ +
+И ходовую осмотр для начала +
+ +
+ +
+ +
+ +
+ +
+13:30 +
+ +
+In reply to this message +
+ +
+Не прочитали даже +
+ +
+ +
+ +
+ +
+ +
+13:30 +
+ +
+С утра написал +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:31 +
+ +
+Rustam Khalimov +
+ +
+Я бы им не доверял +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:31 +
+ +
+Рустам Брат Леськи +
+ +
+Да. Но блин удобное расположение для меня +
+ +
+ +
+ +
+ +
+ +
+13:31 +
+ +
+Я ещё это здание обслуживать сегодня поеду. Где они арендуют +
+ +
+ +
+ +
+ +
+ +
+13:32 +
+ +
+Масло уж поменяют +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:33 +
+ +
+Rustam Khalimov +
+ +
+Понимаю, да, надеюсь всё нормально сделают +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:33 +
+ +
+Рустам Брат Леськи +
+ +
+Да +
+ +
+ +
+ +
+ +
+7 March 2026 +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+17:37 +
+ +
+Rustam Khalimov +
+ +
+Привет, про кредит не забудь +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+21:25 +
+ +
+Рустам Брат Леськи +
+ +
+Привет. Скинул 40к только. Положить наличку не успел сегодня. Завтра ещё 10к скину +
+ + + + + + +👌 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+8 March 2026 +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+18:01 +
+ +
+Рустам Брат Леськи +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+18:01 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+18:01 +
+ +
+Решил ноутбук почистить +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+18:06 +
+ +
+Rustam Khalimov +
+ +
+Пипец +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+18:07 +
+ +
+Рустам Брат Леськи +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+18:07 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+18:07 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+18:07 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+18:13 +
+ +
+Фуух +
+ +
+ +
+ +
+ +
+ +
+18:14 +
+ +
+Собрал. Монитор не запускался, несколько раз шлейф монитора первтыкал проверял никак +
+ +
+ +
+ +
+ +
+ +
+18:14 +
+ +
+Обратно все разобрал, походу оперативка не до конца была засунута +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+18:35 +
+ +
+Рустам Брат Леськи +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+18:35 +
+ +
+In reply to this message +
+ +
+Это Алсу ноутбук +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+18:35 +
+ +
+Rustam Khalimov +
+ +
+Он ещё и на жёстком диске +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+18:36 +
+ +
+Рустам Брат Леськи +
+ +
+Да. Как нибудь на ссд переделать надо будет +
+ + + + + + +💯 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+10 March 2026 +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:47 +
+ +
+Rustam Khalimov +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:48 +
+ +
+Рустам Брат Леськи +
+ +
+Немного конечно осталось +
+ + + + + + +💯 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+13:48 +
+ +
+На два перевода уж только смогу +
+ +
+ +
+ +
+ +
+ +
+13:49 +
+ +
+Я заказал ххд в озоне вчера вс таки +
+ +
+ +
+ +
+ +
+ +
+13:50 +
+ +
+Только странно недорого +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:50 +
+ +
+Rustam Khalimov +
+ +
+Круто, надеюсь нормальный +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:50 +
+ +
+Рустам Брат Леськи +
+ +
+Western Digital 2 ТБ Внутренний жесткий диск (WD20EFRX)
https://ozon.ru/t/DurWSTT +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:50 +
+ +
+Rustam Khalimov +
+ +
+Когда придет твой NAS? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:51 +
+ +
+Рустам Брат Леськи +
+ +
+In reply to this message +
+ +
+С 14 по 22 стоит +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:51 +
+ +
+Rustam Khalimov +
+ +
+Неплохо +
+ +
+ +
+ +
+ +
+ +
+13:52 +
+ +
+In reply to this message +
+ +
+Реально дёшево +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+14:00 +
+ +
+Рустам Брат Леськи +
+ +
+In reply to this message +
+ +
+Ну чё то. Может кирпич просто придет +
+ +
+ +
+ +
+ +
+ +
+14:00 +
+ +
+В озоне прямо посмотрю по виду как придёт +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+14:02 +
+ +
+Rustam Khalimov +
+ +
+И проверить дома нароботку и битые сектора +
+ +
+ +
+ +
+ +
+ +
+14:03 +
+ +
+Хотя по отзывам б/у прошитый +
+ +
+ +
+ +
+ +
+ +
+14:03 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+14:05 +
+ +
+Я бы отказался сразу +
+ +
+ +
+ +
+ +
+ +
+14:05 +
+ +
+Даже ждать смысла нет +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+14:05 +
+ +
+Рустам Брат Леськи +
+ +
+Да согласен +
+ +
+ +
+ +
+ +
+ +
+14:05 +
+ +
+Сейчас +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+14:07 +
+ +
+Rustam Khalimov +
+ +
+За эти деньги лучше возьми ssd nvme на 512 +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+14:14 +
+ +
+Рустам Брат Леськи +
+ +
+Ну да. Но это все nas ради ххд в основном делается же +
+ +
+ +
+ +
+ +
+ +
+14:15 +
+ +
+Поэтому его хотел вначале +
+ +
+ +
+ +
+ +
+ +
+14:15 +
+ +
+Или nvme тоже в райд можно встроить?🤔 +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+14:16 +
+ +
+Rustam Khalimov +
+ +
+Можно, но их 2 надо одинаковых +
+ +
+ +
+ +
+ +
+ +
+14:16 +
+ +
+Как и HDD +
+ +
+ +
+ +
+ +
+ +
+14:21 +
+ +
+Я что то прям вовремя заморочился, сейчас бы мой сервер мне в 400-500к обошелся вместо 150 +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+14:21 +
+ +
+Рустам Брат Леськи +
+ +
+Ну да +
+ +
+ +
+ +
+ + + +
+ +
+ +
+ +
+14:22 +
+ +
+Мне бы тоже пол года назад надо было +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+14:22 +
+ +
+Rustam Khalimov +
+ +
+Может тебе его продать, я про диск, и купить 2 подходящих +
+ +
+ +
+ +
+ +
+ +
+14:23 +
+ +
+Можно и б/у взять под рейд, года 2022 и выше +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+14:24 +
+ +
+Рустам Брат Леськи +
+ +
+Да может. Но и подходящий же стоят очень дорого +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+14:24 +
+ +
+Rustam Khalimov +
+ +
+Но ты можешь сделать иначе, поставить тот что у тебя есть пока что, потом купить на 8тб и туда бэкапы делать +
+ +
+ +
+ +
+ +
+ +
+14:25 +
+ +
+Потом второй купить на 8 и их оба в рейд, а этот на 2 сольёшь +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+14:26 +
+ +
+Рустам Брат Леськи +
+ +
+Я просто боюсь вот я пока буду копить файлы все туда. А потом когда куплю новый поставлю. Тогда когда рейд буду делать, вдруг что-то пойдет не так и все стерется +
+ +
+ +
+ +
+ +
+ +
+14:26 +
+ +
+8 много уж +
+ +
+ +
+ +
+ +
+ +
+14:26 +
+ +
+4 точно хватит +
+ +
+ +
+ +
+ +
+ +
+14:26 +
+ +
+Я уже думаю и 2 хватит +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+14:27 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+Ну если изначально криво сделать, то да, пойдет всё не так, когда рэйд сделаешь, можно не ту схему выбрать и копирнуть пустой на полный, а не наоборот +
+ +
+ +
+ +
+ +
+ +
+14:28 +
+ +
+In reply to this message +
+ +
+Тебе решать, мне пока 2 хватает, но у меня всё хитрее сделано +
+ +
+ +
+ +
+ +
+ +
+14:29 +
+ +
+У меня зеркало на 8ТБ поделенных посекторно на блоки по 2ТБ +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+14:29 +
+ +
+Рустам Брат Леськи +
+ +
+In reply to this message +
+ +
+Ну вот поэтому я хотел на 2тб сразу запустить +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+14:30 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+Не расчитывай, что из коробки сразу всё огонь будет, может не понравится тебе заводская ос и сразу будешь норм делать, пару раз снесешь систему, сразу смысла делать нет, 2-4 недели потестить надо +
+ +
+ +
+ +
+ +
+ +
+14:32 +
+ +
+Я могу тебе б/у 2 тошибы дать, пока не купишь нормальные, они верой и правдой служили, там потом по одному заменишь и мне вернешь +
+ +
+ +
+ +
+ +
+ +
+14:33 +
+ +
+На них потренируемся, посмотрим как работает зеркало, если один убрать и другой воткнуть +
+ +
+ +
+ + + +
+ +
+ +
+14:37 +
+ +
+Этот попизже будет 19к за 6ТБ, чем 12к за 2ТБ +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+14:49 +
+ +
+Рустам Брат Леськи +
+ +
+In reply to this message +
+ +
+Ну да уж. Но это только один же ещё +
+ + + + + + +💯 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+14:55 +
+ +
+Rustam Khalimov +
+ +
+За то на годы хватит.
Короче надо вживую общаться и думать +
+ +
+ +
+ +
+ +
+ +
+14:56 +
+ +
+Моё предложение, как всё придет, собраться у меня, поставить б/у диски, простестить ОС как оно работает и что даёт.
Дальше думать будем. +
+ +
+ +
+ +
+ +
+ +
+14:56 +
+ +
+Ну и оперативку докинем +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+15:05 +
+ +
+Рустам Брат Леськи +
+ +
+Да согласен, лучше вживую вместе потестить посмотреть +
+ + + + + + +🤝 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+13 March 2026 +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+11:56 +
+ +
+Rustam Khalimov +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+Rustam Khalimov 13.03.2026 11:56:09 +
+ +
+ + + + + + + +
+ +
+Пизда вам ресурсы, от локальной ИИ +
+ +
+ +
+ +
+ +
+ +
+ +
+11:56 +
+ +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+16:02 +
+ +
+Рустам Брат Леськи +
+ +
+Ничего себе +
+ +
+ +
+ +
+ +
+14 March 2026 +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+22:11 +
+ +
+Rustam Khalimov +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+Rustam Khalimov 14.03.2026 22:10:02 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+22:11 +
+ +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+22:11 +
+ +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+22:11 +
+ +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+22:11 +
+ +
+ +
+Как тебе дизайн? +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+22:11 +
+ +
+Rustam Khalimov +
+ +
+Я месседжер пишу свой, что бы локально развернуть +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+22:13 +
+ +
+Рустам Брат Леськи +
+ +
+Прикольно +
+ +
+ +
+ +
+ +
+ +
+22:13 +
+ +
+Когда готов будет скажешь +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+22:14 +
+ +
+Rustam Khalimov +
+ +
+Обязательно, я же конфидерацию хочу сделать, что бы люди сами на свои сервера ставили и можно было с другими серверами общаться +
+ +
+ +
+ +
+ +
+ +
+22:14 +
+ +
+Видео и аудио звонки уже сделал и конференции на 200 человек +
+ +
+ +
+ +
+ +
+15 March 2026 +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+11:12 +
+ +
+Rustam Khalimov +
+ +
+Куда едите? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+11:12 +
+ +
+Рустам Брат Леськи +
+ +
+Никуда, только проснулись +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+11:13 +
+ +
+Rustam Khalimov +
+ +
+А, я думал это вы, увидели на дороги вашу копию +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+11:13 +
+ +
+Рустам Брат Леськи +
+ +
+Аа +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:57 +
+ +
+Рустам Брат Леськи +
+ +
+А вы куда ездили? +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:57 +
+ +
+Rustam Khalimov +
+ +
+Порт +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:58 +
+ +
+Рустам Брат Леськи +
+ +
+Мы вот только выходить будем из дома +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:58 +
+ +
+Rustam Khalimov +
+ +
+Понял, NAS пришёл? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:58 +
+ +
+Рустам Брат Леськи +
+ +
+Ещё нет +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:58 +
+ +
+Rustam Khalimov +
+ +
+Кстати приезжайте, купаты приготовим +
+ +
+ +
+ +
+ +
+ +
+13:59 +
+ +
+И бампер поможешь мне скинуть +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:59 +
+ +
+Рустам Брат Леськи +
+ +
+ + + + + + + +
+ + + + + + +🔥 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+14:00 +
+ +
+Rustam Khalimov +
+ +
+Если конечно у вас других планов нет +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+14:00 +
+ +
+Рустам Брат Леськи +
+ +
+In reply to this message +
+ +
+Планы были конечно( +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+14:00 +
+ +
+Rustam Khalimov +
+ +
+Ничего страшного +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+14:00 +
+ +
+Рустам Брат Леськи +
+ +
+Так то приехали бы +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+14:52 +
+ +
+Rustam Khalimov +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+14:52 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+14:54 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+14:54 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+14:54 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+14:54 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+14:54 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+15:09 +
+ +
+Рустам Брат Леськи +
+ +
+А что это когда? Давно было? +
+ +
+ +
+ +
+ +
+ +
+15:10 +
+ +
+А из чего эта платина? Металл или пластик? +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+15:15 +
+ +
+Rustam Khalimov +
+ +
+Пластик, об сугроб уебался при парковке +
+ +
+ +
+ +
+ +
+ +
+15:15 +
+ +
+На той неделе +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+17:03 +
+ +
+Рустам Брат Леськи +
+ +
+Жалко конечно. Крыло очень +
+ + + + + + +💯 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+17:03 +
+ +
+Rustam Khalimov +
+ +
+ + + + + + + +
+ +
+До +
+ +
+ +
+ +
+ +
+ +
+17:03 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+17:03 +
+ +
+ + + + + + + +
+ +
+После +
+ +
+ +
+ +
+ +
+ +
+17:03 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+17:03 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+17:03 +
+ +
+Хули, шпаклевать и красить надо теперь +
+ +
+ +
+ +
+ +
+ +
+17:04 +
+ +
+Выпрямил, уже збс +
+ +
+ +
+ +
+ +
+18 March 2026 +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ + + +
+ +
+ +
+ +
+13:52 +
+ +
+Завтра ДР у нее, как думаешь, понравится ей? +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+17:48 +
+ +
+Rustam Khalimov +
+ + + +
+И вот второй вариант +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+21:01 +
+ +
+Рустам Брат Леськи +
+ +
+Привет. Недавно прослушал только, очень круто получились оба +
+ +
+ +
+ +
+ +
+ +
+21:01 +
+ +
+Прикольно и красиво +
+ +
+ +
+ +
+ +
+ +
+21:02 +
+ +
+Я Чебоксарах застрял на объекте. Сегодня на вторую ночь остаёмся +
+ +
+ +
+ +
+ +
+ +
+21:02 +
+ +
+Ещё на работе нахожусь +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+21:02 +
+ +
+Rustam Khalimov +
+ +
+Эх, надеюсь завтра обратно +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+21:03 +
+ +
+Рустам Брат Леськи +
+ +
+Да надеюсь +
+ +
+ +
+ +
+ +
+ +
+21:04 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+21:23 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+Это что за вечеруха? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+22:11 +
+ +
+Рустам Брат Леськи +
+ +
+Да вот надо все написать +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+22:11 +
+ +
+Rustam Khalimov +
+ + + +
+ +
+ +
+ +
+22 March 2026 +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+10:23 +
+ +
+Рустам Брат Леськи +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+10:25 +
+ +
+И решился заказать 2шт wd red plus 4tb по 17к на Яндекс маркете +
+ +
+ +
+ +
+ +
+ +
+10:25 +
+ +
+29-30 марта жесткие должны прийти +
+ +
+ +
+ +
+ +
+ +
+10:26 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+11:51 +
+ +
+Рустам Брат Леськи +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+11:55 +
+ +
+Антифриз отсюда вытекал немного. Кольцо резиновое уплотнительное задубело +
+ +
+ +
+ +
+ +
+ +
+11:55 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+11:56 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+11:57 +
+ +
+Купил оригинальное с экзиста, вчера за минуту поменял. Оказывается все просто +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+12:07 +
+ +
+Rustam Khalimov +
+ +
+Ну супер, круто +
+ +
+ +
+ +
+ +
+24 March 2026 +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+19:34 +
+ +
+Рустам Брат Леськи +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+19:34 +
+ +
+Ураа +
+ +
+ +
+ +
+ +
+ +
+19:34 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+19:34 +
+ +
+Rustam Khalimov +
+ +
+О, збс +
+ +
+ +
+ +
+ +
+ +
+19:34 +
+ +
+Диски тоже пришли? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+19:35 +
+ +
+Рустам Брат Леськи +
+ +
+In reply to this message +
+ +
+Нет ещё +
+ +
+ +
+ +
+ +
+ +
+19:35 +
+ +
+Это на 2тб который был пока вставил просто +
+ +
+ +
+ +
+ +
+ +
+19:35 +
+ +
+Я только с работы пришел +
+ +
+ +
+ +
+ +
+ +
+19:35 +
+ +
+Ещё даже не раздевался, сразу посмотрел😄 +
+ +
+ +
+ +
+ +
+ +
+19:36 +
+ +
+Попозже через часик включу посмотрю +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+19:36 +
+ +
+Rustam Khalimov +
+ +
+Ага, ну хорошо +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+19:36 +
+ +
+Рустам Брат Леськи +
+ +
+На выходные к тебе +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+19:36 +
+ +
+Rustam Khalimov +
+ +
+Да, вместе глянем +
+ +
+ +
+ +
+ +
+ +
+19:37 +
+ +
+У меня пока жопа полная, к собеседованию готовлюсь в озон, послезавтра уже, ебать будут как никогда +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+19:37 +
+ +
+Рустам Брат Леськи +
+ +
+In reply to this message +
+ +
+(( +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+19:38 +
+ +
+Rustam Khalimov +
+ +
+Первый этап прошел, второй надо, потом ещё один и ещё один, 4 этапа общее +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+19:38 +
+ +
+Рустам Брат Леськи +
+ +
+Ничего себе +
+ +
+ +
+ +
+ +
+ +
+19:38 +
+ +
+Удачи уж +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+19:39 +
+ +
+Rustam Khalimov +
+ +
+400к попросил у них, согласились пока что, после собесов могут урезать +
+ +
+ +
+ +
+ +
+ +
+19:39 +
+ +
+Спасибо +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+19:40 +
+ +
+Рустам Брат Леськи +
+ +
+А с той работой все? +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+19:41 +
+ +
+Rustam Khalimov +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+19:42 +
+ +
+Нихуя не понятно, тянут, а запасы нашы иссякли почти +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+19:46 +
+ +
+Рустам Брат Леськи +
+ +
+Да плохо очень конечно что тянут и не говорят ничего +
+ + + + + + +💯 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+25 March 2026 +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+10:25 +
+ +
+Rustam Khalimov +
+ +
+ +
+ +
+А +
+ +
+ +
+ +
+ +
+Антон Шабашов 24.03.2026 21:05:28 +
+ + + +
+😂 Коротко о ситуации в на Ближнем Востоке +
+ +
+ +
+ +
+ +
+ +
+27 March 2026 +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+11:29 +
+ +
+Rustam Khalimov +
+ +
+ + + + + + + +
+ +
+Во наша статистика на двоих за год +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+11:42 +
+ +
+Рустам Брат Леськи +
+ +
+Нормально +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+12:03 +
+ +
+Рустам Брат Леськи +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+12:04 +
+ +
+Rustam Khalimov +
+ +
+А ты уже затестил? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+12:15 +
+ +
+Рустам Брат Леськи +
+ +
+Да, я подключил так простенько пока +
+ +
+ +
+ +
+ +
+ +
+12:15 +
+ +
+И просто загрузил файлы в диск +
+ +
+ +
+ +
+ +
+ +
+12:15 +
+ +
+Бекап ещё не настроил +
+ +
+ +
+ +
+ +
+ +
+12:15 +
+ +
+Но там оказывается все просто +
+ +
+ +
+ +
+ +
+ +
+12:16 +
+ +
+В выходные если время будет посмотрим вместе ещё +
+ +
+ +
+ +
+ +
+ +
+12:16 +
+ +
+Может и диски прийти успеют +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+12:17 +
+ +
+Rustam Khalimov +
+ +
+Там много чего надо делать +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+12:18 +
+ +
+Рустам Брат Леськи +
+ +
+Вначале там мастер помогает рейд настроить просто +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+12:19 +
+ +
+Rustam Khalimov +
+ +
+Приложение для фоток как тебе или не тестил? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+12:24 +
+ +
+Рустам Брат Леськи +
+ +
+Не тестил ещё +
+ +
+ +
+ +
+ +
+ +
+12:24 +
+ +
+Времени очень мало, поздно домой приезжаю +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+12:30 +
+ +
+Rustam Khalimov +
+ +
+Да, печально +
+ +
+ +
+ +
+ +
+28 March 2026 +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+12:21 +
+ +
+Rustam Khalimov +
+ +
+Ты завтра планируешь? +
+ +
+ +
+ +
+ +
+ +
+12:21 +
+ +
+Мы сегодня на др +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+12:28 +
+ +
+Рустам Брат Леськи +
+ +
+In reply to this message +
+ +
+Да наверно. Точно не сегодня +
+ +
+ +
+ +
+ +
+ +
+12:29 +
+ +
+Там жесткие пришли, с пвз сегодня заберу +
+ + + + + + +🔥 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+14:07 +
+ +
+Рустам Брат Леськи +
+ +
+ + + + + + + +
+ + + + + + +🔥 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+20:13 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+Во по кайфу теперь +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+20:15 +
+ +
+Рустам Брат Леськи +
+ +
+Да, поставил старый на форматирование пока. 4 часа написалось +
+ + + + + + +🔥 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+20:15 +
+ +
+Завтра думаю колеса на летние поменять +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+20:17 +
+ +
+Rustam Khalimov +
+ +
+Го +
+ +
+ +
+ +
+ +
+29 March 2026 +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+01:40 +
+ +
+Рустам Брат Леськи +
+ +
+ +
+ +
+ +
+ +
+ +
+Video file +
+ +
+Exceeds maximum size, change data exporting settings to download. +
+ +
+00:37, 36.9 MB +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+01:40 +
+ +
+Оформляю возврат +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+11:52 +
+ +
+Rustam Khalimov +
+ +
+Диски? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+11:57 +
+ +
+Рустам Брат Леськи +
+ +
+Да +
+ +
+ +
+ +
+ +
+ +
+11:57 +
+ +
+Только воткнул один запищал +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+12:02 +
+ +
+Rustam Khalimov +
+ +
+Скорее всего диски тоже наебка б/у, сейчас много такого. +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+12:20 +
+ +
+Рустам Брат Леськи +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+12:21 +
+ +
+Ну походу да бу +
+ +
+ +
+ +
+ +
+ +
+12:21 +
+ +
+Пока оформил. Жду ответа от продавца +
+ +
+ +
+ +
+ +
+ +
+12:23 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+12:24 +
+ +
+Rustam Khalimov +
+ +
+Оба на возврат или один? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+12:24 +
+ +
+Рустам Брат Леськи +
+ +
+Оба сделаю уж +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+12:25 +
+ +
+Rustam Khalimov +
+ +
+Ну и правильно, лучше в Ситилинк взять через озон +
+ +
+ +
+ +
+ +
+ +
+12:29 +
+ +
+Ты пробивал их по серийному номеру на сайте производителя +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+12:29 +
+ +
+Рустам Брат Леськи +
+ +
+Не. Я где-то читал что не обязательно пробивается +
+ +
+ +
+ +
+ +
+ +
+12:29 +
+ +
+Поэтому не стал +
+ +
+ +
+ +
+ +
+ +
+12:30 +
+ +
+In reply to this message +
+ +
+Ну да, так и сделаю наверно +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+12:30 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+Обязательно, не может такого быть +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+12:30 +
+ +
+Рустам Брат Леськи +
+ +
+Главное чтобы эти продавец взял обратно +
+ +
+ +
+ +
+ +
+ +
+12:30 +
+ +
+Что нибудь ещё напишет +
+ + + + + + +💯 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+12:53 +
+ +
+Rustam Khalimov +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+12:53 +
+ +
+Это точно подделка +
+ +
+ +
+ +
+ +
+ +
+12:53 +
+ +
+Хотя на фото разные +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+12:58 +
+ +
+Рустам Брат Леськи +
+ +
+Один не просто скрипит, а совсем оказывается не работает +
+ + + + + + +😢 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+12:59 +
+ +
+При включении скрипит пытается. Потом все +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:00 +
+ +
+Rustam Khalimov +
+ +
+Если местами менять то так же? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:00 +
+ +
+Рустам Брат Леськи +
+ +
+Ну попал конечно +
+ +
+ +
+ +
+ +
+ +
+13:00 +
+ +
+In reply to this message +
+ +
+Не пробовал +
+ +
+ +
+ +
+ +
+ +
+13:00 +
+ +
+Посмотрю +
+ +
+ +
+ +
+ +
+ +
+13:00 +
+ +
+Я привезу все +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:00 +
+ +
+Rustam Khalimov +
+ +
+Ок +
+ +
+ +
+ +
+ +
+ +
+13:00 +
+ +
+Когда ждать? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:00 +
+ +
+Рустам Брат Леськи +
+ +
+Сейчас в душ схожу постриглась быстренько, и к вам +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:01 +
+ +
+Rustam Khalimov +
+ +
+Алсу приедет? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:01 +
+ +
+Рустам Брат Леськи +
+ +
+Алсу тоже будет, ей тоже ещё время надо. Ужин готовит +
+ + + + + + +👌 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+13:02 +
+ +
+Seagate 4 ТБ Внутренний жесткий диск (ST4000VN006)
https://ozon.ru/t/d7FtTmx +
+ + + + + + +🔥 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+13:02 +
+ +
+Вот этот думаю брать уж +
+ +
+ +
+ +
+ +
+ +
+13:03 +
+ +
+Я в Яндекс маркете сплит брал просто. Там рассрочка, удобно +
+ +
+ +
+ +
+ +
+ +
+13:03 +
+ +
+В озоне нет такого, поэтому ещё думаю +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:14 +
+ +
+Rustam Khalimov +
+ +
+Можешь купаты купить, приготовим, если желание есть +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:29 +
+ +
+Рустам Брат Леськи +
+ +
+Окей, купим +
+ + + + + + +👌 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+30 March 2026 +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+19:17 +
+ +
+Rustam Khalimov +
+ +
+ +
+ +
+С +
+ +
+ +
+ +
+ +
+Соболев LIVE 30.03.2026 17:19:01 +
+ + + +
+Вайб 2026. Кто ещё может смотреть видео - тот молодец.

@sobolevtam +
+ +
+ +
+ +
+ +
+ +
+2 April 2026 +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+00:18 +
+ +
+Рустам Брат Леськи +
+ +
+Привет. С днюхой тебя! Желаю крепкого здоровья и высоких зарплат! +
+ + + + + + +🤝 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+00:37 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+Спасибо! +
+ +
+ +
+ +
+ +
+3 April 2026 +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:50 +
+ +
+Рустам Брат Леськи +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+13:50 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+13:50 +
+ +
+Привет. Вчера забрал +
+ + + + + + +🔥 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+13:51 +
+ +
+Но что-то немного со звуком работают, один который до этого стоял почти не шумел, подумываю что опять попал на брак +
+ +
+ +
+ +
+ +
+ +
+13:51 +
+ +
+Но оба в заводской упаковке были +
+ +
+ +
+ +
+ +
+ +
+13:52 +
+ +
+Пока сильно не разбирался, вечером по одному попробую посмотрю +
+ +
+ +
+ +
+ +
+ +
+13:54 +
+ +
+Ну может конечно это норм. Шумят когда я нагружаю вроде +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:54 +
+ +
+Rustam Khalimov +
+ +
+Под нагрузкой должны шуметь но не сильно +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+17:14 +
+ +
+Рустам Брат Леськи +
+ + + +
+ +
+ +
+ +
+ +
+17:15 +
+ +
+Ну вот такая скорость. Подключен через локалку, никак уж там 100мб/с даже не достигаются, максимум около 20 только +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+17:16 +
+ +
+Rustam Khalimov +
+ +
+Что то не то +
+ +
+ +
+ +
+ +
+ +
+17:31 +
+ +
+А порты роутера у тебя какие? +
+ +
+ +
+ +
+ +
+ +
+17:34 +
+ + + +
+ +
+ +
+ +
+ +
+17:34 +
+ +
+Вот у меня весь гигабит выдает +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+17:44 +
+ +
+Рустам Брат Леськи +
+ +
+In reply to this message +
+ +
+100мб +
+ +
+ +
+ +
+ +
+ +
+17:44 +
+ +
+Но тут же даже не близко +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+17:44 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+100мегабит это 11 мегабайт в сек +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+17:44 +
+ +
+Рустам Брат Леськи +
+ +
+Может с телефона потомучто по вайфаю +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+17:45 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+Может быть или прослойки какие то +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+17:45 +
+ +
+Рустам Брат Леськи +
+ +
+In reply to this message +
+ +
+Хм🤔 +
+ +
+ +
+ +
+ +
+ +
+17:46 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+17:47 +
+ +
+Но походу норм это все +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+17:48 +
+ +
+Rustam Khalimov +
+ +
+У меня с телефона так же, в сеть упирается, 105-110 мегабайт в сек +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+17:48 +
+ +
+Рустам Брат Леськи +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+17:48 +
+ +
+Rustam Khalimov +
+ +
+20-60 это мало +
+ +
+ +
+ +
+ +
+4 April 2026 +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+12:16 +
+ +
+Rustam Khalimov +
+ +
+ + + + + + + +
+ + + + + + +🔥 + + + + +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+12:16 +
+ +
+Пока такой прогресс +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+12:16 +
+ +
+Рустам Брат Леськи +
+ +
+Ничего себе. Круто +
+ +
+ +
+ +
+ +
+ +
+12:16 +
+ +
+У меня +10 к этому +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+12:30 +
+ +
+Rustam Khalimov +
+ +
+Ну тоже скинешь со временем +
+ +
+ +
+ +
+ +
+5 April 2026 +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+18:54 +
+ +
+Rustam Khalimov +
+ +
+Как там твой NAS? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+18:56 +
+ +
+Рустам Брат Леськи +
+ +
+Все норм. Сегодня в шкаф наконец разместил +
+ +
+ +
+ +
+ +
+ +
+18:58 +
+ +
+В первый день как включил индексирование, больше суток без остановки шумел вентилятор. По программе температура процессора больше 80 градусов стояла. +
+ +
+ +
+ +
+ +
+ +
+18:59 +
+ +
+Сейчас уже норм. Тихонечко стоит просто +
+ +
+ +
+ +
+ +
+ +
+19:00 +
+ +
+И жесткие тоже норм уже. Они только при первом включении потрескиванием просто как будто включаются, а дальше все норм работают +
+ +
+ +
+ +
+ +
+ +
+19:01 +
+ +
+Но вентилятор если включается то шумно +
+ +
+ +
+ +
+ +
+ +
+19:01 +
+ +
+Я уже по форумам все поизучал как быть. Решают шум вентилятора заменой на другой +
+ +
+ +
+ +
+ +
+ +
+19:02 +
+ +
+Но в итоге уже нормально +
+ +
+ +
+ +
+ +
+ +
+19:02 +
+ +
+Только английский язык конечно минус +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+19:38 +
+ +
+Рустам Брат Леськи +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+20:04 +
+ +
+Rustam Khalimov +
+ + + +
+ +
+ +
+ +
+ +
+20:05 +
+ +
+Проксмокс надо ставить, но ssd надо +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+20:14 +
+ +
+Рустам Брат Леськи +
+ +
+Я заказал с алика +
+ +
+ +
+ +
+ +
+ +
+20:14 +
+ +
+Через неделю придет примерно +
+ +
+ +
+ +
+ +
+ +
+20:14 +
+ +
+500гб +
+ +
+ +
+ +
+ +
+ +
+20:15 +
+ +
+Проксмос сильно загрузит наверно. +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+20:52 +
+ +
+Rustam Khalimov +
+ +
+Да, нормально жрет он, оперативки мало будет 8гб +
+ +
+ +
+ +
+ +
+ +
+20:54 +
+ +
+Хотя я свой смотрю, у меня 2ГБ он максимум ест +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+20:54 +
+ +
+Рустам Брат Леськи +
+ +
+Я задумывался может на синолоджи перейти просто +
+ +
+ +
+ +
+ +
+ +
+20:54 +
+ +
+Купить синолоджи, а этот просто выставить на продажу +
+ +
+ +
+ +
+ +
+ +
+20:54 +
+ +
+Но и этот норм, пока так уж +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+20:55 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+Ты характеристики синолоджи видел и цены? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+20:56 +
+ +
+Рустам Брат Леськи +
+ +
+In reply to this message +
+ +
+Цены просто смотрел, примерно до 30 вроде такие же на 2 +
+ +
+ +
+ +
+ +
+ +
+20:56 +
+ +
+А характеристики не смотрел +
+ +
+ +
+ +
+ +
+ +
+20:57 +
+ +
+Да нет уж, это просто задумывался +
+ +
+ +
+ +
+ +
+ +
+20:57 +
+ +
+Но вот только на 4 хдд надо было это точно +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+20:57 +
+ +
+Rustam Khalimov +
+ +
+In reply to this message +
+ +
+Это просто операционка и всё, ты за нее переплатишь, а железо там раз в 7 слабее +
+ +
+ +
+ +
+ +
+ +
+20:58 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+21:03 +
+ +
+Если тебе самому ковыряться лень или нет времени, я могу тебе поставить на него Xpenology или проксмокс +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+21:17 +
+ +
+Рустам Брат Леськи +
+ +
+Вижу да разница оказывается огромная +
+ +
+ +
+ +
+ +
+ +
+21:18 +
+ +
+In reply to this message +
+ +
+Не, пока на угрине чисто буду +
+ +
+ +
+ +
+ +
+ +
+21:18 +
+ +
+Может думаю со временем и обновление с русским языком придёт +
+ +
+ +
+ +
+ +
+ +
+21:19 +
+ +
+Так то я все уже в общем там разобрался +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+21:22 +
+ +
+Rustam Khalimov +
+ +
+Хорошо, просто всё намного удобнее может быть, если разобраться и в докере что то развернуть +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+21:23 +
+ +
+Рустам Брат Леськи +
+ +
+Да в докере интересно конечно что нибудь сделать +
+ +
+ +
+ +
+ +
+ +
+21:23 +
+ +
+Сегодня немного почитал про него тоже +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+21:23 +
+ +
+Rustam Khalimov +
+ +
+Проблема, что он у тебя какой то урезанный +
+ +
+ +
+ +
+ +
+6 April 2026 +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+12:59 +
+ +
+Рустам Брат Леськи +
+ +
+ + + + + + + +
+ + + + + + +🔥 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+12:59 +
+ +
+ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+RK +
+ +
+ +
+ +
+ +
+13:03 +
+ +
+Rustam Khalimov +
+ +
+Для камер или сервера? +
+ +
+ +
+ +
+ +
+ +
+ +
+РЛ +
+ +
+ +
+ +
+ +
+13:21 +
+ +
+Рустам Брат Леськи +
+ +
+Для камер +
+ +
+ +
+ +
+ +
+ +
+13:21 +
+ +
+На объекте поменял +
+ + + + + + +🔥 + + + + +
+ +
+RK +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + + + diff --git a/ChatExport_2026-04-06/photos/photo_1000@01-03-2026_22-10-19.jpg b/ChatExport_2026-04-06/photos/photo_1000@01-03-2026_22-10-19.jpg new file mode 100644 index 0000000..7fdd086 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1000@01-03-2026_22-10-19.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1000@01-03-2026_22-10-19_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1000@01-03-2026_22-10-19_thumb.jpg new file mode 100644 index 0000000..78caf22 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1000@01-03-2026_22-10-19_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1001@02-03-2026_11-39-14.jpg b/ChatExport_2026-04-06/photos/photo_1001@02-03-2026_11-39-14.jpg new file mode 100644 index 0000000..04de83b Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1001@02-03-2026_11-39-14.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1001@02-03-2026_11-39-14_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1001@02-03-2026_11-39-14_thumb.jpg new file mode 100644 index 0000000..5558428 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1001@02-03-2026_11-39-14_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1002@02-03-2026_11-39-34.jpg b/ChatExport_2026-04-06/photos/photo_1002@02-03-2026_11-39-34.jpg new file mode 100644 index 0000000..cc09f2d Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1002@02-03-2026_11-39-34.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1002@02-03-2026_11-39-34_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1002@02-03-2026_11-39-34_thumb.jpg new file mode 100644 index 0000000..64929f2 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1002@02-03-2026_11-39-34_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1003@03-03-2026_00-00-09.jpg b/ChatExport_2026-04-06/photos/photo_1003@03-03-2026_00-00-09.jpg new file mode 100644 index 0000000..a1ab3a2 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1003@03-03-2026_00-00-09.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1003@03-03-2026_00-00-09_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1003@03-03-2026_00-00-09_thumb.jpg new file mode 100644 index 0000000..bebb101 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1003@03-03-2026_00-00-09_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1004@03-03-2026_00-00-21.jpg b/ChatExport_2026-04-06/photos/photo_1004@03-03-2026_00-00-21.jpg new file mode 100644 index 0000000..b197aac Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1004@03-03-2026_00-00-21.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1004@03-03-2026_00-00-21_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1004@03-03-2026_00-00-21_thumb.jpg new file mode 100644 index 0000000..236aadd Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1004@03-03-2026_00-00-21_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1005@03-03-2026_00-11-17.jpg b/ChatExport_2026-04-06/photos/photo_1005@03-03-2026_00-11-17.jpg new file mode 100644 index 0000000..87fbc37 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1005@03-03-2026_00-11-17.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1005@03-03-2026_00-11-17_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1005@03-03-2026_00-11-17_thumb.jpg new file mode 100644 index 0000000..23fd391 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1005@03-03-2026_00-11-17_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1006@03-03-2026_00-15-37.jpg b/ChatExport_2026-04-06/photos/photo_1006@03-03-2026_00-15-37.jpg new file mode 100644 index 0000000..ada789d Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1006@03-03-2026_00-15-37.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1006@03-03-2026_00-15-37_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1006@03-03-2026_00-15-37_thumb.jpg new file mode 100644 index 0000000..4727fef Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1006@03-03-2026_00-15-37_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1007@03-03-2026_00-21-14.jpg b/ChatExport_2026-04-06/photos/photo_1007@03-03-2026_00-21-14.jpg new file mode 100644 index 0000000..48c971d Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1007@03-03-2026_00-21-14.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1007@03-03-2026_00-21-14_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1007@03-03-2026_00-21-14_thumb.jpg new file mode 100644 index 0000000..9b71112 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1007@03-03-2026_00-21-14_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1008@03-03-2026_00-21-14.jpg b/ChatExport_2026-04-06/photos/photo_1008@03-03-2026_00-21-14.jpg new file mode 100644 index 0000000..0b31291 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1008@03-03-2026_00-21-14.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1008@03-03-2026_00-21-14_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1008@03-03-2026_00-21-14_thumb.jpg new file mode 100644 index 0000000..11baf12 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1008@03-03-2026_00-21-14_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1009@03-03-2026_00-28-17.jpg b/ChatExport_2026-04-06/photos/photo_1009@03-03-2026_00-28-17.jpg new file mode 100644 index 0000000..76cdbf0 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1009@03-03-2026_00-28-17.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1009@03-03-2026_00-28-17_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1009@03-03-2026_00-28-17_thumb.jpg new file mode 100644 index 0000000..64a916c Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1009@03-03-2026_00-28-17_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1010@03-03-2026_00-28-17.jpg b/ChatExport_2026-04-06/photos/photo_1010@03-03-2026_00-28-17.jpg new file mode 100644 index 0000000..d4ffb16 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1010@03-03-2026_00-28-17.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1010@03-03-2026_00-28-17_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1010@03-03-2026_00-28-17_thumb.jpg new file mode 100644 index 0000000..6ff626c Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1010@03-03-2026_00-28-17_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1011@03-03-2026_00-28-17.jpg b/ChatExport_2026-04-06/photos/photo_1011@03-03-2026_00-28-17.jpg new file mode 100644 index 0000000..9d27bc4 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1011@03-03-2026_00-28-17.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1011@03-03-2026_00-28-17_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1011@03-03-2026_00-28-17_thumb.jpg new file mode 100644 index 0000000..4356cb9 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1011@03-03-2026_00-28-17_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1012@03-03-2026_00-37-20.jpg b/ChatExport_2026-04-06/photos/photo_1012@03-03-2026_00-37-20.jpg new file mode 100644 index 0000000..0f8c9f3 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1012@03-03-2026_00-37-20.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1012@03-03-2026_00-37-20_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1012@03-03-2026_00-37-20_thumb.jpg new file mode 100644 index 0000000..e6ecab3 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1012@03-03-2026_00-37-20_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1013@03-03-2026_00-37-30.jpg b/ChatExport_2026-04-06/photos/photo_1013@03-03-2026_00-37-30.jpg new file mode 100644 index 0000000..e6be01f Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1013@03-03-2026_00-37-30.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1013@03-03-2026_00-37-30_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1013@03-03-2026_00-37-30_thumb.jpg new file mode 100644 index 0000000..56632b6 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1013@03-03-2026_00-37-30_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1014@03-03-2026_00-38-27.jpg b/ChatExport_2026-04-06/photos/photo_1014@03-03-2026_00-38-27.jpg new file mode 100644 index 0000000..d41ffd0 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1014@03-03-2026_00-38-27.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1014@03-03-2026_00-38-27_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1014@03-03-2026_00-38-27_thumb.jpg new file mode 100644 index 0000000..17858e8 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1014@03-03-2026_00-38-27_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1015@03-03-2026_00-38-38.jpg b/ChatExport_2026-04-06/photos/photo_1015@03-03-2026_00-38-38.jpg new file mode 100644 index 0000000..8c4e066 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1015@03-03-2026_00-38-38.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1015@03-03-2026_00-38-38_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1015@03-03-2026_00-38-38_thumb.jpg new file mode 100644 index 0000000..0de0baa Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1015@03-03-2026_00-38-38_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1016@03-03-2026_09-13-36.jpg b/ChatExport_2026-04-06/photos/photo_1016@03-03-2026_09-13-36.jpg new file mode 100644 index 0000000..be39192 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1016@03-03-2026_09-13-36.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1016@03-03-2026_09-13-36_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1016@03-03-2026_09-13-36_thumb.jpg new file mode 100644 index 0000000..4a05fd3 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1016@03-03-2026_09-13-36_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1017@03-03-2026_14-37-12.jpg b/ChatExport_2026-04-06/photos/photo_1017@03-03-2026_14-37-12.jpg new file mode 100644 index 0000000..be39192 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1017@03-03-2026_14-37-12.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1017@03-03-2026_14-37-12_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1017@03-03-2026_14-37-12_thumb.jpg new file mode 100644 index 0000000..4a05fd3 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1017@03-03-2026_14-37-12_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1018@03-03-2026_16-47-56.jpg b/ChatExport_2026-04-06/photos/photo_1018@03-03-2026_16-47-56.jpg new file mode 100644 index 0000000..992cdf0 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1018@03-03-2026_16-47-56.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1018@03-03-2026_16-47-56_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1018@03-03-2026_16-47-56_thumb.jpg new file mode 100644 index 0000000..99dd94e Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1018@03-03-2026_16-47-56_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1019@03-03-2026_16-47-56.jpg b/ChatExport_2026-04-06/photos/photo_1019@03-03-2026_16-47-56.jpg new file mode 100644 index 0000000..5c6ae01 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1019@03-03-2026_16-47-56.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1019@03-03-2026_16-47-56_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1019@03-03-2026_16-47-56_thumb.jpg new file mode 100644 index 0000000..f612b5d Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1019@03-03-2026_16-47-56_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1020@03-03-2026_16-47-56.jpg b/ChatExport_2026-04-06/photos/photo_1020@03-03-2026_16-47-56.jpg new file mode 100644 index 0000000..3298fa4 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1020@03-03-2026_16-47-56.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1020@03-03-2026_16-47-56_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1020@03-03-2026_16-47-56_thumb.jpg new file mode 100644 index 0000000..a349150 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1020@03-03-2026_16-47-56_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1021@03-03-2026_16-48-01.jpg b/ChatExport_2026-04-06/photos/photo_1021@03-03-2026_16-48-01.jpg new file mode 100644 index 0000000..b023f8f Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1021@03-03-2026_16-48-01.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1021@03-03-2026_16-48-01_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1021@03-03-2026_16-48-01_thumb.jpg new file mode 100644 index 0000000..59eec84 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1021@03-03-2026_16-48-01_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1022@08-03-2026_18-01-10.jpg b/ChatExport_2026-04-06/photos/photo_1022@08-03-2026_18-01-10.jpg new file mode 100644 index 0000000..7f6f1ad Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1022@08-03-2026_18-01-10.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1022@08-03-2026_18-01-10_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1022@08-03-2026_18-01-10_thumb.jpg new file mode 100644 index 0000000..f4126fb Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1022@08-03-2026_18-01-10_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1023@08-03-2026_18-01-10.jpg b/ChatExport_2026-04-06/photos/photo_1023@08-03-2026_18-01-10.jpg new file mode 100644 index 0000000..afd870c Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1023@08-03-2026_18-01-10.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1023@08-03-2026_18-01-10_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1023@08-03-2026_18-01-10_thumb.jpg new file mode 100644 index 0000000..0a374c8 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1023@08-03-2026_18-01-10_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1024@08-03-2026_18-07-21.jpg b/ChatExport_2026-04-06/photos/photo_1024@08-03-2026_18-07-21.jpg new file mode 100644 index 0000000..3297d31 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1024@08-03-2026_18-07-21.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1024@08-03-2026_18-07-21_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1024@08-03-2026_18-07-21_thumb.jpg new file mode 100644 index 0000000..5f1d771 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1024@08-03-2026_18-07-21_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1025@08-03-2026_18-07-22.jpg b/ChatExport_2026-04-06/photos/photo_1025@08-03-2026_18-07-22.jpg new file mode 100644 index 0000000..7258ca3 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1025@08-03-2026_18-07-22.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1025@08-03-2026_18-07-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1025@08-03-2026_18-07-22_thumb.jpg new file mode 100644 index 0000000..a20087d Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1025@08-03-2026_18-07-22_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1026@08-03-2026_18-07-22.jpg b/ChatExport_2026-04-06/photos/photo_1026@08-03-2026_18-07-22.jpg new file mode 100644 index 0000000..a715ccf Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1026@08-03-2026_18-07-22.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1026@08-03-2026_18-07-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1026@08-03-2026_18-07-22_thumb.jpg new file mode 100644 index 0000000..94ee828 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1026@08-03-2026_18-07-22_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1027@08-03-2026_18-07-22.jpg b/ChatExport_2026-04-06/photos/photo_1027@08-03-2026_18-07-22.jpg new file mode 100644 index 0000000..f36603a Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1027@08-03-2026_18-07-22.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1027@08-03-2026_18-07-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1027@08-03-2026_18-07-22_thumb.jpg new file mode 100644 index 0000000..9fd4df6 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1027@08-03-2026_18-07-22_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1028@08-03-2026_18-35-05.jpg b/ChatExport_2026-04-06/photos/photo_1028@08-03-2026_18-35-05.jpg new file mode 100644 index 0000000..c2f6027 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1028@08-03-2026_18-35-05.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1028@08-03-2026_18-35-05_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1028@08-03-2026_18-35-05_thumb.jpg new file mode 100644 index 0000000..8ebc1f5 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1028@08-03-2026_18-35-05_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1029@10-03-2026_13-47-29.jpg b/ChatExport_2026-04-06/photos/photo_1029@10-03-2026_13-47-29.jpg new file mode 100644 index 0000000..720a17d Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1029@10-03-2026_13-47-29.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1029@10-03-2026_13-47-29_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1029@10-03-2026_13-47-29_thumb.jpg new file mode 100644 index 0000000..a7169ad Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1029@10-03-2026_13-47-29_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1030@10-03-2026_14-03-55.jpg b/ChatExport_2026-04-06/photos/photo_1030@10-03-2026_14-03-55.jpg new file mode 100644 index 0000000..1063bda Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1030@10-03-2026_14-03-55.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1030@10-03-2026_14-03-55_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1030@10-03-2026_14-03-55_thumb.jpg new file mode 100644 index 0000000..cba1687 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1030@10-03-2026_14-03-55_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1031@13-03-2026_11-56-50.jpg b/ChatExport_2026-04-06/photos/photo_1031@13-03-2026_11-56-50.jpg new file mode 100644 index 0000000..41426e4 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1031@13-03-2026_11-56-50.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1031@13-03-2026_11-56-50_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1031@13-03-2026_11-56-50_thumb.jpg new file mode 100644 index 0000000..4fefd55 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1031@13-03-2026_11-56-50_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1032@13-03-2026_11-56-50.jpg b/ChatExport_2026-04-06/photos/photo_1032@13-03-2026_11-56-50.jpg new file mode 100644 index 0000000..e5753c7 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1032@13-03-2026_11-56-50.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1032@13-03-2026_11-56-50_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1032@13-03-2026_11-56-50_thumb.jpg new file mode 100644 index 0000000..0f83909 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1032@13-03-2026_11-56-50_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1033@14-03-2026_22-11-06.jpg b/ChatExport_2026-04-06/photos/photo_1033@14-03-2026_22-11-06.jpg new file mode 100644 index 0000000..0ff24d3 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1033@14-03-2026_22-11-06.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1033@14-03-2026_22-11-06_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1033@14-03-2026_22-11-06_thumb.jpg new file mode 100644 index 0000000..9374a53 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1033@14-03-2026_22-11-06_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1034@14-03-2026_22-11-06.jpg b/ChatExport_2026-04-06/photos/photo_1034@14-03-2026_22-11-06.jpg new file mode 100644 index 0000000..a661e46 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1034@14-03-2026_22-11-06.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1034@14-03-2026_22-11-06_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1034@14-03-2026_22-11-06_thumb.jpg new file mode 100644 index 0000000..5b06532 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1034@14-03-2026_22-11-06_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1035@14-03-2026_22-11-06.jpg b/ChatExport_2026-04-06/photos/photo_1035@14-03-2026_22-11-06.jpg new file mode 100644 index 0000000..efc3f0c Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1035@14-03-2026_22-11-06.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1035@14-03-2026_22-11-06_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1035@14-03-2026_22-11-06_thumb.jpg new file mode 100644 index 0000000..f3dd432 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1035@14-03-2026_22-11-06_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1036@14-03-2026_22-11-06.jpg b/ChatExport_2026-04-06/photos/photo_1036@14-03-2026_22-11-06.jpg new file mode 100644 index 0000000..c20a791 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1036@14-03-2026_22-11-06.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1036@14-03-2026_22-11-06_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1036@14-03-2026_22-11-06_thumb.jpg new file mode 100644 index 0000000..45c03b3 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1036@14-03-2026_22-11-06_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1037@15-03-2026_13-59-32.jpg b/ChatExport_2026-04-06/photos/photo_1037@15-03-2026_13-59-32.jpg new file mode 100644 index 0000000..3fa03a2 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1037@15-03-2026_13-59-32.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1037@15-03-2026_13-59-32_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1037@15-03-2026_13-59-32_thumb.jpg new file mode 100644 index 0000000..48e2388 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1037@15-03-2026_13-59-32_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1038@15-03-2026_14-52-40.jpg b/ChatExport_2026-04-06/photos/photo_1038@15-03-2026_14-52-40.jpg new file mode 100644 index 0000000..282df6e Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1038@15-03-2026_14-52-40.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1038@15-03-2026_14-52-40_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1038@15-03-2026_14-52-40_thumb.jpg new file mode 100644 index 0000000..507f907 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1038@15-03-2026_14-52-40_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1039@15-03-2026_14-52-40.jpg b/ChatExport_2026-04-06/photos/photo_1039@15-03-2026_14-52-40.jpg new file mode 100644 index 0000000..434fe75 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1039@15-03-2026_14-52-40.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1039@15-03-2026_14-52-40_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1039@15-03-2026_14-52-40_thumb.jpg new file mode 100644 index 0000000..9469d0e Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1039@15-03-2026_14-52-40_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1040@15-03-2026_14-54-45.jpg b/ChatExport_2026-04-06/photos/photo_1040@15-03-2026_14-54-45.jpg new file mode 100644 index 0000000..ee7876a Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1040@15-03-2026_14-54-45.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1040@15-03-2026_14-54-45_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1040@15-03-2026_14-54-45_thumb.jpg new file mode 100644 index 0000000..beb8bd5 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1040@15-03-2026_14-54-45_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1041@15-03-2026_14-54-45.jpg b/ChatExport_2026-04-06/photos/photo_1041@15-03-2026_14-54-45.jpg new file mode 100644 index 0000000..a5d27d8 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1041@15-03-2026_14-54-45.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1041@15-03-2026_14-54-45_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1041@15-03-2026_14-54-45_thumb.jpg new file mode 100644 index 0000000..3f76e2e Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1041@15-03-2026_14-54-45_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1042@15-03-2026_14-54-45.jpg b/ChatExport_2026-04-06/photos/photo_1042@15-03-2026_14-54-45.jpg new file mode 100644 index 0000000..9d71c4b Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1042@15-03-2026_14-54-45.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1042@15-03-2026_14-54-45_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1042@15-03-2026_14-54-45_thumb.jpg new file mode 100644 index 0000000..9522a69 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1042@15-03-2026_14-54-45_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1043@15-03-2026_14-54-45.jpg b/ChatExport_2026-04-06/photos/photo_1043@15-03-2026_14-54-45.jpg new file mode 100644 index 0000000..7e08807 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1043@15-03-2026_14-54-45.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1043@15-03-2026_14-54-45_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1043@15-03-2026_14-54-45_thumb.jpg new file mode 100644 index 0000000..e2f7231 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1043@15-03-2026_14-54-45_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1044@15-03-2026_14-54-45.jpg b/ChatExport_2026-04-06/photos/photo_1044@15-03-2026_14-54-45.jpg new file mode 100644 index 0000000..61770df Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1044@15-03-2026_14-54-45.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1044@15-03-2026_14-54-45_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1044@15-03-2026_14-54-45_thumb.jpg new file mode 100644 index 0000000..54a2b15 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1044@15-03-2026_14-54-45_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1045@15-03-2026_17-03-30.jpg b/ChatExport_2026-04-06/photos/photo_1045@15-03-2026_17-03-30.jpg new file mode 100644 index 0000000..abe9e8c Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1045@15-03-2026_17-03-30.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1045@15-03-2026_17-03-30_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1045@15-03-2026_17-03-30_thumb.jpg new file mode 100644 index 0000000..6491702 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1045@15-03-2026_17-03-30_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1046@15-03-2026_17-03-30.jpg b/ChatExport_2026-04-06/photos/photo_1046@15-03-2026_17-03-30.jpg new file mode 100644 index 0000000..c965440 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1046@15-03-2026_17-03-30.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1046@15-03-2026_17-03-30_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1046@15-03-2026_17-03-30_thumb.jpg new file mode 100644 index 0000000..d2dd8c8 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1046@15-03-2026_17-03-30_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1047@15-03-2026_17-03-38.jpg b/ChatExport_2026-04-06/photos/photo_1047@15-03-2026_17-03-38.jpg new file mode 100644 index 0000000..560e3c2 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1047@15-03-2026_17-03-38.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1047@15-03-2026_17-03-38_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1047@15-03-2026_17-03-38_thumb.jpg new file mode 100644 index 0000000..78a6235 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1047@15-03-2026_17-03-38_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1048@15-03-2026_17-03-38.jpg b/ChatExport_2026-04-06/photos/photo_1048@15-03-2026_17-03-38.jpg new file mode 100644 index 0000000..aba5bfc Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1048@15-03-2026_17-03-38.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1048@15-03-2026_17-03-38_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1048@15-03-2026_17-03-38_thumb.jpg new file mode 100644 index 0000000..046976e Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1048@15-03-2026_17-03-38_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1049@15-03-2026_17-03-38.jpg b/ChatExport_2026-04-06/photos/photo_1049@15-03-2026_17-03-38.jpg new file mode 100644 index 0000000..767a262 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1049@15-03-2026_17-03-38.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1049@15-03-2026_17-03-38_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1049@15-03-2026_17-03-38_thumb.jpg new file mode 100644 index 0000000..098cc78 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1049@15-03-2026_17-03-38_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1050@18-03-2026_21-04-04.jpg b/ChatExport_2026-04-06/photos/photo_1050@18-03-2026_21-04-04.jpg new file mode 100644 index 0000000..91610fc Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1050@18-03-2026_21-04-04.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1050@18-03-2026_21-04-04_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1050@18-03-2026_21-04-04_thumb.jpg new file mode 100644 index 0000000..8c249d5 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1050@18-03-2026_21-04-04_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1051@22-03-2026_10-23-32.jpg b/ChatExport_2026-04-06/photos/photo_1051@22-03-2026_10-23-32.jpg new file mode 100644 index 0000000..b9a23c4 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1051@22-03-2026_10-23-32.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1051@22-03-2026_10-23-32_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1051@22-03-2026_10-23-32_thumb.jpg new file mode 100644 index 0000000..c3ab513 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1051@22-03-2026_10-23-32_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1052@22-03-2026_10-26-17.jpg b/ChatExport_2026-04-06/photos/photo_1052@22-03-2026_10-26-17.jpg new file mode 100644 index 0000000..08351e4 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1052@22-03-2026_10-26-17.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1052@22-03-2026_10-26-17_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1052@22-03-2026_10-26-17_thumb.jpg new file mode 100644 index 0000000..4087bd4 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1052@22-03-2026_10-26-17_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1053@22-03-2026_11-51-54.jpg b/ChatExport_2026-04-06/photos/photo_1053@22-03-2026_11-51-54.jpg new file mode 100644 index 0000000..c49b7d5 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1053@22-03-2026_11-51-54.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1053@22-03-2026_11-51-54_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1053@22-03-2026_11-51-54_thumb.jpg new file mode 100644 index 0000000..b9e5b04 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1053@22-03-2026_11-51-54_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1054@22-03-2026_11-55-56.jpg b/ChatExport_2026-04-06/photos/photo_1054@22-03-2026_11-55-56.jpg new file mode 100644 index 0000000..60c2645 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1054@22-03-2026_11-55-56.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1054@22-03-2026_11-55-56_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1054@22-03-2026_11-55-56_thumb.jpg new file mode 100644 index 0000000..640285b Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1054@22-03-2026_11-55-56_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1055@22-03-2026_11-56-05.jpg b/ChatExport_2026-04-06/photos/photo_1055@22-03-2026_11-56-05.jpg new file mode 100644 index 0000000..ed94ec3 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1055@22-03-2026_11-56-05.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1055@22-03-2026_11-56-05_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1055@22-03-2026_11-56-05_thumb.jpg new file mode 100644 index 0000000..82d87e4 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1055@22-03-2026_11-56-05_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1056@24-03-2026_19-34-19.jpg b/ChatExport_2026-04-06/photos/photo_1056@24-03-2026_19-34-19.jpg new file mode 100644 index 0000000..dfdec8b Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1056@24-03-2026_19-34-19.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1056@24-03-2026_19-34-19_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1056@24-03-2026_19-34-19_thumb.jpg new file mode 100644 index 0000000..44d3c3c Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1056@24-03-2026_19-34-19_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1057@24-03-2026_19-34-41.jpg b/ChatExport_2026-04-06/photos/photo_1057@24-03-2026_19-34-41.jpg new file mode 100644 index 0000000..3fad496 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1057@24-03-2026_19-34-41.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1057@24-03-2026_19-34-41_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1057@24-03-2026_19-34-41_thumb.jpg new file mode 100644 index 0000000..ad2059a Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1057@24-03-2026_19-34-41_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1058@24-03-2026_19-41-35.jpg b/ChatExport_2026-04-06/photos/photo_1058@24-03-2026_19-41-35.jpg new file mode 100644 index 0000000..6c3d951 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1058@24-03-2026_19-41-35.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1058@24-03-2026_19-41-35_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1058@24-03-2026_19-41-35_thumb.jpg new file mode 100644 index 0000000..11f96b5 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1058@24-03-2026_19-41-35_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1059@27-03-2026_11-29-45.jpg b/ChatExport_2026-04-06/photos/photo_1059@27-03-2026_11-29-45.jpg new file mode 100644 index 0000000..1787c38 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1059@27-03-2026_11-29-45.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1059@27-03-2026_11-29-45_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1059@27-03-2026_11-29-45_thumb.jpg new file mode 100644 index 0000000..13c292b Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1059@27-03-2026_11-29-45_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1060@27-03-2026_12-03-14.jpg b/ChatExport_2026-04-06/photos/photo_1060@27-03-2026_12-03-14.jpg new file mode 100644 index 0000000..6e77004 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1060@27-03-2026_12-03-14.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1060@27-03-2026_12-03-14_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1060@27-03-2026_12-03-14_thumb.jpg new file mode 100644 index 0000000..2ff401a Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1060@27-03-2026_12-03-14_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1061@28-03-2026_14-07-06.jpg b/ChatExport_2026-04-06/photos/photo_1061@28-03-2026_14-07-06.jpg new file mode 100644 index 0000000..f81ca93 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1061@28-03-2026_14-07-06.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1061@28-03-2026_14-07-06_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1061@28-03-2026_14-07-06_thumb.jpg new file mode 100644 index 0000000..f5ef0f8 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1061@28-03-2026_14-07-06_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1062@29-03-2026_12-20-14.jpg b/ChatExport_2026-04-06/photos/photo_1062@29-03-2026_12-20-14.jpg new file mode 100644 index 0000000..a7a3d47 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1062@29-03-2026_12-20-14.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1062@29-03-2026_12-20-14_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1062@29-03-2026_12-20-14_thumb.jpg new file mode 100644 index 0000000..7f144d6 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1062@29-03-2026_12-20-14_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1063@29-03-2026_12-23-22.jpg b/ChatExport_2026-04-06/photos/photo_1063@29-03-2026_12-23-22.jpg new file mode 100644 index 0000000..da8509e Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1063@29-03-2026_12-23-22.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1063@29-03-2026_12-23-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1063@29-03-2026_12-23-22_thumb.jpg new file mode 100644 index 0000000..1f07dce Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1063@29-03-2026_12-23-22_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1064@29-03-2026_12-53-07.jpg b/ChatExport_2026-04-06/photos/photo_1064@29-03-2026_12-53-07.jpg new file mode 100644 index 0000000..f668a8f Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1064@29-03-2026_12-53-07.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1064@29-03-2026_12-53-07_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1064@29-03-2026_12-53-07_thumb.jpg new file mode 100644 index 0000000..407898c Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1064@29-03-2026_12-53-07_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1065@03-04-2026_13-50-03.jpg b/ChatExport_2026-04-06/photos/photo_1065@03-04-2026_13-50-03.jpg new file mode 100644 index 0000000..a2d83e0 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1065@03-04-2026_13-50-03.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1065@03-04-2026_13-50-03_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1065@03-04-2026_13-50-03_thumb.jpg new file mode 100644 index 0000000..5b19871 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1065@03-04-2026_13-50-03_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1066@03-04-2026_13-50-03.jpg b/ChatExport_2026-04-06/photos/photo_1066@03-04-2026_13-50-03.jpg new file mode 100644 index 0000000..cf20f9c Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1066@03-04-2026_13-50-03.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1066@03-04-2026_13-50-03_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1066@03-04-2026_13-50-03_thumb.jpg new file mode 100644 index 0000000..5decfa7 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1066@03-04-2026_13-50-03_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1067@03-04-2026_17-46-54.jpg b/ChatExport_2026-04-06/photos/photo_1067@03-04-2026_17-46-54.jpg new file mode 100644 index 0000000..007e9d3 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1067@03-04-2026_17-46-54.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1067@03-04-2026_17-46-54_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1067@03-04-2026_17-46-54_thumb.jpg new file mode 100644 index 0000000..c609b06 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1067@03-04-2026_17-46-54_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1068@03-04-2026_17-48-16.jpg b/ChatExport_2026-04-06/photos/photo_1068@03-04-2026_17-48-16.jpg new file mode 100644 index 0000000..b91955a Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1068@03-04-2026_17-48-16.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1068@03-04-2026_17-48-16_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1068@03-04-2026_17-48-16_thumb.jpg new file mode 100644 index 0000000..83ec719 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1068@03-04-2026_17-48-16_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1069@04-04-2026_12-16-09.jpg b/ChatExport_2026-04-06/photos/photo_1069@04-04-2026_12-16-09.jpg new file mode 100644 index 0000000..2abffbe Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1069@04-04-2026_12-16-09.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1069@04-04-2026_12-16-09_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1069@04-04-2026_12-16-09_thumb.jpg new file mode 100644 index 0000000..9287a57 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1069@04-04-2026_12-16-09_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1070@05-04-2026_19-38-29.jpg b/ChatExport_2026-04-06/photos/photo_1070@05-04-2026_19-38-29.jpg new file mode 100644 index 0000000..1ad5903 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1070@05-04-2026_19-38-29.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1070@05-04-2026_19-38-29_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1070@05-04-2026_19-38-29_thumb.jpg new file mode 100644 index 0000000..dc89b9e Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1070@05-04-2026_19-38-29_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1071@05-04-2026_20-58-56.jpg b/ChatExport_2026-04-06/photos/photo_1071@05-04-2026_20-58-56.jpg new file mode 100644 index 0000000..f3bf9b1 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1071@05-04-2026_20-58-56.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1071@05-04-2026_20-58-56_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1071@05-04-2026_20-58-56_thumb.jpg new file mode 100644 index 0000000..2c0626d Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1071@05-04-2026_20-58-56_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1072@06-04-2026_12-59-05.jpg b/ChatExport_2026-04-06/photos/photo_1072@06-04-2026_12-59-05.jpg new file mode 100644 index 0000000..8657e92 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1072@06-04-2026_12-59-05.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1072@06-04-2026_12-59-05_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1072@06-04-2026_12-59-05_thumb.jpg new file mode 100644 index 0000000..6f316a5 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1072@06-04-2026_12-59-05_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1073@06-04-2026_12-59-05.jpg b/ChatExport_2026-04-06/photos/photo_1073@06-04-2026_12-59-05.jpg new file mode 100644 index 0000000..ffc512a Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1073@06-04-2026_12-59-05.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_1073@06-04-2026_12-59-05_thumb.jpg b/ChatExport_2026-04-06/photos/photo_1073@06-04-2026_12-59-05_thumb.jpg new file mode 100644 index 0000000..48b47d3 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_1073@06-04-2026_12-59-05_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_991@01-03-2026_13-12-22.jpg b/ChatExport_2026-04-06/photos/photo_991@01-03-2026_13-12-22.jpg new file mode 100644 index 0000000..7907343 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_991@01-03-2026_13-12-22.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_991@01-03-2026_13-12-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_991@01-03-2026_13-12-22_thumb.jpg new file mode 100644 index 0000000..03d9786 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_991@01-03-2026_13-12-22_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_992@01-03-2026_13-12-22.jpg b/ChatExport_2026-04-06/photos/photo_992@01-03-2026_13-12-22.jpg new file mode 100644 index 0000000..dace5fa Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_992@01-03-2026_13-12-22.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_992@01-03-2026_13-12-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_992@01-03-2026_13-12-22_thumb.jpg new file mode 100644 index 0000000..0397a01 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_992@01-03-2026_13-12-22_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_993@01-03-2026_13-12-22.jpg b/ChatExport_2026-04-06/photos/photo_993@01-03-2026_13-12-22.jpg new file mode 100644 index 0000000..84e94bd Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_993@01-03-2026_13-12-22.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_993@01-03-2026_13-12-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_993@01-03-2026_13-12-22_thumb.jpg new file mode 100644 index 0000000..eb86a25 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_993@01-03-2026_13-12-22_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_994@01-03-2026_13-12-22.jpg b/ChatExport_2026-04-06/photos/photo_994@01-03-2026_13-12-22.jpg new file mode 100644 index 0000000..90642ff Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_994@01-03-2026_13-12-22.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_994@01-03-2026_13-12-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_994@01-03-2026_13-12-22_thumb.jpg new file mode 100644 index 0000000..3034a96 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_994@01-03-2026_13-12-22_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_995@01-03-2026_13-12-22.jpg b/ChatExport_2026-04-06/photos/photo_995@01-03-2026_13-12-22.jpg new file mode 100644 index 0000000..dc4af9b Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_995@01-03-2026_13-12-22.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_995@01-03-2026_13-12-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_995@01-03-2026_13-12-22_thumb.jpg new file mode 100644 index 0000000..5afbc7b Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_995@01-03-2026_13-12-22_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_996@01-03-2026_13-12-22.jpg b/ChatExport_2026-04-06/photos/photo_996@01-03-2026_13-12-22.jpg new file mode 100644 index 0000000..2dec6ed Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_996@01-03-2026_13-12-22.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_996@01-03-2026_13-12-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_996@01-03-2026_13-12-22_thumb.jpg new file mode 100644 index 0000000..d1df3cd Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_996@01-03-2026_13-12-22_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_997@01-03-2026_13-12-22.jpg b/ChatExport_2026-04-06/photos/photo_997@01-03-2026_13-12-22.jpg new file mode 100644 index 0000000..076a354 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_997@01-03-2026_13-12-22.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_997@01-03-2026_13-12-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_997@01-03-2026_13-12-22_thumb.jpg new file mode 100644 index 0000000..563feab Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_997@01-03-2026_13-12-22_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_998@01-03-2026_13-12-22.jpg b/ChatExport_2026-04-06/photos/photo_998@01-03-2026_13-12-22.jpg new file mode 100644 index 0000000..3163ef3 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_998@01-03-2026_13-12-22.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_998@01-03-2026_13-12-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_998@01-03-2026_13-12-22_thumb.jpg new file mode 100644 index 0000000..002a155 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_998@01-03-2026_13-12-22_thumb.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_999@01-03-2026_13-12-22.jpg b/ChatExport_2026-04-06/photos/photo_999@01-03-2026_13-12-22.jpg new file mode 100644 index 0000000..a96a1e4 Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_999@01-03-2026_13-12-22.jpg differ diff --git a/ChatExport_2026-04-06/photos/photo_999@01-03-2026_13-12-22_thumb.jpg b/ChatExport_2026-04-06/photos/photo_999@01-03-2026_13-12-22_thumb.jpg new file mode 100644 index 0000000..575f83d Binary files /dev/null and b/ChatExport_2026-04-06/photos/photo_999@01-03-2026_13-12-22_thumb.jpg differ diff --git a/ChatExport_2026-04-06/stickers/AnimatedSticker.tgs b/ChatExport_2026-04-06/stickers/AnimatedSticker.tgs new file mode 100644 index 0000000..0aea9df Binary files /dev/null and b/ChatExport_2026-04-06/stickers/AnimatedSticker.tgs differ diff --git a/ChatExport_2026-04-06/video_files/185.mp4 b/ChatExport_2026-04-06/video_files/185.mp4 new file mode 100644 index 0000000..c808ae0 Binary files /dev/null and b/ChatExport_2026-04-06/video_files/185.mp4 differ diff --git a/ChatExport_2026-04-06/video_files/Animated_Creature_Sings_Sweet_Russian_Song.mp4 b/ChatExport_2026-04-06/video_files/Animated_Creature_Sings_Sweet_Russian_Song.mp4 new file mode 100644 index 0000000..24a55e0 Binary files /dev/null and b/ChatExport_2026-04-06/video_files/Animated_Creature_Sings_Sweet_Russian_Song.mp4 differ diff --git a/ChatExport_2026-04-06/video_files/Animated_Creature_Sings_Sweet_Russian_Song.mp4_thumb.jpg b/ChatExport_2026-04-06/video_files/Animated_Creature_Sings_Sweet_Russian_Song.mp4_thumb.jpg new file mode 100644 index 0000000..383d31d Binary files /dev/null and b/ChatExport_2026-04-06/video_files/Animated_Creature_Sings_Sweet_Russian_Song.mp4_thumb.jpg differ diff --git a/ChatExport_2026-04-06/video_files/IMG_9101.MP4 b/ChatExport_2026-04-06/video_files/IMG_9101.MP4 new file mode 100644 index 0000000..4350b42 Binary files /dev/null and b/ChatExport_2026-04-06/video_files/IMG_9101.MP4 differ diff --git a/ChatExport_2026-04-06/video_files/IMG_9101.MP4_thumb.jpg b/ChatExport_2026-04-06/video_files/IMG_9101.MP4_thumb.jpg new file mode 100644 index 0000000..6468967 Binary files /dev/null and b/ChatExport_2026-04-06/video_files/IMG_9101.MP4_thumb.jpg differ diff --git a/ChatExport_2026-04-06/video_files/IMG_9327.MP4 b/ChatExport_2026-04-06/video_files/IMG_9327.MP4 new file mode 100644 index 0000000..041681f Binary files /dev/null and b/ChatExport_2026-04-06/video_files/IMG_9327.MP4 differ diff --git a/ChatExport_2026-04-06/video_files/IMG_9327.MP4_thumb.jpg b/ChatExport_2026-04-06/video_files/IMG_9327.MP4_thumb.jpg new file mode 100644 index 0000000..dee5a73 Binary files /dev/null and b/ChatExport_2026-04-06/video_files/IMG_9327.MP4_thumb.jpg differ diff --git a/ChatExport_2026-04-06/video_files/animation.gif.mp4 b/ChatExport_2026-04-06/video_files/animation.gif.mp4 new file mode 100644 index 0000000..feca56a Binary files /dev/null and b/ChatExport_2026-04-06/video_files/animation.gif.mp4 differ diff --git a/ChatExport_2026-04-06/video_files/animation.gif.mp4_thumb.jpg b/ChatExport_2026-04-06/video_files/animation.gif.mp4_thumb.jpg new file mode 100644 index 0000000..d13fca7 Binary files /dev/null and b/ChatExport_2026-04-06/video_files/animation.gif.mp4_thumb.jpg differ diff --git a/ChatExport_2026-04-06/video_files/baby-cry-плачющиймальчик.mp4 b/ChatExport_2026-04-06/video_files/baby-cry-плачющиймальчик.mp4 new file mode 100644 index 0000000..7150bf9 Binary files /dev/null and b/ChatExport_2026-04-06/video_files/baby-cry-плачющиймальчик.mp4 differ diff --git a/ChatExport_2026-04-06/video_files/explorer_emaqIc6QUB.mp4 b/ChatExport_2026-04-06/video_files/explorer_emaqIc6QUB.mp4 new file mode 100644 index 0000000..c4fdd45 Binary files /dev/null and b/ChatExport_2026-04-06/video_files/explorer_emaqIc6QUB.mp4 differ diff --git a/ChatExport_2026-04-06/video_files/explorer_emaqIc6QUB.mp4_thumb.jpg b/ChatExport_2026-04-06/video_files/explorer_emaqIc6QUB.mp4_thumb.jpg new file mode 100644 index 0000000..593023c Binary files /dev/null and b/ChatExport_2026-04-06/video_files/explorer_emaqIc6QUB.mp4_thumb.jpg differ diff --git a/ChatExport_2026-04-06/video_files/video_84@25-03-2026_10-25-42.mp4 b/ChatExport_2026-04-06/video_files/video_84@25-03-2026_10-25-42.mp4 new file mode 100644 index 0000000..4cb1e5c Binary files /dev/null and b/ChatExport_2026-04-06/video_files/video_84@25-03-2026_10-25-42.mp4 differ diff --git a/ChatExport_2026-04-06/video_files/video_84@25-03-2026_10-25-42.mp4_thumb.jpg b/ChatExport_2026-04-06/video_files/video_84@25-03-2026_10-25-42.mp4_thumb.jpg new file mode 100644 index 0000000..3deec6e Binary files /dev/null and b/ChatExport_2026-04-06/video_files/video_84@25-03-2026_10-25-42.mp4_thumb.jpg differ diff --git a/ChatExport_2026-04-06/video_files/video_86@03-04-2026_17-14-33.mp4 b/ChatExport_2026-04-06/video_files/video_86@03-04-2026_17-14-33.mp4 new file mode 100644 index 0000000..28e1172 Binary files /dev/null and b/ChatExport_2026-04-06/video_files/video_86@03-04-2026_17-14-33.mp4 differ diff --git a/ChatExport_2026-04-06/video_files/video_86@03-04-2026_17-14-33.mp4_thumb.jpg b/ChatExport_2026-04-06/video_files/video_86@03-04-2026_17-14-33.mp4_thumb.jpg new file mode 100644 index 0000000..0e6ef47 Binary files /dev/null and b/ChatExport_2026-04-06/video_files/video_86@03-04-2026_17-14-33.mp4_thumb.jpg differ diff --git a/ChatExport_2026-04-06/video_files/этопечально-печально.mp4 b/ChatExport_2026-04-06/video_files/этопечально-печально.mp4 new file mode 100644 index 0000000..3ba5194 Binary files /dev/null and b/ChatExport_2026-04-06/video_files/этопечально-печально.mp4 differ diff --git a/ChatExport_2026-04-06/voice_messages/audio_18@03-03-2026_12-38-40.ogg b/ChatExport_2026-04-06/voice_messages/audio_18@03-03-2026_12-38-40.ogg new file mode 100644 index 0000000..0632f2f Binary files /dev/null and b/ChatExport_2026-04-06/voice_messages/audio_18@03-03-2026_12-38-40.ogg differ diff --git a/ChatExport_2026-04-06/voice_messages/audio_19@03-03-2026_12-38-40.ogg b/ChatExport_2026-04-06/voice_messages/audio_19@03-03-2026_12-38-40.ogg new file mode 100644 index 0000000..ad3170e Binary files /dev/null and b/ChatExport_2026-04-06/voice_messages/audio_19@03-03-2026_12-38-40.ogg differ diff --git a/ChatExport_2026-04-06/voice_messages/audio_20@10-03-2026_14-21-53.ogg b/ChatExport_2026-04-06/voice_messages/audio_20@10-03-2026_14-21-53.ogg new file mode 100644 index 0000000..64d9e5a Binary files /dev/null and b/ChatExport_2026-04-06/voice_messages/audio_20@10-03-2026_14-21-53.ogg differ diff --git a/backend/src/Contracts/Conversations/Domain/Chat.cs b/backend/src/Contracts/Conversations/Domain/Chat.cs index 6d2b79a..58082b6 100644 --- a/backend/src/Contracts/Conversations/Domain/Chat.cs +++ b/backend/src/Contracts/Conversations/Domain/Chat.cs @@ -36,17 +36,21 @@ public sealed class Chat : AggregateRoot public string? Avatar { get; private set; } public DateTime CreatedAt { get; private set; } public long LastMessageSequenceId { get; private set; } + public bool IsImporting { get; private set; } + public Guid? ImportJobId { get; private set; } private readonly List _members = new(); public IReadOnlyCollection Members => _members.AsReadOnly(); - private Chat(Guid id, ChatType type, string? name, string? avatar, string? description = null) : base(id) + private Chat(Guid id, ChatType type, string? name, string? avatar, string? description = null, bool isImporting = false, Guid? importJobId = null) : base(id) { Type = type; Name = name; Avatar = avatar; Description = description; CreatedAt = DateTime.UtcNow; + IsImporting = isImporting; + ImportJobId = importJobId; } public static Chat CreatePersonal() @@ -63,13 +67,18 @@ public sealed class Chat : AggregateRoot return chat; } - public static Chat Create(string? name, ChatType type, string? avatar = null, string? description = null) + public static Chat Create(string? name, ChatType type, string? avatar = null, string? description = null, bool isImporting = false, Guid? importJobId = null) { - var chat = new Chat(Guid.NewGuid(), type, name, avatar, description); + var chat = new Chat(Guid.NewGuid(), type, name, avatar, description, isImporting, importJobId); chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat)); return chat; } + public void CompleteImport() + { + IsImporting = false; + } + public void AddMember(Guid userId, string role = "member") { if (_members.Any(m => m.UserId == userId)) diff --git a/backend/src/Contracts/Conversations/Domain/ChatConstants.cs b/backend/src/Contracts/Conversations/Domain/ChatConstants.cs new file mode 100644 index 0000000..27a7a7c --- /dev/null +++ b/backend/src/Contracts/Conversations/Domain/ChatConstants.cs @@ -0,0 +1,8 @@ +namespace Knot.Contracts.Conversations.Domain; + +public static class ChatConstants +{ + public const int DefaultMessageQueryLimit = 50; + public const int MaxSharedMediaQueryLimit = 1000; + public const int MaxGroupNameLength = 100; +} diff --git a/backend/src/Contracts/Conversations/Domain/ChatErrors.cs b/backend/src/Contracts/Conversations/Domain/ChatErrors.cs new file mode 100644 index 0000000..cca14d2 --- /dev/null +++ b/backend/src/Contracts/Conversations/Domain/ChatErrors.cs @@ -0,0 +1,19 @@ +using Knot.Shared.Kernel; + +namespace Knot.Contracts.Conversations.Domain; + +public static class ChatErrors +{ + public static readonly Error ChatNotFound = new Error("Chat.NotFound", "Чат не найден"); + public static readonly Error OnlyOwnerCanUpdate = new Error("Chat.OnlyOwnerCanUpdate", "Только владелец может редактировать чат"); + public static readonly Error Unauthorized = new Error("Chat.Unauthorized", "Нет доступа к этому чату"); + public static readonly Error FoldersDisabled = new Error("Chat.FoldersDisabled", "Папки отключены"); + public static readonly Error FileEmpty = new Error("Chat.FileEmpty", "Файл пуст"); + public static Error FileTooLarge(long maxMb) => new Error("Chat.FileTooLarge", $"Файл слишком большой (максимум {maxMb} МБ)"); + public static readonly Error ChatsNotFound = new Error("Chat.NotFound", "Чат не найден"); + public static readonly Error ChatsForbidden = new Error("Chat.Forbidden", "Доступ запрещен"); + public static readonly Error MediaDisabled = new Error("Chat.MediaDisabled", "Медиафайлы отключены"); + public static readonly Error PollsDisabled = new Error("Chat.PollsDisabled", "Опросы отключены"); + public static readonly Error NotFound = new Error("Chat.NotFound", "Не найдено"); + public static readonly Error NotMember = new Error("Chat.NotMember", "Вы не являетесь участником чата"); +} diff --git a/backend/src/Contracts/Messaging/Application/Abstractions/IMessageRepository.cs b/backend/src/Contracts/Messaging/Application/Abstractions/IMessageRepository.cs index 8feac1a..22013c6 100644 --- a/backend/src/Contracts/Messaging/Application/Abstractions/IMessageRepository.cs +++ b/backend/src/Contracts/Messaging/Application/Abstractions/IMessageRepository.cs @@ -10,7 +10,7 @@ public interface IMessageRepository Task GetLatestChatMessageAsync(Guid chatId, CancellationToken cancellationToken); Task> SearchMessagesAsync(string query, Guid? chatId, Guid requestingUserId, CancellationToken cancellationToken); - Task> GetChatMessagesCursorAsync(Guid chatId, DateTime? cursor, int limit, CancellationToken cancellationToken); + Task> GetChatMessagesCursorAsync(Guid chatId, DateTime? cursor, long? sequenceId, int limit, CancellationToken cancellationToken); Task GetLastStoryMessageAsync(Guid chatId, Guid storyId, CancellationToken cancellationToken); Task UpdateAsync(Message message, CancellationToken cancellationToken); diff --git a/backend/src/Host/Program.cs b/backend/src/Host/Program.cs index f59c0ea..e73f20a 100644 --- a/backend/src/Host/Program.cs +++ b/backend/src/Host/Program.cs @@ -41,6 +41,7 @@ using MediatR; JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); +Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); var builder = WebApplication.CreateBuilder(args); @@ -81,6 +82,7 @@ builder.Services.AddStoriesModule(builder.Configuration); builder.Services.AddKlipyModule(); builder.Services.AddAdminModule(); builder.Services.AddWebRtcModule(); +builder.Services.AddTelegramImportModule(); builder.Services.AddSharedInfrastructure(builder.Configuration); // CQRS / MediatR для команд в Host (например, AdminController) @@ -93,7 +95,10 @@ builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies( typeof(Knot.Modules.Stories.DependencyInjection).Assembly, typeof(Knot.Modules.Klipy.DependencyInjection).Assembly, typeof(Knot.Modules.Relations.DependencyInjection).Assembly, - typeof(Knot.Modules.WebRtc.DependencyInjection).Assembly + typeof(Knot.Modules.WebRtc.DependencyInjection).Assembly, + typeof(Knot.Modules.TelegramImport.DependencyInjection).Assembly, + typeof(Knot.Modules.Auth.Infrastructure.Persistence.AuthDbContext).Assembly, + typeof(Knot.Modules.Profiles.DependencyInjection).Assembly )); // Настройка CORS diff --git a/backend/src/Modules/Admin/Application/Admin/Commands/CleanRun/CleanRunCommand.cs b/backend/src/Modules/Admin/Application/Admin/Commands/CleanRun/CleanRunCommand.cs index 01f1db6..406f0ed 100644 --- a/backend/src/Modules/Admin/Application/Admin/Commands/CleanRun/CleanRunCommand.cs +++ b/backend/src/Modules/Admin/Application/Admin/Commands/CleanRun/CleanRunCommand.cs @@ -8,7 +8,7 @@ using Knot.Contracts.Auth.Infrastructure.Persistence; using Knot.Contracts.Conversations.Domain; using Knot.Contracts.Conversations.Infrastructure.Persistence; using Knot.Contracts.Messaging.Application.Abstractions; -using Knot.Contracts.Storage.Abstractions; +using Knot.Shared.Kernel.Storage; using Knot.Contracts.Stories.Infrastructure.Persistence; using Knot.Modules.Admin.Application.Admin.DTOs; using MediatR; @@ -51,7 +51,7 @@ internal sealed class CleanRunCommandHandler : ICommandHandler !orphanMessages.Any(om => om.Id == m.Id)) + .Where(m => !orphanMessages.Any(om => om.Id == m.Id) && !m.IsDeleted) .ToList(); var allMinioFiles = (await _fileStorage.ListFilesAsync()).ToList(); diff --git a/backend/src/Modules/Admin/Application/Admin/Queries/CleanDryRun/CleanDryRunQuery.cs b/backend/src/Modules/Admin/Application/Admin/Queries/CleanDryRun/CleanDryRunQuery.cs index e0cc42c..1ba60d5 100644 --- a/backend/src/Modules/Admin/Application/Admin/Queries/CleanDryRun/CleanDryRunQuery.cs +++ b/backend/src/Modules/Admin/Application/Admin/Queries/CleanDryRun/CleanDryRunQuery.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -8,6 +8,7 @@ using Knot.Contracts.Auth.Infrastructure.Persistence; using Knot.Contracts.Conversations.Domain; using Knot.Contracts.Conversations.Infrastructure.Persistence; using Knot.Contracts.Messaging.Application.Abstractions; +using Knot.Shared.Kernel.Storage; using Knot.Contracts.Stories.Infrastructure.Persistence; using Knot.Modules.Admin.Application.Admin.DTOs; using MediatR; @@ -23,17 +24,20 @@ internal sealed class CleanDryRunQueryHandler : IQueryHandler> Handle(CleanDryRunQuery request, CancellationToken ct) @@ -46,25 +50,42 @@ internal sealed class CleanDryRunQueryHandler : IQueryHandler m.MediaUrl != null); var orphanedMessageCount = orphanedMessages.Count; - var validIds = new HashSet(); - foreach (var msg in orphanedMessages.Where(m => m.MediaUrl != null)) - { - var parts = msg.MediaUrl.Split('/'); - var fileId = parts.LastOrDefault(); - if (!string.IsNullOrEmpty(fileId)) - { - validIds.Add(fileId); - } - } - + var allMessages = await _messageService.GetAllMessagesAsync(ct); + var keptMessages = allMessages + .Where(m => !orphanedMessages.Any(om => om.Id == m.Id) && !m.IsDeleted) + .ToList(); + var allUsers = await _authDbContext.Users.ToListAsync(ct); var stories = await _storyCollection.GetAllAsync(ct); + + var validUrls = new HashSet(); + + var activeMessageUrls = keptMessages.Where(m => m.Media != null).SelectMany(m => m.Media!).Select(x => x.Url).Where(u => !string.IsNullOrEmpty(u)); + var activeChatUrls = _chatsDbContext.Chats.Select(c => c.Avatar).Where(u => !string.IsNullOrEmpty(u)); + var activeUserUrls = allUsers.Select(u => u.Avatar).Where(u => !string.IsNullOrEmpty(u)); + var activeStoryUrls = stories.Select(s => s.MediaUrl).Where(u => !string.IsNullOrEmpty(u)); + + foreach (var u in activeMessageUrls) validUrls.Add(u!); + foreach (var u in activeChatUrls) validUrls.Add(u!); + foreach (var u in activeUserUrls) validUrls.Add(u!); + foreach (var u in activeStoryUrls) validUrls.Add(u!); + + var validFileIds = validUrls + .Where(u => u.Contains("/api/files/")) + .Select(u => u.Split('/').Last()) + .ToHashSet(); + + var allMinioFiles = await _fileStorage.ListFilesAsync(); + long orphanedFileSize = allMinioFiles + .Where(f => !validFileIds.Contains(f.FileId)) + .Sum(f => f.Size); + var expiredStoriesCount = stories.Count(s => s.ExpiresAt.HasValue && s.ExpiresAt.Value < DateTime.UtcNow); var expiredStoriesSize = stories.Where(s => s.ExpiresAt.HasValue && s.ExpiresAt.Value < DateTime.UtcNow).Sum(s => s.MediaUrl?.Length ?? 0); return Result.Success(new CleanDryRunResult( orphanedMessageCount, orphanedMediaCount, - 0, + orphanedFileSize, expiredStoriesCount, expiredStoriesSize )); diff --git a/backend/src/Modules/Admin/Presentation/Endpoints/AdminEndpoints.cs b/backend/src/Modules/Admin/Presentation/Endpoints/AdminEndpoints.cs index 18c6eb0..c49e6c8 100644 --- a/backend/src/Modules/Admin/Presentation/Endpoints/AdminEndpoints.cs +++ b/backend/src/Modules/Admin/Presentation/Endpoints/AdminEndpoints.cs @@ -131,6 +131,17 @@ public static class AdminEndpoints return Results.Ok(result.Value); }); + group.MapPost("clean/run", async (ISender sender, CancellationToken ct) => + { + var result = await sender.Send(new CleanRunCommand(), ct); + if (!result.IsSuccess) + { + Console.WriteLine($"[Admin] Cleanup Run Error: {result.Error.Description}"); + return Results.BadRequest(new { error = result.Error.Description }); + } + return Results.Ok(result.Value); + }); + group.MapGet("timezones", () => { // Получаем все системные часовые пояса и формируем удобный для фронтенда формат diff --git a/backend/src/Modules/Conversations/Application/Abstractions/IChatsUnitOfWork.cs b/backend/src/Modules/Conversations/Application/Abstractions/IChatsUnitOfWork.cs deleted file mode 100644 index 6c9bdb2..0000000 --- a/backend/src/Modules/Conversations/Application/Abstractions/IChatsUnitOfWork.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Knot.Shared.Kernel; - -namespace Knot.Modules.Conversations.Application.Abstractions; - -/// -/// Unit of Work специфичный для модуля Chats. -/// -public interface IChatsUnitOfWork : IUnitOfWork -{ -} - diff --git a/backend/src/Modules/Conversations/Application/Abstractions/IUserDeleterService.cs b/backend/src/Modules/Conversations/Application/Abstractions/IUserDeleterService.cs deleted file mode 100644 index 5565e80..0000000 --- a/backend/src/Modules/Conversations/Application/Abstractions/IUserDeleterService.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Knot.Shared.Kernel; - -namespace Knot.Modules.Conversations.Application.Abstractions; - -public interface IUserDeleterService -{ - Task DeleteUserAsync(Guid userId, CancellationToken cancellationToken); -} diff --git a/backend/src/Modules/Conversations/Application/Abstractions/IUserStatusService.cs b/backend/src/Modules/Conversations/Application/Abstractions/IUserStatusService.cs deleted file mode 100644 index d4a8761..0000000 --- a/backend/src/Modules/Conversations/Application/Abstractions/IUserStatusService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Knot.Modules.Conversations.Application.Abstractions; - -public interface IUserStatusService -{ - bool IsUserOnline(string userId); -} diff --git a/backend/src/Modules/Conversations/Application/Chats/Avatar/Avatar.cs b/backend/src/Modules/Conversations/Application/Chats/Avatar/Avatar.cs index 64189be..68cb10f 100644 --- a/backend/src/Modules/Conversations/Application/Chats/Avatar/Avatar.cs +++ b/backend/src/Modules/Conversations/Application/Chats/Avatar/Avatar.cs @@ -4,8 +4,8 @@ using System.Threading; using System.Threading.Tasks; using Knot.Shared.Kernel; using Knot.Shared.Kernel.Storage; -using Knot.Modules.Conversations.Domain; -using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Contracts.Conversations.Domain; +using Knot.Contracts.Conversations.Application.Abstractions; using MediatR; using System.Linq; using SixLabors.ImageSharp; diff --git a/backend/src/Modules/Conversations/Application/Chats/Clear/ClearChat.cs b/backend/src/Modules/Conversations/Application/Chats/Clear/ClearChat.cs index 3bd1d4a..462cd6a 100644 --- a/backend/src/Modules/Conversations/Application/Chats/Clear/ClearChat.cs +++ b/backend/src/Modules/Conversations/Application/Chats/Clear/ClearChat.cs @@ -2,8 +2,8 @@ using System; using System.Threading; using System.Threading.Tasks; using Knot.Shared.Kernel; -using Knot.Modules.Conversations.Domain; -using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Contracts.Conversations.Domain; +using Knot.Contracts.Conversations.Application.Abstractions; using MediatR; using System.Linq; diff --git a/backend/src/Modules/Conversations/Application/Chats/Create/CreateChatCommandHandler.cs b/backend/src/Modules/Conversations/Application/Chats/Create/CreateChatCommandHandler.cs index a2e327f..fb8e8cd 100644 --- a/backend/src/Modules/Conversations/Application/Chats/Create/CreateChatCommandHandler.cs +++ b/backend/src/Modules/Conversations/Application/Chats/Create/CreateChatCommandHandler.cs @@ -1,7 +1,7 @@ -using Knot.Modules.Conversations.Domain; +using Knot.Contracts.Conversations.Domain; using Knot.Shared.Kernel; -using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Contracts.Conversations.Application.Abstractions; namespace Knot.Modules.Conversations.Application.Chats.Create; diff --git a/backend/src/Modules/Conversations/Application/Chats/GetChatById/GetChatById.cs b/backend/src/Modules/Conversations/Application/Chats/GetChatById/GetChatById.cs index e9445ab..7251d31 100644 --- a/backend/src/Modules/Conversations/Application/Chats/GetChatById/GetChatById.cs +++ b/backend/src/Modules/Conversations/Application/Chats/GetChatById/GetChatById.cs @@ -5,9 +5,9 @@ using System.Threading; using System.Threading.Tasks; using Knot.Contracts.Messaging.Application.Abstractions; using Knot.Contracts.Messaging.Domain; -using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Contracts.Conversations.Application.Abstractions; using Knot.Modules.Conversations.Application.DTOs; -using Knot.Modules.Conversations.Domain; +using Knot.Contracts.Conversations.Domain; using Knot.Shared.Kernel; using MediatR; diff --git a/backend/src/Modules/Conversations/Application/Chats/GetChats/GetChats.cs b/backend/src/Modules/Conversations/Application/Chats/GetChats/GetChats.cs index 0d0f8d1..951cac2 100644 --- a/backend/src/Modules/Conversations/Application/Chats/GetChats/GetChats.cs +++ b/backend/src/Modules/Conversations/Application/Chats/GetChats/GetChats.cs @@ -5,9 +5,9 @@ using System.Threading; using System.Threading.Tasks; using Knot.Contracts.Messaging.Application.Abstractions; using Knot.Contracts.Messaging.Domain; -using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Contracts.Conversations.Application.Abstractions; using Knot.Modules.Conversations.Application.DTOs; -using Knot.Modules.Conversations.Domain; +using Knot.Contracts.Conversations.Domain; using Knot.Shared.Kernel; using MediatR; @@ -147,7 +147,9 @@ internal sealed class GetChatsQueryHandler : IQueryHandler { private readonly IChatRepository _chatRepository; + private readonly IMessageRepository _messageRepository; + private readonly IFileStorageService _fileStorage; private readonly IChatsUnitOfWork _uow; - public LeaveOrDeleteChatCommandHandler(IChatRepository chatRepository, IChatsUnitOfWork uow) + public LeaveOrDeleteChatCommandHandler( + IChatRepository chatRepository, + IMessageRepository messageRepository, + IFileStorageService fileStorage, + IChatsUnitOfWork uow) { _chatRepository = chatRepository; + _messageRepository = messageRepository; + _fileStorage = fileStorage; _uow = uow; } @@ -36,13 +45,18 @@ internal sealed class LeaveOrDeleteChatCommandHandler : ICommandHandler(ChatErrors.Unauthorized); } - if (chat.Type == ChatType.Group) + // If it's a private chat or the last member leaving a group, delete everything + bool shouldDeleteEverything = chat.Type != ChatType.Group || chat.Members.Count <= 1; + + if (chat.Type == ChatType.Group && !shouldDeleteEverything) { chat.RemoveMember(request.UserId); _chatRepository.Update(chat); } else { + // DELETE ALL MESSAGES AND FILES FIRST + await DeleteChatMediaAndMessagesAsync(chat.Id, cancellationToken); _chatRepository.Remove(chat); } @@ -50,5 +64,45 @@ internal sealed class LeaveOrDeleteChatCommandHandler : ICommandHandler Members, List Messages, - int UnreadCount + int UnreadCount, + bool IsImporting = false, + Guid? ImportJobId = null ); diff --git a/backend/src/Modules/Conversations/Application/DTOs/ChatMemberDto.cs b/backend/src/Modules/Conversations/Application/DTOs/ChatMemberDto.cs index 70a9024..b7cbe69 100644 --- a/backend/src/Modules/Conversations/Application/DTOs/ChatMemberDto.cs +++ b/backend/src/Modules/Conversations/Application/DTOs/ChatMemberDto.cs @@ -1,4 +1,4 @@ -using Knot.Modules.Conversations.Domain; +using Knot.Contracts.Conversations.Domain; using System; namespace Knot.Modules.Conversations.Application.DTOs; diff --git a/backend/src/Modules/Conversations/Application/DTOs/CreateChatRequest.cs b/backend/src/Modules/Conversations/Application/DTOs/CreateChatRequest.cs index 638431b..349145c 100644 --- a/backend/src/Modules/Conversations/Application/DTOs/CreateChatRequest.cs +++ b/backend/src/Modules/Conversations/Application/DTOs/CreateChatRequest.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Knot.Modules.Conversations.Domain; +using Knot.Contracts.Conversations.Domain; namespace Knot.Modules.Conversations.Application.DTOs; diff --git a/backend/src/Modules/Conversations/Application/Folders/Commands/AddToFolder/AddToFolderCommand.cs b/backend/src/Modules/Conversations/Application/Folders/Commands/AddToFolder/AddToFolderCommand.cs index 1be4fec..beb6b97 100644 --- a/backend/src/Modules/Conversations/Application/Folders/Commands/AddToFolder/AddToFolderCommand.cs +++ b/backend/src/Modules/Conversations/Application/Folders/Commands/AddToFolder/AddToFolderCommand.cs @@ -1,5 +1,5 @@ -using Knot.Modules.Conversations.Domain; -using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Contracts.Conversations.Domain; +using Knot.Contracts.Conversations.Application.Abstractions; using Knot.Contracts.Settings.Application.Abstractions; using Knot.Shared.Kernel; using MediatR; diff --git a/backend/src/Modules/Conversations/Application/Folders/Commands/RemoveFromFolder/RemoveFromFolderCommand.cs b/backend/src/Modules/Conversations/Application/Folders/Commands/RemoveFromFolder/RemoveFromFolderCommand.cs index d136ffb..bf3bf93 100644 --- a/backend/src/Modules/Conversations/Application/Folders/Commands/RemoveFromFolder/RemoveFromFolderCommand.cs +++ b/backend/src/Modules/Conversations/Application/Folders/Commands/RemoveFromFolder/RemoveFromFolderCommand.cs @@ -1,5 +1,5 @@ -using Knot.Modules.Conversations.Domain; -using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Contracts.Conversations.Domain; +using Knot.Contracts.Conversations.Application.Abstractions; using Knot.Shared.Kernel; using MediatR; diff --git a/backend/src/Modules/Conversations/Application/Messages/Delete/DeleteMessagesCommand.cs b/backend/src/Modules/Conversations/Application/Messages/Delete/DeleteMessagesCommand.cs index 9b2d667..d90fce4 100644 --- a/backend/src/Modules/Conversations/Application/Messages/Delete/DeleteMessagesCommand.cs +++ b/backend/src/Modules/Conversations/Application/Messages/Delete/DeleteMessagesCommand.cs @@ -1,5 +1,5 @@ -using global::Knot.Modules.Conversations.Application.Abstractions; -using global::Knot.Modules.Conversations.Domain; +using global::Knot.Contracts.Conversations.Application.Abstractions; +using global::Knot.Contracts.Conversations.Domain; using global::Knot.Modules.Conversations.Infrastructure.SignalR; using global::Knot.Shared.Kernel; using MediatR; diff --git a/backend/src/Modules/Conversations/Application/Messages/GetMessages/GetMessagesQuery.cs b/backend/src/Modules/Conversations/Application/Messages/GetMessages/GetMessagesQuery.cs index c90ab27..a590b22 100644 --- a/backend/src/Modules/Conversations/Application/Messages/GetMessages/GetMessagesQuery.cs +++ b/backend/src/Modules/Conversations/Application/Messages/GetMessages/GetMessagesQuery.cs @@ -5,9 +5,9 @@ using System.Threading; using System.Threading.Tasks; using Knot.Contracts.Messaging.Application.Abstractions; using Knot.Contracts.Messaging.Domain; -using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Contracts.Conversations.Application.Abstractions; using Knot.Modules.Conversations.Application.DTOs; -using Knot.Modules.Conversations.Domain; +using Knot.Contracts.Conversations.Domain; using Knot.Shared.Kernel; using MediatR; @@ -39,12 +39,21 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler(); var userIdsToFetch = new HashSet(); @@ -88,7 +97,8 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler { var mType = media.Type?.ToLower() ?? "file"; - var isGif = mType == "image" && media.Url != null && (media.Url.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) || media.Url.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)); + var filename = media.Filename?.ToLower() ?? ""; + var url = media.Url?.ToLower() ?? ""; + + var isGif = mType == "gif" || + (mType == "image" && (filename.EndsWith(".mp4") || filename.EndsWith(".gif") || url.EndsWith(".gif") || filename.Contains("gif"))) || + (mType == "video" && (filename.Contains("animation") || filename.Contains("gif"))); if (filterType == "gifs") { diff --git a/backend/src/Modules/Conversations/Application/Messages/React/AddReactionCommand.cs b/backend/src/Modules/Conversations/Application/Messages/React/AddReactionCommand.cs index f2652bc..d6641b0 100644 --- a/backend/src/Modules/Conversations/Application/Messages/React/AddReactionCommand.cs +++ b/backend/src/Modules/Conversations/Application/Messages/React/AddReactionCommand.cs @@ -1,7 +1,7 @@ using Knot.Contracts.Messaging.Application.Abstractions; using Knot.Contracts.Messaging.Domain; -using Knot.Modules.Conversations.Application.Abstractions; -using Knot.Modules.Conversations.Domain; +using Knot.Contracts.Conversations.Application.Abstractions; +using Knot.Contracts.Conversations.Domain; using Knot.Modules.Conversations.Infrastructure.SignalR; using Knot.Shared.Kernel; using MediatR; diff --git a/backend/src/Modules/Conversations/Application/Messages/React/RemoveReactionCommand.cs b/backend/src/Modules/Conversations/Application/Messages/React/RemoveReactionCommand.cs index 445d794..51a2222 100644 --- a/backend/src/Modules/Conversations/Application/Messages/React/RemoveReactionCommand.cs +++ b/backend/src/Modules/Conversations/Application/Messages/React/RemoveReactionCommand.cs @@ -1,7 +1,7 @@ using Knot.Contracts.Messaging.Application.Abstractions; using Knot.Contracts.Messaging.Domain; -using Knot.Modules.Conversations.Application.Abstractions; -using Knot.Modules.Conversations.Domain; +using Knot.Contracts.Conversations.Application.Abstractions; +using Knot.Contracts.Conversations.Domain; using Knot.Modules.Conversations.Infrastructure.SignalR; using Knot.Shared.Kernel; using MediatR; diff --git a/backend/src/Modules/Conversations/Application/Messages/Read/ReadMessagesCommandHandler.cs b/backend/src/Modules/Conversations/Application/Messages/Read/ReadMessagesCommandHandler.cs index d0f9c3b..6a72426 100644 --- a/backend/src/Modules/Conversations/Application/Messages/Read/ReadMessagesCommandHandler.cs +++ b/backend/src/Modules/Conversations/Application/Messages/Read/ReadMessagesCommandHandler.cs @@ -1,7 +1,7 @@ using MediatR; -using Knot.Modules.Conversations.Domain; +using Knot.Contracts.Conversations.Domain; using Knot.Shared.Kernel; -using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Contracts.Conversations.Application.Abstractions; namespace Knot.Modules.Conversations.Application.Messages.Read; diff --git a/backend/src/Modules/Conversations/Application/Messages/SearchMessages/SearchMessagesQuery.cs b/backend/src/Modules/Conversations/Application/Messages/SearchMessages/SearchMessagesQuery.cs index 69250e8..1bd2c5f 100644 --- a/backend/src/Modules/Conversations/Application/Messages/SearchMessages/SearchMessagesQuery.cs +++ b/backend/src/Modules/Conversations/Application/Messages/SearchMessages/SearchMessagesQuery.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Knot.Contracts.Messaging.Application.Abstractions; using Knot.Contracts.Messaging.Domain; using Knot.Modules.Conversations.Application.DTOs; -using Knot.Modules.Conversations.Domain; +using Knot.Contracts.Conversations.Domain; using Knot.Shared.Kernel; using MediatR; diff --git a/backend/src/Modules/Conversations/Application/Messages/Send/SendMessageCommandHandler.cs b/backend/src/Modules/Conversations/Application/Messages/Send/SendMessageCommandHandler.cs index 35ed35e..5accf6d 100644 --- a/backend/src/Modules/Conversations/Application/Messages/Send/SendMessageCommandHandler.cs +++ b/backend/src/Modules/Conversations/Application/Messages/Send/SendMessageCommandHandler.cs @@ -1,8 +1,8 @@ using Knot.Contracts.Messaging.Application.Abstractions; using Knot.Contracts.Messaging.Domain; using Knot.Contracts.Settings.Application.Abstractions; -using Knot.Modules.Conversations.Application.Abstractions; -using Knot.Modules.Conversations.Domain; +using Knot.Contracts.Conversations.Application.Abstractions; +using Knot.Contracts.Conversations.Domain; using Knot.Shared.Kernel; namespace Knot.Modules.Conversations.Application.Messages.Send; diff --git a/backend/src/Modules/Conversations/Application/Messages/UploadFile/UploadFileCommand.cs b/backend/src/Modules/Conversations/Application/Messages/UploadFile/UploadFileCommand.cs index 236ca92..8ba2e96 100644 --- a/backend/src/Modules/Conversations/Application/Messages/UploadFile/UploadFileCommand.cs +++ b/backend/src/Modules/Conversations/Application/Messages/UploadFile/UploadFileCommand.cs @@ -1,4 +1,4 @@ -using Knot.Modules.Conversations.Domain; +using Knot.Contracts.Conversations.Domain; using System; using System.IO; using System.Threading; diff --git a/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs b/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs index a78d58c..e24aa91 100644 --- a/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs +++ b/backend/src/Modules/Conversations/Application/Users/Commands/DeleteUser/DeleteUserCommand.cs @@ -2,8 +2,8 @@ using System.Text.RegularExpressions; using Knot.Contracts.Auth.Application.Abstractions; using Knot.Contracts.Auth.Domain; using Knot.Contracts.Messaging.Domain; -using Knot.Modules.Conversations.Application.Abstractions; -using Knot.Modules.Conversations.Domain; +using Knot.Contracts.Conversations.Application.Abstractions; +using Knot.Contracts.Conversations.Domain; using Knot.Shared.Kernel; using Knot.Shared.Kernel.Storage; using MediatR; @@ -55,15 +55,18 @@ internal sealed class DeleteUserCommandHandler : ICommandHandler m.Id != msg.Id && - m is MediaMessage mm && mm.Media.Any(ame => ame.Url == media.Url)); - - if (!isUsedElsewhere) + if (!string.IsNullOrEmpty(media.Url)) { - var fileId = ExtractFileId(media.Url); - if (!string.IsNullOrEmpty(fileId)) + bool isUsedElsewhere = allMessages.Any(m => m.Id != msg.Id && + m is MediaMessage mm && mm.Media.Any(ame => !string.IsNullOrEmpty(ame.Url) && ame.Url == media.Url)); + + if (!isUsedElsewhere) { - await _fileStorage.DeleteFileAsync(fileId); + var fileId = ExtractFileId(media.Url); + if (!string.IsNullOrEmpty(fileId)) + { + await _fileStorage.DeleteFileAsync(fileId); + } } } } diff --git a/backend/src/Modules/Conversations/DependencyInjection.cs b/backend/src/Modules/Conversations/DependencyInjection.cs index 9f23bd0..9e60630 100644 --- a/backend/src/Modules/Conversations/DependencyInjection.cs +++ b/backend/src/Modules/Conversations/DependencyInjection.cs @@ -1,16 +1,14 @@ using Knot.Contracts.Conversations.Application.Abstractions; using Knot.Contracts.Conversations.Infrastructure.Persistence; -using Knot.Contracts.Messaging.Application.Abstractions; -using Knot.Contracts.Messaging.Domain; -using Knot.Modules.Conversations.Application.Abstractions; -using Knot.Modules.Conversations.Domain; +using Knot.Contracts.Conversations.Domain; using Knot.Modules.Conversations.Infrastructure.Persistence; using Knot.Modules.Conversations.Infrastructure.Persistence.Mongo; +using Knot.Modules.Conversations.Infrastructure.Services; using Knot.Shared.Kernel; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using ConversationsAbstractions = Knot.Modules.Conversations.Application.Abstractions; +using Knot.Contracts.Messaging.Application.Abstractions; namespace Knot.Modules.Conversations; @@ -29,21 +27,22 @@ public static class DependencyInjection var mongoConnectionString = configuration.GetConnectionString("MongoConnection") ?? "mongodb://localhost:27017"; - services.AddScoped(sp => sp.GetRequiredService()); + services.AddScoped(sp => sp.GetRequiredService()); services.AddScoped(sp => sp.GetRequiredService()); services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddMediatR(config => config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly)); - services.AddScoped(); + services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + return services; } } diff --git a/backend/src/Modules/Conversations/Domain/Chat.cs b/backend/src/Modules/Conversations/Domain/Chat.cs deleted file mode 100644 index 73417e9..0000000 --- a/backend/src/Modules/Conversations/Domain/Chat.cs +++ /dev/null @@ -1,157 +0,0 @@ -using Knot.Shared.Kernel; - -namespace Knot.Modules.Conversations.Domain; - -public sealed record ChatCreatedDomainEvent(Chat Chat) : IDomainEvent; -public sealed record ChatMemberAddedDomainEvent(Guid ChatId, Guid UserId) : IDomainEvent; - -/// -/// Тип чата: личный или групповой. -/// -public enum ChatType -{ - Personal, - Group, - Favorites -} - -/// -/// Роль участника в чате. -/// -public static class ChatRole -{ - public const string Owner = "owner"; - public const string Admin = "admin"; - public const string Member = "member"; -} - -/// -/// Сущность чата (Агрегат). -/// -public sealed class Chat : AggregateRoot -{ - public ChatType Type { get; private set; } - public string? Name { get; private set; } - public string? Description { get; private set; } - public string? Avatar { get; private set; } - public DateTime CreatedAt { get; private set; } - public long LastMessageSequenceId { get; private set; } - - private readonly List _members = new(); - public IReadOnlyCollection Members => _members.AsReadOnly(); - - private Chat(Guid id, ChatType type, string? name, string? avatar, string? description = null) : base(id) - { - Type = type; - Name = name; - Avatar = avatar; - Description = description; - CreatedAt = DateTime.UtcNow; - } - - /// - /// Создает личный чат между двумя пользователями. - /// - public static Chat CreatePersonal() - { - var chat = new Chat(Guid.NewGuid(), ChatType.Personal, null, null); - chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat)); - return chat; - } - - /// - /// Создает групповой чат. - /// - public static Chat CreateGroup(string name, string? avatar = null) - { - var chat = new Chat(Guid.NewGuid(), ChatType.Group, name, avatar); - chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat)); - return chat; - } - - /// - /// Фабричный метод для создания чата. - /// - public static Chat Create(string? name, ChatType type, string? avatar = null, string? description = null) - { - var chat = new Chat(Guid.NewGuid(), type, name, avatar, description); - chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat)); - return chat; - } - - public void AddMember(Guid userId, string role = "member") - { - if (_members.Any(m => m.UserId == userId)) - { - return; - } - - _members.Add(new ChatMember(Id, userId, role)); - RaiseDomainEvent(new ChatMemberAddedDomainEvent(Id, userId)); - } - - public void RemoveMember(Guid userId) - { - var member = _members.FirstOrDefault(m => m.UserId == userId); - if (member != null) - { - _members.Remove(member); - } - } - - public void UpdateName(string name) => Name = name; - - public void UpdateDescription(string? description) => Description = description; - - public void UpdateAvatar(string? avatarUrl) => Avatar = avatarUrl; - - public long IncrementSequenceId() - { - return ++LastMessageSequenceId; - } -} - -/// -/// Участник чата. -/// -public sealed class ChatMember : Entity -{ - public Guid ChatId { get; private set; } - public Guid UserId { get; private set; } - public string Role { get; private set; } - public DateTime JoinedAt { get; private set; } - public bool IsPinned { get; private set; } - public bool IsMuted { get; private set; } - - public Guid? LastReadMessageId { get; private set; } - public long LastReadSequenceId { get; private set; } - public Guid? LastDeliveredMessageId { get; private set; } - - // For EF Core - private ChatMember() : base(Guid.Empty) { Role = "member"; } - - internal ChatMember(Guid chatId, Guid userId, string role) : base(Guid.NewGuid()) - { - ChatId = chatId; - UserId = userId; - Role = role; - JoinedAt = DateTime.UtcNow; - } - - public void TogglePin() => IsPinned = !IsPinned; - - public void UpdateReadCursor(Guid messageId, long sequenceId) - { - if (sequenceId > LastReadSequenceId) - { - LastReadMessageId = messageId; - LastReadSequenceId = sequenceId; - } - } - - public void UpdateDeliveredCursor(Guid messageId) - { - LastDeliveredMessageId = messageId; - } -} - diff --git a/backend/src/Modules/Conversations/Domain/ChatConstants.cs b/backend/src/Modules/Conversations/Domain/ChatConstants.cs deleted file mode 100644 index 33e0890..0000000 --- a/backend/src/Modules/Conversations/Domain/ChatConstants.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Knot.Modules.Conversations.Domain; - -public static class ChatConstants -{ - public const int DefaultMessageQueryLimit = 100; - public const int MaxSharedMediaQueryLimit = 300; - public const int SearchMessagesLimit = 50; - public const int MaxFileUploadSizeMb = 50; -} - diff --git a/backend/src/Modules/Conversations/Domain/ChatErrors.cs b/backend/src/Modules/Conversations/Domain/ChatErrors.cs deleted file mode 100644 index 5ae9405..0000000 --- a/backend/src/Modules/Conversations/Domain/ChatErrors.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Knot.Shared.Kernel; - -namespace Knot.Modules.Conversations.Domain; - -public static class ChatErrors -{ - public static readonly Error FileEmpty = new Error("File.Empty", "No file uploaded"); - public static readonly Error FileInvalidExtension = new Error("File.InvalidExtension", "Must be a ZIP archive"); - public static readonly Error ImportExpired = new Error("Import.Expired", "Session not found or expired"); - public static readonly Error ImportMissing = new Error("Import.Missing", "ZIP file lost"); - public static readonly Error ChatNotFound = new Error("Chat.NotFound", "Chat not found or access denied"); - public static readonly Error NotFound = new Error("Chat.NotFound", "Chat not found"); // Alias - public static readonly Error NotMember = new Error("Chat.NotMember", "You are not a member of this chat"); - public static readonly Error ChatsForbidden = new Error("Chats.Forbidden", "Вы не являетесь участником этого чата."); - public static readonly Error MessagesNotFound = new Error("Messages.NotFound", "Message not found."); - public static readonly Error ChatsNotFound = new Error("Chats.NotFound", "Чат не найден."); - public static readonly Error Unauthorized = new Error("Chats.Unauthorized", "Access denied"); - public static readonly Error FoldersDisabled = new Error("Folders.Disabled", "Folders feature is disabled by the administrator."); - public static readonly Error PollsDisabled = new Error("Polls.Disabled", "Polls are disabled by the administrator."); - public static readonly Error MediaDisabled = new Error("Media.Disabled", "Media messages are disabled by the administrator."); - - public static Error ImportCreateChatFailed(string msg) => new Error("Import.CreateChatFailed", msg); - public static Error FileTooLarge(int maxMb) => new Error("File.TooLarge", $"File exceeds the maximum allowed size of {maxMb}MB."); -} - diff --git a/backend/src/Modules/Conversations/Domain/Folder.cs b/backend/src/Modules/Conversations/Domain/Folder.cs deleted file mode 100644 index b9a0545..0000000 --- a/backend/src/Modules/Conversations/Domain/Folder.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Collections.Generic; -using Knot.Shared.Kernel; - -namespace Knot.Modules.Conversations.Domain; - -/// -/// Сущность папки для группировки чатов. -/// -public sealed class Folder : AggregateRoot -{ - public string Name { get; private set; } - public string? Icon { get; private set; } // URL из хранилища - public bool IsDefault { get; private set; } - public FolderType Type { get; private set; } - - public Folder(Guid id, string name, string? icon = null, bool isDefault = false, FolderType type = FolderType.Custom) - : base(id) - { - Name = name; - Icon = icon; - IsDefault = isDefault; - Type = type; - } - - public void Update(string name, string? icon) - { - if (IsDefault) throw new InvalidOperationException("Cannot rename default folders."); - Name = name; - Icon = icon; - } -} - -public enum FolderType -{ - All, // Все чаты - New, // Новые (с непрочитанными) - Muted, // Без звука - Custom // Пользовательская -} - -/// -/// Настройки конкретного чата для конкретного пользователя. -/// Хранятся в PostgreSQL (связь User <-> Chat). -/// -public sealed class UserChatSettings : Entity -{ - public Guid UserId { get; private set; } - public Guid ChatId { get; private set; } - - // Список папок, в которые входит чат для этого пользователя - private readonly List _folderIds = new(); - public IReadOnlyCollection FolderIds => _folderIds.AsReadOnly(); - - public bool IsMuted { get; private set; } - - private UserChatSettings() : base(Guid.NewGuid()) { } - - public UserChatSettings(Guid userId, Guid chatId) : base(Guid.NewGuid()) - { - UserId = userId; - ChatId = chatId; - } - - public static UserChatSettings Create(Guid userId, Guid chatId) => new(userId, chatId); - - public void AddToFolder(Guid folderId) - { - if (!_folderIds.Contains(folderId)) _folderIds.Add(folderId); - } - - public void RemoveFromFolder(Guid folderId) - { - _folderIds.Remove(folderId); - } - - public void SetMute(bool isMuted) => IsMuted = isMuted; -} - -/// -/// Глобальные настройки папок пользователя (скрытие дефолтных и т.д.). -/// Будет храниться в MongoDB. -/// -public sealed class UserFolderSettings : AggregateRoot -{ - public Guid UserId { get; private set; } - - // Список ID папок, которые пользователь скрыл (только для дефолтных) - public List HiddenDefaultFolderIds { get; private set; } = new(); - - // Список пользовательских папок (Guid созданных Folder) - public List CustomFolderIds { get; private set; } = new(); - - public UserFolderSettings(Guid userId) : base(Guid.NewGuid()) - { - UserId = userId; - } - - public void HideFolder(Guid folderId) - { - if (!HiddenDefaultFolderIds.Contains(folderId)) HiddenDefaultFolderIds.Add(folderId); - } - - public void ShowFolder(Guid folderId) - { - HiddenDefaultFolderIds.Remove(folderId); - } -} diff --git a/backend/src/Modules/Conversations/Domain/IChatRepository.cs b/backend/src/Modules/Conversations/Domain/IChatRepository.cs deleted file mode 100644 index e2b5335..0000000 --- a/backend/src/Modules/Conversations/Domain/IChatRepository.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Knot.Modules.Conversations.Domain; - -namespace Knot.Modules.Conversations.Domain; - -public interface IChatRepository -{ - void Add(Chat chat); - void Update(Chat chat); - void Remove(Chat chat); - Task GetByIdAsync(Guid id, CancellationToken cancellationToken); - Task GetFavoritesAsync(Guid userId, CancellationToken cancellationToken); - Task> GetUserChatsAsync(Guid userId, CancellationToken cancellationToken); -} - -public interface IFolderRepository -{ - void Add(Folder folder); - void Update(Folder folder); - void Remove(Folder folder); - Task GetByIdAsync(Guid id, CancellationToken cancellationToken); - Task> GetUserFoldersAsync(Guid userId, CancellationToken cancellationToken); -} - -public interface IUserChatSettingsRepository -{ - void Add(UserChatSettings settings); - void Update(UserChatSettings settings); - void Remove(UserChatSettings settings); - void RemoveRange(IEnumerable settings); - Task GetAsync(Guid userId, Guid chatId, CancellationToken cancellationToken); - Task> GetByUserIdAsync(Guid userId, CancellationToken cancellationToken); -} - -public interface IUserFolderSettingsRepository -{ - Task GetByUserIdAsync(Guid userId, CancellationToken cancellationToken); - Task UpdateAsync(UserFolderSettings settings, CancellationToken cancellationToken); - Task RemoveByUserIdAsync(Guid userId, CancellationToken cancellationToken); -} diff --git a/backend/src/Modules/Conversations/Infrastructure/Handlers/ChatCreatedDomainEventHandler.cs b/backend/src/Modules/Conversations/Infrastructure/Handlers/ChatCreatedDomainEventHandler.cs index 4dd2403..22094e7 100644 --- a/backend/src/Modules/Conversations/Infrastructure/Handlers/ChatCreatedDomainEventHandler.cs +++ b/backend/src/Modules/Conversations/Infrastructure/Handlers/ChatCreatedDomainEventHandler.cs @@ -1,5 +1,5 @@ using Knot.Shared.Kernel; -using Knot.Modules.Conversations.Domain; +using Knot.Contracts.Conversations.Domain; using Knot.Modules.Conversations.Infrastructure.SignalR; using MediatR; using Microsoft.AspNetCore.SignalR; diff --git a/backend/src/Modules/Conversations/Infrastructure/Persistence/ChatRepository.cs b/backend/src/Modules/Conversations/Infrastructure/Persistence/ChatRepository.cs index 06d4c8b..4cfb2d7 100644 --- a/backend/src/Modules/Conversations/Infrastructure/Persistence/ChatRepository.cs +++ b/backend/src/Modules/Conversations/Infrastructure/Persistence/ChatRepository.cs @@ -1,5 +1,5 @@ +using Knot.Contracts.Conversations.Domain; using Microsoft.EntityFrameworkCore; -using Knot.Modules.Conversations.Domain; namespace Knot.Modules.Conversations.Infrastructure.Persistence; diff --git a/backend/src/Modules/Conversations/Infrastructure/Persistence/ChatsDbContextFactory.cs b/backend/src/Modules/Conversations/Infrastructure/Persistence/ChatsDbContextFactory.cs index 88a5a4b..9e022e1 100644 --- a/backend/src/Modules/Conversations/Infrastructure/Persistence/ChatsDbContextFactory.cs +++ b/backend/src/Modules/Conversations/Infrastructure/Persistence/ChatsDbContextFactory.cs @@ -32,4 +32,4 @@ public class ChatsDbContextFactory : IDesignTimeDbContextFactory optionsBuilder.UseNpgsql(connectionString); return new ChatsDbContext(optionsBuilder.Options); } -} \ No newline at end of file +} diff --git a/backend/src/Modules/Conversations/Infrastructure/Persistence/ConversationsDbContext.cs b/backend/src/Modules/Conversations/Infrastructure/Persistence/ConversationsDbContext.cs index 6ec5a42..9071606 100644 --- a/backend/src/Modules/Conversations/Infrastructure/Persistence/ConversationsDbContext.cs +++ b/backend/src/Modules/Conversations/Infrastructure/Persistence/ConversationsDbContext.cs @@ -4,19 +4,18 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Knot.Contracts.Conversations.Application.Abstractions; +using Knot.Contracts.Conversations.Domain; using Knot.Contracts.Conversations.Infrastructure.Persistence; -using Knot.Modules.Conversations.Application.Abstractions; -using Knot.Modules.Conversations.Domain; using Knot.Shared.Kernel; using Knot.Shared.Kernel.Security; using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; -using DomainChat = Knot.Modules.Conversations.Domain.Chat; +using DomainChat = Knot.Contracts.Conversations.Domain.Chat; namespace Knot.Modules.Conversations.Infrastructure.Persistence; -public sealed class ChatsDbContext : DbContext, Knot.Modules.Conversations.Application.Abstractions.IChatsUnitOfWork, Knot.Contracts.Conversations.Infrastructure.Persistence.IChatsDbContext +public sealed class ChatsDbContext : DbContext, Knot.Contracts.Conversations.Application.Abstractions.IChatsUnitOfWork, Knot.Contracts.Conversations.Infrastructure.Persistence.IChatsDbContext { private readonly IMediator? _mediator; private readonly IEncryptionService? _encryptionService; @@ -54,6 +53,8 @@ public sealed class ChatsDbContext : DbContext, Knot.Modules.Conversations.Appli { builder.ToTable("Chats"); builder.HasKey(c => c.Id); + builder.Property(c => c.IsImporting); + builder.Property(c => c.ImportJobId); builder.Property(c => c.Type).HasConversion(); builder.OwnsMany(c => c.Members, mb => diff --git a/backend/src/Modules/Conversations/Infrastructure/Persistence/Mongo/ConversationsMongoDbMapConfigurator.cs b/backend/src/Modules/Conversations/Infrastructure/Persistence/Mongo/ConversationsMongoDbMapConfigurator.cs index 6813fdd..a74c062 100644 --- a/backend/src/Modules/Conversations/Infrastructure/Persistence/Mongo/ConversationsMongoDbMapConfigurator.cs +++ b/backend/src/Modules/Conversations/Infrastructure/Persistence/Mongo/ConversationsMongoDbMapConfigurator.cs @@ -1,4 +1,4 @@ -using Knot.Modules.Conversations.Domain; +using Knot.Contracts.Conversations.Domain; using MongoDB.Bson.Serialization; namespace Knot.Modules.Conversations.Infrastructure.Persistence.Mongo; diff --git a/backend/src/Modules/Conversations/Infrastructure/Persistence/Mongo/UserFolderSettingsRepository.cs b/backend/src/Modules/Conversations/Infrastructure/Persistence/Mongo/UserFolderSettingsRepository.cs index e1d6442..f0e46a4 100644 --- a/backend/src/Modules/Conversations/Infrastructure/Persistence/Mongo/UserFolderSettingsRepository.cs +++ b/backend/src/Modules/Conversations/Infrastructure/Persistence/Mongo/UserFolderSettingsRepository.cs @@ -1,4 +1,4 @@ -using Knot.Modules.Conversations.Domain; +using Knot.Contracts.Conversations.Domain; using MongoDB.Driver; namespace Knot.Modules.Conversations.Infrastructure.Persistence.Mongo; diff --git a/backend/src/Modules/Conversations/Infrastructure/Services/UserDeleterService.cs b/backend/src/Modules/Conversations/Infrastructure/Services/UserDeleterService.cs index 73ceced..ca8f4e2 100644 --- a/backend/src/Modules/Conversations/Infrastructure/Services/UserDeleterService.cs +++ b/backend/src/Modules/Conversations/Infrastructure/Services/UserDeleterService.cs @@ -18,4 +18,4 @@ public sealed class UserDeleterService : IUserDeleterService { return await _sender.Send(new DeleteUserCommand(userId), cancellationToken); } -} \ No newline at end of file +} diff --git a/backend/src/Modules/Conversations/Infrastructure/Services/UserStatusService.cs b/backend/src/Modules/Conversations/Infrastructure/Services/UserStatusService.cs index ce6ecd4..88f100a 100644 --- a/backend/src/Modules/Conversations/Infrastructure/Services/UserStatusService.cs +++ b/backend/src/Modules/Conversations/Infrastructure/Services/UserStatusService.cs @@ -1,9 +1,11 @@ -using Knot.Modules.Conversations.Application.Abstractions; +using Knot.Contracts.Conversations.Application.Abstractions; using Knot.Modules.Conversations.Infrastructure.SignalR; namespace Knot.Modules.Conversations.Infrastructure.Services; -public sealed class UserStatusService : IUserStatusService, Knot.Contracts.Conversations.Abstractions.IUserStatusService +public sealed class UserStatusService : + Knot.Contracts.Conversations.Application.Abstractions.IUserStatusService, + Knot.Contracts.Conversations.Abstractions.IUserStatusService { public bool IsUserOnline(string userId) { diff --git a/backend/src/Modules/Conversations/Infrastructure/SignalR/ChatHub.cs b/backend/src/Modules/Conversations/Infrastructure/SignalR/ChatHub.cs index 9175ac7..5ba8259 100644 --- a/backend/src/Modules/Conversations/Infrastructure/SignalR/ChatHub.cs +++ b/backend/src/Modules/Conversations/Infrastructure/SignalR/ChatHub.cs @@ -8,7 +8,7 @@ using Knot.Modules.Conversations.Application.Messages.Send; using Knot.Modules.Conversations.Application.Messages.Read; using Knot.Modules.Conversations.Application.Messages.Delete; using Knot.Modules.Conversations.Application.Messages.React; -using Knot.Modules.Conversations.Domain; +using Knot.Contracts.Conversations.Domain; using Knot.Shared.Kernel; using Microsoft.Extensions.Caching.Memory; using Knot.Contracts.Auth.Domain; diff --git a/backend/src/Modules/Conversations/Infrastructure/SignalR/MessageNotifier.cs b/backend/src/Modules/Conversations/Infrastructure/SignalR/MessageNotifier.cs index 4224841..c93b5d8 100644 --- a/backend/src/Modules/Conversations/Infrastructure/SignalR/MessageNotifier.cs +++ b/backend/src/Modules/Conversations/Infrastructure/SignalR/MessageNotifier.cs @@ -5,4 +5,4 @@ using System.Threading; using System.Threading.Tasks; using Knot.Contracts.Messaging.Application.Abstractions; using Microsoft.AspNetCore.SignalR; -public class MessageNotifier : IMessageNotifier { private readonly IHubContext _hubContext; public MessageNotifier(IHubContext hubContext) { _hubContext = hubContext; } public Task NotifyNewMessageAsync(Guid chatId, object messagePayload, CancellationToken cancellationToken) { return _hubContext.Clients.Group(chatId.ToString()).SendAsync("new_message", messagePayload, cancellationToken); } } \ No newline at end of file +public class MessageNotifier : IMessageNotifier { private readonly IHubContext _hubContext; public MessageNotifier(IHubContext hubContext) { _hubContext = hubContext; } public Task NotifyNewMessageAsync(Guid chatId, object messagePayload, CancellationToken cancellationToken) { return _hubContext.Clients.Group(chatId.ToString()).SendAsync("new_message", messagePayload, cancellationToken); } } diff --git a/backend/src/Modules/Conversations/Migrations/20260322191932_InitialConversations.Designer.cs b/backend/src/Modules/Conversations/Migrations/20260322191932_InitialConversations.Designer.cs index 4ddafa9..31a27ab 100644 --- a/backend/src/Modules/Conversations/Migrations/20260322191932_InitialConversations.Designer.cs +++ b/backend/src/Modules/Conversations/Migrations/20260322191932_InitialConversations.Designer.cs @@ -26,7 +26,7 @@ namespace Knot.Modules.Conversations.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b => + modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -56,9 +56,9 @@ namespace Knot.Modules.Conversations.Migrations b.ToTable("Chats", "chats"); }); - modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b => + modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b => { - b.OwnsMany("Knot.Modules.Conversations.Domain.ChatMember", "Members", b1 => + b.OwnsMany("Knot.Contracts.Conversations.Domain.ChatMember", "Members", b1 => { b1.Property("Id") .ValueGeneratedOnAdd() diff --git a/backend/src/Modules/Conversations/Migrations/20260406122703_AddIsImportingToChat.Designer.cs b/backend/src/Modules/Conversations/Migrations/20260406122703_AddIsImportingToChat.Designer.cs new file mode 100644 index 0000000..380089d --- /dev/null +++ b/backend/src/Modules/Conversations/Migrations/20260406122703_AddIsImportingToChat.Designer.cs @@ -0,0 +1,166 @@ +// +using System; +using Knot.Modules.Conversations.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Knot.Modules.Conversations.Migrations +{ + [DbContext(typeof(ChatsDbContext))] + [Migration("20260406122703_AddIsImportingToChat")] + partial class AddIsImportingToChat + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("chats") + .HasAnnotation("ProductVersion", "10.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsImporting") + .HasColumnType("boolean"); + + b.Property("LastMessageSequenceId") + .HasColumnType("bigint"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Chats", "chats"); + }); + + modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Icon") + .HasColumnType("text"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Folders", "chats"); + }); + + modelBuilder.Entity("Knot.Contracts.Conversations.Domain.UserChatSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChatId") + .HasColumnType("uuid"); + + b.Property("FolderIds") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsMuted") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ChatId") + .IsUnique(); + + b.ToTable("UserChatSettings", "chats"); + }); + + modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b => + { + b.OwnsMany("Knot.Contracts.Conversations.Domain.ChatMember", "Members", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b1.Property("ChatId") + .HasColumnType("uuid"); + + b1.Property("IsMuted") + .HasColumnType("boolean"); + + b1.Property("IsPinned") + .HasColumnType("boolean"); + + b1.Property("JoinedAt") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastDeliveredMessageId") + .HasColumnType("uuid"); + + b1.Property("LastReadMessageId") + .HasColumnType("uuid"); + + b1.Property("LastReadSequenceId") + .HasColumnType("bigint"); + + b1.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b1.Property("UserId") + .HasColumnType("uuid"); + + b1.HasKey("Id"); + + b1.HasIndex("ChatId", "UserId") + .IsUnique(); + + b1.ToTable("ChatMembers", "chats"); + + b1.WithOwner() + .HasForeignKey("ChatId"); + }); + + b.Navigation("Members"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Modules/Conversations/Migrations/20260406122703_AddIsImportingToChat.cs b/backend/src/Modules/Conversations/Migrations/20260406122703_AddIsImportingToChat.cs new file mode 100644 index 0000000..c65ec3c --- /dev/null +++ b/backend/src/Modules/Conversations/Migrations/20260406122703_AddIsImportingToChat.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Knot.Modules.Conversations.Migrations +{ + /// + public partial class AddIsImportingToChat : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsImporting", + schema: "chats", + table: "Chats", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsImporting", + schema: "chats", + table: "Chats"); + } + } +} diff --git a/backend/src/Modules/Conversations/Migrations/20260406123444_AddImportJobIdToChat.Designer.cs b/backend/src/Modules/Conversations/Migrations/20260406123444_AddImportJobIdToChat.Designer.cs new file mode 100644 index 0000000..bb6a049 --- /dev/null +++ b/backend/src/Modules/Conversations/Migrations/20260406123444_AddImportJobIdToChat.Designer.cs @@ -0,0 +1,169 @@ +// +using System; +using Knot.Modules.Conversations.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Knot.Modules.Conversations.Migrations +{ + [DbContext(typeof(ChatsDbContext))] + [Migration("20260406123444_AddImportJobIdToChat")] + partial class AddImportJobIdToChat + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("chats") + .HasAnnotation("ProductVersion", "10.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ImportJobId") + .HasColumnType("uuid"); + + b.Property("IsImporting") + .HasColumnType("boolean"); + + b.Property("LastMessageSequenceId") + .HasColumnType("bigint"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Chats", "chats"); + }); + + modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Icon") + .HasColumnType("text"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Folders", "chats"); + }); + + modelBuilder.Entity("Knot.Contracts.Conversations.Domain.UserChatSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChatId") + .HasColumnType("uuid"); + + b.Property("FolderIds") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsMuted") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ChatId") + .IsUnique(); + + b.ToTable("UserChatSettings", "chats"); + }); + + modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b => + { + b.OwnsMany("Knot.Contracts.Conversations.Domain.ChatMember", "Members", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b1.Property("ChatId") + .HasColumnType("uuid"); + + b1.Property("IsMuted") + .HasColumnType("boolean"); + + b1.Property("IsPinned") + .HasColumnType("boolean"); + + b1.Property("JoinedAt") + .HasColumnType("timestamp with time zone"); + + b1.Property("LastDeliveredMessageId") + .HasColumnType("uuid"); + + b1.Property("LastReadMessageId") + .HasColumnType("uuid"); + + b1.Property("LastReadSequenceId") + .HasColumnType("bigint"); + + b1.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b1.Property("UserId") + .HasColumnType("uuid"); + + b1.HasKey("Id"); + + b1.HasIndex("ChatId", "UserId") + .IsUnique(); + + b1.ToTable("ChatMembers", "chats"); + + b1.WithOwner() + .HasForeignKey("ChatId"); + }); + + b.Navigation("Members"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Modules/Conversations/Migrations/20260406123444_AddImportJobIdToChat.cs b/backend/src/Modules/Conversations/Migrations/20260406123444_AddImportJobIdToChat.cs new file mode 100644 index 0000000..e3de486 --- /dev/null +++ b/backend/src/Modules/Conversations/Migrations/20260406123444_AddImportJobIdToChat.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Knot.Modules.Conversations.Migrations +{ + /// + public partial class AddImportJobIdToChat : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ImportJobId", + schema: "chats", + table: "Chats", + type: "uuid", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ImportJobId", + schema: "chats", + table: "Chats"); + } + } +} diff --git a/backend/src/Modules/Conversations/Migrations/ChatsDbContextModelSnapshot.cs b/backend/src/Modules/Conversations/Migrations/ChatsDbContextModelSnapshot.cs index b0df6c5..1931b65 100644 --- a/backend/src/Modules/Conversations/Migrations/ChatsDbContextModelSnapshot.cs +++ b/backend/src/Modules/Conversations/Migrations/ChatsDbContextModelSnapshot.cs @@ -23,7 +23,7 @@ namespace Knot.Modules.Conversations.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b => + modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -38,6 +38,12 @@ namespace Knot.Modules.Conversations.Migrations b.Property("Description") .HasColumnType("text"); + b.Property("ImportJobId") + .HasColumnType("uuid"); + + b.Property("IsImporting") + .HasColumnType("boolean"); + b.Property("LastMessageSequenceId") .HasColumnType("bigint"); @@ -53,9 +59,61 @@ namespace Knot.Modules.Conversations.Migrations b.ToTable("Chats", "chats"); }); - modelBuilder.Entity("Knot.Modules.Conversations.Domain.Chat", b => + modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Folder", b => { - b.OwnsMany("Knot.Modules.Conversations.Domain.ChatMember", "Members", b1 => + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Icon") + .HasColumnType("text"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Folders", "chats"); + }); + + modelBuilder.Entity("Knot.Contracts.Conversations.Domain.UserChatSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChatId") + .HasColumnType("uuid"); + + b.Property("FolderIds") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsMuted") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ChatId") + .IsUnique(); + + b.ToTable("UserChatSettings", "chats"); + }); + + modelBuilder.Entity("Knot.Contracts.Conversations.Domain.Chat", b => + { + b.OwnsMany("Knot.Contracts.Conversations.Domain.ChatMember", "Members", b1 => { b1.Property("Id") .ValueGeneratedOnAdd() diff --git a/backend/src/Modules/Conversations/Presentation/Endpoints/ChatsEndpoints.cs b/backend/src/Modules/Conversations/Presentation/Endpoints/ChatsEndpoints.cs index 1d9d1fd..daa0697 100644 --- a/backend/src/Modules/Conversations/Presentation/Endpoints/ChatsEndpoints.cs +++ b/backend/src/Modules/Conversations/Presentation/Endpoints/ChatsEndpoints.cs @@ -9,7 +9,7 @@ using Knot.Modules.Conversations.Application.Chats.Members; using Knot.Modules.Conversations.Application.Chats.TogglePin; using Knot.Modules.Conversations.Application.Chats.Update; using Knot.Modules.Conversations.Application.DTOs; -using Knot.Modules.Conversations.Domain; +using Knot.Contracts.Conversations.Domain; using Knot.Shared.Kernel; using MediatR; using Microsoft.AspNetCore.Builder; diff --git a/backend/src/Modules/Messaging/Infrastructure/Persistence/MessageQueryService.cs b/backend/src/Modules/Messaging/Infrastructure/Persistence/MessageQueryService.cs index 7cc3719..1e67390 100644 --- a/backend/src/Modules/Messaging/Infrastructure/Persistence/MessageQueryService.cs +++ b/backend/src/Modules/Messaging/Infrastructure/Persistence/MessageQueryService.cs @@ -32,6 +32,18 @@ public sealed class MessageQueryService : IMessageQueryService public async Task> GetOrphanedMessagesAsync(HashSet activeChatIds, CancellationToken cancellationToken) { + if (activeChatIds == null || activeChatIds.Count == 0) + { + var allMessages = await _messages.Find(_ => true).ToListAsync(cancellationToken); + return allMessages.Select(m => new MessageInfo( + m.Id, + m.ChatId, + m.State.HasFlag(MessageState.IsDeleted), + m is MediaMessage mm && mm.Media.Any() ? mm.Media.First().Url : null, + m is MediaMessage mm2 && mm2.Media.Any() ? mm2.Media.Select(media => new Contracts.Messaging.Application.Abstractions.MediaInfo(media.Url)).ToList() : null + )).ToList(); + } + var builder = Builders.Filter; var inFilter = builder.In(m => m.ChatId, activeChatIds); var filter = builder.Not(inFilter); diff --git a/backend/src/Modules/Messaging/Infrastructure/Persistence/MessageRepository.cs b/backend/src/Modules/Messaging/Infrastructure/Persistence/MessageRepository.cs index 3a40eea..8baab52 100644 --- a/backend/src/Modules/Messaging/Infrastructure/Persistence/MessageRepository.cs +++ b/backend/src/Modules/Messaging/Infrastructure/Persistence/MessageRepository.cs @@ -62,18 +62,22 @@ public sealed class MessageRepository : IMessageRepository .FirstOrDefaultAsync(cancellationToken); } - public async Task> GetChatMessagesCursorAsync(Guid chatId, DateTime? cursor, int limit, CancellationToken cancellationToken) + public async Task> GetChatMessagesCursorAsync(Guid chatId, DateTime? cursor, long? sequenceId, int limit, CancellationToken cancellationToken) { var builder = Builders.Filter; var filter = builder.Eq(m => m.ChatId, chatId); - if (cursor.HasValue) + if (sequenceId.HasValue) + { + filter &= builder.Lt(m => m.SequenceId, sequenceId.Value); + } + else if (cursor.HasValue) { filter &= builder.Lt(m => m.CreatedAt, cursor.Value); } return await _messages.Find(filter) - .SortByDescending(m => m.CreatedAt) + .SortByDescending(m => m.SequenceId) .Limit(limit) .ToListAsync(cancellationToken); } diff --git a/backend/src/Modules/TelegramImport/Application/Abstractions/ITelegramHtmlParser.cs b/backend/src/Modules/TelegramImport/Application/Abstractions/ITelegramHtmlParser.cs index 633dc59..f8e478b 100644 --- a/backend/src/Modules/TelegramImport/Application/Abstractions/ITelegramHtmlParser.cs +++ b/backend/src/Modules/TelegramImport/Application/Abstractions/ITelegramHtmlParser.cs @@ -10,4 +10,5 @@ public interface ITelegramHtmlParser { Task> ParseMessagesAsync(Stream htmlStream, string baseDirInZip, CancellationToken ct = default); Task> ExtractAllUserNamesAsync(Stream htmlStream, CancellationToken ct = default); + Task ExtractGroupNameAsync(Stream htmlStream, CancellationToken ct = default); } diff --git a/backend/src/Modules/TelegramImport/Application/TelegramImport/AnalyzeImportCommand.cs b/backend/src/Modules/TelegramImport/Application/TelegramImport/AnalyzeImportCommand.cs index da971b1..be99cb9 100644 --- a/backend/src/Modules/TelegramImport/Application/TelegramImport/AnalyzeImportCommand.cs +++ b/backend/src/Modules/TelegramImport/Application/TelegramImport/AnalyzeImportCommand.cs @@ -10,6 +10,7 @@ using AngleSharp.Dom; using AngleSharp.Html.Parser; using Knot.Contracts.Settings.Application.Abstractions; using Knot.Contracts.Settings.Application.DTOs; +using Knot.Modules.TelegramImport.Application.Abstractions; using Knot.Shared.Kernel; using MediatR; @@ -25,18 +26,23 @@ public record ImportConflictDto(string Type, string Message, bool Blocked); public record AnalyzeImportResponseDto( Guid Token, List Names, - List Conflicts); + List Conflicts, + int TotalMessages, + string? GroupName); public record AnalyzeImportCommand(Stream FileStream, string FileName) : ICommand; internal sealed class AnalyzeImportCommandHandler : ICommandHandler { private readonly ISettingsService _settingsService; + private readonly ITelegramHtmlParser _htmlParser; - public AnalyzeImportCommandHandler(ISettingsService settingsService) + public AnalyzeImportCommandHandler(ISettingsService settingsService, ITelegramHtmlParser htmlParser) { _settingsService = settingsService; + _htmlParser = htmlParser; } + public async Task> Handle(AnalyzeImportCommand request, CancellationToken cancellationToken) { if (request.FileStream == null || request.FileStream.Length == 0) @@ -52,68 +58,77 @@ internal sealed class AnalyzeImportCommandHandler : ICommandHandler(); - - using (var archive = ZipFile.OpenRead(tempPath)) - { - var htmlEntries = archive.Entries - .Where(e => e.FullName.EndsWith(".html", StringComparison.OrdinalIgnoreCase) && e.Name.StartsWith("messages", StringComparison.OrdinalIgnoreCase)); - - foreach (var entry in htmlEntries) + await using (var fs = new FileStream(tempPath, FileMode.Create)) { - using var stream = entry.Open(); - var parser = new HtmlParser(); - var doc = parser.ParseDocument(stream); + await request.FileStream.CopyToAsync(fs, cancellationToken); + } - var messageNodes = doc.QuerySelectorAll(".message"); - if (messageNodes == null) - { - continue; - } + var names = new HashSet(); + int totalMessages = 0; + string? groupName = null; - foreach (var node in messageNodes) + using (var archive = ZipFile.OpenRead(tempPath)) + { + var htmlEntries = archive.Entries + .Where(e => e.FullName.EndsWith(".html", StringComparison.OrdinalIgnoreCase) && + (e.Name.StartsWith("messages", StringComparison.OrdinalIgnoreCase) || e.Name == "export_results.html")) + .OrderBy(e => e.FullName.Length) + .ThenBy(e => e.FullName) + .ToList(); + + foreach (var entry in htmlEntries) { - var fromNameNode = node.QuerySelector(".from_name"); - if (fromNameNode != null) + using var stream = entry.Open(); + + if (string.IsNullOrEmpty(groupName)) { - var nameNodeText = (IElement)fromNameNode.Clone(); - var innerSpans = nameNodeText.QuerySelectorAll("span"); - foreach (var span in innerSpans) - { - span.Remove(); - } + groupName = await _htmlParser.ExtractGroupNameAsync(stream, cancellationToken); + // Reset stream position if possible? No, entry.Open() returns a new stream. + // But wait! ExtractGroupNameAsync consumess the stream! + // I'll reopen it for messages if it's the same entry. + } - var name = nameNodeText.TextContent.Trim(); - if (!string.IsNullOrWhiteSpace(name)) - { - names.Add(name); - } + using var stream2 = entry.Open(); + var messages = await _htmlParser.ParseMessagesAsync(stream2, "", cancellationToken); + totalMessages += messages.Count; + + foreach (var m in messages) + { + if (!string.IsNullOrEmpty(m.SenderName)) + names.Add(m.SenderName); } } } + + if (names.Count == 0 && totalMessages == 0) + { + return Result.Failure(new Error("TelegramImport.NoMessagesFound", "В архиве не найдено сообщений Telegram или формат HTML не распознан.")); + } + + // Анализ конфликтов политик + var settings = await _settingsService.GetSettingsAsync(cancellationToken); + var conflicts = new List(); + + if (!settings.Messages.AllowMedia) + conflicts.Add(new ImportConflictDto("Media", "Медиафайлы (фото/видео) отключены на сервере. Они не будут импортированы.", true)); + + if (!settings.Messages.AllowVoiceMessages) + conflicts.Add(new ImportConflictDto("Voice", "Голосовые сообщения запрещены администратором. Будут пропущены.", true)); + + if (!settings.Messages.AllowPolls) + conflicts.Add(new ImportConflictDto("Polls", "Опросы не поддерживаются текущими настройками сервера.", true)); + + TelegramImportState.TempZips[token] = tempPath; + + return Result.Success(new AnalyzeImportResponseDto(token, names.OrderBy(x => x).ToList(), conflicts, totalMessages, groupName)); + } + catch (Exception ex) + { + if (File.Exists(tempPath)) File.Delete(tempPath); + return Result.Failure(new Error("TelegramImport.ProcessError", $"Ошибка при обработке архива: {ex.Message}")); } - - // Анализ конфликтов политик - var settings = await _settingsService.GetSettingsAsync(cancellationToken); - var conflicts = new List(); - - if (!settings.Messages.AllowMedia) - conflicts.Add(new ImportConflictDto("Media", "Медиафайлы (фото/видео) отключены на сервере. Они не будут импортированы.", true)); - - if (!settings.Messages.AllowVoiceMessages) - conflicts.Add(new ImportConflictDto("Voice", "Голосовые сообщения запрещены администратором. Будут пропущены.", true)); - - if (!settings.Messages.AllowPolls) - conflicts.Add(new ImportConflictDto("Polls", "Опросы не поддерживаются текущими настройками сервера.", true)); - - TelegramImportState.TempZips[token] = tempPath; - - return Result.Success(new AnalyzeImportResponseDto(token, names.ToList(), conflicts)); } } diff --git a/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/ExecuteImportRequest.cs b/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/ExecuteImportRequest.cs index 09dd5a1..5704a2a 100644 --- a/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/ExecuteImportRequest.cs +++ b/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/ExecuteImportRequest.cs @@ -6,7 +6,8 @@ namespace Knot.Modules.TelegramImport.Application.TelegramImport.DTOs; public record ExecuteImportRequest( Guid Token, Dictionary Mapping, - string? GroupName + string? GroupName, + int TotalMessages = 0 ); diff --git a/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/TelegramMessage.cs b/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/TelegramMessage.cs index d0098d0..0330cb4 100644 --- a/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/TelegramMessage.cs +++ b/backend/src/Modules/TelegramImport/Application/TelegramImport/DTOs/TelegramMessage.cs @@ -11,7 +11,8 @@ public record TelegramMessage( string? ReplyToId = null, string? ForwardedFrom = null, List? Media = null, - List? Reactions = null + List? Reactions = null, + long OrderIndex = 0 ); public record TelegramMedia(string FilePath, string FileName, string MimeType); diff --git a/backend/src/Modules/TelegramImport/Application/TelegramImport/ExecuteImportCommand.cs b/backend/src/Modules/TelegramImport/Application/TelegramImport/ExecuteImportCommand.cs index ec4b668..c77457a 100644 --- a/backend/src/Modules/TelegramImport/Application/TelegramImport/ExecuteImportCommand.cs +++ b/backend/src/Modules/TelegramImport/Application/TelegramImport/ExecuteImportCommand.cs @@ -1,30 +1,47 @@ using System; using System.Collections.Generic; +using System.IO.Compression; +using System.Linq; using System.Threading; using System.Threading.Tasks; -using Knot.Shared.Kernel; +using Knot.Contracts.Conversations.Application.Abstractions; +using Knot.Contracts.Conversations.Domain; +using Knot.Modules.TelegramImport.Application.Abstractions; using Knot.Modules.TelegramImport.Infrastructure.Background; +using Knot.Shared.Kernel; namespace Knot.Modules.TelegramImport.Application.TelegramImport; -public record ExecuteImportResponseDto(Guid JobId, string Status); +public record ExecuteImportResponseDto(Guid JobId, string Status, Guid ChatId); public record ExecuteImportCommand( Guid CurrentUserId, Guid Token, + string? GroupName, Dictionary Mapping, - string? GroupName + int TotalMessages = 0 ) : ICommand; internal sealed class ExecuteImportCommandHandler : ICommandHandler { private readonly TelegramImportWorker _worker; private readonly IImportJobStore _jobStore; + private readonly IChatRepository _chatRepository; + private readonly IChatsUnitOfWork _unitOfWork; + private readonly ITelegramHtmlParser _htmlParser; - public ExecuteImportCommandHandler(TelegramImportWorker worker, IImportJobStore jobStore) + public ExecuteImportCommandHandler( + TelegramImportWorker worker, + IImportJobStore jobStore, + IChatRepository chatRepository, + IChatsUnitOfWork unitOfWork, + ITelegramHtmlParser htmlParser) { _worker = worker; _jobStore = jobStore; + _chatRepository = chatRepository; + _unitOfWork = unitOfWork; + _htmlParser = htmlParser; } public async Task> Handle(ExecuteImportCommand request, CancellationToken cancellationToken) @@ -34,19 +51,66 @@ internal sealed class ExecuteImportCommandHandler : ICommandHandler(new Error("Import.Expired", "Import session expired or file not found.")); } + var groupName = request.GroupName; + if (string.IsNullOrEmpty(groupName)) + { + try + { + using var archive = ZipFile.OpenRead(tempPath); + var firstHtml = archive.Entries.FirstOrDefault(e => e.FullName.EndsWith(".html", StringComparison.OrdinalIgnoreCase)); + if (firstHtml != null) + { + using var stream = firstHtml.Open(); + groupName = await _htmlParser.ExtractGroupNameAsync(stream, cancellationToken); + } + } + catch { } + } + + // 1. Создаем чат сразу (синхронно), чтобы пользователь его увидел + // 1. Создаем чат сразу (синхронно), чтобы пользователь его увидел + var memberIdList = request.Mapping.Values.Where(v => v != Guid.Empty).Distinct().ToList(); + if (!memberIdList.Contains(request.CurrentUserId)) memberIdList.Add(request.CurrentUserId); var jobId = Guid.NewGuid(); - // Ставим задачу в фоне. Worker сам удалит файл и обновит статус. - // Мы не ждем завершения, а возвращаем JobId мгновенно. - _ = _worker.ProcessImportAsync(request, jobId, tempPath, CancellationToken.None); + // Решаем какой тип чата: если 2 участника или 1 участник (Saved Messages) + bool isPersonal = memberIdList.Count <= 2; + var chatType = isPersonal ? ChatType.Personal : ChatType.Group; + + string finalChatName = groupName ?? "Telegram Import"; + if (isPersonal) + { + // Берем имя собеседника из маппинга + var otherUserId = memberIdList.FirstOrDefault(id => id != request.CurrentUserId); + var otherUserName = request.Mapping.FirstOrDefault(m => m.Value == otherUserId).Key; + if (!string.IsNullOrEmpty(otherUserName)) finalChatName = otherUserName; + } + + // 2. Создание чата (в скрытом состоянии) + var chat = Chat.Create(finalChatName, chatType, isImporting: true, importJobId: jobId); + chat.AddMember(request.CurrentUserId, ChatRole.Owner); + + foreach (var memberId in memberIdList) + { + if (memberId != request.CurrentUserId) + { + chat.AddMember(memberId, ChatRole.Member); + } + } + + _chatRepository.Add(chat); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + // 3. Фоновая обработка сообщений + _ = _worker.ProcessImportAsync(request, jobId, chat.Id, tempPath, CancellationToken.None); _jobStore.AddOrUpdate(new ImportJobInfo { JobId = jobId, Status = ImportJobStatus.Queued, - TotalMessages = 0 + TotalMessages = request.TotalMessages }); - return Result.Success(new ExecuteImportResponseDto(jobId, "Queued")); + return Result.Success(new ExecuteImportResponseDto(jobId, "Processing", chat.Id)); } } diff --git a/backend/src/Modules/TelegramImport/DependencyInjection.cs b/backend/src/Modules/TelegramImport/DependencyInjection.cs index 9b5b893..cab44b9 100644 --- a/backend/src/Modules/TelegramImport/DependencyInjection.cs +++ b/backend/src/Modules/TelegramImport/DependencyInjection.cs @@ -1,3 +1,6 @@ +using Knot.Modules.TelegramImport.Application.Abstractions; +using Knot.Modules.TelegramImport.Infrastructure.Background; +using Knot.Modules.TelegramImport.Infrastructure.Parser; using Microsoft.Extensions.DependencyInjection; namespace Knot.Modules.TelegramImport; @@ -6,6 +9,13 @@ public static class DependencyInjection { public static IServiceCollection AddTelegramImportModule(this IServiceCollection services) { + services.AddSingleton(); + services.AddSingleton(); + + // Register worker as itself and as a hosted service + services.AddSingleton(); + services.AddHostedService(sp => sp.GetRequiredService()); + return services; } } diff --git a/backend/src/Modules/TelegramImport/Infrastructure/Background/TelegramImportWorker.cs b/backend/src/Modules/TelegramImport/Infrastructure/Background/TelegramImportWorker.cs index 58d8eb3..d616491 100644 --- a/backend/src/Modules/TelegramImport/Infrastructure/Background/TelegramImportWorker.cs +++ b/backend/src/Modules/TelegramImport/Infrastructure/Background/TelegramImportWorker.cs @@ -1,7 +1,10 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using Knot.Contracts.Conversations.Application.Abstractions; @@ -10,6 +13,7 @@ using Knot.Contracts.Messaging.Application.Abstractions; using Knot.Contracts.Messaging.Domain; using Knot.Modules.TelegramImport.Application.Abstractions; using Knot.Modules.TelegramImport.Application.TelegramImport; +using Knot.Modules.TelegramImport.Application.TelegramImport.DTOs; using Knot.Modules.TelegramImport.Infrastructure.Background; using Knot.Shared.Kernel.Storage; using Microsoft.Extensions.DependencyInjection; @@ -24,7 +28,10 @@ public class TelegramImportWorker : BackgroundService private readonly ILogger _logger; private readonly IImportJobStore _jobStore; - public TelegramImportWorker(IServiceProvider serviceProvider, ILogger logger, IImportJobStore jobStore) + public TelegramImportWorker( + IServiceProvider serviceProvider, + ILogger logger, + IImportJobStore jobStore) { _serviceProvider = serviceProvider; _logger = logger; @@ -34,64 +41,203 @@ public class TelegramImportWorker : BackgroundService protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Telegram Import Worker started."); - - // В реальном проекте здесь будет чтение из Channels или RabbitMQ - // Для примера оставим заглушку цикла while (!stoppingToken.IsCancellationRequested) { await Task.Delay(5000, stoppingToken); } } - public async Task ProcessImportAsync(ExecuteImportCommand request, Guid jobId, string zipPath, CancellationToken ct) + public async Task ProcessImportAsync(ExecuteImportCommand request, Guid jobId, Guid chatId, string zipPath, CancellationToken ct) { using var scope = _serviceProvider.CreateScope(); var parser = scope.ServiceProvider.GetRequiredService(); var msgRepo = scope.ServiceProvider.GetRequiredService(); + var reactionRepo = scope.ServiceProvider.GetRequiredService(); var chatRepo = scope.ServiceProvider.GetRequiredService(); var uow = scope.ServiceProvider.GetRequiredService(); var fileStorage = scope.ServiceProvider.GetRequiredService(); - var jobInfo = new ImportJobInfo { JobId = jobId, Status = ImportJobStatus.Processing }; + var jobInfo = new ImportJobInfo + { + JobId = jobId, + Status = ImportJobStatus.Processing, + TotalMessages = request.TotalMessages, + ProcessedMessages = 0 + }; _jobStore.AddOrUpdate(jobInfo); try { - using var archive = ZipFile.OpenRead(zipPath); - var entries = archive.Entries.Where(e => e.Name.StartsWith("messages") && e.Name.EndsWith(".html")).ToList(); + var chat = await chatRepo.GetByIdAsync(chatId, ct); + if (chat == null) throw new Exception("Chat not found"); - // 1. Создание чата (уже было в оригинале, но здесь в фоне) - Guid targetChatId = Guid.NewGuid(); // Упростим логику для демонстрации рефакторинга + _logger.LogInformation("Processing messages for ChatId: {ChatId}, JobId: {JobId}", chatId, jobId); - foreach (var entry in entries) + // TRY TO FIND THE BEST ENCODING (UTF8 or CP866) + ZipArchive archive; + try { - using var stream = entry.Open(); - var messages = await parser.ParseMessagesAsync(stream, "", ct); - - foreach (var m in messages) + // Try UTF8 first + archive = ZipFile.OpenRead(zipPath); + var messagesHtml = archive.Entries.FirstOrDefault(e => e.Name.Equals("messages.html", StringComparison.OrdinalIgnoreCase)); + if (messagesHtml == null) { - Guid senderGuid = request.Mapping.TryGetValue(m.SenderName ?? "", out var sid) ? sid : request.CurrentUserId; - - var textMsg = new TextMessage(Guid.NewGuid(), targetChatId, senderGuid, m.Content, null, null, null, m.CreatedAt, true); - msgRepo.Add(textMsg); - - jobInfo.ProcessedMessages++; - _jobStore.AddOrUpdate(jobInfo); + // If not found in root, maybe it's CP866 + archive.Dispose(); + archive = ZipFile.Open(zipPath, ZipArchiveMode.Read, Encoding.GetEncoding(866)); } - await uow.SaveChangesAsync(ct); + } + catch + { + archive = ZipFile.OpenRead(zipPath); } - jobInfo.Status = ImportJobStatus.Completed; + using (archive) + { + var entries = archive.Entries + .Where(e => e.FullName.EndsWith(".html", StringComparison.OrdinalIgnoreCase) && + (e.Name.StartsWith("messages", StringComparison.OrdinalIgnoreCase) || + e.Name.Equals("export_results.html", StringComparison.OrdinalIgnoreCase))) + .OrderBy(e => e.FullName.Length) + .ThenBy(e => e.FullName) + .ToList(); + + var allMessages = new List(); + + foreach (var entry in entries) + { + var entryPath = entry.FullName.Replace("\\", "/"); + var lastSlash = entryPath.LastIndexOf('/'); + var baseDir = lastSlash >= 0 ? entryPath.Substring(0, lastSlash + 1) : ""; + + using var stream = entry.Open(); + var messages = await parser.ParseMessagesAsync(stream, baseDir, ct); + allMessages.AddRange(messages); + } + + var orderedMessages = allMessages + .OrderBy(m => m.CreatedAt) + .ThenBy(m => m.OrderIndex) + .ToList(); + + _logger.LogInformation("Total messages parsed: {Count}", orderedMessages.Count); + + var telegramToKnotIdMap = new Dictionary(); + var processedCount = 0; + + foreach (var m in orderedMessages) + { + Guid senderId = request.Mapping.TryGetValue(m.SenderName ?? "", out var sid) ? sid : request.CurrentUserId; + Guid? replyToId = m.ReplyToId != null && telegramToKnotIdMap.TryGetValue(m.ReplyToId, out var rid) ? rid : null; + + Guid knotMsgId = Guid.NewGuid(); + Message? knotMsg = null; + + if (m.Media != null && m.Media.Any()) + { + var mediaType = m.Media.Any(mi => mi.MimeType.StartsWith("image")) ? MediaType.Image : + m.Media.Any(mi => mi.MimeType.StartsWith("video")) ? MediaType.Video : + m.Media.Any(mi => mi.MimeType.Contains("voice")) ? MediaType.Voice : MediaType.File; + + var mediaMsg = new MediaMessage(knotMsgId, chat.Id, senderId, mediaType, m.Content, replyToId, null, m.CreatedAt, true); + + foreach (var mediaItem in m.Media) + { + var targetPath = mediaItem.FilePath.Replace("\\", "/").TrimStart('/'); + var unescapedPath = Uri.UnescapeDataString(targetPath); + + var zipEntry = archive.Entries.FirstOrDefault(e => { + var entryPath = e.FullName.Replace("\\", "/").TrimStart('/'); + return entryPath.Equals(targetPath, StringComparison.OrdinalIgnoreCase) || + entryPath.Equals(unescapedPath, StringComparison.OrdinalIgnoreCase); + }); + + // IF NOT FOUND, try a desperate match by stripping path and comparing filenames + if (zipEntry == null) + { + var fileName = Path.GetFileName(targetPath); + zipEntry = archive.Entries.FirstOrDefault(e => e.Name.Equals(fileName, StringComparison.OrdinalIgnoreCase)); + } + + if (zipEntry != null) + { + using var mediaStream = zipEntry.Open(); + string itemType = "file"; + string mimeToUpload = mediaItem.MimeType; + + if (mediaItem.MimeType.Equals("image/gif", StringComparison.OrdinalIgnoreCase)) + { + itemType = "gif"; + if (mediaItem.FileName.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase)) mimeToUpload = "video/mp4"; + } + else if (mediaItem.MimeType.StartsWith("image")) itemType = "image"; + else if (mediaItem.MimeType.StartsWith("video")) itemType = "video"; + else if (mediaItem.MimeType.Contains("voice") || mediaItem.MimeType.Contains("audio")) itemType = "audio"; + + var fileId = await fileStorage.UploadFileAsync(mediaStream, mediaItem.FileName, mimeToUpload); + mediaMsg.AddMedia(itemType, $"/api/files/{fileId}", mediaItem.FileName, zipEntry.Length); + } + else + { + _logger.LogWarning("[Import] Media entry NOT FOUND in ZIP: {Path}", targetPath); + } + } + + if (mediaMsg.Media.Any()) knotMsg = mediaMsg; + } + + if (knotMsg == null) + { + knotMsg = new TextMessage(knotMsgId, chat.Id, senderId, m.Content, replyToId, null, null, m.CreatedAt, true); + } + + chat.IncrementSequenceId(); + knotMsg.SetSequenceId(chat.LastMessageSequenceId); + msgRepo.Add(knotMsg); + + if (m.Reactions != null) + { + foreach (var r in m.Reactions) + { + foreach (var name in r.UserNames) + { + var userId = request.Mapping.TryGetValue(name, out var mappedId) && mappedId != Guid.Empty ? mappedId : request.CurrentUserId; + var knotReaction = new MessageReaction(knotMsgId, userId, r.Emoji); + await reactionRepo.AddAsync(knotReaction, ct); + } + } + } + + telegramToKnotIdMap[m.Id] = knotMsgId; + processedCount++; + + if (processedCount % 50 == 0) + { + await uow.SaveChangesAsync(ct); + jobInfo.ProcessedMessages = processedCount; + _jobStore.AddOrUpdate(jobInfo); + } + } + + chat.CompleteImport(); + await uow.SaveChangesAsync(ct); + + _logger.LogInformation("Import Completed. Messages: {Count}", processedCount); + jobInfo.ProcessedMessages = processedCount; + jobInfo.Status = ImportJobStatus.Completed; + } } catch (Exception ex) { + _logger.LogError(ex, "Error during Telegram import process."); jobInfo.Status = ImportJobStatus.Failed; jobInfo.ErrorMessage = ex.Message; } finally { _jobStore.AddOrUpdate(jobInfo); - try { File.Delete(zipPath); } catch { } + try { if (File.Exists(zipPath)) File.Delete(zipPath); } catch { } + TelegramImportState.TempZips.TryRemove(request.Token, out _); } } } diff --git a/backend/src/Modules/TelegramImport/Infrastructure/Parser/TelegramHtmlParser.cs b/backend/src/Modules/TelegramImport/Infrastructure/Parser/TelegramHtmlParser.cs index 8df23eb..9883e5e 100644 --- a/backend/src/Modules/TelegramImport/Infrastructure/Parser/TelegramHtmlParser.cs +++ b/backend/src/Modules/TelegramImport/Infrastructure/Parser/TelegramHtmlParser.cs @@ -8,16 +8,19 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; namespace Knot.Modules.TelegramImport.Infrastructure.Parser; public sealed class TelegramHtmlParser : ITelegramHtmlParser { private readonly HtmlParser _parser; + private readonly ILogger _logger; - public TelegramHtmlParser() + public TelegramHtmlParser(ILogger logger) { _parser = new HtmlParser(); + _logger = logger; } public async Task> ExtractAllUserNamesAsync(Stream htmlStream, CancellationToken ct = default) @@ -39,42 +42,207 @@ public sealed class TelegramHtmlParser : ITelegramHtmlParser { var doc = await _parser.ParseDocumentAsync(htmlStream, ct); var messages = new List(); + string? lastSenderName = null; var messageNodes = doc.QuerySelectorAll(".message"); foreach (var node in messageNodes) { - var msg = ParseSingleMessage(node, baseDirInZip); + // STOP ALL MERGING OF SEPARATE NODES. + // Telegram HTML nodes represent visual bubbles. If we merge them, we break the original hierarchy + // and often misplace captions or hide media. + var msg = ParseSingleMessage(node, baseDirInZip, ref lastSenderName); if (msg != null) messages.Add(msg); } return messages; } - private TelegramMessage? ParseSingleMessage(IElement node, string baseDir) + public async Task ExtractGroupNameAsync(Stream htmlStream, CancellationToken ct = default) { + var doc = await _parser.ParseDocumentAsync(htmlStream, ct); + var header = doc.QuerySelector(".page_header .text"); + var name = header?.TextContent?.Trim(); + + if (string.IsNullOrEmpty(name)) + { + name = doc.Title?.Replace("Chat Export with ", "")?.Trim(); + } + + return name; + } + + private TelegramMessage? ParseSingleMessage(IElement node, string baseDir, ref string? lastSenderName) + { + if (node.ClassList.Contains("service")) return null; + try { - var id = node.GetAttribute("id") ?? Guid.NewGuid().ToString(); - var fromNameNode = node.QuerySelector(".from_name"); - var senderName = fromNameNode != null ? CleanName(fromNameNode) : null; - + var idAttr = node.GetAttribute("id") ?? Guid.NewGuid().ToString(); + long numericId = 0; + if (idAttr.StartsWith("message")) long.TryParse(idAttr.Replace("message", ""), out numericId); + + var fromNameNode = node.QuerySelector(".body > .from_name"); + // Ignore forwarded from_name as message sender + if (fromNameNode != null && fromNameNode.Closest(".forwarded") != null) fromNameNode = null; + + var senderName = fromNameNode != null ? CleanName(fromNameNode) : lastSenderName; + // If it's a new bubble group (no "joined"), update lastSenderName + if (senderName != null && !node.ClassList.Contains("joined")) lastSenderName = senderName; + var textNode = node.QuerySelector(".text"); var content = textNode?.TextContent?.Trim() ?? ""; - // Дата (парсинг из title) var dateNode = node.QuerySelector(".date[title]") ?? node.QuerySelector("[title]"); var dateStr = dateNode?.GetAttribute("title") ?? ""; - DateTime.TryParse(dateStr.Replace("UTC", "").Trim(), out var createdAt); + DateTime createdAt = DateTime.UtcNow; - return new TelegramMessage(id, senderName, createdAt, content); + if (!string.IsNullOrEmpty(dateStr)) + { + var cleanDateStr = dateStr.Trim(); + var formats = new[] { "dd.MM.yyyy HH:mm:ss 'UTC'zzz", "dd.MM.yyyy HH:mm:ss", "d.MM.yyyy HH:mm:ss", "dd.M.yyyy HH:mm:ss", "d.M.yyyy HH:mm:ss" }; + + try + { + if (DateTimeOffset.TryParseExact(cleanDateStr, formats, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var dto)) + { + createdAt = dto.UtcDateTime; + } + else if (DateTimeOffset.TryParse(cleanDateStr.Replace("UTC", ""), System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var dto2)) + { + createdAt = dto2.UtcDateTime; + } + } + catch { } + } + + var mediaList = ExtractMedia(node, baseDir); + + // Replies + string? replyToId = null; + var replyNode = node.QuerySelector(".reply_to a[href^=\"#go_to_message\"]"); + if (replyNode != null) + { + var href = replyNode.GetAttribute("href"); + replyToId = href?.Replace("#go_to_message", "message"); + } + + // Forwards + string? forwardedFrom = null; + var forwardNode = node.QuerySelector(".forwarded .from_name"); + if (forwardNode != null) forwardedFrom = CleanName(forwardNode); + + // Reactions + var reactions = new List(); + var reactionNodes = node.QuerySelectorAll(".reactions .reaction"); + foreach (var r in reactionNodes) + { + var emoji = r.QuerySelector(".emoji")?.TextContent?.Trim() ?? ""; + var names = r.QuerySelectorAll(".userpics [title]") + .Select(x => x.GetAttribute("title") ?? "") + .Where(x => !string.IsNullOrEmpty(x)) + .ToList(); + if (!string.IsNullOrEmpty(emoji)) reactions.Add(new TelegramReaction(emoji, names)); + } + + return new TelegramMessage(idAttr, senderName, createdAt, content, replyToId, forwardedFrom, mediaList.Any() ? mediaList : null, reactions.Any() ? reactions : null, numericId); } - catch { return null; } + catch (Exception ex) + { + _logger.LogError(ex, "Error parsing single Telegram message"); + return null; + } + } + + private List ExtractMedia(IElement node, string baseDir) + { + var mediaList = new List(); + + // Look for links that represent media containers + var allLinks = node.QuerySelectorAll("a[href]").ToList(); + + foreach (var link in allLinks) + { + var href = NormalizeHref(link.GetAttribute("href")); + if (href == null || !href.Contains("/")) continue; + + // Skip internal pages + if (href.EndsWith(".html", StringComparison.OrdinalIgnoreCase)) continue; + + var fullPath = BuildPath(baseDir, href); + if (mediaList.Any(m => m.FilePath == fullPath)) continue; + + var fileName = Path.GetFileName(href); + var mimeType = GetMimeType(href); + + // ANIMATION DETECTION: + // 1. Check classes (standard TG export) + bool hasGifClass = link.ClassList.Contains("animated_wrap") || link.ClassList.Contains("media_video"); + + // 2. Check full text of the link container for "Animation" string + // This is the most reliable way to find TG "GIFs" which are actually MP4s + bool hasAnimationText = link.TextContent.Contains("Animation", StringComparison.OrdinalIgnoreCase); + + if (hasGifClass || hasAnimationText) + { + mimeType = "image/gif"; + } + + if (link.ClassList.Contains("media_voice_message") || href.Contains("voice_messages")) + { + mimeType = "audio/ogg"; + } + + mediaList.Add(new TelegramMedia(fullPath, fileName, mimeType)); + } + + // Alternative check for voice messages in audio tags + foreach (var audioEl in node.QuerySelectorAll("audio[src]")) + { + var href = NormalizeHref(audioEl.GetAttribute("src")); + if (href == null) continue; + var fullPath = BuildPath(baseDir, href); + if (!mediaList.Any(m => m.FilePath == fullPath)) + mediaList.Add(new TelegramMedia(fullPath, Path.GetFileName(href), "audio/ogg")); + } + + return mediaList; + } + + private static string? NormalizeHref(string? href) + { + if (string.IsNullOrWhiteSpace(href)) return null; + if (href.Contains('#')) href = href[..href.IndexOf('#')]; + if (href.Contains('?')) href = href[..href.IndexOf('?')]; + return string.IsNullOrWhiteSpace(href) ? null : href.Replace("\\", "/"); + } + + private static string BuildPath(string baseDir, string href) + { + var normalized = href.Replace("\\", "/"); + return normalized.StartsWith(baseDir, StringComparison.OrdinalIgnoreCase) + ? normalized + : (baseDir.TrimEnd('/') + "/" + normalized.TrimStart('/')).Replace("//", "/"); } private string CleanName(IElement node) { var clone = (IElement)node.Clone(); - foreach (var span in clone.QuerySelectorAll("span")) span.Remove(); + foreach (var span in clone.QuerySelectorAll("span, a, div")) span.Remove(); return clone.TextContent.Trim(); } + + private string GetMimeType(string path) + { + var ext = Path.GetExtension(path).ToLowerInvariant(); + return ext switch + { + ".jpg" or ".jpeg" => "image/jpeg", + ".png" => "image/png", + ".webp" => "image/webp", + ".mp4" => "video/mp4", + ".mov" => "video/quicktime", + ".webm" => "video/webm", + _ => "application/octet-stream" + }; + } } diff --git a/backend/src/Modules/TelegramImport/Presentation/Endpoints/TelegramImportEndpoints.cs b/backend/src/Modules/TelegramImport/Presentation/Endpoints/TelegramImportEndpoints.cs index fc47191..b304c5d 100644 --- a/backend/src/Modules/TelegramImport/Presentation/Endpoints/TelegramImportEndpoints.cs +++ b/backend/src/Modules/TelegramImport/Presentation/Endpoints/TelegramImportEndpoints.cs @@ -1,5 +1,6 @@ using Knot.Modules.TelegramImport.Application.TelegramImport; using Knot.Modules.TelegramImport.Application.TelegramImport.DTOs; +using Knot.Modules.TelegramImport.Infrastructure.Background; using Knot.Shared.Kernel; using MediatR; using Microsoft.AspNetCore.Builder; @@ -25,14 +26,23 @@ public static class TelegramImportEndpoints using var stream = file.OpenReadStream(); var result = await sender.Send(new AnalyzeImportCommand(stream, file.FileName), ct); return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error.Description ?? result.Error.Code); - }).DisableAntiforgery(); + }).DisableAntiforgery().WithMetadata(new Microsoft.AspNetCore.Mvc.RequestSizeLimitAttribute(1000000000)); group.MapPost("execute", async ([FromBody] ExecuteImportRequest req, ISender sender, IUserContext userContext, CancellationToken ct) => { - var command = new ExecuteImportCommand(userContext.UserId, req.Token, req.Mapping, req.GroupName); + var command = new ExecuteImportCommand(userContext.UserId, req.Token, req.GroupName, req.Mapping, req.TotalMessages); var result = await sender.Send(command, ct); return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Error.Description ?? result.Error.Code); }); + + group.MapGet("status/{jobId:guid}", (Guid jobId, IImportJobStore jobStore) => + { + if (jobStore.TryGet(jobId, out var info)) + { + return Results.Ok(info); + } + return Results.NotFound(); + }); } } diff --git a/client-web/src/core/domain/types.ts b/client-web/src/core/domain/types.ts index 899db30..a2e8df5 100644 --- a/client-web/src/core/domain/types.ts +++ b/client-web/src/core/domain/types.ts @@ -124,6 +124,8 @@ export interface Chat { members: ChatMember[]; messages: Message[]; unreadCount: number; + isImporting?: boolean; + importJobId?: string | null; pinnedMessages?: Array<{ id: string; message: Message; diff --git a/client-web/src/core/infrastructure/i18n.ts b/client-web/src/core/infrastructure/i18n.ts index 8773627..0c4a259 100644 --- a/client-web/src/core/infrastructure/i18n.ts +++ b/client-web/src/core/infrastructure/i18n.ts @@ -44,6 +44,21 @@ const translations = { storageAndData: 'Хранилище и данные', importTelegram: 'Импорт истории Telegram', importTelegramDesc: 'Перенести сообщения и медиа из Telegram', + importSuccess: 'Импорт завершен', + importStarted: 'Импорт запущен в фоновом режиме', + importSelectArchive: 'Загрузите архив', + importSelectArchiveDesc: 'Экспортируйте чат из Telegram (HTML формат, вкл. медиа) и загрузите полученный ZIP архив.', + importLimit: 'Лимит: до 1 ГБ', + importSelectFile: 'Выбрать ZIP-архив', + importParticipants: 'Соответствие участников', + importParticipantsDesc: 'Назначьте участников из архива пользователям в Knot.', + importInArchive: 'В архиве', + importSelectContact: '-- Выберите контакт --', + importGroupName: 'Название чата', + importGroupNameHint: 'Например, Моя группа', + importStart: 'Запустить импорт', + importRestart: 'Начать заново', + importCompletedDesc: 'Мы успешно создали чат и начали перенос сообщений в фоновом режиме.', // Chat chat: 'Чат', group: 'Группа', @@ -413,6 +428,21 @@ const translations = { storageAndData: 'Storage and Data', importTelegram: 'Import Telegram History', importTelegramDesc: 'Move messages and media from Telegram', + importSuccess: 'Import completed', + importStarted: 'Import started in background', + importSelectArchive: 'Upload archive', + importSelectArchiveDesc: 'Export chat from Telegram (HTML format, incl. media) and upload the resulting ZIP archive.', + importLimit: 'Limit: up to 1 GB', + importSelectFile: 'Select ZIP archive', + importParticipants: 'Participant mapping', + importParticipantsDesc: 'Assign participants from the archive to Knot users.', + importInArchive: 'In archive', + importSelectContact: '-- Select contact --', + importGroupName: 'Chat name', + importGroupNameHint: 'e.g., My Group', + importStart: 'Start import', + importRestart: 'Start over', + importCompletedDesc: 'We successfully created the chat and started importing messages in the background.', searchChats: 'Search chats...', chat: 'Chat', group: 'Group', diff --git a/client-web/src/core/presentation/components/ui/Avatar.tsx b/client-web/src/core/presentation/components/ui/Avatar.tsx index 37e0d2f..8738ce3 100644 --- a/client-web/src/core/presentation/components/ui/Avatar.tsx +++ b/client-web/src/core/presentation/components/ui/Avatar.tsx @@ -46,7 +46,7 @@ function AvatarInner({ src, name, size = 'md', className = '', online }: AvatarP const gradientClass = generateAvatarColor(name || ''); return ( -
+
{src ? ( ) : (
{initials}
diff --git a/client-web/src/core/presentation/components/ui/ConfirmModal.tsx b/client-web/src/core/presentation/components/ui/ConfirmModal.tsx index 67cffc5..660927d 100644 --- a/client-web/src/core/presentation/components/ui/ConfirmModal.tsx +++ b/client-web/src/core/presentation/components/ui/ConfirmModal.tsx @@ -1,5 +1,6 @@ import { motion, AnimatePresence } from 'framer-motion'; import { AlertTriangle } from 'lucide-react'; +import { createPortal } from 'react-dom'; import { useLang } from '../../../infrastructure/i18n'; interface ConfirmModalProps { @@ -25,49 +26,50 @@ export default function ConfirmModal({ }: ConfirmModalProps) { const { t } = useLang(); - return ( + const content = ( {open && ( { if (e.target === e.currentTarget) onCancel(); }} > -
-
- +
+
+
{title && ( -

{title}

+

{title}

)} -

{message}

+

+ {message} +

-
+ +
-
- )} - {pinnedMsg && ( - - )} - {/* Сообщения */} -
0 ? 'invisible' : ''}`} - > - {isLoadingMessages && chatMessages.length === 0 ? ( -
-
-
- ) : chatMessages.length === 0 ? ( -
-

{t('noMessages')}

-
- ) : ( -
- {isLoadingMessages && ( -
-
+
+
+ + {importStatus?.status === 'Processing' ? 'Обработка' : + importStatus?.status === 'Queued' ? 'В очереди' : 'Загрузка'} + + + {importStatus?.processed || 0} / {importStatus?.total || 0} + +
+ +
+ +
+ +

+ Чат станет доступен автоматически +

+
+ +
+ ) : ( + <> + {chat?.type === 'group' && config?.webRtc?.enabled && activeGroupCallParticipants.length > 0 && ( + + )} + + {pinnedMsg && ( + + )} + +
0 ? 'invisible' : ''}`} + > + {isLoadingMessages && chatMessages.length === 0 ? ( +
+
+
+ ) : chatMessages.length === 0 ? ( +
+
+ +

{t('noMessages')}

+
+
+ ) : ( +
+ {isLoadingMessages && ( +
+
+
+ )} + {chatMessages.map((msg, i) => { + const prevMsg = i > 0 ? chatMessages[i - 1] : null; + const showAvatar = !prevMsg || prevMsg.senderId !== msg.senderId; + const showDate = + !prevMsg || + new Date(msg.createdAt).toDateString() !== new Date(prevMsg.createdAt).toDateString(); + + const isFirstUnread = firstUnreadId === msg.id; + + return ( +
r.userId === user?.id) ? 'unread-detector' : ''}`} + > + {isFirstUnread && ( +
+
+ + {t('unreadMessages')} + +
+
+ )} + {showDate && ( +
+ + {new Date(msg.createdAt).toLocaleDateString(lang === 'ru' ? 'ru' : 'en', { + day: 'numeric', + month: 'long', + })} + +
+ )} + setProfileUserId(userId)} + selectionMode={selectionMode} + isSelected={selectedMessages.has(msg.id)} + onToggleSelect={handleToggleSelect} + onStartSelectionMode={handleStartSelection} + onForward={(id) => { + setSelectedMessages(new Set([id])); + setShowForwardModal(true); + }} + /> +
+ ); + })} +
)} - {chatMessages.map((msg, i) => { - const prevMsg = i > 0 ? chatMessages[i - 1] : null; - const showAvatar = !prevMsg || prevMsg.senderId !== msg.senderId; - const showDate = - !prevMsg || - new Date(msg.createdAt).toDateString() !== new Date(prevMsg.createdAt).toDateString(); - - const isFirstUnread = firstUnreadId === msg.id; - - return ( -
r.userId === user?.id) ? 'unread-detector' : ''}`} - > - {isFirstUnread && ( -
-
- - {t('unreadMessages')} - -
-
- )} - {showDate && ( -
- - {new Date(msg.createdAt).toLocaleDateString(lang === 'ru' ? 'ru-RU' : 'en-US', { - day: 'numeric', - month: 'long', - })} - -
- )} - setProfileUserId(userId)} - selectionMode={selectionMode} - isSelected={selectedMessages.has(msg.id)} - onToggleSelect={handleToggleSelect} - onStartSelectionMode={handleStartSelection} - onForward={(id) => { - setSelectedMessages(new Set([id])); - setShowForwardModal(true); - }} - /> -
- ); - })} -
{/* Empty spacer for the bottom scroll boundary */}
- )} -
- {/* Кнопка прокрутки вниз */} - - {showScrollDown && ( - { - scrollToBottom(true); - if (activeChat && unreadCount > 0) { - useChatStore.getState().markAllAsRead(activeChat); - } - }} - className="absolute bottom-24 right-8 w-14 h-14 rounded-2xl bg-gradient-to-br from-primary to-primary-container text-on-primary-container shadow-[0_8px_30px_rgba(48,150,229,0.3)] flex items-center justify-center transition-all z-10 border border-white/10 backdrop-blur-md" - > - arrow_downward - {unreadCount > 0 && ( - + {showScrollDown && ( + scrollToBottom(true)} + className="absolute bottom-24 right-8 w-14 h-14 rounded-2xl bg-primary text-on-primary shadow-2xl flex items-center justify-center z-20 hover:shadow-primary/20 transition-all" > - {unreadCount > 99 ? '99+' : unreadCount} - + + {unreadCount > 0 && ( + + {unreadCount > 99 ? '99+' : unreadCount} + + )} + )} - - )} - + + +
+ +
+ + )} - {/* Typing индикатор */} {typingInChat.length > 0 && (
)} - {/* Ввод сообщения */} - {activeChat && } - {(() => { const handleJumpToMessage = async (msgId: string, cleanup?: () => void, targetCreatedAt?: string) => { cleanup?.(); @@ -1173,29 +1216,23 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal if (!activeChat) return; const NotificationStore = await import('../../../../core/application/stores/notificationStore'); - NotificationStore.useNotificationStore.getState().addNotification('info', t('searchingHistory' as any) || 'Searching message in history...'); + NotificationStore.useNotificationStore.getState().addNotification('info', (t as any)('searchingHistory') || 'Searching message in history...'); const chatStore = useChatStore.getState(); let found = false; const targetDate = targetCreatedAt ? new Date(targetCreatedAt).getTime() : 0; - // Smart timeline-based search for (let i = 0; i < 100; i++) { const chatMessages = chatStore.messages[activeChat] || []; const oldestLoaded = chatMessages.length > 0 ? new Date(chatMessages[0].createdAt).getTime() : Date.now(); - // If target is newer than oldest loaded, and not found, maybe it's in a gap or we need to keep loading? - // Actually target is almost always older if not found. - // If we don't have targetCreatedAt, we guess (up to 100 attempts) if (targetDate && targetDate > oldestLoaded && chatMessages.some(m => m.id === msgId)) { - // Should have been found by tryScroll, but lets try one last time if (tryScroll()) { found = true; break; } } if (chatStore.hasMoreMessages[activeChat] === false && oldestLoaded <= targetDate) break; await chatStore.loadMessages(activeChat, false, true); - // Give React 150ms to render the new messages await new Promise(resolve => setTimeout(resolve, 150)); if (tryScroll()) { @@ -1203,21 +1240,18 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal break; } - // Stop if we have gone way past the target date if (targetDate && oldestLoaded < (targetDate - 1000 * 60 * 60)) { - // We are 1 hour before the message and still haven't found it? might be deleted if (i > 10) break; } } if (!found) { - NotificationStore.useNotificationStore.getState().addNotification('warning', t('messageNotFound' as any) || 'Message not found'); + NotificationStore.useNotificationStore.getState().addNotification('warning', (t as any)('messageNotFound') || 'Message not found'); } }; return ( <> - {/* Профиль пользователя */} {profileUserId && ( - {/* Настройки группы */} {showGroupSettings && chat && chat.type === 'group' && (
-
-
+
+
-