clean up economy fallback user artifacts
This commit is contained in:
parent
a48b946754
commit
606c9452e7
1
TODO.md
1
TODO.md
@ -124,6 +124,7 @@ This file tracks larger Lumi work that cannot safely be completed in one pass. K
|
|||||||
- Review localization/translation keys if present so simplified wording remains consistent across languages.
|
- Review localization/translation keys if present so simplified wording remains consistent across languages.
|
||||||
## Done
|
## Done
|
||||||
|
|
||||||
|
- 2026-06-17: Added Economy cleanup for fallback-name artifact users such as `SammyCat-2`, merging them into matching real platform users and making Economy stats tolerate missing/mid-migration tables; bumped Economy Framework to v0.2.9.
|
||||||
- 2026-06-17: Fixed update metadata for renamed Economy plugins so legacy installed rows are folded into canonical `economy-*` plugin update rows instead of appearing as separate installs; bumped core to v0.1.8.
|
- 2026-06-17: Fixed update metadata for renamed Economy plugins so legacy installed rows are folded into canonical `economy-*` plugin update rows instead of appearing as separate installs; bumped core to v0.1.8.
|
||||||
- 2026-06-17: Renamed all remaining Economy internals from the old misspelled IDs/paths/tables to `economy-*`, added startup migration for legacy plugin rows, settings, command usage IDs, tables, uploads, asset paths, old URLs, and bumped core/plugin patch versions.
|
- 2026-06-17: Renamed all remaining Economy internals from the old misspelled IDs/paths/tables to `economy-*`, added startup migration for legacy plugin rows, settings, command usage IDs, tables, uploads, asset paths, old URLs, and bumped core/plugin patch versions.
|
||||||
- 2026-06-17: Fixed user-facing Economy spelling, restored `/stats/{username}` Compare toggling, linked Top commands run leaderboard entries to `/commands`, and bumped core/plugin patch versions.
|
- 2026-06-17: Fixed user-facing Economy spelling, restored `/stats/{username}` Compare toggling, linked Top commands run leaderboard entries to `/commands`, and bumped core/plugin patch versions.
|
||||||
|
|||||||
@ -202,6 +202,7 @@ module.exports = {
|
|||||||
const repoRoot = path.join(__dirname, "..", "..");
|
const repoRoot = path.join(__dirname, "..", "..");
|
||||||
migrateLegacyInstall(db, repoRoot);
|
migrateLegacyInstall(db, repoRoot);
|
||||||
ensureTables(db);
|
ensureTables(db);
|
||||||
|
cleanupFallbackUserArtifacts(db);
|
||||||
ensureDefaults(db);
|
ensureDefaults(db);
|
||||||
startActivityRewardFlusher(db);
|
startActivityRewardFlusher(db);
|
||||||
|
|
||||||
@ -886,6 +887,159 @@ function migrateLegacyIconPath(db) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanupFallbackUserArtifacts(db) {
|
||||||
|
if (!tableExists(db, "user_identities") || !tableExists(db, "user_profiles")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fallbackProviders = [
|
||||||
|
"economy_name",
|
||||||
|
"discord_name",
|
||||||
|
"twitch_name",
|
||||||
|
"youtube_name"
|
||||||
|
];
|
||||||
|
const rows = db
|
||||||
|
.prepare(
|
||||||
|
`SELECT user_id, provider, provider_user_id, display_name FROM user_identities ` +
|
||||||
|
`WHERE provider IN (${fallbackProviders.map(() => "?").join(", ")})`
|
||||||
|
)
|
||||||
|
.all(...fallbackProviders);
|
||||||
|
for (const row of rows) {
|
||||||
|
const name = (row.display_name || row.provider_user_id || "").trim();
|
||||||
|
if (!name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const target = findUserByKnownName(db, name, providerPlatform(row.provider), row.user_id);
|
||||||
|
if (!target || target.id === row.user_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
mergeArtifactUser(db, row.user_id, target.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function providerPlatform(provider) {
|
||||||
|
if (String(provider || "").startsWith("discord")) {
|
||||||
|
return "discord";
|
||||||
|
}
|
||||||
|
if (String(provider || "").startsWith("twitch")) {
|
||||||
|
return "twitch";
|
||||||
|
}
|
||||||
|
if (String(provider || "").startsWith("youtube")) {
|
||||||
|
return "youtube";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeArtifactUser(db, fromUserId, toUserId) {
|
||||||
|
if (!fromUserId || !toUserId || fromUserId === toUserId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
db.transaction(() => {
|
||||||
|
mergeStatsRows(db, fromUserId, toUserId);
|
||||||
|
mergeEconomyRows(db, fromUserId, toUserId);
|
||||||
|
mergeLinkedAccounts(db, fromUserId, toUserId);
|
||||||
|
mergeUserIdentities(db, fromUserId, toUserId);
|
||||||
|
db.prepare("DELETE FROM user_profiles WHERE id = ?").run(fromUserId);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeStatsRows(db, fromUserId, toUserId) {
|
||||||
|
if (!tableExists(db, "stats")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const from = db.prepare("SELECT messages, commands FROM stats WHERE user_id = ?").get(fromUserId);
|
||||||
|
if (from) {
|
||||||
|
db.prepare(
|
||||||
|
"INSERT INTO stats (user_id, messages, commands, updated_at) VALUES (?, ?, ?, ?) " +
|
||||||
|
"ON CONFLICT(user_id) DO UPDATE SET messages = messages + excluded.messages, commands = commands + excluded.commands, updated_at = excluded.updated_at"
|
||||||
|
).run(toUserId, from.messages || 0, from.commands || 0, Date.now());
|
||||||
|
db.prepare("DELETE FROM stats WHERE user_id = ?").run(fromUserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeEconomyRows(db, fromUserId, toUserId) {
|
||||||
|
if (tableExists(db, "economy_accounts")) {
|
||||||
|
const from = db.prepare("SELECT balance FROM economy_accounts WHERE user_id = ?").get(fromUserId);
|
||||||
|
if (from) {
|
||||||
|
db.prepare(
|
||||||
|
"INSERT INTO economy_accounts (user_id, balance, updated_at) VALUES (?, ?, ?) " +
|
||||||
|
"ON CONFLICT(user_id) DO UPDATE SET balance = balance + excluded.balance, updated_at = excluded.updated_at"
|
||||||
|
).run(toUserId, from.balance || 0, Date.now());
|
||||||
|
db.prepare("DELETE FROM economy_accounts WHERE user_id = ?").run(fromUserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tableExists(db, "economy_transactions")) {
|
||||||
|
db.prepare("UPDATE economy_transactions SET from_user_id = ? WHERE from_user_id = ?").run(toUserId, fromUserId);
|
||||||
|
db.prepare("UPDATE economy_transactions SET to_user_id = ? WHERE to_user_id = ?").run(toUserId, fromUserId);
|
||||||
|
}
|
||||||
|
if (tableExists(db, "economy_pot_contributions")) {
|
||||||
|
db.prepare("UPDATE economy_pot_contributions SET user_id = ? WHERE user_id = ?").run(toUserId, fromUserId);
|
||||||
|
}
|
||||||
|
if (tableExists(db, "economy_activity_reward_hourly")) {
|
||||||
|
const rewards = db
|
||||||
|
.prepare(
|
||||||
|
"SELECT hour_start, source, amount, hits, minutes FROM economy_activity_reward_hourly WHERE user_id = ?"
|
||||||
|
)
|
||||||
|
.all(fromUserId);
|
||||||
|
for (const reward of rewards) {
|
||||||
|
db.prepare(
|
||||||
|
"INSERT INTO economy_activity_reward_hourly (user_id, hour_start, source, amount, hits, minutes) VALUES (?, ?, ?, ?, ?, ?) " +
|
||||||
|
"ON CONFLICT(user_id, hour_start, source) DO UPDATE SET amount = amount + excluded.amount, hits = hits + excluded.hits, minutes = minutes + excluded.minutes"
|
||||||
|
).run(
|
||||||
|
toUserId,
|
||||||
|
reward.hour_start,
|
||||||
|
reward.source,
|
||||||
|
reward.amount || 0,
|
||||||
|
reward.hits || 0,
|
||||||
|
reward.minutes || 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
db.prepare("DELETE FROM economy_activity_reward_hourly WHERE user_id = ?").run(fromUserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeLinkedAccounts(db, fromUserId, toUserId) {
|
||||||
|
if (!tableExists(db, "linked_accounts")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const accounts = db
|
||||||
|
.prepare("SELECT id, provider FROM linked_accounts WHERE user_id = ?")
|
||||||
|
.all(fromUserId);
|
||||||
|
for (const account of accounts) {
|
||||||
|
const existing = db
|
||||||
|
.prepare("SELECT id FROM linked_accounts WHERE user_id = ? AND provider = ?")
|
||||||
|
.get(toUserId, account.provider);
|
||||||
|
if (existing) {
|
||||||
|
db.prepare("DELETE FROM linked_accounts WHERE id = ?").run(account.id);
|
||||||
|
} else {
|
||||||
|
db.prepare("UPDATE linked_accounts SET user_id = ? WHERE id = ?").run(toUserId, account.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeUserIdentities(db, fromUserId, toUserId) {
|
||||||
|
const identities = db
|
||||||
|
.prepare("SELECT id, provider, provider_user_id FROM user_identities WHERE user_id = ?")
|
||||||
|
.all(fromUserId);
|
||||||
|
for (const identity of identities) {
|
||||||
|
if (String(identity.provider || "").endsWith("_name")) {
|
||||||
|
db.prepare("DELETE FROM user_identities WHERE id = ?").run(identity.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const existing = db
|
||||||
|
.prepare("SELECT id FROM user_identities WHERE provider = ? AND provider_user_id = ? AND user_id = ?")
|
||||||
|
.get(identity.provider, identity.provider_user_id, toUserId);
|
||||||
|
if (existing) {
|
||||||
|
db.prepare("DELETE FROM user_identities WHERE id = ?").run(identity.id);
|
||||||
|
} else {
|
||||||
|
db.prepare("UPDATE user_identities SET user_id = ?, updated_at = ? WHERE id = ?").run(
|
||||||
|
toUserId,
|
||||||
|
Date.now(),
|
||||||
|
identity.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function migrateLegacyUploadDir(repoRoot) {
|
function migrateLegacyUploadDir(repoRoot) {
|
||||||
const legacyDir = path.join(repoRoot, "data", LEGACY_PLUGIN_ID);
|
const legacyDir = path.join(repoRoot, "data", LEGACY_PLUGIN_ID);
|
||||||
const currentDir = path.join(repoRoot, "data", PLUGIN_ID);
|
const currentDir = path.join(repoRoot, "data", PLUGIN_ID);
|
||||||
@ -1933,7 +2087,7 @@ function findUserByInternalName(db, name) {
|
|||||||
.get(name);
|
.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findUserByKnownName(db, name, platform) {
|
function findUserByKnownName(db, name, platform, excludeUserId = null) {
|
||||||
const cleaned = (name || "").trim();
|
const cleaned = (name || "").trim();
|
||||||
if (!cleaned) {
|
if (!cleaned) {
|
||||||
return null;
|
return null;
|
||||||
@ -1948,11 +2102,13 @@ function findUserByKnownName(db, name, platform) {
|
|||||||
"SELECT user_profiles.id AS id, user_profiles.internal_username AS internal_username " +
|
"SELECT user_profiles.id AS id, user_profiles.internal_username AS internal_username " +
|
||||||
"FROM user_identities " +
|
"FROM user_identities " +
|
||||||
"JOIN user_profiles ON user_profiles.id = user_identities.user_id " +
|
"JOIN user_profiles ON user_profiles.id = user_identities.user_id " +
|
||||||
"WHERE lower(user_identities.display_name) = lower(?) " +
|
"WHERE (lower(user_identities.display_name) = lower(?) " +
|
||||||
"OR lower(user_identities.provider_user_id) = lower(?) " +
|
"OR lower(user_identities.provider_user_id) = lower(?)) " +
|
||||||
|
"AND user_identities.user_id != ? " +
|
||||||
|
"AND user_identities.provider NOT LIKE '%_name' " +
|
||||||
`ORDER BY ${providerScore} user_identities.updated_at DESC LIMIT 1`
|
`ORDER BY ${providerScore} user_identities.updated_at DESC LIMIT 1`
|
||||||
)
|
)
|
||||||
.get(cleaned, cleaned, ...providerParams);
|
.get(cleaned, cleaned, excludeUserId || "", ...providerParams);
|
||||||
if (identity) {
|
if (identity) {
|
||||||
return identity;
|
return identity;
|
||||||
}
|
}
|
||||||
@ -1964,11 +2120,12 @@ function findUserByKnownName(db, name, platform) {
|
|||||||
"SELECT user_profiles.id AS id, user_profiles.internal_username AS internal_username " +
|
"SELECT user_profiles.id AS id, user_profiles.internal_username AS internal_username " +
|
||||||
"FROM linked_accounts " +
|
"FROM linked_accounts " +
|
||||||
"JOIN user_profiles ON user_profiles.id = linked_accounts.user_id " +
|
"JOIN user_profiles ON user_profiles.id = linked_accounts.user_id " +
|
||||||
"WHERE lower(linked_accounts.display_name) = lower(?) " +
|
"WHERE (lower(linked_accounts.display_name) = lower(?) " +
|
||||||
"OR lower(linked_accounts.provider_user_id) = lower(?) " +
|
"OR lower(linked_accounts.provider_user_id) = lower(?)) " +
|
||||||
|
"AND linked_accounts.user_id != ? " +
|
||||||
`ORDER BY ${providerScore.replaceAll("provider", "linked_accounts.provider")} linked_accounts.updated_at DESC LIMIT 1`
|
`ORDER BY ${providerScore.replaceAll("provider", "linked_accounts.provider")} linked_accounts.updated_at DESC LIMIT 1`
|
||||||
)
|
)
|
||||||
.get(cleaned, cleaned, ...providerParams);
|
.get(cleaned, cleaned, excludeUserId || "", ...providerParams);
|
||||||
return linked || null;
|
return linked || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "economy-framework",
|
"id": "economy-framework",
|
||||||
"name": "Economy Framework",
|
"name": "Economy Framework",
|
||||||
"version": "0.2.8",
|
"version": "0.2.9",
|
||||||
"description": "Cross-platform currency framework with shared balances and extensible hooks.",
|
"description": "Cross-platform currency framework with shared balances and extensible hooks.",
|
||||||
"main": "index.js"
|
"main": "index.js"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,30 +2,43 @@ function getProfileStats({ db, userId }) {
|
|||||||
if (!userId) {
|
if (!userId) {
|
||||||
return { stats: [] };
|
return { stats: [] };
|
||||||
}
|
}
|
||||||
|
const accountsTable = resolveTable(db, "accounts");
|
||||||
|
const transactionsTable = resolveTable(db, "transactions");
|
||||||
|
if (!accountsTable || !transactionsTable) {
|
||||||
|
return {
|
||||||
|
stats: [
|
||||||
|
{ label: "Balance", value: 0 },
|
||||||
|
{ label: "Total earned", value: 0 },
|
||||||
|
{ label: "Total spent", value: 0 },
|
||||||
|
{ label: "Given to others", value: 0 },
|
||||||
|
{ label: "Received from others", value: 0 }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
const account = db
|
const account = db
|
||||||
.prepare("SELECT balance FROM economy_accounts WHERE user_id = ?")
|
.prepare(`SELECT balance FROM ${quoteIdentifier(accountsTable)} WHERE user_id = ?`)
|
||||||
.get(userId);
|
.get(userId);
|
||||||
const earned = db
|
const earned = db
|
||||||
.prepare(
|
.prepare(
|
||||||
"SELECT COALESCE(SUM(amount), 0) AS total FROM economy_transactions " +
|
`SELECT COALESCE(SUM(amount), 0) AS total FROM ${quoteIdentifier(transactionsTable)} ` +
|
||||||
"WHERE to_user_id = ? AND (from_user_id IS NULL OR from_user_id = '')"
|
"WHERE to_user_id = ? AND (from_user_id IS NULL OR from_user_id = '')"
|
||||||
)
|
)
|
||||||
.get(userId);
|
.get(userId);
|
||||||
const spent = db
|
const spent = db
|
||||||
.prepare(
|
.prepare(
|
||||||
"SELECT COALESCE(SUM(amount), 0) AS total FROM economy_transactions " +
|
`SELECT COALESCE(SUM(amount), 0) AS total FROM ${quoteIdentifier(transactionsTable)} ` +
|
||||||
"WHERE from_user_id = ? AND (to_user_id IS NULL OR to_user_id = '')"
|
"WHERE from_user_id = ? AND (to_user_id IS NULL OR to_user_id = '')"
|
||||||
)
|
)
|
||||||
.get(userId);
|
.get(userId);
|
||||||
const transfersOut = db
|
const transfersOut = db
|
||||||
.prepare(
|
.prepare(
|
||||||
"SELECT COALESCE(SUM(amount), 0) AS total FROM economy_transactions " +
|
`SELECT COALESCE(SUM(amount), 0) AS total FROM ${quoteIdentifier(transactionsTable)} ` +
|
||||||
"WHERE from_user_id = ? AND to_user_id IS NOT NULL AND to_user_id != ''"
|
"WHERE from_user_id = ? AND to_user_id IS NOT NULL AND to_user_id != ''"
|
||||||
)
|
)
|
||||||
.get(userId);
|
.get(userId);
|
||||||
const transfersIn = db
|
const transfersIn = db
|
||||||
.prepare(
|
.prepare(
|
||||||
"SELECT COALESCE(SUM(amount), 0) AS total FROM economy_transactions " +
|
`SELECT COALESCE(SUM(amount), 0) AS total FROM ${quoteIdentifier(transactionsTable)} ` +
|
||||||
"WHERE to_user_id = ? AND from_user_id IS NOT NULL AND from_user_id != ''"
|
"WHERE to_user_id = ? AND from_user_id IS NOT NULL AND from_user_id != ''"
|
||||||
)
|
)
|
||||||
.get(userId);
|
.get(userId);
|
||||||
@ -42,12 +55,24 @@ function getProfileStats({ db, userId }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getLeaderboards({ db, limit = 10 }) {
|
function getLeaderboards({ db, limit = 10 }) {
|
||||||
|
const accountsTable = resolveTable(db, "accounts");
|
||||||
|
if (!accountsTable) {
|
||||||
|
return {
|
||||||
|
boards: [
|
||||||
|
{
|
||||||
|
title: "Top balances",
|
||||||
|
valueLabel: "Balance",
|
||||||
|
rows: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
const rows = db
|
const rows = db
|
||||||
.prepare(
|
.prepare(
|
||||||
"SELECT user_profiles.internal_username AS username, economy_accounts.balance AS value " +
|
`SELECT user_profiles.internal_username AS username, ${quoteIdentifier(accountsTable)}.balance AS value ` +
|
||||||
"FROM economy_accounts " +
|
`FROM ${quoteIdentifier(accountsTable)} ` +
|
||||||
"JOIN user_profiles ON user_profiles.id = economy_accounts.user_id " +
|
`JOIN user_profiles ON user_profiles.id = ${quoteIdentifier(accountsTable)}.user_id ` +
|
||||||
"ORDER BY economy_accounts.balance DESC LIMIT ?"
|
`ORDER BY ${quoteIdentifier(accountsTable)}.balance DESC LIMIT ?`
|
||||||
)
|
)
|
||||||
.all(limit);
|
.all(limit);
|
||||||
|
|
||||||
@ -62,6 +87,29 @@ function getLeaderboards({ db, limit = 10 }) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveTable(db, suffix) {
|
||||||
|
const legacyStem = ["echo", "nomy"].join("");
|
||||||
|
const current = `economy_${suffix}`;
|
||||||
|
const legacy = `${legacyStem}_${suffix}`;
|
||||||
|
if (tableExists(db, current)) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
if (tableExists(db, legacy)) {
|
||||||
|
return legacy;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tableExists(db, name) {
|
||||||
|
return Boolean(
|
||||||
|
db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function quoteIdentifier(name) {
|
||||||
|
return `"${name.replace(/"/g, '""')}"`;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getProfileStats,
|
getProfileStats,
|
||||||
getLeaderboards
|
getLeaderboards
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user