feat: nickname system + friend requests + privacy settings

- Nicknames: unique, 2-16 chars, alphanumeric + russian + underscore
- Friend requests: send by nickname, accept/decline with confirmation
- No raw TG IDs exposed — only nicknames
- Privacy: allow_requests toggle, notify_online toggle
- Bot: /code shows nickname, /add <nick> sends request, /friends shows list + requests
- Client: full friends UI with requests, accept/decline, nickname setup
This commit is contained in:
Mk 2026-05-27 04:30:52 +00:00
parent 3733b4b94f
commit f14b61d7d9
2 changed files with 133 additions and 50 deletions

181
game.js
View File

@ -220,7 +220,14 @@ function customConfirm(msg, onYes) {
btnInvite.textContent = '📤 Пригласить друзей'; btnInvite.textContent = '📤 Пригласить друзей';
btnInvite.onclick = () => { btnInvite.onclick = () => {
const code = worldId || generateShortWorldCode(); const code = worldId || generateShortWorldCode();
Telegram.WebApp.switchInlineQuery('play ' + code); // Share: open TG share dialog with game link
const url = 'https://t.me/Grechkacraft_bot/app?startapp=' + encodeURIComponent(code);
if (Telegram.WebApp && Telegram.WebApp.openTelegramLink) {
Telegram.WebApp.openTelegramLink('https://t.me/share/url?url=' + encodeURIComponent(url) + '&text=' + encodeURIComponent('🏗️ Заходи в GrechkaCraft! Мир: ' + code));
} else {
// Fallback: copy invite link
navigator.clipboard.writeText(url).then(() => showToast('📋 Скопировано!')).catch(() => showToast(url));
}
}; };
box.appendChild(btnInvite); box.appendChild(btnInvite);
@ -234,69 +241,145 @@ function customConfirm(msg, onYes) {
try { try {
const resp = await fetch(SERVER_URL + '/api/tg/friends?tg_id=' + tgUser.id); const resp = await fetch(SERVER_URL + '/api/tg/friends?tg_id=' + tgUser.id);
const data = await resp.json(); const data = await resp.json();
if (!data.ok || !data.friends || !data.friends.length) {
showToast('👥 Нет друзей. Поделись своим TG ID: ' + tgUser.id);
return;
}
let html = '<div style="text-align:left;margin-bottom:16px;">';
for (const f of data.friends) {
if (f.online) {
html += '<div style="padding:8px 0;border-bottom:1px solid #333;">🟢 <b>' + f.name + '</b> — мир ' + f.world_id + '</div>';
} else {
html += '<div style="padding:8px 0;border-bottom:1px solid #333;">⚫ ' + f.name + '</div>';
}
}
html += '</div>';
box.innerHTML = ''; box.innerHTML = '';
const ft = document.createElement('div'); const ft = document.createElement('div');
ft.style.cssText = 'font-size:20px;font-weight:bold;margin-bottom:8px;color:#f39c12;'; ft.style.cssText = 'font-size:20px;font-weight:bold;margin-bottom:8px;color:#f39c12;';
ft.textContent = '👥 Друзья'; ft.textContent = '👥 Друзья';
box.appendChild(ft); box.appendChild(ft);
const list = document.createElement('div'); // Show nickname
list.innerHTML = html; if (data.nickname) {
box.appendChild(list); const nickDiv = document.createElement('div');
// Add friend by TG ID nickDiv.style.cssText = 'color:#2ecc71;font-size:14px;margin-bottom:12px;';
nickDiv.textContent = '🔗 Твой ник: ' + data.nickname;
box.appendChild(nickDiv);
} else {
// No nickname yet — set one
const nickDiv = document.createElement('div');
nickDiv.style.cssText = 'margin-bottom:12px;';
nickDiv.innerHTML = '<div style="color:#e74c3c;font-size:14px;margin-bottom:8px;">Задай ник, чтобы друзья могли тебя найти:</div>';
const nickInput = document.createElement('input');
nickInput.type = 'text';
nickInput.maxLength = 16;
nickInput.placeholder = 'Твой ник';
nickInput.style.cssText = 'width:65%;padding:10px;border:2px solid #f39c12;border-radius:8px;background:#16213e;color:#fff;font-size:16px;';
const nickBtn = document.createElement('button');
nickBtn.style.cssText = 'width:28%;padding:10px;background:#f39c12;color:#fff;border:none;border-radius:8px;font-size:16px;cursor:pointer;margin-left:4px;';
nickBtn.textContent = '✓';
nickBtn.onclick = async () => {
const n = nickInput.value.trim().toLowerCase();
if (!n || n.length < 2) { showToast('❌ Минимум 2 символа'); return; }
const check = await fetch(SERVER_URL + '/api/nick/check?nick=' + encodeURIComponent(n));
const cd = await check.json();
if (!cd.available) { showToast('❌ Ник занят'); return; }
const set = await fetch(SERVER_URL + '/api/nick/set', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,nickname:n})});
const sd = await set.json();
if (sd.ok) { showToast('✅ Ник: ' + sd.nickname); btnFriends.onclick(); } else { showToast('❌ Ошибка'); }
};
nickDiv.appendChild(nickInput);
nickDiv.appendChild(nickBtn);
box.appendChild(nickDiv);
}
// Friends list
if (data.friends && data.friends.length) {
const list = document.createElement('div');
list.style.cssText = 'margin-bottom:12px;';
for (const f of data.friends) {
const fDiv = document.createElement('div');
fDiv.style.cssText = 'padding:6px 0;border-bottom:1px solid #333;display:flex;justify-content:space-between;align-items:center;';
const statusDot = f.online ? '🟢' : '⚫';
const info = statusDot + ' <b>' + f.nickname + '</b>' + (f.world_id ? ' — мир ' + f.world_id : '');
fDiv.innerHTML = '<div>' + info + '</div>';
if (f.online && f.world_id) {
const joinBtn = document.createElement('button');
joinBtn.style.cssText = 'padding:4px 10px;background:#27ae60;color:#fff;border:none;border-radius:6px;font-size:12px;cursor:pointer;';
joinBtn.textContent = '🎮 Вступить';
joinBtn.onclick = () => { worldId = f.world_id; overlay.remove(); resolve(worldId); };
fDiv.appendChild(joinBtn);
}
list.appendChild(fDiv);
}
box.appendChild(list);
} else if (data.friends && !data.friends.length) {
const noFriends = document.createElement('div');
noFriends.style.cssText = 'color:#888;font-size:14px;margin-bottom:12px;';
noFriends.textContent = 'Пока нет друзей';
box.appendChild(noFriends);
}
// Pending friend requests
if (data.pending && data.pending.length) {
const pDiv = document.createElement('div');
pDiv.style.cssText = 'margin-bottom:12px;';
pDiv.innerHTML = '<div style="color:#f39c12;font-weight:bold;margin-bottom:6px;">📨 Запросы в друзья:</div>';
for (const p of data.pending) {
const pItem = document.createElement('div');
pItem.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:4px 0;border-bottom:1px solid #222;';
const span = document.createElement('span');
span.textContent = p.from;
pItem.appendChild(span);
const btns = document.createElement('div');
const acceptBtn = document.createElement('button');
acceptBtn.style.cssText = 'padding:4px 8px;background:#27ae60;color:#fff;border:none;border-radius:4px;font-size:12px;cursor:pointer;margin-right:4px;';
acceptBtn.textContent = '✓';
acceptBtn.onclick = async () => {
const r = await fetch(SERVER_URL + '/api/friends/accept', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,request_id:p.id})});
const d = await r.json();
if (d.ok) { showToast('✅ Друг добавлен!'); btnFriends.onclick(); } else { showToast('❌ Ошибка'); }
};
const declineBtn = document.createElement('button');
declineBtn.style.cssText = 'padding:4px 8px;background:#e74c3c;color:#fff;border:none;border-radius:4px;font-size:12px;cursor:pointer;';
declineBtn.textContent = '✕';
declineBtn.onclick = async () => {
await fetch(SERVER_URL + '/api/friends/decline', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,request_id:p.id})});
showToast('Отклонено');
btnFriends.onclick();
};
btns.appendChild(acceptBtn);
btns.appendChild(declineBtn);
pItem.appendChild(btns);
pDiv.appendChild(pItem);
}
box.appendChild(pDiv);
}
// Add friend by nickname
const addDiv = document.createElement('div'); const addDiv = document.createElement('div');
addDiv.style.cssText = 'margin-top:16px;'; addDiv.style.cssText = 'margin-top:12px;';
addDiv.innerHTML = '<div style="color:#888;font-size:14px;margin-bottom:8px;">Добавить друга по TG ID:</div>'; addDiv.innerHTML = '<div style="color:#888;font-size:14px;margin-bottom:8px;">Добавить друга по нику:</div>';
const addInput = document.createElement('input'); const addInput = document.createElement('input');
addInput.type = 'number'; addInput.type = 'text';
addInput.placeholder = 'TG ID друга'; addInput.maxLength = 16;
addInput.style.cssText = 'width:70%;padding:10px;border:2px solid #f39c12;border-radius:8px;background:#16213e;color:#fff;font-size:16px;'; addInput.placeholder = 'Ник друга';
addInput.style.cssText = 'width:65%;padding:10px;border:2px solid #f39c12;border-radius:8px;background:#16213e;color:#fff;font-size:16px;';
const addBtn = document.createElement('button'); const addBtn = document.createElement('button');
addBtn.style.cssText = 'width:25%;padding:10px;background:#f39c12;color:#fff;border:none;border-radius:8px;font-size:16px;cursor:pointer;margin-left:4px;'; addBtn.style.cssText = 'width:28%;padding:10px;background:#3498db;color:#fff;border:none;border-radius:8px;font-size:16px;cursor:pointer;margin-left:4px;';
addBtn.textContent = '+'; addBtn.textContent = '+';
addBtn.onclick = async () => { addBtn.onclick = async () => {
const fid = parseInt(addInput.value); const nick = addInput.value.trim().toLowerCase();
if (!fid) return; if (!nick || nick.length < 2) { showToast('❌ Минимум 2 символа'); return; }
const r = await fetch(SERVER_URL + '/api/tg/friends/add', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,friend_id:fid})}); const r = await fetch(SERVER_URL + '/api/friends/request', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,nickname:nick})});
const d = await r.json(); const d = await r.json();
if (d.ok) { showToast('✅ Друг добавлен!'); btnFriends.click(); } if (d.ok) { showToast(d.message === 'Already friends' ? '✅ Уже друзья!' : '📨 Запрос отправлен!'); }
else { showToast('❌ Ошибка'); } else { showToast('❌ ' + (d.error || 'Ошибка')); }
}; };
addDiv.appendChild(addInput); addDiv.appendChild(addInput);
addDiv.appendChild(addBtn); addDiv.appendChild(addBtn);
box.appendChild(addDiv); box.appendChild(addDiv);
// My TG ID // Privacy settings
const myId = document.createElement('div'); const settings = data.settings || { allow_requests: 1, notify_online: 1 };
myId.style.cssText = 'margin-top:12px;color:#888;font-size:13px;'; const settingsDiv = document.createElement('div');
myId.textContent = '🆔 Твой ID: ' + tgUser.id + ' — отправь другу, чтобы он добавил тебя'; settingsDiv.style.cssText = 'margin-top:12px;display:flex;gap:12px;font-size:13px;color:#888;';
box.appendChild(myId); const reqLabel = document.createElement('label');
// Join online friend buttons reqLabel.style.cssText = 'cursor:pointer;';
const onlineFriends = (data.friends||[]).filter(f => f.online); const reqCb = document.createElement('input');
if (onlineFriends.length) { reqCb.type = 'checkbox';
for (const of2 of onlineFriends) { reqCb.checked = settings.allow_requests;
const joinBtn = document.createElement('button'); reqCb.style.cssText = 'margin-right:4px;';
joinBtn.style.cssText = 'width:100%;padding:12px;margin-top:8px;background:#27ae60;color:#fff;border:none;border-radius:10px;font-size:16px;cursor:pointer;'; reqCb.onchange = async () => {
joinBtn.textContent = '🎮 Вступить к ' + of2.name + ' (мир ' + of2.world_id + ')'; await fetch(SERVER_URL + '/api/friends/settings', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,allow_requests:reqCb.checked?1:0,notify_online:settings.notify_online})});
joinBtn.onclick = () => { };
worldId = of2.world_id; reqLabel.appendChild(reqCb);
overlay.remove(); reqLabel.appendChild(document.createTextNode(' Запросы в друзья'));
resolve(worldId); settingsDiv.appendChild(reqLabel);
}; box.appendChild(settingsDiv);
box.appendChild(joinBtn);
}
}
// Back button // Back button
const btnBackF = document.createElement('button'); const btnBackF = document.createElement('button');
btnBackF.style.cssText = 'width:100%;padding:10px;margin-top:12px;background:transparent;color:#888;border:1px solid #444;border-radius:8px;cursor:pointer;font-size:14px;'; btnBackF.style.cssText = 'width:100%;padding:10px;margin-top:12px;background:transparent;color:#888;border:1px solid #444;border-radius:8px;cursor:pointer;font-size:14px;';

View File

@ -95,6 +95,6 @@
</div> </div>
</div> </div>
<script src="game.js?v=48"></script> <script src="game.js?v=50"></script>
</body> </body>
</html> </html>