This commit is contained in:
2026-07-16 00:54:16 +02:00
parent d3550f9648
commit 64ca845ec5
3 changed files with 104 additions and 11 deletions

View File

@@ -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\/(?<id>\d+)\/rename/, async (req, res) => { router.post(/\/api\/v2\/admin\/sticker-packs\/(?<id>\d+)\/rename/, async (req, res) => {
if (!req.session || !req.session.admin) { if (!req.session || !req.session.admin) {
return res.reply({ code: 403, body: JSON.stringify({ success: false, message: "Forbidden" }) }); 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" }) }); return res.reply({ code: 403, body: JSON.stringify({ success: false, message: "Invalid CSRF token" }) });
} }
const id = req.params.id; 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) { if (!newName) {
return res.reply({ code: 400, body: JSON.stringify({ success: false, message: 'name is required' }) }); return res.reply({ code: 400, body: JSON.stringify({ success: false, message: 'name is required' }) });
} }
try { try {
if (!backfill) {
// Simple rename only
await db`UPDATE sticker_packs SET name = ${newName} WHERE id = ${id}`; await db`UPDATE sticker_packs SET name = ${newName} WHERE id = ${id}`;
await db`NOTIFY emojis_updated, '{}'`; 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 }) });
}
// ── 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, emojiRenamed, commentRowsUpdated })
});
} catch (e) { } catch (e) {
console.error(e); console.error('[RENAME PACK]', e);
return res.reply({ code: 500, body: JSON.stringify({ success: false, message: "Database error" }) }); return res.reply({ code: 500, body: JSON.stringify({ success: false, message: "Database error: " + e.message }) });
} }
}); });

View File

@@ -199,6 +199,14 @@ export const handleImportTelegramPack = async (req, res) => {
let failed = 0; let failed = 0;
const errors = []; 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++) { for (let i = 0; i < stickers.length; i++) {
const sticker = stickers[i]; const sticker = stickers[i];
try { try {
@@ -226,8 +234,7 @@ export const handleImportTelegramPack = async (req, res) => {
const downloadUrl = 'https://api.telegram.org/file/bot' + botToken + '/' + filePath; const downloadUrl = 'https://api.telegram.org/file/bot' + botToken + '/' + filePath;
const buffer = await downloadBuffer(downloadUrl); const buffer = await downloadBuffer(downloadUrl);
// Generate emoji name from pack name + index // Generate emoji name: <pack_display_slug>_<padded_index>
const safeName = tgName.replace(/[^a-z0-9_]/gi, '_').toLowerCase();
const emojiName = safeName + '_' + String(i + 1).padStart(3, '0'); const emojiName = safeName + '_' + String(i + 1).padStart(3, '0');
// Check for name conflict // Check for name conflict

View File

@@ -185,6 +185,8 @@
function renderPackGrid(packId) { function renderPackGrid(packId) {
var container = document.getElementById('sp-emojis-' + packId); var container = document.getElementById('sp-emojis-' + packId);
if (!container) return; 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 pack = (window.emojiAdmin.packs || []).find(function(p) { return p.id === packId; });
var currentThumb = pack ? (pack.thumb_url || '') : ''; var currentThumb = pack ? (pack.thumb_url || '') : '';
var packEmojis = (window.emojiAdmin.emojis || []).filter(function(e) { return e.pack_id === packId; }); 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 btnBg = isCurrent ? 'rgba(40,167,69,0.18)' : 'rgba(255,200,0,0.15)';
var btnClr = isCurrent ? '#4caf50' : '#ffd700'; var btnClr = isCurrent ? '#4caf50' : '#ffd700';
var btnBdr = isCurrent ? 'rgba(40,167,69,0.35)' : 'rgba(255,200,0,0.3)'; var btnBdr = isCurrent ? 'rgba(40,167,69,0.35)' : 'rgba(255,200,0,0.3)';
var btnLbl = isCurrent ? '&#10003; Current Icon' : '&#11088; Set as icon'; var btnLbl = isCurrent ? '\u2713 Current Icon' : '\u2B50 Set as icon';
return '<div class="emoji-card" style="position:relative;">' + return '<div class="emoji-card" style="position:relative;">' +
'<button class="emoji-delete" onclick="window.emojiAdmin.deleteEmoji(' + e.id + ')" title="Delete">x</button>' + '<button class="emoji-delete" onclick="window.emojiAdmin.deleteEmoji(' + e.id + ')" title="Delete">x</button>' +
stickerEl(e.url, e.name, ' loading="lazy"') + stickerEl(e.url, e.name, ' loading="lazy"') +
@@ -207,6 +209,9 @@
'</div>'; '</div>';
}).join('') + }).join('') +
'</div>'; '</div>';
// Restore scroll position
var newGrid = container.querySelector('.emoji-grid');
if (newGrid && prevScroll) newGrid.scrollTop = prevScroll;
} }
container.dataset.loaded = '1'; container.dataset.loaded = '1';
} }
@@ -380,13 +385,26 @@
var currentName = pack ? pack.name : ''; var currentName = pack ? pack.name : '';
var newName = prompt('New pack name:', currentName); var newName = prompt('New pack name:', currentName);
if (!newName || newName.trim() === currentName) return; 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', { fetch('/api/v2/admin/sticker-packs/' + id + '/rename', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf, 'X-Requested-With': 'XMLHttpRequest' }, 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(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); }); .catch(function(err) { alert('Error: ' + err.message); });
} }