mirror of
https://github.com/619dev/PaperPhone.git
synced 2026-05-06 22:12:41 +08:00
增加消息自动删除
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
| 🗝️ 零知识服务器 | 服务器只存储密文,私钥仅在设备本地(四层持久化) |
|
||||
| 📹 视频/语音通话 | WebRTC P2P(1:1)+ Mesh(多人),Cloudflare TURN 穿透 |
|
||||
| 👥 群聊 | 最多 2000 人群组,纯文本消息(无加密),免打扰模式,成员管理 |
|
||||
| ⏱️ 消息自动删除 | 5 档可选(永不/1天/3天/1周/1月),私聊双方均可设置,群聊群主专属 |
|
||||
| 🔔 消息推送 | Web Push (VAPID) + OneSignal 双通道,离线也能收到通知 |
|
||||
| 🌐 多语言 | 中文、英文、日语、韩语、法语(自动检测 + 手动切换) |
|
||||
| 📱 iOS 永久免签 | PWA H5 → Safari「添加到主屏幕」,无需企业证书 |
|
||||
|
||||
@@ -16,6 +16,7 @@ A WeChat-style end-to-end encrypted instant messaging app with stateless ECDH +
|
||||
| 🗑️ Zero-Knowledge Server | Server stores only ciphertext; private keys never leave the device |
|
||||
| 📹 Video & Voice Calls | WebRTC P2P (1:1) + Mesh (group), Cloudflare TURN for NAT traversal |
|
||||
| 👥 Group Chat | Up to 2000 members, plain-text messages (no encryption), Do Not Disturb mode, member management |
|
||||
| ⏱️ Auto-Delete Messages | 5 tiers (never / 1 day / 3 days / 1 week / 1 month), settable by either party in DMs, owner-only in groups |
|
||||
| 🔔 Push Notifications | Web Push (VAPID) + OneSignal dual-channel — reach users even when offline |
|
||||
| 🌐 Multi-Language | Chinese, English, Japanese, Korean, French — auto-detect + manual switch |
|
||||
| 📱 iOS — No Enterprise Cert | PWA via Safari "Add to Home Screen", works permanently without Apple signing |
|
||||
|
||||
@@ -65,6 +65,8 @@ export const api = {
|
||||
leaveGroup: id => req('DELETE', `/api/groups/${id}/members/me`),
|
||||
disbandGroup: id => req('DELETE', `/api/groups/${id}`),
|
||||
muteGroup: (id, muted) => req('PATCH', `/api/groups/${id}/mute`, { muted }),
|
||||
setAutoDelete: (friendId, val) => req('PATCH', `/api/friends/${friendId}/auto-delete`, { auto_delete: val }),
|
||||
setGroupAutoDelete: (groupId, val) => req('PATCH', `/api/groups/${groupId}/auto-delete`, { auto_delete: val }),
|
||||
|
||||
// Messages
|
||||
privateHistory: pid => req('GET', `/api/messages/private/${pid}`),
|
||||
|
||||
@@ -282,6 +282,17 @@ function setupGlobalSocketHandlers() {
|
||||
}
|
||||
});
|
||||
|
||||
// ── Auto-delete setting changed by other party ───────────────────────
|
||||
onEvent('auto_delete_changed', ({ chat_id, chat_type, auto_delete }) => {
|
||||
if (chat_type === 'private') {
|
||||
const contact = state.contacts.find(c => c.id === chat_id);
|
||||
if (contact) contact.auto_delete = auto_delete;
|
||||
} else {
|
||||
const group = (state.groupsList || []).find(g => g.id === chat_id);
|
||||
if (group) group.auto_delete = auto_delete;
|
||||
}
|
||||
});
|
||||
|
||||
// ── Session Revoked (device kicked by another session) ───────────────
|
||||
onEvent('session_revoked', () => {
|
||||
clearToken();
|
||||
|
||||
@@ -184,6 +184,14 @@ const TRANSLATIONS = {
|
||||
muteGroup: '消息免打扰',
|
||||
muteEnabled: '已开启免打扰',
|
||||
muteDisabled: '已关闭免打扰',
|
||||
autoDeleteTitle: '自动删除消息',
|
||||
autoDeleteNever: '永不删除',
|
||||
autoDelete1d: '1 天后',
|
||||
autoDelete3d: '3 天后',
|
||||
autoDelete7d: '1 周后',
|
||||
autoDelete30d: '1 个月后',
|
||||
autoDeleteUpdated: '自动删除已更新',
|
||||
autoDeleteOwnerOnly: '只有群主可以设置',
|
||||
},
|
||||
|
||||
en: {
|
||||
@@ -358,6 +366,14 @@ const TRANSLATIONS = {
|
||||
muteGroup: 'Mute Notifications',
|
||||
muteEnabled: 'Notifications muted',
|
||||
muteDisabled: 'Notifications unmuted',
|
||||
autoDeleteTitle: 'Auto-Delete Messages',
|
||||
autoDeleteNever: 'Never',
|
||||
autoDelete1d: 'After 1 Day',
|
||||
autoDelete3d: 'After 3 Days',
|
||||
autoDelete7d: 'After 1 Week',
|
||||
autoDelete30d: 'After 1 Month',
|
||||
autoDeleteUpdated: 'Auto-delete updated',
|
||||
autoDeleteOwnerOnly: 'Only owner can set',
|
||||
},
|
||||
|
||||
ja: {
|
||||
@@ -532,6 +548,14 @@ const TRANSLATIONS = {
|
||||
muteGroup: '通知ミュート',
|
||||
muteEnabled: '通知をミュートしました',
|
||||
muteDisabled: '通知のミュートを解除しました',
|
||||
autoDeleteTitle: 'メッセージ自动削除',
|
||||
autoDeleteNever: '削除しない',
|
||||
autoDelete1d: '1日後',
|
||||
autoDelete3d: '3日後',
|
||||
autoDelete7d: '1週間後',
|
||||
autoDelete30d: '1か月後',
|
||||
autoDeleteUpdated: '自动削除が更新されました',
|
||||
autoDeleteOwnerOnly: 'オーナーのみ設定可',
|
||||
},
|
||||
|
||||
ko: {
|
||||
@@ -706,6 +730,14 @@ const TRANSLATIONS = {
|
||||
muteGroup: '알림 음소거',
|
||||
muteEnabled: '알림이 음소거되었습니다',
|
||||
muteDisabled: '알림 음소거가 해제되었습니다',
|
||||
autoDeleteTitle: '메시지 자동 삭제',
|
||||
autoDeleteNever: '삭제 안 함',
|
||||
autoDelete1d: '1일 후',
|
||||
autoDelete3d: '3일 후',
|
||||
autoDelete7d: '1주일 후',
|
||||
autoDelete30d: '1개월 후',
|
||||
autoDeleteUpdated: '자동 삭제가 업데이트되었습니다',
|
||||
autoDeleteOwnerOnly: '그룹장만 설정 가능',
|
||||
},
|
||||
|
||||
fr: {
|
||||
@@ -880,6 +912,14 @@ const TRANSLATIONS = {
|
||||
muteGroup: 'Mettre en sourdine',
|
||||
muteEnabled: 'Notifications désactivées',
|
||||
muteDisabled: 'Notifications réactivées',
|
||||
autoDeleteTitle: 'Suppression auto des messages',
|
||||
autoDeleteNever: 'Jamais',
|
||||
autoDelete1d: 'Après 1 jour',
|
||||
autoDelete3d: 'Après 3 jours',
|
||||
autoDelete7d: 'Après 1 semaine',
|
||||
autoDelete30d: 'Après 1 mois',
|
||||
autoDeleteUpdated: 'Suppression auto mise à jour',
|
||||
autoDeleteOwnerOnly: 'Seul le propriétaire peut définir',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -32,6 +32,12 @@ export async function renderChat(root, chat) {
|
||||
<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>
|
||||
</svg>
|
||||
</button>` : ''}
|
||||
${chat.type === 'private' ? `
|
||||
<button class="topbar-btn" id="auto-delete-btn" title="${t('autoDeleteTitle')}">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
||||
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"/>
|
||||
</svg>
|
||||
</button>` : ''}
|
||||
<button class="topbar-btn" id="voice-call-btn" title="${t('callVoice')}">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
||||
<path d="M6.6 10.8c1.4 2.8 3.8 5.1 6.6 6.6l2.2-2.2c.27-.27.67-.36 1.02-.23 1.12.45 2.34.68 3.58.68.55 0 1 .45 1 1V20c0 .55-.45 1-1 1C10.3 21 3 13.7 3 4.5c0-.55.45-1 1-1H8c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.1.35.02.74-.22 1.02L6.6 10.8z"/>
|
||||
@@ -53,6 +59,19 @@ export async function renderChat(root, chat) {
|
||||
openGroupInfo(chat.id);
|
||||
});
|
||||
|
||||
// Auto-delete button (private chats)
|
||||
topbar.querySelector('#auto-delete-btn')?.addEventListener('click', () => {
|
||||
const contact = state.contacts.find(c => c.id === chat.id);
|
||||
const current = contact?.auto_delete ?? 604800;
|
||||
showAutoDeletePicker(current, async (val) => {
|
||||
try {
|
||||
await api.setAutoDelete(chat.id, val);
|
||||
if (contact) contact.auto_delete = val;
|
||||
showToast(t('autoDeleteUpdated'));
|
||||
} catch (err) { showToast(err.message); }
|
||||
});
|
||||
});
|
||||
|
||||
// Call buttons
|
||||
topbar.querySelector('#voice-call-btn').onclick = async () => {
|
||||
if (callManager.state !== 'idle') { showToast(t('callBusy')); return; }
|
||||
@@ -531,3 +550,44 @@ function showImageViewer(src) {
|
||||
viewer.onclick = e => { if (e.target === viewer) viewer.remove(); };
|
||||
document.body.appendChild(viewer);
|
||||
}
|
||||
|
||||
function showAutoDeletePicker(current, onSelect) {
|
||||
const options = [
|
||||
{ value: 0, label: t('autoDeleteNever') },
|
||||
{ value: 86400, label: t('autoDelete1d') },
|
||||
{ value: 259200, label: t('autoDelete3d') },
|
||||
{ value: 604800, label: t('autoDelete7d') },
|
||||
{ value: 2592000, label: t('autoDelete30d') },
|
||||
];
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'modal-overlay';
|
||||
overlay.innerHTML = `
|
||||
<div class="modal-card" style="width:85%;max-width:360px;">
|
||||
<div class="modal-header">
|
||||
<div style="min-width:44px"></div>
|
||||
<div class="topbar-title" style="font-size:16px">${t('autoDeleteTitle')}</div>
|
||||
<div style="min-width:44px"></div>
|
||||
</div>
|
||||
<div id="ad-options" style="padding:8px 0;"></div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
overlay.onclick = e => { if (e.target === overlay) overlay.remove(); };
|
||||
|
||||
const list = overlay.querySelector('#ad-options');
|
||||
options.forEach(opt => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'list-item';
|
||||
row.style.cssText = 'cursor:pointer;justify-content:space-between;padding:14px 20px;';
|
||||
const isActive = current === opt.value;
|
||||
row.innerHTML = `
|
||||
<span style="font-size:15px;font-weight:${isActive ? '600' : '400'};color:${isActive ? 'var(--green)' : 'var(--text)'};">${opt.label}</span>
|
||||
${isActive ? '<svg viewBox="0 0 24 24" width="20" height="20" fill="var(--green)"><path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/></svg>' : ''}
|
||||
`;
|
||||
row.onclick = () => {
|
||||
overlay.remove();
|
||||
if (opt.value !== current) onSelect(opt.value);
|
||||
};
|
||||
list.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -116,6 +116,48 @@ export async function renderGroupInfo(root, groupId) {
|
||||
muteSection.appendChild(muteRow);
|
||||
content.appendChild(muteSection);
|
||||
|
||||
// ── Auto-Delete Setting ──
|
||||
const autoDeleteLabels = {
|
||||
0: t('autoDeleteNever'), 86400: t('autoDelete1d'),
|
||||
259200: t('autoDelete3d'), 604800: t('autoDelete7d'),
|
||||
2592000: t('autoDelete30d'),
|
||||
};
|
||||
const groupAutoDelete = info.auto_delete ?? 604800;
|
||||
|
||||
const adSection = document.createElement('div');
|
||||
adSection.className = 'settings-section';
|
||||
const adRow = document.createElement('div');
|
||||
adRow.className = 'list-item';
|
||||
adRow.style.cssText = isOwner ? 'cursor:pointer;' : '';
|
||||
adRow.innerHTML = `
|
||||
<div style="flex:1;display:flex;align-items:center;gap:10px;">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="var(--text-muted)">
|
||||
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"/>
|
||||
</svg>
|
||||
<span style="font-size:15px;font-weight:500;">${t('autoDeleteTitle')}</span>
|
||||
</div>
|
||||
<span id="ad-value" class="text-muted" style="font-size:14px;">${autoDeleteLabels[groupAutoDelete] || autoDeleteLabels[604800]}</span>
|
||||
`;
|
||||
if (isOwner) {
|
||||
adRow.onclick = () => {
|
||||
showGroupAutoDeletePicker(groupAutoDelete, async (val) => {
|
||||
try {
|
||||
await api.setGroupAutoDelete(groupId, val);
|
||||
adRow.querySelector('#ad-value').textContent = autoDeleteLabels[val];
|
||||
showToast(t('autoDeleteUpdated'));
|
||||
} catch (err) { showToast(err.message); }
|
||||
});
|
||||
};
|
||||
}
|
||||
adSection.appendChild(adRow);
|
||||
if (!isOwner) {
|
||||
const hint = document.createElement('div');
|
||||
hint.style.cssText = 'padding:4px 16px 8px;font-size:12px;color:var(--text-muted);';
|
||||
hint.textContent = t('autoDeleteOwnerOnly');
|
||||
adSection.appendChild(hint);
|
||||
}
|
||||
content.appendChild(adSection);
|
||||
|
||||
// ── Members ──
|
||||
const membersSection = document.createElement('div');
|
||||
membersSection.className = 'settings-section';
|
||||
@@ -269,3 +311,44 @@ function showAddMemberModal(groupId, existingIds, contentEl, membersSection) {
|
||||
pickerEl.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function showGroupAutoDeletePicker(current, onSelect) {
|
||||
const options = [
|
||||
{ value: 0, label: t('autoDeleteNever') },
|
||||
{ value: 86400, label: t('autoDelete1d') },
|
||||
{ value: 259200, label: t('autoDelete3d') },
|
||||
{ value: 604800, label: t('autoDelete7d') },
|
||||
{ value: 2592000, label: t('autoDelete30d') },
|
||||
];
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'modal-overlay';
|
||||
overlay.innerHTML = `
|
||||
<div class="modal-card" style="width:85%;max-width:360px;">
|
||||
<div class="modal-header">
|
||||
<div style="min-width:44px"></div>
|
||||
<div class="topbar-title" style="font-size:16px">${t('autoDeleteTitle')}</div>
|
||||
<div style="min-width:44px"></div>
|
||||
</div>
|
||||
<div id="ad-options" style="padding:8px 0;"></div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
overlay.onclick = e => { if (e.target === overlay) overlay.remove(); };
|
||||
|
||||
const list = overlay.querySelector('#ad-options');
|
||||
options.forEach(opt => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'list-item';
|
||||
row.style.cssText = 'cursor:pointer;justify-content:space-between;padding:14px 20px;';
|
||||
const isActive = current === opt.value;
|
||||
row.innerHTML = `
|
||||
<span style="font-size:15px;font-weight:${isActive ? '600' : '400'};color:${isActive ? 'var(--green)' : 'var(--text)'};">${opt.label}</span>
|
||||
${isActive ? '<svg viewBox="0 0 24 24" width="20" height="20" fill="var(--green)"><path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/></svg>' : ''}
|
||||
`;
|
||||
row.onclick = () => {
|
||||
overlay.remove();
|
||||
if (opt.value !== current) onSelect(opt.value);
|
||||
};
|
||||
list.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,12 +46,23 @@ CREATE TABLE IF NOT EXISTS friends (
|
||||
user_id VARCHAR(36) NOT NULL,
|
||||
friend_id VARCHAR(36) NOT NULL,
|
||||
status ENUM('pending','accepted','blocked') NOT NULL DEFAULT 'pending',
|
||||
auto_delete INT NOT NULL DEFAULT 604800,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_pair (user_id, friend_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (friend_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Migration: add auto_delete to friends (idempotent)
|
||||
SET @f_ad = (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'friends' AND COLUMN_NAME = 'auto_delete');
|
||||
SET @f_sql = IF(@f_ad = 0,
|
||||
'ALTER TABLE friends ADD COLUMN auto_delete INT NOT NULL DEFAULT 604800 AFTER status',
|
||||
'SELECT 1');
|
||||
PREPARE f_stmt FROM @f_sql;
|
||||
EXECUTE f_stmt;
|
||||
DEALLOCATE PREPARE f_stmt;
|
||||
|
||||
-- ── Groups ────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS `groups` (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
@@ -59,10 +70,21 @@ CREATE TABLE IF NOT EXISTS `groups` (
|
||||
avatar VARCHAR(512) DEFAULT NULL,
|
||||
owner_id VARCHAR(36) NOT NULL,
|
||||
notice TEXT DEFAULT NULL,
|
||||
auto_delete INT NOT NULL DEFAULT 604800,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (owner_id) REFERENCES users(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Migration: add auto_delete to groups (idempotent)
|
||||
SET @g_ad = (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'groups' AND COLUMN_NAME = 'auto_delete');
|
||||
SET @g_sql = IF(@g_ad = 0,
|
||||
'ALTER TABLE `groups` ADD COLUMN auto_delete INT NOT NULL DEFAULT 604800 AFTER notice',
|
||||
'SELECT 1');
|
||||
PREPARE g_stmt FROM @g_sql;
|
||||
EXECUTE g_stmt;
|
||||
DEALLOCATE PREPARE g_stmt;
|
||||
|
||||
-- ── Group Members ─────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS group_members (
|
||||
group_id VARCHAR(36) NOT NULL,
|
||||
|
||||
@@ -37,9 +37,57 @@ async function main() {
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`🚀 PaperPhone server running on http://localhost:${PORT}`);
|
||||
startMessageCleanup();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodic cleanup — delete expired messages based on auto_delete settings.
|
||||
* Runs every hour.
|
||||
*/
|
||||
function startMessageCleanup() {
|
||||
const INTERVAL = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
async function cleanup() {
|
||||
try {
|
||||
const db = getDb();
|
||||
|
||||
// Delete expired private messages
|
||||
const [privResult] = await db.query(`
|
||||
DELETE m FROM messages m
|
||||
JOIN friends f ON (
|
||||
(f.user_id = m.from_id AND f.friend_id = m.to_id)
|
||||
OR (f.user_id = m.to_id AND f.friend_id = m.from_id)
|
||||
)
|
||||
WHERE m.type = 'private'
|
||||
AND f.auto_delete > 0
|
||||
AND m.created_at < DATE_SUB(NOW(), INTERVAL f.auto_delete SECOND)
|
||||
`);
|
||||
|
||||
// Delete expired group messages
|
||||
const [grpResult] = await db.query(`
|
||||
DELETE m FROM messages m
|
||||
JOIN \`groups\` g ON g.id = m.to_id
|
||||
WHERE m.type = 'group'
|
||||
AND g.auto_delete > 0
|
||||
AND m.created_at < DATE_SUB(NOW(), INTERVAL g.auto_delete SECOND)
|
||||
`);
|
||||
|
||||
const total = (privResult.affectedRows || 0) + (grpResult.affectedRows || 0);
|
||||
if (total > 0) {
|
||||
console.log(`🗑️ Auto-deleted ${total} expired messages`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Message cleanup error:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Run once on startup, then every hour
|
||||
cleanup();
|
||||
setInterval(cleanup, INTERVAL);
|
||||
console.log('⏱️ Message auto-delete cleanup scheduled (every 1h)');
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Failed to start server:', err);
|
||||
process.exit(1);
|
||||
|
||||
@@ -13,7 +13,7 @@ router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT u.id, u.username, u.nickname, u.avatar, u.is_online, u.last_seen
|
||||
`SELECT u.id, u.username, u.nickname, u.avatar, u.is_online, u.last_seen, f.auto_delete
|
||||
FROM friends f
|
||||
JOIN users u ON u.id = f.friend_id
|
||||
WHERE f.user_id = ? AND f.status = 'accepted'`,
|
||||
@@ -98,4 +98,28 @@ router.delete('/:id', async (req, res, next) => {
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
// PATCH /api/friends/:id/auto-delete — set auto-delete timer for private chat
|
||||
router.patch('/:id/auto-delete', async (req, res, next) => {
|
||||
try {
|
||||
const allowed = [0, 86400, 259200, 604800, 2592000];
|
||||
const val = parseInt(req.body.auto_delete);
|
||||
if (!allowed.includes(val)) return res.status(400).json({ error: 'Invalid auto_delete value' });
|
||||
const db = getDb();
|
||||
// Update both directions of the friendship
|
||||
await db.query(
|
||||
`UPDATE friends SET auto_delete = ? WHERE
|
||||
(user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)`,
|
||||
[val, req.user.id, req.params.id, req.params.id, req.user.id]
|
||||
);
|
||||
// Notify the other party via WS
|
||||
sendToUser(req.params.id, {
|
||||
type: 'auto_delete_changed',
|
||||
chat_id: req.user.id,
|
||||
chat_type: 'private',
|
||||
auto_delete: val,
|
||||
});
|
||||
res.json({ ok: true, auto_delete: val });
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -14,7 +14,7 @@ router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT g.id, g.name, g.avatar, g.notice, g.owner_id, gm.role, gm.muted,
|
||||
`SELECT g.id, g.name, g.avatar, g.notice, g.owner_id, g.auto_delete, gm.role, gm.muted,
|
||||
(SELECT COUNT(*) FROM group_members WHERE group_id = g.id) AS member_count
|
||||
FROM group_members gm
|
||||
JOIN \`groups\` g ON g.id = gm.group_id
|
||||
@@ -189,6 +189,29 @@ router.delete('/:id', async (req, res, next) => {
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
// PATCH /api/groups/:id/auto-delete — set auto-delete timer (owner only)
|
||||
router.patch('/:id/auto-delete', async (req, res, next) => {
|
||||
try {
|
||||
const allowed = [0, 86400, 259200, 604800, 2592000];
|
||||
const val = parseInt(req.body.auto_delete);
|
||||
if (!allowed.includes(val)) return res.status(400).json({ error: 'Invalid auto_delete value' });
|
||||
const db = getDb();
|
||||
const [groups] = await db.query('SELECT owner_id FROM `groups` WHERE id = ?', [req.params.id]);
|
||||
if (!groups.length) return res.status(404).json({ error: 'Group not found' });
|
||||
if (groups[0].owner_id !== req.user.id) {
|
||||
return res.status(403).json({ error: 'Only the owner can set auto-delete' });
|
||||
}
|
||||
await db.query('UPDATE `groups` SET auto_delete = ? WHERE id = ?', [val, req.params.id]);
|
||||
await sendToGroup(req.params.id, {
|
||||
type: 'auto_delete_changed',
|
||||
chat_id: req.params.id,
|
||||
chat_type: 'group',
|
||||
auto_delete: val,
|
||||
});
|
||||
res.json({ ok: true, auto_delete: val });
|
||||
} catch (err) { next(err); }
|
||||
});
|
||||
|
||||
// PATCH /api/groups/:id/mute — toggle mute (any member)
|
||||
router.patch('/:id/mute', async (req, res, next) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user