diff --git a/README.md b/README.md
index 168858e..4d3bc3a 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,41 @@
-# buhapp-telegram
+# BuhApp Telegram WebApp
-Telegram bot + Mini App (WebApp)
\ No newline at end of file
+Mini App (WebApp) для Telegram, открывается по кнопке в боте @BuhAppBot.
+
+## Запуск
+
+Статика (HTML/JS) раздаётся с `https://app.buhapp.mygoodservice.ru/`.
+
+## Структура
+
+```
+buhapp-telegram/
+├── public/
+│ ├── index.html — главная страница (карта/чаты/профиль/настройки)
+│ ├── login.html — онбординг (если запустили без Telegram)
+│ ├── css/
+│ │ └── app.css — стили
+│ └── js/
+│ ├── api.js — HTTP клиент + TG SDK
+│ ├── tg.js — обёртки Telegram WebApp SDK
+│ ├── app.js — главный контроллер
+│ └── views/
+│ ├── map.js
+│ ├── chats.js
+│ ├── chat.js
+│ ├── profile.js
+│ ├── review.js
+│ └── settings.js
+└── README.md
+```
+
+## Telegram Bot
+
+- Создай бота через [@BotFather](https://t.me/BotFather) → `/newbot`
+- Установи WebApp URL: `/setdomain` → `app.buhapp.mygoodservice.ru`
+- Скопируй токен → `TELEGRAM_BOT_TOKEN` в `.env` бэкенда
+
+## Деплой
+
+Все файлы из `public/` монтируются в nginx (location /).
+Бэкенд читает `initData` от Telegram и авторизует через `POST /api/v1/auth/telegram`.
\ No newline at end of file
diff --git a/public/css/app.css b/public/css/app.css
new file mode 100644
index 0000000..768853a
--- /dev/null
+++ b/public/css/app.css
@@ -0,0 +1,238 @@
+:root {
+ --primary: #FF6B35;
+ --primary-dark: #E64A19;
+ --secondary: #1E88E5;
+ --bg: #FFFFFF;
+ --bg-elev: #F5F5F7;
+ --bg-dark: #1C1C1E;
+ --text: #111114;
+ --text-dim: #6B6B73;
+ --text-inverse: #FFFFFF;
+ --border: #E0E0E5;
+ --danger: #E53935;
+ --success: #4CAF50;
+ --warning: #FFB300;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --bg: #1C1C1E;
+ --bg-elev: #2C2C2E;
+ --text: #FFFFFF;
+ --text-dim: #98989D;
+ --border: #38383A;
+ }
+}
+
+* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
+html, body { height: 100%; overflow: hidden; }
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Display', Roboto, sans-serif;
+ font-size: 15px;
+ color: var(--text);
+ background: var(--bg);
+ padding-top: env(safe-area-inset-top);
+ padding-bottom: env(safe-area-inset-bottom);
+ -webkit-user-select: none;
+ user-select: none;
+}
+
+#app {
+ height: calc(100vh - 60px - env(safe-area-inset-top) - env(safe-area-inset-bottom));
+ overflow-y: auto;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
+}
+
+#tabbar {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: calc(60px + env(safe-area-inset-bottom));
+ padding-bottom: env(safe-area-inset-bottom);
+ background: var(--bg-elev);
+ border-top: 1px solid var(--border);
+ display: flex;
+ z-index: 100;
+}
+.tab {
+ flex: 1;
+ background: none;
+ border: none;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 2px;
+ color: var(--text-dim);
+ font-size: 11px;
+ cursor: pointer;
+ position: relative;
+}
+.tab svg { fill: currentColor; }
+.tab.active { color: var(--primary); }
+.tab .badge {
+ position: absolute;
+ top: 4px;
+ right: calc(50% - 18px);
+ background: var(--primary);
+ color: var(--text-inverse);
+ font-size: 10px;
+ font-weight: 700;
+ border-radius: 999px;
+ min-width: 16px;
+ height: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 4px;
+}
+
+.view { padding: 16px; }
+.view h1 { font-size: 28px; font-weight: 700; margin-bottom: 12px; }
+.view h2 { font-size: 18px; font-weight: 600; margin: 12px 0 8px; }
+.view .sub { color: var(--text-dim); margin-bottom: 16px; }
+
+.card {
+ background: var(--bg-elev);
+ border-radius: 12px;
+ padding: 16px;
+ margin-bottom: 12px;
+}
+.card.clickable { cursor: pointer; }
+
+.btn {
+ display: block;
+ width: 100%;
+ background: var(--primary);
+ color: var(--text-inverse);
+ border: none;
+ border-radius: 10px;
+ padding: 12px 16px;
+ font-size: 16px;
+ font-weight: 600;
+ margin-top: 8px;
+ cursor: pointer;
+}
+.btn.ghost {
+ background: transparent;
+ color: var(--text);
+ border: 1px solid var(--border);
+}
+.btn.danger { background: var(--danger); }
+.btn:disabled { opacity: 0.4; }
+
+.input {
+ width: 100%;
+ background: var(--bg-elev);
+ color: var(--text);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 12px;
+ font-size: 15px;
+ margin-bottom: 12px;
+ font-family: inherit;
+}
+
+label { font-size: 13px; color: var(--text-dim); margin-bottom: 4px; display: block; }
+
+.chip {
+ display: inline-block;
+ padding: 6px 12px;
+ border-radius: 999px;
+ background: var(--bg-elev);
+ border: 1px solid var(--border);
+ margin: 0 6px 6px 0;
+ font-size: 13px;
+ cursor: pointer;
+}
+.chip.active { background: var(--primary); color: var(--text-inverse); border-color: var(--primary); }
+
+.list-row {
+ display: flex;
+ align-items: center;
+ padding: 12px 0;
+ border-bottom: 1px solid var(--border);
+ cursor: pointer;
+}
+.avatar {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ background: var(--primary);
+ color: var(--text-inverse);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 700;
+ margin-right: 12px;
+ flex-shrink: 0;
+ overflow: hidden;
+ background-size: cover;
+}
+.list-row .body { flex: 1; min-width: 0; }
+.list-row .name { font-weight: 600; }
+.list-row .last { color: var(--text-dim); font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+
+.bubble {
+ max-width: 75%;
+ padding: 8px 12px;
+ border-radius: 16px;
+ margin-bottom: 6px;
+ word-wrap: break-word;
+}
+.bubble.mine { background: var(--primary); color: var(--text-inverse); align-self: flex-end; border-bottom-right-radius: 4px; }
+.bubble.theirs { background: var(--bg-elev); color: var(--text); align-self: flex-start; border-bottom-left-radius: 4px; }
+.bubble .meta { font-size: 11px; opacity: 0.7; margin-top: 2px; display: flex; gap: 6px; }
+.bubble .edited { font-style: italic; }
+
+.chat-input {
+ display: flex;
+ padding: 8px;
+ background: var(--bg);
+ border-top: 1px solid var(--border);
+ position: sticky;
+ bottom: 0;
+}
+.chat-input input { flex: 1; margin: 0 8px 0 0; }
+.chat-input button { width: 40px; padding: 0; margin: 0; }
+
+#map { height: calc(100vh - 60px - 32px); width: 100%; }
+
+.stars { display: flex; gap: 4px; font-size: 32px; }
+.stars span { cursor: pointer; color: var(--border); }
+.stars span.on { color: var(--warning); }
+
+.row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
+
+#consent-overlay {
+ position: fixed; inset: 0;
+ background: var(--bg);
+ z-index: 200;
+ padding: 20px;
+ overflow-y: auto;
+}
+
+.empty { color: var(--text-dim); text-align: center; padding: 32px 16px; }
+
+.toast {
+ position: fixed;
+ top: 16px; left: 16px; right: 16px;
+ background: var(--bg-dark);
+ color: var(--text-inverse);
+ padding: 12px;
+ border-radius: 10px;
+ z-index: 1000;
+ text-align: center;
+ font-size: 14px;
+}
+
+.loader { text-align: center; padding: 32px; color: var(--text-dim); }
+
+.consent-line { display: flex; align-items: center; padding: 8px 0; gap: 8px; }
+.consent-line input[type="checkbox"] { width: 20px; height: 20px; }
+
+.rating-big { font-size: 36px; font-weight: 700; }
+
+.privacy-note { font-size: 12px; color: var(--text-dim); margin-top: 4px; }
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..a14d766
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+ BuhApp
+
+
+
+
+
+
+
+
+
+
+ Карта
+
+
+
+ Чаты
+
+
+
+
+ Профиль
+
+
+
+ Настройки
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/js/api.js b/public/js/api.js
new file mode 100644
index 0000000..92aa11b
--- /dev/null
+++ b/public/js/api.js
@@ -0,0 +1,151 @@
+// API клиент
+const API_BASE = window.API_BASE || 'https://api.buhapp.mygoodservice.ru';
+
+const Store = {
+ access: null,
+ refresh: null,
+ user: null,
+
+ save(tokens) {
+ this.access = tokens.access;
+ this.refresh = tokens.refresh;
+ try { localStorage.setItem('buhapp', JSON.stringify({ access, refresh, user: this.user })); } catch {}
+ },
+
+ load() {
+ try {
+ const s = JSON.parse(localStorage.getItem('buhapp') || 'null');
+ if (s) { this.access = s.access; this.refresh = s.refresh; this.user = s.user; }
+ } catch {}
+ return this.access;
+ },
+
+ clear() {
+ this.access = null;
+ this.refresh = null;
+ this.user = null;
+ try { localStorage.removeItem('buhapp'); } catch {}
+ },
+};
+
+async function api(path, opts = {}) {
+ opts.headers = opts.headers || {};
+ if (Store.access && !opts.headers.Authorization) {
+ opts.headers.Authorization = `Bearer ${Store.access}`;
+ }
+ if (opts.body && typeof opts.body === 'object' && !(opts.body instanceof FormData)) {
+ opts.headers['Content-Type'] = 'application/json';
+ opts.body = JSON.stringify(opts.body);
+ }
+ let r = await fetch(API_BASE + path, opts);
+ if (r.status === 401 && Store.refresh) {
+ // refresh
+ const r2 = await fetch(API_BASE + '/api/v1/auth/refresh', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ refresh: Store.refresh }),
+ });
+ if (r2.ok) {
+ const tokens = await r2.json();
+ Store.save(tokens);
+ opts.headers.Authorization = `Bearer ${tokens.access}`;
+ r = await fetch(API_BASE + path, opts);
+ } else {
+ Store.clear();
+ }
+ }
+ const text = await r.text();
+ let data = null;
+ try { data = text ? JSON.parse(text) : null; } catch { data = text; }
+ if (!r.ok) {
+ const msg = (data && data.error) || `HTTP ${r.status}`;
+ const err = new Error(msg);
+ err.status = r.status;
+ err.data = data;
+ throw err;
+ }
+ return data;
+}
+
+// === Auth ===
+
+const Auth = {
+ async loginWithTelegram(initData) {
+ const r = await api('/api/v1/auth/telegram', {
+ method: 'POST',
+ body: { init_data: initData },
+ });
+ Store.save(r.tokens);
+ Store.user = r.user;
+ return r;
+ },
+
+ async registerEmail(data) {
+ const r = await api('/api/v1/auth/register', { method: 'POST', body: data });
+ Store.save(r.tokens);
+ Store.user = r.user;
+ return r;
+ },
+
+ async loginEmail(email, password) {
+ const r = await api('/api/v1/auth/login', { method: 'POST', body: { email, password } });
+ Store.save(r.tokens);
+ Store.user = r.user;
+ return r;
+ },
+
+ async me() {
+ const r = await api('/api/v1/me');
+ Store.user = r.user;
+ return r;
+ },
+
+ async updateMe(p) {
+ return api('/api/v1/me', { method: 'PUT', body: p });
+ },
+
+ async getPrefs() { return api('/api/v1/me/prefs'); },
+ async updatePrefs(p) { return api('/api/v1/me/prefs', { method: 'PUT', body: p }); },
+ async updateLocation(lat, lng, visible) {
+ return api('/api/v1/me/location', { method: 'PUT', body: { lat, lng, visible } });
+ },
+ async setVisibility(v) { return api('/api/v1/me/visibility', { method: 'PUT', body: { visible: v } }); },
+ async searchNearby(lat, lng, radiusKm) {
+ return api('/api/v1/search/nearby?lat=' + lat + '&lng=' + lng + '&radius=' + radiusKm);
+ },
+ async getUser(id) { return api('/api/v1/users/' + id); },
+};
+
+// === Chat ===
+
+const ChatApi = {
+ async ensureChat(otherId) { return api('/api/v1/chats', { method: 'POST', body: { other_id: otherId } }); },
+ async listChats() { return api('/api/v1/chats'); },
+ async listMessages(chatId) { return api('/api/v1/chats/' + chatId + '/messages'); },
+ async sendMessage(chatId, body, photoUrl) {
+ return api('/api/v1/chats/' + chatId + '/messages', { method: 'POST', body: { body, photo_url: photoUrl } });
+ },
+ async editMessage(msgId, body) {
+ return api('/api/v1/messages/' + msgId, { method: 'PUT', body: { body } });
+ },
+ async deleteMessage(msgId) {
+ return api('/api/v1/messages/' + msgId, { method: 'DELETE' });
+ },
+ async markRead(chatId) {
+ return api('/api/v1/chats/' + chatId + '/read', { method: 'PUT', body: {} });
+ },
+ async block(userId) { return api('/api/v1/blocks', { method: 'POST', body: { user_id: userId } }); },
+ async report(type, id, reason) { return api('/api/v1/reports', { method: 'POST', body: { target_type: type, target_id: id, reason } }); },
+};
+
+// === Reviews ===
+
+const ReviewApi = {
+ async create(chatId, rating, body, anonymous) {
+ return api('/api/v1/reviews', { method: 'POST', body: { chat_id: chatId, rating, body, anonymous } });
+ },
+ async listForUser(userId) { return api('/api/v1/users/' + userId + '/reviews'); },
+ async stats(userId) { return api('/api/v1/users/' + userId + '/stats'); },
+};
+
+window.API = { api, Auth, ChatApi, ReviewApi, Store };
\ No newline at end of file
diff --git a/public/js/app.js b/public/js/app.js
new file mode 100644
index 0000000..394d872
--- /dev/null
+++ b/public/js/app.js
@@ -0,0 +1,183 @@
+// Main app controller
+const App = {
+ consentGiven: false,
+
+ async init() {
+ // bind tabbar
+ document.querySelectorAll('.tab').forEach(btn => {
+ btn.onclick = () => this.showView(btn.dataset.view);
+ });
+
+ // restore tokens from localStorage
+ API.Store.load();
+
+ if (TG_APP.isAvailable && TG_APP.initData) {
+ // авторизация через Telegram
+ try {
+ await API.Auth.loginWithTelegram(TG_APP.initData);
+ await this.checkConsent();
+ this.showView('map');
+ } catch (e) {
+ this.showError('Не удалось войти через Telegram: ' + e.message);
+ }
+ } else if (API.Store.access) {
+ // уже залогинен
+ try {
+ await API.Auth.me();
+ await this.checkConsent();
+ this.showView('map');
+ } catch (e) {
+ API.Store.clear();
+ this.showError('Сессия истекла: ' + e.message);
+ }
+ } else {
+ this.showError('Откройте BuhApp через бота @buhoiapp_bot');
+ }
+
+ // bind input events for chat
+ document.body.addEventListener('click', (e) => {
+ const sendBtn = e.target.closest('#msg-send');
+ if (sendBtn) { window.ChatView.send(); return; }
+ const cancel = e.target.closest('#edit-cancel');
+ if (cancel) { document.getElementById('msg-input').value = ''; document.getElementById('edit-bar').style.display='none'; return; }
+ });
+ document.body.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' && e.target.id === 'msg-input') {
+ if (!e.shiftKey) { e.preventDefault(); window.ChatView.send(); }
+ }
+ });
+ },
+
+ async checkConsent() {
+ // показываем согласия при первом входе
+ if (API.Store.user && !API.Store.user.consents_given) {
+ this.showConsent();
+ return false;
+ }
+ return true;
+ },
+
+ showConsent() {
+ const ov = document.createElement('div');
+ ov.id = 'consent-overlay';
+ ov.innerHTML = `
+ 🍻 Добро пожаловать в BuhApp
+ Приложение для поиска компании. Только для лиц 18+.
+
+
+ ⚠️ Внимание : BuhApp — это платформа для знакомств и встреч с другими пользователями.
+ Включает в себя риски, связанные с употреблением алкоголя и личными встречами.
+
+
+ Пожалуйста, ознакомьтесь и примите следующие документы:
+
+
Мне исполнилось 18 лет (обязательно)
+
Пользовательское соглашение (v1.0)
+
Политика конфиденциальности (152-ФЗ, v1.0)
+
Отказ от ответственности (v1.0)
+
Принять и продолжить
+
+ `;
+ document.body.appendChild(ov);
+
+ document.getElementById('c-accept').onclick = async () => {
+ const a = document.getElementById('c-adult').checked;
+ const t = document.getElementById('c-terms').checked;
+ const p = document.getElementById('c-privacy').checked;
+ const d = document.getElementById('c-disclaimer').checked;
+ if (!a || !t || !p || !d) {
+ TG_APP.showAlert('Нужно принять все 4 пункта');
+ return;
+ }
+ try {
+ await API.api('/api/v1/auth/consents', {
+ method: 'POST',
+ body: {
+ adult: a, terms: t, privacy: p, disclaimer: d,
+ },
+ });
+ TG_APP.haptic('notification');
+ ov.remove();
+ this.consentGiven = true;
+ this.showView('map');
+ } catch (e) {
+ TG_APP.showAlert('Ошибка: ' + e.message);
+ }
+ };
+ },
+
+ showError(msg) {
+ document.getElementById('app').innerHTML = `
+
+ `;
+ },
+
+ showView(name, params = {}) {
+ document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.view === name));
+
+ const root = document.getElementById('app');
+ if (window.ChatView?.cleanup) window.ChatView.cleanup();
+
+ switch (name) {
+ case 'map': MapView.render(root); break;
+ case 'chats': ChatsView.render(root); break;
+ case 'chat': ChatView.render(root, params); break;
+ case 'profile': ProfileView.render(root); break;
+ case 'settings': SettingsView.render(root); break;
+ case 'review': ReviewView.render(root, params); break;
+ case 'user-reviews': UserReviewsView.render(root, params); break;
+ case 'user-profile': UserProfileView.render(root, params); break;
+ default: root.innerHTML = 'Неизвестный экран
';
+ }
+ },
+
+ openChat(chatId, otherId, otherName) {
+ this.showView('chat', { chatId, otherId, otherName });
+ },
+
+ openReview(chatId, otherId, otherName) {
+ this.showView('review', { chatId, otherId, otherName });
+ },
+
+ openUserProfile(userId, name) {
+ this.showView('user-profile', { userId, name });
+ },
+
+ toast(msg) {
+ const t = document.createElement('div');
+ t.className = 'toast';
+ t.textContent = msg;
+ document.body.appendChild(t);
+ setTimeout(() => t.remove(), 2000);
+ },
+};
+
+// helpers
+function escapeHtml(s) {
+ if (!s) return '';
+ return String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
+}
+
+function formatTime(iso) {
+ if (!iso) return '';
+ const d = new Date(iso);
+ const today = new Date();
+ if (d.toDateString() === today.toDateString()) {
+ return d.getHours().toString().padStart(2, '0') + ':' + d.getMinutes().toString().padStart(2, '0');
+ }
+ return d.getDate().toString().padStart(2, '0') + '.' + (d.getMonth() + 1).toString().padStart(2, '0');
+}
+
+function formatDate(iso) {
+ if (!iso) return '';
+ const d = new Date(iso);
+ return d.getDate().toString().padStart(2, '0') + '.' + (d.getMonth() + 1).toString().padStart(2, '0') + '.' + d.getFullYear();
+}
+
+window.App = App;
+document.addEventListener('DOMContentLoaded', () => App.init());
\ No newline at end of file
diff --git a/public/js/tg.js b/public/js/tg.js
new file mode 100644
index 0000000..22c8aa6
--- /dev/null
+++ b/public/js/tg.js
@@ -0,0 +1,33 @@
+// Telegram WebApp SDK обёртки
+const TG = window.Telegram?.WebApp;
+if (TG) {
+ TG.ready();
+ TG.expand();
+}
+
+window.TG_APP = {
+ initData: TG?.initData || '',
+ initDataUnsafe: TG?.initDataUnsafe || {},
+ user: TG?.initDataUnsafe?.user || null,
+ colorScheme: TG?.colorScheme || 'light',
+
+ isAvailable: !!TG,
+
+ close: () => TG?.close(),
+ sendData: (data) => TG?.sendData(JSON.stringify(data)),
+ haptic: (type) => {
+ try {
+ TG?.HapticFeedback?.impactOccurred(type || 'light');
+ } catch (e) {}
+ },
+ showAlert: (msg) => {
+ if (TG?.showAlert) TG.showAlert(msg);
+ else alert(msg);
+ },
+ showConfirm: (msg) => {
+ if (TG?.showConfirm) return new Promise((r) => TG.showConfirm(msg, r));
+ return Promise.resolve(confirm(msg));
+ },
+
+ theme: () => TG?.themeParams || {},
+};
\ No newline at end of file
diff --git a/public/js/views/chat.js b/public/js/views/chat.js
new file mode 100644
index 0000000..0f85d3a
--- /dev/null
+++ b/public/js/views/chat.js
@@ -0,0 +1,152 @@
+// Single chat view
+const ChatView = {
+ chatId: null,
+ otherId: null,
+ otherName: null,
+ ws: null,
+
+ async render(root, params) {
+ this.chatId = params.chatId;
+ this.otherId = params.otherId;
+ this.otherName = params.otherName;
+ root.innerHTML = `
+
+
+
←
+
${escapeHtml(this.otherName || '')}
+
👤
+
⭐
+
+
+
+ Редактирование
+ Отмена
+
+
+
+ ➤
+
+
+ `;
+
+ document.getElementById('chat-back').onclick = () => App.showView('chats');
+ document.getElementById('chat-profile').onclick = () => App.openUserProfile(this.otherId, this.otherName);
+ document.getElementById('chat-review').onclick = () => App.openReview(this.chatId, this.otherId, this.otherName);
+
+ this.loadMessages();
+ this.setupWS();
+ this.markRead();
+ },
+
+ async loadMessages() {
+ try {
+ const r = await API.ChatApi.listMessages(this.chatId);
+ const box = document.getElementById('messages');
+ box.innerHTML = '';
+ r.messages.forEach((m) => this.appendMessage(m));
+ box.scrollTop = box.scrollHeight;
+ } catch (e) {
+ console.warn(e);
+ }
+ },
+
+ appendMessage(m) {
+ const box = document.getElementById('messages');
+ const me = API.Store.user && m.sender_id === API.Store.user.id;
+ const div = document.createElement('div');
+ div.className = 'bubble ' + (me ? 'mine' : 'theirs');
+ div.dataset.id = m.id;
+ if (m.deleted) {
+ div.innerHTML = 'Удалено ';
+ } else {
+ div.innerHTML = `
+ ${escapeHtml(m.body)}
+
+ ${formatTime(m.created_at)}
+ ${m.edited ? 'ред. ' : ''}
+
+ `;
+ if (me) {
+ div.onclick = () => this.onMessageClick(m);
+ }
+ }
+ box.appendChild(div);
+ box.scrollTop = box.scrollHeight;
+ },
+
+ onMessageClick(m) {
+ TG_APP.showConfirm('Удалить сообщение?').then((ok) => {
+ if (ok) this.deleteMessage(m);
+ });
+ },
+
+ async deleteMessage(m) {
+ try {
+ await API.ChatApi.deleteMessage(m.id);
+ const el = document.querySelector(`.bubble[data-id="${m.id}"]`);
+ if (el) el.innerHTML = 'Удалено ';
+ } catch (e) {
+ TG_APP.showAlert('Ошибка: ' + e.message);
+ }
+ },
+
+ setupWS() {
+ if (!API.Store.access) return;
+ const url = (window.API_BASE || 'https://api.buhapp.mygoodservice.ru').replace(/^http/, 'ws') + '/ws?token=' + API.Store.access;
+ try {
+ this.ws = new WebSocket(url);
+ this.ws.onopen = () => console.log('WS connected');
+ this.ws.onmessage = (ev) => {
+ try {
+ const d = JSON.parse(ev.data);
+ if (d.type === 'message' && d.payload.chat_id === this.chatId) {
+ this.appendMessage(d.payload);
+ } else if (d.type === 'message_edited' && d.payload.chat_id === this.chatId) {
+ this.updateMessage(d.payload);
+ }
+ } catch {}
+ };
+ this.ws.onclose = () => setTimeout(() => this.setupWS(), 3000);
+ this.ws.onerror = (e) => console.warn('WS error', e);
+ } catch (e) {
+ console.warn(e);
+ }
+ },
+
+ updateMessage(p) {
+ const el = document.querySelector(`.bubble[data-id="${p.id}"]`);
+ if (el) {
+ el.querySelector('div:first-child') && (el.querySelector('div:first-child').textContent = p.body);
+ const meta = el.querySelector('.meta');
+ if (meta && !meta.querySelector('.edited')) {
+ const ed = document.createElement('span');
+ ed.className = 'edited';
+ ed.textContent = 'ред.';
+ meta.appendChild(ed);
+ }
+ }
+ },
+
+ async send() {
+ const inp = document.getElementById('msg-input');
+ const body = inp.value.trim();
+ if (!body) return;
+ inp.value = '';
+ try {
+ await API.ChatApi.sendMessage(this.chatId, body);
+ // WS пришлёт обратно
+ } catch (e) {
+ TG_APP.showAlert('Ошибка: ' + e.message);
+ }
+ },
+
+ async markRead() {
+ try { await API.ChatApi.markRead(this.chatId); } catch {}
+ },
+
+ cleanup() {
+ if (this.ws) { this.ws.close(); this.ws = null; }
+ },
+};
+
+window.ChatView = ChatView;
\ No newline at end of file
diff --git a/public/js/views/chats.js b/public/js/views/chats.js
new file mode 100644
index 0000000..8563f99
--- /dev/null
+++ b/public/js/views/chats.js
@@ -0,0 +1,41 @@
+// Chats list view
+const ChatsView = {
+ async render(root) {
+ root.innerHTML = `
+
+ `;
+ try {
+ const r = await API.ChatApi.listChats();
+ const list = document.getElementById('chats-list');
+ if (!r.chats || r.chats.length === 0) {
+ list.innerHTML = 'Нет чатов. Открой карту и найди кого-нибудь!
';
+ return;
+ }
+ list.innerHTML = '';
+ list.className = '';
+ r.chats.forEach((item) => {
+ const row = document.createElement('div');
+ row.className = 'list-row';
+ row.innerHTML = `
+
+ ${item.other_photo ? '' : escapeHtml((item.other_name || '?')[0])}
+
+
+
${escapeHtml(item.other_name)}
+
${escapeHtml(item.last_message || 'Нет сообщений')}
+
+ ${item.unread_count > 0 ? `${item.unread_count}
` : ''}
+ `;
+ row.onclick = () => App.openChat(item.chat.id, item.other_id, item.other_name);
+ list.appendChild(row);
+ });
+ } catch (e) {
+ document.getElementById('chats-list').innerHTML = 'Ошибка: ' + e.message + '
';
+ }
+ },
+};
+
+window.ChatsView = ChatsView;
\ No newline at end of file
diff --git a/public/js/views/map.js b/public/js/views/map.js
new file mode 100644
index 0000000..5884474
--- /dev/null
+++ b/public/js/views/map.js
@@ -0,0 +1,127 @@
+// Map view — главный экран
+const MapView = {
+ map: null,
+ markers: [],
+ radius: 5,
+ visible: true,
+ nearby: [],
+
+ async render(root) {
+ root.innerHTML = `
+
+
🗺️ Карта
+
+
+
+
Я на карте
+
Другие пользователи видят твоё местоположение ±300м
+
+
+
+
+
+
+
+
+
+ Радиус: ${this.radius} км
+
+ 1 км
+ 5 км
+ 10 км
+ 25 км
+ 50 км
+
+
+
—
+
+ `;
+
+ if (!this.map) {
+ this.map = L.map('map').setView([55.7558, 37.6173], 12);
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ attribution: '© OpenStreetMap',
+ maxZoom: 19,
+ }).addTo(this.map);
+ }
+ setTimeout(() => this.map.invalidateSize(), 100);
+
+ document.getElementById('visible-toggle').addEventListener('change', async (e) => {
+ this.visible = e.target.checked;
+ try {
+ await API.Auth.setVisibility(this.visible);
+ this.refreshLocation();
+ } catch (err) {
+ API.Store && console.warn(err);
+ TG_APP.showAlert('Не удалось обновить видимость');
+ }
+ });
+
+ document.getElementById('radius-sel').addEventListener('change', (e) => {
+ this.radius = parseInt(e.target.value, 10);
+ document.getElementById('radius-label').textContent = this.radius + ' км';
+ this.refreshNearby();
+ });
+
+ this.refreshLocation();
+ },
+
+ async refreshLocation() {
+ if (!navigator.geolocation) {
+ document.getElementById('nearby-count').textContent = 'Геолокация недоступна';
+ return;
+ }
+ navigator.geolocation.getCurrentPosition(
+ async (pos) => {
+ const lat = pos.coords.latitude;
+ const lng = pos.coords.longitude;
+ try {
+ await API.Auth.updateLocation(lat, lng, this.visible);
+ } catch (e) {
+ console.warn(e);
+ }
+ this.map.setView([lat, lng], 13);
+ L.circle([lat, lng], {
+ radius: this.radius * 1000,
+ color: '#FF6B35', fillColor: '#FF6B35', fillOpacity: 0.1, weight: 2,
+ }).addTo(this.map);
+ this.refreshNearby();
+ },
+ (err) => {
+ document.getElementById('nearby-count').textContent = 'Нет доступа к GPS: ' + err.message;
+ },
+ { enableHighAccuracy: true, timeout: 15000 }
+ );
+ },
+
+ async refreshNearby() {
+ if (!navigator.geolocation) return;
+ navigator.geolocation.getCurrentPosition(async (pos) => {
+ try {
+ const r = await API.Auth.searchNearby(pos.coords.latitude, pos.coords.longitude, this.radius);
+ this.nearby = r.results || [];
+ document.getElementById('nearby-count').textContent = this.nearby.length + ' чел. рядом';
+
+ // удалить старые маркеры (кроме своего круга)
+ this.markers.forEach(m => this.map.removeLayer(m));
+ this.markers = [];
+
+ this.nearby.forEach((n) => {
+ const icon = L.divIcon({
+ className: 'custom-marker',
+ html: `${escapeHtml((n.name || '?')[0])}
`,
+ iconSize: [36, 36],
+ });
+ const m = L.marker([n.lat, n.lng], { icon }).addTo(this.map);
+ m.bindTooltip(n.name + ' • ' + (n.distance_m / 1000).toFixed(1) + ' км', { direction: 'top' });
+ m.on('click', () => App.openUserProfile(n.user_id, n.name));
+ this.markers.push(m);
+ });
+ } catch (e) {
+ console.warn(e);
+ }
+ });
+ },
+};
+
+window.MapView = MapView;
\ No newline at end of file
diff --git a/public/js/views/profile.js b/public/js/views/profile.js
new file mode 100644
index 0000000..852796a
--- /dev/null
+++ b/public/js/views/profile.js
@@ -0,0 +1,115 @@
+// Profile view (свой профиль)
+const ProfileView = {
+ drinks: new Set(),
+ activities: new Set(),
+ purposes: new Set(),
+ anonymous: false,
+
+ DRINKS: [
+ ['beer', '🍺 Пиво'], ['wine', '🍷 Вино'], ['whiskey', '🥃 Виски'], ['vodka', '🍸 Водка'],
+ ['cocktail', '🍹 Коктейль'], ['rum', '🥃 Ром'], ['gin', '🍸 Джин'], ['tequila', '🌵 Текила'],
+ ['champagne', '🍾 Шампанское'], ['non_alcoholic', '🥤 Безалкогольное'],
+ ],
+ ACTIVITIES: [
+ ['walk', '🚶 Прогулка'], ['dinner', '🍽️ Ужин'], ['bar', '🍻 Бар'], ['cafe', '☕ Кафе'],
+ ['travel', '✈️ Путешествие'], ['movie', '🎬 Кино'], ['concert', '🎵 Концерт'],
+ ['sport', '⚽ Спорт'], ['gaming', '🎮 Игры'], ['reading', '📚 Чтение'],
+ ],
+ PURPOSES: [
+ ['chat', '💬 Пообщаться'], ['drink', '🍻 Выпить'], ['walk', '🌳 Погулять'],
+ ['dinner', '🍽️ Поужинать'], ['travel', '✈️ Поехать'], ['friendship', '🤝 Дружба'],
+ ['relationship', '❤️ Отношения'],
+ ],
+
+ async render(root) {
+ try {
+ const me = await API.Auth.me();
+ API.Store.user = me.user;
+ const prefs = me.prefs || {};
+ this.drinks = new Set(prefs.drinks || []);
+ this.activities = new Set(prefs.activities || []);
+ this.purposes = new Set(prefs.purposes || []);
+ this.renderForm(root, me.user);
+ } catch (e) {
+ root.innerHTML = 'Не удалось загрузить: ' + e.message + '
';
+ }
+ },
+
+ renderForm(root, u) {
+ root.innerHTML = `
+
+
👤 Профиль
+
+
${escapeHtml(u.name || '')}
+ ${u.telegram_id ? `
@${escapeHtml(u.username || '')}
` : ''}
+ ${u.telegram_id ? '' : `
${escapeHtml(u.email || '')}
`}
+
+
Имя
+
+
Город
+
+
О себе
+
+
Пол
+
+ Не указан
+ Мужской
+ Женский
+ Другое
+
+
+
🍻 Что пью
+
+
🎯 Чем занимаюсь
+
+
💡 Цели
+
+
+
Сохранить
+
+ `;
+
+ this.renderChips('p-drinks', this.DRINKS, this.drinks);
+ this.renderChips('p-activities', this.ACTIVITIES, this.activities);
+ this.renderChips('p-purposes', this.PURPOSES, this.purposes);
+
+ document.getElementById('p-save').onclick = () => this.save();
+ },
+
+ renderChips(elId, items, selected) {
+ const el = document.getElementById(elId);
+ items.forEach(([key, label]) => {
+ const chip = document.createElement('span');
+ chip.className = 'chip' + (selected.has(key) ? ' active' : '');
+ chip.textContent = label;
+ chip.onclick = () => {
+ if (selected.has(key)) selected.delete(key); else selected.add(key);
+ chip.classList.toggle('active');
+ TG_APP.haptic('selection');
+ };
+ el.appendChild(chip);
+ });
+ },
+
+ async save() {
+ try {
+ await API.Auth.updateMe({
+ name: document.getElementById('p-name').value.trim(),
+ city: document.getElementById('p-city').value.trim(),
+ bio: document.getElementById('p-bio').value.trim(),
+ gender: document.getElementById('p-gender').value,
+ });
+ await API.Auth.updatePrefs({
+ drinks: [...this.drinks],
+ activities: [...this.activities],
+ purposes: [...this.purposes],
+ });
+ TG_APP.haptic('notification');
+ App.toast('Сохранено ✓');
+ } catch (e) {
+ TG_APP.showAlert('Ошибка: ' + e.message);
+ }
+ },
+};
+
+window.ProfileView = ProfileView;
\ No newline at end of file
diff --git a/public/js/views/review.js b/public/js/views/review.js
new file mode 100644
index 0000000..d96206c
--- /dev/null
+++ b/public/js/views/review.js
@@ -0,0 +1,116 @@
+// Review screens — create + user-reviews list
+const ReviewView = {
+ rating: 5,
+ chatId: null,
+ otherId: null,
+ otherName: null,
+
+ async render(root, params) {
+ this.chatId = params.chatId;
+ this.otherId = params.otherId;
+ this.otherName = params.otherName;
+ this.rating = 5;
+ root.innerHTML = `
+
+
⭐ Отзыв
+
О ${escapeHtml(this.otherName || 'пользователе')}
+
+
Ваша оценка
+
+ ${[1,2,3,4,5].map(n => `★ `).join('')}
+
+
Комментарий (необязательно)
+
+
+ Анонимно
+
+
+
Отправить
+
+
+ `;
+ document.querySelectorAll('#stars span').forEach(s => {
+ s.onclick = () => {
+ this.rating = parseInt(s.dataset.n, 10);
+ document.querySelectorAll('#stars span').forEach((x, i) => {
+ x.classList.toggle('on', i < this.rating);
+ });
+ TG_APP.haptic('selection');
+ };
+ });
+ document.getElementById('r-submit').onclick = () => this.submit();
+ },
+
+ async submit() {
+ try {
+ await API.ReviewApi.create(
+ this.chatId,
+ this.rating,
+ document.getElementById('r-body').value.trim(),
+ document.getElementById('r-anon').checked
+ );
+ TG_APP.haptic('notification');
+ App.toast('Отзыв отправлен ✓');
+ setTimeout(() => App.showView('chats'), 800);
+ } catch (e) {
+ TG_APP.showAlert('Ошибка: ' + e.message);
+ }
+ },
+};
+
+const UserReviewsView = {
+ async render(root, params) {
+ const userId = params.userId;
+ const userName = params.userName || 'Пользователь';
+ root.innerHTML = `
+
+
⭐ Отзывы
+
+
+ `;
+ try {
+ const [stats, list] = await Promise.all([
+ API.ReviewApi.stats(userId),
+ API.ReviewApi.listForUser(userId),
+ ]);
+ const header = document.getElementById('ur-header');
+ header.className = 'card';
+ header.innerHTML = `
+
+
${stats.rating_avg ? stats.rating_avg.toFixed(1) : '—'}
+
+
+ ${[1,2,3,4,5].map(n => `★ `).join('')}
+
+
${stats.rating_count} отзывов
+
+
+ `;
+ const v = document.createElement('div');
+ v.innerHTML = '' + escapeHtml(userName) + ' ';
+ if (!list.reviews || list.reviews.length === 0) {
+ v.innerHTML += 'Пока нет отзывов
';
+ } else {
+ list.reviews.forEach((r) => {
+ const card = document.createElement('div');
+ card.className = 'card';
+ card.innerHTML = `
+
+
${[1,2,3,4,5].map(n=>`★ `).join('')}
+
${formatDate(r.created_at)}
+
+ ${r.anonymous ? 'Аноним' : 'ID: ' + r.reviewer_id.slice(0,8)}
+ ${r.body ? '' + escapeHtml(r.body) + '
' : ''}
+ `;
+ v.appendChild(card);
+ });
+ }
+ header.parentElement.appendChild(v);
+ } catch (e) {
+ document.getElementById('ur-header').innerHTML = 'Ошибка: ' + e.message + '
';
+ }
+ },
+};
+
+window.ReviewView = ReviewView;
+window.UserReviewsView = UserReviewsView;
\ No newline at end of file
diff --git a/public/js/views/settings.js b/public/js/views/settings.js
new file mode 100644
index 0000000..ec4a772
--- /dev/null
+++ b/public/js/views/settings.js
@@ -0,0 +1,115 @@
+// Settings view + consent gate + user profile (public)
+const SettingsView = {
+ async render(root) {
+ const u = API.Store.user;
+ root.innerHTML = `
+
+
⚙️ Настройки
+
+
${escapeHtml(u?.name || '')}
+ ${u?.telegram_id ? `
Telegram ID: ${u.telegram_id}
` : ''}
+ ${u?.email ? `
${escapeHtml(u.email)}
` : ''}
+
+
+
Выйти
+
+ `;
+ document.getElementById('logout').onclick = async () => {
+ const ok = await TG_APP.showConfirm('Выйти из BuhApp?');
+ if (!ok) return;
+ API.Store.clear();
+ location.reload();
+ };
+ },
+};
+
+// Public user profile (включая рейтинг и кнопку "написать")
+const UserProfileView = {
+ async render(root, params) {
+ const userId = params.userId;
+ const name = params.name || '';
+ root.innerHTML = '';
+ try {
+ const [profile, stats] = await Promise.all([
+ API.Auth.getUser(userId),
+ API.ReviewApi.stats(userId),
+ ]);
+ root.innerHTML = `
+
+
← Назад
+
+
+ ${profile.photo ? `
` :
+ `
${escapeHtml((profile.name||'?')[0])}
`}
+
+
${escapeHtml(profile.name || '')}
+ ${profile.city ? `
📍 ${escapeHtml(profile.city)}
` : ''}
+ ${profile.age ? `
${profile.age} лет
` : ''}
+
+
+
+
${stats.rating_avg ? stats.rating_avg.toFixed(1) : '—'}
+
+
+ ${[1,2,3,4,5].map(n=>`★ `).join('')}
+
+
${stats.rating_count || 0} отзывов
+
+
+
+
+ ${profile.bio ? `
О себе ${escapeHtml(profile.bio)}
` : ''}
+
+ ${profile.prefs ? this.renderPrefs(profile.prefs) : ''}
+
+
💬 Написать
+
⭐ Все отзывы (${stats.rating_count || 0})
+
🚫 Заблокировать
+
⚠️ Пожаловаться
+
+ `;
+
+ document.getElementById('up-back').onclick = () => history.back();
+ document.getElementById('up-chat').onclick = async () => {
+ try {
+ const chat = await API.ChatApi.ensureChat(userId);
+ App.openChat(chat.id, userId, profile.name);
+ } catch (e) { TG_APP.showAlert('Ошибка: ' + e.message); }
+ };
+ document.getElementById('up-reviews').onclick = () => App.showView('user-reviews', { userId, userName: profile.name });
+ document.getElementById('up-block').onclick = async () => {
+ const ok = await TG_APP.showConfirm('Заблокировать ' + profile.name + '?');
+ if (!ok) return;
+ try { await API.ChatApi.block(userId); TG_APP.showAlert('Заблокировано'); history.back(); }
+ catch (e) { TG_APP.showAlert('Ошибка: ' + e.message); }
+ };
+ document.getElementById('up-report').onclick = async () => {
+ const reason = prompt('Причина жалобы:');
+ if (!reason) return;
+ try { await API.ChatApi.report('user', userId, reason); TG_APP.showAlert('Жалоба отправлена'); }
+ catch (e) { TG_APP.showAlert('Ошибка: ' + e.message); }
+ };
+ } catch (e) {
+ root.innerHTML = 'Ошибка: ' + e.message + '
';
+ }
+ },
+
+ renderPrefs(p) {
+ const blocks = [];
+ if (p.drinks?.length) {
+ blocks.push(`Предпочтения ${p.drinks.map(d => `${escapeHtml(d)} `).join('')}
`);
+ }
+ return blocks.join('');
+ },
+};
+
+window.SettingsView = SettingsView;
+window.UserProfileView = UserProfileView;
\ No newline at end of file
diff --git a/public/legal/DISCLAIMER.html b/public/legal/DISCLAIMER.html
new file mode 100644
index 0000000..cfdc047
--- /dev/null
+++ b/public/legal/DISCLAIMER.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+ Отказ от ответственности — BuhApp
+
+
+
+
+
+
← Назад
+
Отказ от ответственности (Disclaimer)
+
Версия: 1.0 от 20.08.2026
+
+
1. Назначение документа
+
Настоящий документ устанавливает пределы ответственности Администрации Сервиса BuhApp.
+
+
2. Отказ от гарантий
+
+ Сервис предоставляется «как есть», без каких-либо гарантий.
+ Администрация не гарантирует, что Пользователи будут соблюдать правила, закон или быть адекватными.
+
+
+
3. Алкоголь
+
+ Сервис не продаёт и не доставляет алкоголь.
+ Пользователи самостоятельно несут ответственность за своё поведение при употреблении алкоголя.
+ Администрация не несёт ответственности за вред, причинённый в состоянии алкогольного опьянения.
+
+
+
4. Личные встречи
+
+ Сервис является исключительно платформой для знакомств.
+ Администрация не организует, не контролирует и не отвечает за личные встречи Пользователей.
+ Пользователи самостоятельно оценивают риски и принимают решения о встречах.
+ Администрация не является стороной отношений между Пользователями.
+
+
+
5. Безопасность
+
+ Сервис предоставляет базовые инструменты безопасности (жалобы, блокировки).
+ Пользователи обязаны сообщать о нарушениях через кнопку «Пожаловаться».
+ В экстренных ситуациях Пользователь обязан обратиться в полицию (112).
+
+
+
6. Ограничение ответственности
+
+ Администрация не несёт ответственности за любой ущерб, причинённый Пользователями друг другу.
+ Совокупная ответственность Администрации ограничивается суммой, уплаченной Пользователем за платные услуги (при наличии).
+
+
+
Полный текст: github
+
+
+
\ No newline at end of file
diff --git a/public/legal/PRIVACY.html b/public/legal/PRIVACY.html
new file mode 100644
index 0000000..8ac130b
--- /dev/null
+++ b/public/legal/PRIVACY.html
@@ -0,0 +1,60 @@
+
+
+
+
+
+ Политика конфиденциальности — BuhApp
+
+
+
+
+
+
← Назад
+
Политика конфиденциальности BuhApp
+
Версия: 1.0 от 20.08.2026
+
+
1. Какие данные мы собираем
+
+ Данные Telegram (ID, имя, фото) — при авторизации
+ Геолокация — если Пользователь разрешил
+ Сообщения в чатах — между Пользователями
+ Отзывы и рейтинги
+ Журнал действий (audit log) — IP, user-agent, время
+
+
+
2. Цели обработки
+
+ Предоставление работы Сервиса
+ Безопасность и предотвращение нарушений
+ Улучшение Сервиса
+
+
+
3. Хранение данных
+
+ Данные хранятся на серверах в РФ.
+ Данные хранятся до удаления аккаунта Пользователем.
+
+
+
4. Передача третьим лицам
+
+ Мы не передаём данные третьим лицам, кроме случаев, предусмотренных законом.
+
+
+
5. Права Пользователя
+
+ Запросить свои данные
+ Удалить аккаунт и все данные
+ Отозвать согласие на обработку
+
+
+
Полный текст: github
+
+
+
\ No newline at end of file
diff --git a/public/legal/TERMS.html b/public/legal/TERMS.html
new file mode 100644
index 0000000..e483fb4
--- /dev/null
+++ b/public/legal/TERMS.html
@@ -0,0 +1,65 @@
+
+
+
+
+
+ Пользовательское соглашение — BuhApp
+
+
+
+
+
+
← Назад
+
Пользовательское соглашение BuhApp
+
Версия: 1.0 от 20.08.2026
+
+
1. Общие положения
+
+ Настоящее Пользовательское соглашение (далее — «Соглашение») регулирует отношения между Пользователем и Сервисом BuhApp (далее — «Сервис»).
+ Сервис предоставляет возможность совершеннолетним пользователям искать друг друга для совместного времяпрепровождения и общения.
+ Использование Сервиса означает безусловное согласие Пользователя с настоящим Соглашением.
+
+
+
2. Возрастные ограничения
+
+ Сервис предназначен исключительно для лиц, достигших 18 лет.
+ Регистрация в Сервисе подтверждает совершеннолетие Пользователя.
+ Администрация оставляет за собой право запросить подтверждение возраста в любой момент.
+
+
+
3. Безопасность и поведение
+
+ Пользователь обязуется соблюдать законодательство РФ.
+ Запрещены: оскорбления, угрозы, спам, мошенничество, распространение запрещённых материалов.
+ В случае опасности Пользователь может обратиться в полицию, а также воспользоваться функцией SOS.
+
+
+
4. Персональные данные
+
+ Обработка персональных данных осуществляется в соответствии с Политикой конфиденциальности и ФЗ-152.
+ Пользователь даёт согласие на обработку указанных данных для целей работы Сервиса.
+
+
+
5. Встречи и риски
+
+ Сервис не несёт ответственности за поведение Пользователей при личных встречах.
+ Пользователь осознаёт и принимает все риски, связанные с личными встречами.
+
+
+
6. Права Администрации
+
+ Администрация вправе заблокировать Пользователя за нарушение Соглашения.
+ Администрация вправе изменить Соглашение, уведомив Пользователя.
+
+
+
Полный текст: github
+
+
+
\ No newline at end of file