From 64ca845ec54453db0db8fc55fe0de7432eb944c7 Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Thu, 16 Jul 2026 00:54:16 +0200 Subject: [PATCH] renaming --- src/inc/routes/emojis.mjs | 80 +++++++++++++++++++++++++++++++++--- src/sticker_pack_handler.mjs | 11 ++++- views/admin/emojis.html | 24 +++++++++-- 3 files changed, 104 insertions(+), 11 deletions(-) diff --git a/src/inc/routes/emojis.mjs b/src/inc/routes/emojis.mjs index dc8734c..85a6a2b 100644 --- a/src/inc/routes/emojis.mjs +++ b/src/inc/routes/emojis.mjs @@ -168,7 +168,7 @@ export default (router, tpl) => { } }); - // Rename a sticker pack (Admin) + // Rename a sticker pack (Admin) — optionally backfills emoji names and comment references router.post(/\/api\/v2\/admin\/sticker-packs\/(?\d+)\/rename/, async (req, res) => { if (!req.session || !req.session.admin) { return res.reply({ code: 403, body: JSON.stringify({ success: false, message: "Forbidden" }) }); @@ -178,17 +178,85 @@ export default (router, tpl) => { return res.reply({ code: 403, body: JSON.stringify({ success: false, message: "Invalid CSRF token" }) }); } const id = req.params.id; - const newName = (req.body?.name || req.post?.name || '').trim(); + let body = {}; + try { + const raw = req.body || req.post || {}; + body = typeof raw === 'string' ? JSON.parse(raw) : raw; + } catch (_) {} + const newName = (body.name || '').trim(); + const backfill = body.backfill === true; if (!newName) { return res.reply({ code: 400, body: JSON.stringify({ success: false, message: 'name is required' }) }); } + try { - await db`UPDATE sticker_packs SET name = ${newName} WHERE id = ${id}`; + if (!backfill) { + // Simple rename only + await db`UPDATE sticker_packs SET name = ${newName} WHERE id = ${id}`; + await db`NOTIFY emojis_updated, '{}'`; + return res.reply({ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ success: true }) }); + } + + // ── Backfill: rename emojis + update comment references ────────── + // Build new slug from new display name (same logic as import) + const newSlug = newName + .replace(/[^a-z0-9]/gi, '_') + .toLowerCase() + .replace(/_+/g, '_') + .replace(/^_|_$/, ''); + + // Load all emojis in this pack + const emojis = await db`SELECT id, name FROM custom_emojis WHERE pack_id = ${id} ORDER BY id`; + + // Build rename map: old_name → new_name + const renames = []; + for (const emoji of emojis) { + const suffixMatch = emoji.name.match(/_(\d+)$/); + if (!suffixMatch) continue; // skip non-standard names + const newEmojiName = newSlug + '_' + suffixMatch[1].padStart(3, '0'); + if (newEmojiName !== emoji.name) { + renames.push({ id: emoji.id, oldName: emoji.name, newName: newEmojiName }); + } + } + + // Apply everything in a transaction + let emojiRenamed = 0; + let commentRowsUpdated = 0; + + await db.begin(async sql => { + // Rename the pack + await sql`UPDATE sticker_packs SET name = ${newName} WHERE id = ${id}`; + + for (const r of renames) { + // Check name conflict (another emoji already has this name) + const [conflict] = await sql`SELECT id FROM custom_emojis WHERE name = ${r.newName} AND id != ${r.id} LIMIT 1`; + if (conflict) { + console.warn(`[RENAME] Skipping ${r.oldName} → ${r.newName}: name conflict`); + continue; + } + // Rename the emoji + await sql`UPDATE custom_emojis SET name = ${r.newName} WHERE id = ${r.id}`; + emojiRenamed++; + + // Replace :old_name: with :new_name: in all comments + const result = await sql` + UPDATE comments + SET content = REPLACE(content, ${':' + r.oldName + ':'}, ${':' + r.newName + ':'}) + WHERE content LIKE ${'%:' + r.oldName + ':%'} + `; + commentRowsUpdated += result.count ?? 0; + } + }); + await db`NOTIFY emojis_updated, '{}'`; - return res.reply({ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ success: true }) }); + return res.reply({ + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ success: true, emojiRenamed, commentRowsUpdated }) + }); + } catch (e) { - console.error(e); - return res.reply({ code: 500, body: JSON.stringify({ success: false, message: "Database error" }) }); + console.error('[RENAME PACK]', e); + return res.reply({ code: 500, body: JSON.stringify({ success: false, message: "Database error: " + e.message }) }); } }); diff --git a/src/sticker_pack_handler.mjs b/src/sticker_pack_handler.mjs index 375eb87..c56f02f 100644 --- a/src/sticker_pack_handler.mjs +++ b/src/sticker_pack_handler.mjs @@ -199,6 +199,14 @@ export const handleImportTelegramPack = async (req, res) => { let failed = 0; const errors = []; + // Derive a slug from the pack's display name (admin-supplied > Telegram title > URL slug) + // e.g. "My Cool Pack" → "my_cool_pack" + const safeName = packTitle + .replace(/[^a-z0-9]/gi, '_') + .toLowerCase() + .replace(/_+/g, '_') + .replace(/^_|_$/g, '') || tgName.replace(/[^a-z0-9_]/gi, '_').toLowerCase(); + for (let i = 0; i < stickers.length; i++) { const sticker = stickers[i]; try { @@ -226,8 +234,7 @@ export const handleImportTelegramPack = async (req, res) => { const downloadUrl = 'https://api.telegram.org/file/bot' + botToken + '/' + filePath; const buffer = await downloadBuffer(downloadUrl); - // Generate emoji name from pack name + index - const safeName = tgName.replace(/[^a-z0-9_]/gi, '_').toLowerCase(); + // Generate emoji name: _ const emojiName = safeName + '_' + String(i + 1).padStart(3, '0'); // Check for name conflict diff --git a/views/admin/emojis.html b/views/admin/emojis.html index ba53b2e..452a474 100644 --- a/views/admin/emojis.html +++ b/views/admin/emojis.html @@ -185,6 +185,8 @@ function renderPackGrid(packId) { var container = document.getElementById('sp-emojis-' + packId); if (!container) return; + // Preserve scroll position of the inner grid div + var prevScroll = (container.querySelector('.emoji-grid') || {}).scrollTop || 0; var pack = (window.emojiAdmin.packs || []).find(function(p) { return p.id === packId; }); var currentThumb = pack ? (pack.thumb_url || '') : ''; var packEmojis = (window.emojiAdmin.emojis || []).filter(function(e) { return e.pack_id === packId; }); @@ -197,7 +199,7 @@ var btnBg = isCurrent ? 'rgba(40,167,69,0.18)' : 'rgba(255,200,0,0.15)'; var btnClr = isCurrent ? '#4caf50' : '#ffd700'; var btnBdr = isCurrent ? 'rgba(40,167,69,0.35)' : 'rgba(255,200,0,0.3)'; - var btnLbl = isCurrent ? '✓ Current Icon' : '⭐ Set as icon'; + var btnLbl = isCurrent ? '\u2713 Current Icon' : '\u2B50 Set as icon'; return '
' + '' + stickerEl(e.url, e.name, ' loading="lazy"') + @@ -207,6 +209,9 @@ '
'; }).join('') + ''; + // Restore scroll position + var newGrid = container.querySelector('.emoji-grid'); + if (newGrid && prevScroll) newGrid.scrollTop = prevScroll; } container.dataset.loaded = '1'; } @@ -380,13 +385,26 @@ var currentName = pack ? pack.name : ''; var newName = prompt('New pack name:', currentName); if (!newName || newName.trim() === currentName) return; + newName = newName.trim(); + + var backfill = confirm('Rename all stickers too? OK = rename sticker names (old_001 -> new_001) and update comments. Cancel = pack label only.'); + fetch('/api/v2/admin/sticker-packs/' + id + '/rename', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf, 'X-Requested-With': 'XMLHttpRequest' }, - body: JSON.stringify({ name: newName.trim() }) + body: JSON.stringify({ name: newName, backfill: backfill }) }) .then(function(r) { return r.json(); }) - .then(function(data) { if (data.success) loadAll(); else alert('Rename failed: ' + (data.message || 'Unknown error')); }) + .then(function(data) { + if (data.success) { + if (backfill) { + alert('Done! ' + (data.emojiRenamed || 0) + ' sticker(s) renamed, ' + (data.commentRowsUpdated || 0) + ' comment row(s) updated.'); + } + loadAll(); + } else { + alert('Rename failed: ' + (data.message || 'Unknown error')); + } + }) .catch(function(err) { alert('Error: ' + err.message); }); }