rename economy plugins and migrate legacy ids

This commit is contained in:
Franz Rolfsvaag 2026-06-17 22:32:51 +02:00
parent 471218a79d
commit 580b4392b4
23 changed files with 1430 additions and 1060 deletions

View File

@ -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: 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.
- 2026-06-17: Fixed custom command Edit buttons and `/commands` Copy Link / expand buttons with delegated handlers, clipboard fallback, and v0.1.5 patch bump. - 2026-06-17: Fixed custom command Edit buttons and `/commands` Copy Link / expand buttons with delegated handlers, clipboard fallback, and v0.1.5 patch bump.
- 2026-06-17: Fixed repo-based core updates deleting `data/update-cache/repo` during apply, added a verification guard, and bumped core package version to v0.1.4. - 2026-06-17: Fixed repo-based core updates deleting `data/update-cache/repo` during apply, added a verification guard, and bumped core package version to v0.1.4.

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "lumi-bot", "name": "lumi-bot",
"version": "0.1.6", "version": "0.1.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "lumi-bot", "name": "lumi-bot",
"version": "0.1.6", "version": "0.1.7",
"dependencies": { "dependencies": {
"adm-zip": "^0.5.12", "adm-zip": "^0.5.12",
"better-sqlite3": "^11.5.0", "better-sqlite3": "^11.5.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "lumi-bot", "name": "lumi-bot",
"version": "0.1.6", "version": "0.1.7",
"private": true, "private": true,
"type": "commonjs", "type": "commonjs",
"scripts": { "scripts": {

View File

@ -591,7 +591,7 @@ function reserveDelivery(db, userId, deliveryKey, type, status, details) {
function grantGiftOnce(db, userId, deliveryKey, mode) { function grantGiftOnce(db, userId, deliveryKey, mode) {
const config = getConfig(db); const config = getConfig(db);
const amount = config.gift_amount; const amount = config.gift_amount;
const framework = global.lumiFrameworks?.echonomy; const framework = global.lumiFrameworks?.economy;
if (!amount || !framework || typeof framework.addBalance !== "function") { if (!amount || !framework || typeof framework.addBalance !== "function") {
return null; return null;
} }
@ -919,7 +919,7 @@ async function buildDiagnostics(discordClient, config) {
discordAvailable: Boolean(discordClient), discordAvailable: Boolean(discordClient),
discordReady: Boolean(discordClient?.readyAt), discordReady: Boolean(discordClient?.readyAt),
channel, channel,
echonomyAvailable: Boolean(global.lumiFrameworks?.echonomy?.addBalance), economyAvailable: Boolean(global.lumiFrameworks?.economy?.addBalance),
currentDate: getZonedDateParts(config.timezone) currentDate: getZonedDateParts(config.timezone)
}; };
} }

View File

@ -1,7 +1,7 @@
{ {
"id": "birthday", "id": "birthday",
"name": "Birthday", "name": "Birthday",
"version": "0.1.1", "version": "0.1.2",
"description": "Birthday profiles, announcements, lookup commands, and optional birthday currency gifts.", "description": "Birthday profiles, announcements, lookup commands, and optional birthday currency gifts.",
"main": "index.js" "main": "index.js"
} }

View File

@ -12,7 +12,7 @@
<tbody> <tbody>
<tr><th>Discord client</th><td><%= diagnostics.discordAvailable ? (diagnostics.discordReady ? "Ready" : "Available") : "Unavailable" %></td></tr> <tr><th>Discord client</th><td><%= diagnostics.discordAvailable ? (diagnostics.discordReady ? "Ready" : "Available") : "Unavailable" %></td></tr>
<tr><th>Announcement channel</th><td><%= diagnostics.channel.message %></td></tr> <tr><th>Announcement channel</th><td><%= diagnostics.channel.message %></td></tr>
<tr><th>Economy</th><td><%= diagnostics.echonomyAvailable ? "Available" : "Unavailable" %></td></tr> <tr><th>Economy</th><td><%= diagnostics.economyAvailable ? "Available" : "Unavailable" %></td></tr>
<tr><th>Current plugin date</th><td><%= diagnostics.currentDate.year %>-<%= String(diagnostics.currentDate.month).padStart(2, "0") %>-<%= String(diagnostics.currentDate.day).padStart(2, "0") %></td></tr> <tr><th>Current plugin date</th><td><%= diagnostics.currentDate.year %>-<%= String(diagnostics.currentDate.month).padStart(2, "0") %>-<%= String(diagnostics.currentDate.day).padStart(2, "0") %></td></tr>
</tbody> </tbody>
</table> </table>

View File

@ -1,5 +1,5 @@
{ {
"pluginId": "echonomy-framework", "pluginId": "economy-framework",
"pluginName": "Economy Framework", "pluginName": "Economy Framework",
"platformKeys": { "platformKeys": {
"discord": "platform_discord", "discord": "platform_discord",

View File

@ -7,7 +7,11 @@ const multer = require("multer");
const EventEmitter = require("events"); const EventEmitter = require("events");
const { ensureUserForIdentity } = require("../../src/services/users"); const { ensureUserForIdentity } = require("../../src/services/users");
const PLUGIN_ID = "echonomy-framework"; const PLUGIN_ID = "economy-framework";
const LEGACY_STEM = ["echo", "nomy"].join("");
const LEGACY_PLUGIN_ID = `${LEGACY_STEM}-framework`;
const LEGACY_FRAMEWORK_KEY = LEGACY_STEM;
const LEGACY_NAME_PROVIDER = `${LEGACY_STEM}_name`;
const DEFAULT_SETTINGS = { const DEFAULT_SETTINGS = {
currency_name: "Coin", currency_name: "Coin",
currency_name_plural: "Coins", currency_name_plural: "Coins",
@ -195,6 +199,8 @@ module.exports = {
twitchClient twitchClient
}) { }) {
settingsApi = settings; settingsApi = settings;
const repoRoot = path.join(__dirname, "..", "..");
migrateLegacyInstall(db, repoRoot);
ensureTables(db); ensureTables(db);
ensureDefaults(db); ensureDefaults(db);
startActivityRewardFlusher(db); startActivityRewardFlusher(db);
@ -207,8 +213,7 @@ module.exports = {
attachTwitchListeners({ db, settings, twitchClient }); attachTwitchListeners({ db, settings, twitchClient });
installProfileHook(app, () => getConfig(db)); installProfileHook(app, () => getConfig(db));
const repoRoot = path.join(__dirname, "..", ".."); const uploadDir = path.join(repoRoot, "data", "economy-framework");
const uploadDir = path.join(repoRoot, "data", "echonomy-framework");
fs.mkdirSync(uploadDir, { recursive: true }); fs.mkdirSync(uploadDir, { recursive: true });
const upload = multer({ const upload = multer({
dest: uploadDir, dest: uploadDir,
@ -239,7 +244,7 @@ module.exports = {
const events = getCustomEvents(config); const events = getCustomEvents(config);
const responses = Object.values(config.responses || {}); const responses = Object.values(config.responses || {});
res.render(path.join(__dirname, "views", "echonomy.ejs"), { res.render(path.join(__dirname, "views", "economy.ejs"), {
title: "Economy Framework", title: "Economy Framework",
config, config,
user, user,
@ -707,7 +712,7 @@ module.exports = {
return res.redirect("/profile/banking"); return res.redirect("/profile/banking");
} }
const fund = db const fund = db
.prepare("SELECT * FROM echonomy_pots WHERE id = ?") .prepare("SELECT * FROM economy_pots WHERE id = ?")
.get(req.params.id); .get(req.params.id);
if (!fund || fund.status !== "active") { if (!fund || fund.status !== "active") {
req.session.flash = { req.session.flash = {
@ -737,6 +742,7 @@ module.exports = {
role: "public", role: "public",
section: "plugins" section: "plugins"
}); });
mountLegacyRedirect(web, LEGACY_PLUGIN_ID, PLUGIN_ID);
web.mount("/profile/banking", bankRouter); web.mount("/profile/banking", bankRouter);
} }
}; };
@ -750,13 +756,13 @@ function deny(res) {
function ensureTables(db) { function ensureTables(db) {
db.exec(` db.exec(`
CREATE TABLE IF NOT EXISTS echonomy_accounts ( CREATE TABLE IF NOT EXISTS economy_accounts (
user_id TEXT PRIMARY KEY, user_id TEXT PRIMARY KEY,
balance INTEGER NOT NULL DEFAULT 0, balance INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL updated_at INTEGER NOT NULL
); );
CREATE TABLE IF NOT EXISTS echonomy_transactions ( CREATE TABLE IF NOT EXISTS economy_transactions (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
type TEXT NOT NULL, type TEXT NOT NULL,
amount INTEGER NOT NULL, amount INTEGER NOT NULL,
@ -767,7 +773,7 @@ function ensureTables(db) {
created_at INTEGER NOT NULL created_at INTEGER NOT NULL
); );
CREATE TABLE IF NOT EXISTS echonomy_pots ( CREATE TABLE IF NOT EXISTS economy_pots (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE,
description TEXT, description TEXT,
@ -778,7 +784,7 @@ function ensureTables(db) {
updated_at INTEGER NOT NULL updated_at INTEGER NOT NULL
); );
CREATE TABLE IF NOT EXISTS echonomy_pot_contributions ( CREATE TABLE IF NOT EXISTS economy_pot_contributions (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
pot_id TEXT NOT NULL, pot_id TEXT NOT NULL,
user_id TEXT NOT NULL, user_id TEXT NOT NULL,
@ -786,7 +792,7 @@ function ensureTables(db) {
created_at INTEGER NOT NULL created_at INTEGER NOT NULL
); );
CREATE TABLE IF NOT EXISTS echonomy_activity_reward_hourly ( CREATE TABLE IF NOT EXISTS economy_activity_reward_hourly (
user_id TEXT NOT NULL, user_id TEXT NOT NULL,
hour_start INTEGER NOT NULL, hour_start INTEGER NOT NULL,
source TEXT NOT NULL, source TEXT NOT NULL,
@ -796,14 +802,162 @@ function ensureTables(db) {
PRIMARY KEY (user_id, hour_start, source) PRIMARY KEY (user_id, hour_start, source)
); );
CREATE INDEX IF NOT EXISTS echonomy_transactions_created_at_idx CREATE INDEX IF NOT EXISTS economy_transactions_created_at_idx
ON echonomy_transactions (created_at); ON economy_transactions (created_at);
CREATE INDEX IF NOT EXISTS echonomy_activity_reward_hourly_hour_idx CREATE INDEX IF NOT EXISTS economy_activity_reward_hourly_hour_idx
ON echonomy_activity_reward_hourly (hour_start); ON economy_activity_reward_hourly (hour_start);
`); `);
} }
function migrateLegacyInstall(db, repoRoot) {
migratePluginSettings(db);
migrateLegacyTables(db);
migrateLegacyNameProvider(db);
migrateLegacyIconPath(db);
migrateLegacyUploadDir(repoRoot);
mergeCommandUsage(db, `${LEGACY_FRAMEWORK_KEY}:root`, "economy:root");
}
function migratePluginSettings(db) {
const rows = db
.prepare("SELECT key, value, updated_at FROM plugin_settings WHERE plugin_id = ?")
.all(LEGACY_PLUGIN_ID);
if (!rows.length) {
return;
}
const insert = db.prepare(
"INSERT OR IGNORE INTO plugin_settings (plugin_id, key, value, updated_at) VALUES (?, ?, ?, ?)"
);
for (const row of rows) {
insert.run(PLUGIN_ID, row.key, row.value, row.updated_at || Date.now());
}
db.prepare("DELETE FROM plugin_settings WHERE plugin_id = ?").run(LEGACY_PLUGIN_ID);
}
function migrateLegacyTables(db) {
[
"accounts",
"transactions",
"pots",
"pot_contributions",
"activity_reward_hourly"
].forEach((suffix) => {
migrateTable(db, `${LEGACY_STEM}_${suffix}`, `economy_${suffix}`);
});
}
function migrateTable(db, legacyName, currentName) {
if (!tableExists(db, legacyName)) {
return;
}
if (!tableExists(db, currentName)) {
db.prepare(`ALTER TABLE ${quoteIdentifier(legacyName)} RENAME TO ${quoteIdentifier(currentName)}`).run();
return;
}
db.prepare(
`INSERT OR IGNORE INTO ${quoteIdentifier(currentName)} SELECT * FROM ${quoteIdentifier(legacyName)}`
).run();
db.prepare(`DROP TABLE ${quoteIdentifier(legacyName)}`).run();
}
function migrateLegacyNameProvider(db) {
if (!tableExists(db, "linked_accounts")) {
return;
}
db.prepare("UPDATE linked_accounts SET provider = ? WHERE provider = ?").run(
"economy_name",
LEGACY_NAME_PROVIDER
);
}
function migrateLegacyIconPath(db) {
const legacyPrefix = `/plugins/${LEGACY_PLUGIN_ID}/assets/`;
const currentPrefix = `/plugins/${PLUGIN_ID}/assets/`;
const rows = db
.prepare(
"SELECT key, value FROM plugin_settings WHERE plugin_id = ? AND key = 'currency_icon_path'"
)
.all(PLUGIN_ID);
for (const row of rows) {
if (row.value && row.value.startsWith(legacyPrefix)) {
setPluginSetting(db, row.key, row.value.replace(legacyPrefix, currentPrefix));
}
}
}
function migrateLegacyUploadDir(repoRoot) {
const legacyDir = path.join(repoRoot, "data", LEGACY_PLUGIN_ID);
const currentDir = path.join(repoRoot, "data", PLUGIN_ID);
if (!fs.existsSync(legacyDir)) {
return;
}
if (!fs.existsSync(currentDir)) {
fs.renameSync(legacyDir, currentDir);
return;
}
mergeDirectory(legacyDir, currentDir);
}
function mergeDirectory(sourceDir, targetDir) {
fs.mkdirSync(targetDir, { recursive: true });
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
const sourcePath = path.join(sourceDir, entry.name);
const targetPath = path.join(targetDir, entry.name);
if (!fs.existsSync(targetPath)) {
fs.renameSync(sourcePath, targetPath);
continue;
}
if (entry.isDirectory()) {
mergeDirectory(sourcePath, targetPath);
}
}
try {
fs.rmSync(sourceDir, { recursive: true, force: true });
} catch {
// Leaving the old directory is safer than failing startup.
}
}
function mergeCommandUsage(db, legacyId, currentId) {
if (!tableExists(db, "command_usage")) {
return;
}
const legacy = db
.prepare("SELECT count, updated_at FROM command_usage WHERE command_id = ?")
.get(legacyId);
if (!legacy) {
return;
}
db.prepare(
"INSERT INTO command_usage (command_id, count, updated_at) VALUES (?, ?, ?) " +
"ON CONFLICT(command_id) DO UPDATE SET count = command_usage.count + excluded.count, updated_at = MAX(command_usage.updated_at, excluded.updated_at)"
).run(currentId, legacy.count || 0, legacy.updated_at || Date.now());
db.prepare("DELETE FROM command_usage WHERE command_id = ?").run(legacyId);
}
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, '""')}"`;
}
function mountLegacyRedirect(web, legacyId, currentId) {
const legacyMount = `/plugins/${legacyId}`;
const router = web.createRouter();
router.use((req, res) => {
const suffix = req.originalUrl.startsWith(legacyMount)
? req.originalUrl.slice(legacyMount.length)
: "";
res.redirect(308, `/plugins/${currentId}${suffix || ""}`);
});
web.mount(legacyMount, router);
}
function ensureDefaults(db) { function ensureDefaults(db) {
const existing = getPluginSettings(db); const existing = getPluginSettings(db);
for (const [key, value] of Object.entries(DEFAULT_SETTINGS)) { for (const [key, value] of Object.entries(DEFAULT_SETTINGS)) {
@ -1058,7 +1212,7 @@ function queueActivityReward(
const numericHits = Number.isFinite(Number(hits)) ? Number(hits) : 0; const numericHits = Number.isFinite(Number(hits)) ? Number(hits) : 0;
const numericMinutes = Number.isFinite(Number(minutes)) ? Number(minutes) : 0; const numericMinutes = Number.isFinite(Number(minutes)) ? Number(minutes) : 0;
db.prepare( db.prepare(
"INSERT INTO echonomy_activity_reward_hourly (user_id, hour_start, source, amount, hits, minutes) " + "INSERT INTO economy_activity_reward_hourly (user_id, hour_start, source, amount, hits, minutes) " +
"VALUES (?, ?, ?, ?, ?, ?) " + "VALUES (?, ?, ?, ?, ?, ?) " +
"ON CONFLICT(user_id, hour_start, source) DO UPDATE SET " + "ON CONFLICT(user_id, hour_start, source) DO UPDATE SET " +
"amount = amount + excluded.amount, " + "amount = amount + excluded.amount, " +
@ -1093,7 +1247,7 @@ function flushActivityRewards(db) {
const rows = db const rows = db
.prepare( .prepare(
"SELECT user_id, hour_start, source, amount, hits, minutes " + "SELECT user_id, hour_start, source, amount, hits, minutes " +
"FROM echonomy_activity_reward_hourly " + "FROM economy_activity_reward_hourly " +
"WHERE hour_start < ? " + "WHERE hour_start < ? " +
"ORDER BY hour_start ASC" "ORDER BY hour_start ASC"
) )
@ -1129,7 +1283,7 @@ function flushActivityRewards(db) {
); );
if (totalAmount <= 0) { if (totalAmount <= 0) {
db.prepare( db.prepare(
"DELETE FROM echonomy_activity_reward_hourly WHERE user_id = ? AND hour_start = ?" "DELETE FROM economy_activity_reward_hourly WHERE user_id = ? AND hour_start = ?"
).run(group.userId, group.hourStart); ).run(group.userId, group.hourStart);
continue; continue;
} }
@ -1146,7 +1300,7 @@ function flushActivityRewards(db) {
} }
}); });
db.prepare( db.prepare(
"DELETE FROM echonomy_activity_reward_hourly WHERE user_id = ? AND hour_start = ?" "DELETE FROM economy_activity_reward_hourly WHERE user_id = ? AND hour_start = ?"
).run(group.userId, group.hourStart); ).run(group.userId, group.hourStart);
} catch (error) { } catch (error) {
console.error("Failed to apply queued activity reward", error); console.error("Failed to apply queued activity reward", error);
@ -1158,7 +1312,8 @@ function registerFramework(api) {
if (!global.lumiFrameworks) { if (!global.lumiFrameworks) {
global.lumiFrameworks = {}; global.lumiFrameworks = {};
} }
global.lumiFrameworks.echonomy = api; global.lumiFrameworks.economy = api;
global.lumiFrameworks[LEGACY_FRAMEWORK_KEY] = api;
} }
function buildApi({ db }) { function buildApi({ db }) {
@ -1200,7 +1355,7 @@ function registerCommands({ db, settings, commandRouter }) {
const triggers = [config.command.root, ...config.command.aliases]; const triggers = [config.command.root, ...config.command.aliases];
commandRouter.registerCommands(PLUGIN_ID, [ commandRouter.registerCommands(PLUGIN_ID, [
{ {
id: "echonomy:root", id: "economy:root",
triggers, triggers,
platforms, platforms,
handler: (ctx) => handleCoinsCommand({ ctx, db, settings }) handler: (ctx) => handleCoinsCommand({ ctx, db, settings })
@ -1642,10 +1797,10 @@ function escapeHtml(value) {
} }
function installProfileHook(app, getConfig) { function installProfileHook(app, getConfig) {
if (!app || app.__echonomyProfileHookInstalled) { if (!app || app.__economyProfileHookInstalled) {
return; return;
} }
app.__echonomyProfileHookInstalled = true; app.__economyProfileHookInstalled = true;
const originalRender = app.render.bind(app); const originalRender = app.render.bind(app);
app.render = (view, options, callback) => { app.render = (view, options, callback) => {
if (typeof options === "function") { if (typeof options === "function") {
@ -1700,8 +1855,9 @@ async function resolveTargetUser(db, ctx, token) {
} }
if (ctx.platform === "discord") { if (ctx.platform === "discord") {
const message = ctx.meta?.message; const message = ctx.meta?.message;
if (message?.mentions?.users?.first) { const mentionId = token.match(/^<@!?(\d+)>$/)?.[1] || null;
const mention = message.mentions.users.first(); const mention = mentionId ? message?.mentions?.users?.get?.(mentionId) : null;
if (mention?.id) {
const display = const display =
mention.globalName || mention.username || mention.tag || mention.id; mention.globalName || mention.username || mention.tag || mention.id;
const profile = ensureUserForIdentity({ const profile = ensureUserForIdentity({
@ -1714,7 +1870,7 @@ async function resolveTargetUser(db, ctx, token) {
}); });
return { profile, label: `<@${mention.id}>` }; return { profile, label: `<@${mention.id}>` };
} }
const idMatch = token.match(/^<@!?(\d+)>$/) || token.match(/^(\d{15,})$/); const idMatch = token.match(/^(\d{15,})$/) || (mentionId ? [token, mentionId] : null);
if (idMatch) { if (idMatch) {
const profile = ensureUserForIdentity({ const profile = ensureUserForIdentity({
provider: "discord", provider: "discord",
@ -1735,6 +1891,11 @@ async function resolveTargetUser(db, ctx, token) {
return { profile: internal, label: internal.internal_username }; return { profile: internal, label: internal.internal_username };
} }
const knownUser = findUserByKnownName(db, cleaned, ctx.platform);
if (knownUser) {
return { profile: knownUser, label: knownUser.internal_username };
}
if (ctx.platform === "twitch") { if (ctx.platform === "twitch") {
const profile = ensureUserForIdentity({ const profile = ensureUserForIdentity({
provider: "twitch_login", provider: "twitch_login",
@ -1756,7 +1917,7 @@ async function resolveTargetUser(db, ctx, token) {
} }
const profile = ensureUserForIdentity({ const profile = ensureUserForIdentity({
provider: "echonomy_name", provider: "economy_name",
providerUserId: cleaned.toLowerCase(), providerUserId: cleaned.toLowerCase(),
displayName: cleaned, displayName: cleaned,
fallbackName: cleaned fallbackName: cleaned
@ -1770,11 +1931,63 @@ function findUserByInternalName(db, name) {
"SELECT id, internal_username FROM user_profiles WHERE lower(internal_username) = lower(?)" "SELECT id, internal_username FROM user_profiles WHERE lower(internal_username) = lower(?)"
) )
.get(name); .get(name);
} }
function findUserByKnownName(db, name, platform) {
const cleaned = (name || "").trim();
if (!cleaned) {
return null;
}
const providerHints = getPlatformProviderHints(platform);
const providerScore = providerHints.length
? `CASE WHEN provider IN (${providerHints.map(() => "?").join(", ")}) THEN 0 ELSE 1 END,`
: "";
const providerParams = providerHints.length ? providerHints : [];
const identity = db
.prepare(
"SELECT user_profiles.id AS id, user_profiles.internal_username AS internal_username " +
"FROM user_identities " +
"JOIN user_profiles ON user_profiles.id = user_identities.user_id " +
"WHERE lower(user_identities.display_name) = lower(?) " +
"OR lower(user_identities.provider_user_id) = lower(?) " +
`ORDER BY ${providerScore} user_identities.updated_at DESC LIMIT 1`
)
.get(cleaned, cleaned, ...providerParams);
if (identity) {
return identity;
}
if (!tableExists(db, "linked_accounts")) {
return null;
}
const linked = db
.prepare(
"SELECT user_profiles.id AS id, user_profiles.internal_username AS internal_username " +
"FROM linked_accounts " +
"JOIN user_profiles ON user_profiles.id = linked_accounts.user_id " +
"WHERE lower(linked_accounts.display_name) = lower(?) " +
"OR lower(linked_accounts.provider_user_id) = lower(?) " +
`ORDER BY ${providerScore.replaceAll("provider", "linked_accounts.provider")} linked_accounts.updated_at DESC LIMIT 1`
)
.get(cleaned, cleaned, ...providerParams);
return linked || null;
}
function getPlatformProviderHints(platform) {
if (platform === "discord") {
return ["discord"];
}
if (platform === "twitch") {
return ["twitch", "twitch_login"];
}
if (platform === "youtube") {
return ["youtube", "youtube_name"];
}
return [];
}
function ensureAccount(db, userId) { function ensureAccount(db, userId) {
db.prepare( db.prepare(
"INSERT INTO echonomy_accounts (user_id, balance, updated_at) VALUES (?, 0, ?) " + "INSERT INTO economy_accounts (user_id, balance, updated_at) VALUES (?, 0, ?) " +
"ON CONFLICT(user_id) DO UPDATE SET updated_at = excluded.updated_at" "ON CONFLICT(user_id) DO UPDATE SET updated_at = excluded.updated_at"
).run(userId, Date.now()); ).run(userId, Date.now());
} }
@ -1784,7 +1997,7 @@ function getBalance(db, userId) {
return 0; return 0;
} }
const row = db const row = db
.prepare("SELECT balance FROM echonomy_accounts WHERE user_id = ?") .prepare("SELECT balance FROM economy_accounts WHERE user_id = ?")
.get(userId); .get(userId);
return row?.balance ?? 0; return row?.balance ?? 0;
} }
@ -1792,7 +2005,7 @@ function getBalance(db, userId) {
function updateBalance(db, userId, delta) { function updateBalance(db, userId, delta) {
ensureAccount(db, userId); ensureAccount(db, userId);
db.prepare( db.prepare(
"UPDATE echonomy_accounts SET balance = balance + ?, updated_at = ? WHERE user_id = ?" "UPDATE economy_accounts SET balance = balance + ?, updated_at = ? WHERE user_id = ?"
).run(delta, Date.now(), userId); ).run(delta, Date.now(), userId);
} }
@ -1838,7 +2051,7 @@ function applyTransaction(db, payload) {
updateBalance(db, toUserId, amount); updateBalance(db, toUserId, amount);
} }
db.prepare( db.prepare(
"INSERT INTO echonomy_transactions (id, type, amount, from_user_id, to_user_id, note, meta, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" "INSERT INTO economy_transactions (id, type, amount, from_user_id, to_user_id, note, meta, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
).run( ).run(
id, id,
payload.type || "transaction", payload.type || "transaction",
@ -1951,7 +2164,7 @@ function listTransactions(db, { userId, limit }) {
return db return db
.prepare( .prepare(
"SELECT t.*, fromUser.internal_username AS from_name, toUser.internal_username AS to_name " + "SELECT t.*, fromUser.internal_username AS from_name, toUser.internal_username AS to_name " +
"FROM echonomy_transactions t " + "FROM economy_transactions t " +
"LEFT JOIN user_profiles AS fromUser ON fromUser.id = t.from_user_id " + "LEFT JOIN user_profiles AS fromUser ON fromUser.id = t.from_user_id " +
"LEFT JOIN user_profiles AS toUser ON toUser.id = t.to_user_id " + "LEFT JOIN user_profiles AS toUser ON toUser.id = t.to_user_id " +
`${where} ORDER BY t.created_at DESC LIMIT ?` `${where} ORDER BY t.created_at DESC LIMIT ?`
@ -2013,16 +2226,16 @@ function parseTransactionMeta(rawMeta) {
function buildGlobalStats(db) { function buildGlobalStats(db) {
const totalBalance = db const totalBalance = db
.prepare("SELECT COALESCE(SUM(balance), 0) AS total FROM echonomy_accounts") .prepare("SELECT COALESCE(SUM(balance), 0) AS total FROM economy_accounts")
.get(); .get();
const totalSpent = db const totalSpent = db
.prepare( .prepare(
"SELECT COALESCE(SUM(amount), 0) AS total FROM echonomy_transactions " + "SELECT COALESCE(SUM(amount), 0) AS total FROM economy_transactions " +
"WHERE from_user_id IS NOT NULL AND (to_user_id IS NULL OR to_user_id = '')" "WHERE from_user_id IS NOT NULL AND (to_user_id IS NULL OR to_user_id = '')"
) )
.get(); .get();
const totalTransactions = db const totalTransactions = db
.prepare("SELECT COUNT(*) AS count FROM echonomy_transactions") .prepare("SELECT COUNT(*) AS count FROM economy_transactions")
.get(); .get();
return { return {
totalBalance: totalBalance?.total || 0, totalBalance: totalBalance?.total || 0,
@ -2044,25 +2257,25 @@ function buildUserStats(db, userId) {
const balance = getBalance(db, userId); const balance = getBalance(db, userId);
const totalEarned = db const totalEarned = db
.prepare( .prepare(
"SELECT COALESCE(SUM(amount), 0) AS total FROM echonomy_transactions " + "SELECT COALESCE(SUM(amount), 0) AS total FROM economy_transactions " +
"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 totalSpent = db const totalSpent = db
.prepare( .prepare(
"SELECT COALESCE(SUM(amount), 0) AS total FROM echonomy_transactions " + "SELECT COALESCE(SUM(amount), 0) AS total FROM economy_transactions " +
"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 totalReceived = db const totalReceived = db
.prepare( .prepare(
"SELECT COALESCE(SUM(amount), 0) AS total FROM echonomy_transactions " + "SELECT COALESCE(SUM(amount), 0) AS total FROM economy_transactions " +
"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);
const totalSent = db const totalSent = db
.prepare( .prepare(
"SELECT COALESCE(SUM(amount), 0) AS total FROM echonomy_transactions " + "SELECT COALESCE(SUM(amount), 0) AS total FROM economy_transactions " +
"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);
@ -2078,17 +2291,17 @@ function buildUserStats(db, userId) {
function listTopBalances(db, limit) { function listTopBalances(db, limit) {
return db return db
.prepare( .prepare(
"SELECT user_profiles.internal_username AS username, echonomy_accounts.balance AS balance " + "SELECT user_profiles.internal_username AS username, economy_accounts.balance AS balance " +
"FROM echonomy_accounts " + "FROM economy_accounts " +
"JOIN user_profiles ON user_profiles.id = echonomy_accounts.user_id " + "JOIN user_profiles ON user_profiles.id = economy_accounts.user_id " +
"ORDER BY echonomy_accounts.balance DESC LIMIT ?" "ORDER BY economy_accounts.balance DESC LIMIT ?"
) )
.all(limit); .all(limit);
} }
function listFunds(db) { function listFunds(db) {
return db return db
.prepare("SELECT * FROM echonomy_pots WHERE status != 'archived' ORDER BY name") .prepare("SELECT * FROM economy_pots WHERE status != 'archived' ORDER BY name")
.all(); .all();
} }
@ -2100,7 +2313,7 @@ function formatProviderLabel(provider) {
twitch_login: "Twitch", twitch_login: "Twitch",
youtube: "YouTube", youtube: "YouTube",
youtube_name: "YouTube", youtube_name: "YouTube",
echonomy_name: "Internal" economy_name: "Internal"
}; };
if (map[normalized]) { if (map[normalized]) {
return map[normalized]; return map[normalized];
@ -2145,20 +2358,20 @@ function listUserDirectory(db) {
function findFund(db, name) { function findFund(db, name) {
return db return db
.prepare("SELECT * FROM echonomy_pots WHERE lower(name) = lower(?)") .prepare("SELECT * FROM economy_pots WHERE lower(name) = lower(?)")
.get(name); .get(name);
} }
function createFund(db, { name, description, targetAmount }) { function createFund(db, { name, description, targetAmount }) {
const now = Date.now(); const now = Date.now();
db.prepare( db.prepare(
"INSERT INTO echonomy_pots (id, name, description, target_amount, current_amount, status, created_at, updated_at) VALUES (?, ?, ?, ?, 0, 'active', ?, ?)" "INSERT INTO economy_pots (id, name, description, target_amount, current_amount, status, created_at, updated_at) VALUES (?, ?, ?, ?, 0, 'active', ?, ?)"
).run(crypto.randomUUID(), name, description || "", targetAmount || 0, now, now); ).run(crypto.randomUUID(), name, description || "", targetAmount || 0, now, now);
} }
function updateFund(db, { id, name, description, targetAmount, status }) { function updateFund(db, { id, name, description, targetAmount, status }) {
db.prepare( db.prepare(
"UPDATE echonomy_pots SET name = ?, description = ?, target_amount = ?, status = ?, updated_at = ? WHERE id = ?" "UPDATE economy_pots SET name = ?, description = ?, target_amount = ?, status = ?, updated_at = ? WHERE id = ?"
).run( ).run(
name, name,
description || "", description || "",
@ -2173,10 +2386,10 @@ function addFundContribution(db, fundId, userId, amount) {
const now = Date.now(); const now = Date.now();
db.transaction(() => { db.transaction(() => {
db.prepare( db.prepare(
"INSERT INTO echonomy_pot_contributions (id, pot_id, user_id, amount, created_at) VALUES (?, ?, ?, ?, ?)" "INSERT INTO economy_pot_contributions (id, pot_id, user_id, amount, created_at) VALUES (?, ?, ?, ?, ?)"
).run(crypto.randomUUID(), fundId, userId, amount, now); ).run(crypto.randomUUID(), fundId, userId, amount, now);
db.prepare( db.prepare(
"UPDATE echonomy_pots SET current_amount = current_amount + ?, updated_at = ? WHERE id = ?" "UPDATE economy_pots SET current_amount = current_amount + ?, updated_at = ? WHERE id = ?"
).run(amount, now, fundId); ).run(amount, now, fundId);
})(); })();
} }

View File

@ -1,7 +1,7 @@
{ {
"id": "echonomy-framework", "id": "economy-framework",
"name": "Economy Framework", "name": "Economy Framework",
"version": "0.2.7", "version": "0.2.8",
"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"
} }

View File

@ -3,29 +3,29 @@ function getProfileStats({ db, userId }) {
return { stats: [] }; return { stats: [] };
} }
const account = db const account = db
.prepare("SELECT balance FROM echonomy_accounts WHERE user_id = ?") .prepare("SELECT balance FROM economy_accounts WHERE user_id = ?")
.get(userId); .get(userId);
const earned = db const earned = db
.prepare( .prepare(
"SELECT COALESCE(SUM(amount), 0) AS total FROM echonomy_transactions " + "SELECT COALESCE(SUM(amount), 0) AS total FROM economy_transactions " +
"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 echonomy_transactions " + "SELECT COALESCE(SUM(amount), 0) AS total FROM economy_transactions " +
"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 echonomy_transactions " + "SELECT COALESCE(SUM(amount), 0) AS total FROM economy_transactions " +
"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 echonomy_transactions " + "SELECT COALESCE(SUM(amount), 0) AS total FROM economy_transactions " +
"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);
@ -44,10 +44,10 @@ function getProfileStats({ db, userId }) {
function getLeaderboards({ db, limit = 10 }) { function getLeaderboards({ db, limit = 10 }) {
const rows = db const rows = db
.prepare( .prepare(
"SELECT user_profiles.internal_username AS username, echonomy_accounts.balance AS value " + "SELECT user_profiles.internal_username AS username, economy_accounts.balance AS value " +
"FROM echonomy_accounts " + "FROM economy_accounts " +
"JOIN user_profiles ON user_profiles.id = echonomy_accounts.user_id " + "JOIN user_profiles ON user_profiles.id = economy_accounts.user_id " +
"ORDER BY echonomy_accounts.balance DESC LIMIT ?" "ORDER BY economy_accounts.balance DESC LIMIT ?"
) )
.all(limit); .all(limit);

View File

@ -1,5 +1,5 @@
{ {
"pluginId": "echonomy-framework", "pluginId": "economy-framework",
"pluginName": "Economy Framework", "pluginName": "Economy Framework",
"provider": "stats.js", "provider": "stats.js",
"profile": { "profile": {

View File

@ -1,12 +1,12 @@
<%- include("../../../src/web/views/partials/layout-top", { title }) %> <%- include("../../../src/web/views/partials/layout-top", { title }) %>
<style> <style>
.echonomy-grid { .economy-grid {
display: grid; display: grid;
gap: 16px; gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
} }
.echonomy-card { .economy-card {
padding: 14px; padding: 14px;
border-radius: 14px; border-radius: 14px;
background: var(--surface-2); background: var(--surface-2);
@ -14,28 +14,28 @@
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
} }
.echonomy-label { .economy-label {
color: var(--ink-soft); color: var(--ink-soft);
font-size: 0.85rem; font-size: 0.85rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.08em;
} }
.echonomy-value { .economy-value {
font-size: 1.4rem; font-size: 1.4rem;
font-weight: 700; font-weight: 700;
} }
.echonomy-currency { .economy-currency {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
} }
.echonomy-currency img { .economy-currency img {
width: 32px; width: 32px;
height: 32px; height: 32px;
border-radius: 8px; border-radius: 8px;
object-fit: cover; object-fit: cover;
} }
.echonomy-list { .economy-list {
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
@ -43,7 +43,7 @@
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
} }
.echonomy-list li { .economy-list li {
background: var(--surface-2); background: var(--surface-2);
padding: 10px 12px; padding: 10px 12px;
border-radius: 12px; border-radius: 12px;
@ -52,7 +52,7 @@
justify-content: space-between; justify-content: space-between;
gap: 10px; gap: 10px;
} }
.echonomy-table td small { .economy-table td small {
color: var(--ink-soft); color: var(--ink-soft);
} }
.response-grid { .response-grid {
@ -133,7 +133,7 @@
<div> <div>
<h1>Economy Framework</h1> <h1>Economy Framework</h1>
<p class="command-subtitle">Unified, cross-platform currency tooling and stats.</p> <p class="command-subtitle">Unified, cross-platform currency tooling and stats.</p>
<div class="echonomy-currency"> <div class="economy-currency">
<% if (config.currency.icon) { %> <% if (config.currency.icon) { %>
<img src="<%= config.currency.icon %>" alt="Currency icon" /> <img src="<%= config.currency.icon %>" alt="Currency icon" />
<% } %> <% } %>
@ -146,27 +146,27 @@
<section class="card"> <section class="card">
<h2>Overview</h2> <h2>Overview</h2>
<div class="echonomy-grid"> <div class="economy-grid">
<div class="echonomy-card"> <div class="economy-card">
<span class="echonomy-label">Your balance</span> <span class="economy-label">Your balance</span>
<span class="echonomy-value"><%= userBalance %></span> <span class="economy-value"><%= userBalance %></span>
</div> </div>
<div class="echonomy-card"> <div class="economy-card">
<span class="echonomy-label">Command root</span> <span class="economy-label">Command root</span>
<span class="echonomy-value">!<%= config.command.root %></span> <span class="economy-value">!<%= config.command.root %></span>
</div> </div>
<div class="echonomy-card"> <div class="economy-card">
<span class="echonomy-label">Cooldown</span> <span class="economy-label">Cooldown</span>
<span class="echonomy-value"><%= config.cooldownSeconds %>s</span> <span class="economy-value"><%= config.cooldownSeconds %>s</span>
</div> </div>
<% if (isAdmin) { %> <% if (isAdmin) { %>
<div class="echonomy-card"> <div class="economy-card">
<span class="echonomy-label">Total in circulation</span> <span class="economy-label">Total in circulation</span>
<span class="echonomy-value"><%= globalStats.totalBalance %></span> <span class="economy-value"><%= globalStats.totalBalance %></span>
</div> </div>
<div class="echonomy-card"> <div class="economy-card">
<span class="echonomy-label">Total spent</span> <span class="economy-label">Total spent</span>
<span class="echonomy-value"><%= globalStats.totalSpent %></span> <span class="economy-value"><%= globalStats.totalSpent %></span>
</div> </div>
<% } %> <% } %>
</div> </div>
@ -174,7 +174,7 @@
<% if (isAdmin) { %> <% if (isAdmin) { %>
<section class="card"> <section class="card">
<h2>Currency settings</h2> <h2>Currency settings</h2>
<form method="post" action="/plugins/echonomy-framework/settings/currency" class="form-grid"> <form method="post" action="/plugins/economy-framework/settings/currency" class="form-grid">
<div class="field"> <div class="field">
<label>Currency name (singular)</label> <label>Currency name (singular)</label>
<input name="currency_name" value="<%= config.currency.name %>" /> <input name="currency_name" value="<%= config.currency.name %>" />
@ -199,7 +199,7 @@
<section class="card"> <section class="card">
<h2>Currency icon</h2> <h2>Currency icon</h2>
<form method="post" action="/plugins/echonomy-framework/settings/icon" enctype="multipart/form-data" class="form-grid"> <form method="post" action="/plugins/economy-framework/settings/icon" enctype="multipart/form-data" class="form-grid">
<div class="field"> <div class="field">
<label>Upload PNG icon</label> <label>Upload PNG icon</label>
<input type="file" name="currency_icon" accept="image/png" /> <input type="file" name="currency_icon" accept="image/png" />
@ -211,7 +211,7 @@
<section class="card"> <section class="card">
<h2>Banking labels</h2> <h2>Banking labels</h2>
<form method="post" action="/plugins/echonomy-framework/settings/banking" class="form-grid"> <form method="post" action="/plugins/economy-framework/settings/banking" class="form-grid">
<div class="field"> <div class="field">
<label>Banking page label</label> <label>Banking page label</label>
<input name="banking_label" value="<%= config.banking.label %>" /> <input name="banking_label" value="<%= config.banking.label %>" />
@ -244,7 +244,7 @@
<section class="card"> <section class="card">
<h2>Platforms</h2> <h2>Platforms</h2>
<form method="post" action="/plugins/echonomy-framework/settings/platforms" class="form-grid"> <form method="post" action="/plugins/economy-framework/settings/platforms" class="form-grid">
<div class="field"> <div class="field">
<label>Enable on Discord</label> <label>Enable on Discord</label>
<label class="switch"> <label class="switch">
@ -290,7 +290,7 @@
<section class="card"> <section class="card">
<h2>Currency earning rules</h2> <h2>Currency earning rules</h2>
<form method="post" action="/plugins/echonomy-framework/settings/earn" class="form-grid"> <form method="post" action="/plugins/economy-framework/settings/earn" class="form-grid">
<div class="field"> <div class="field">
<label>Discord message rewards</label> <label>Discord message rewards</label>
<label class="switch"> <label class="switch">
@ -360,7 +360,7 @@
<section class="card"> <section class="card">
<h2>Monetization tiers</h2> <h2>Monetization tiers</h2>
<form method="post" action="/plugins/echonomy-framework/settings/tiers" class="form-grid"> <form method="post" action="/plugins/economy-framework/settings/tiers" class="form-grid">
<div class="field"> <div class="field">
<label>Discord server booster multiplier</label> <label>Discord server booster multiplier</label>
<input name="tier_discord_booster_multiplier" value="<%= config.tiers.discordBooster %>" /> <input name="tier_discord_booster_multiplier" value="<%= config.tiers.discordBooster %>" />
@ -390,7 +390,7 @@
<% if (!funds.length) { %> <% if (!funds.length) { %>
<p>No <%= config.communityFunds.plural.toLowerCase() %> configured yet.</p> <p>No <%= config.communityFunds.plural.toLowerCase() %> configured yet.</p>
<% } else { %> <% } else { %>
<ul class="echonomy-list"> <ul class="economy-list">
<% funds.forEach((fund) => { %> <% funds.forEach((fund) => { %>
<li> <li>
<span><strong><%= fund.name %></strong> - <%= fund.current_amount %>/<%= fund.target_amount %></span> <span><strong><%= fund.name %></strong> - <%= fund.current_amount %>/<%= fund.target_amount %></span>
@ -401,7 +401,7 @@
<% } %> <% } %>
<% if (isAdmin) { %> <% if (isAdmin) { %>
<h3>Create <%= config.communityFunds.name %></h3> <h3>Create <%= config.communityFunds.name %></h3>
<form method="post" action="/plugins/echonomy-framework/funds/create" class="form-grid"> <form method="post" action="/plugins/economy-framework/funds/create" class="form-grid">
<div class="field"> <div class="field">
<label>Name</label> <label>Name</label>
<input name="name" /> <input name="name" />
@ -419,7 +419,7 @@
<h3>Update <%= config.communityFunds.plural %></h3> <h3>Update <%= config.communityFunds.plural %></h3>
<% funds.forEach((fund) => { %> <% funds.forEach((fund) => { %>
<form method="post" action="/plugins/echonomy-framework/funds/<%= fund.id %>/update" class="form-grid"> <form method="post" action="/plugins/economy-framework/funds/<%= fund.id %>/update" class="form-grid">
<div class="field"> <div class="field">
<label>Name</label> <label>Name</label>
<input name="name" value="<%= fund.name %>" /> <input name="name" value="<%= fund.name %>" />
@ -450,18 +450,18 @@
<% if (!events.length) { %> <% if (!events.length) { %>
<p>No custom events configured yet.</p> <p>No custom events configured yet.</p>
<% } else { %> <% } else { %>
<ul class="echonomy-list"> <ul class="economy-list">
<% events.forEach((event) => { %> <% events.forEach((event) => { %>
<li> <li>
<span><strong><%= event.name %></strong> (<%= event.amount %>)</span> <span><strong><%= event.name %></strong> (<%= event.amount %>)</span>
<form method="post" action="/plugins/echonomy-framework/events/<%= event.id %>/delete"> <form method="post" action="/plugins/economy-framework/events/<%= event.id %>/delete">
<button type="submit" class="button subtle">Delete</button> <button type="submit" class="button subtle">Delete</button>
</form> </form>
</li> </li>
<% }) %> <% }) %>
</ul> </ul>
<% } %> <% } %>
<form method="post" action="/plugins/echonomy-framework/events/create" class="form-grid"> <form method="post" action="/plugins/economy-framework/events/create" class="form-grid">
<div class="field"> <div class="field">
<label>Event name</label> <label>Event name</label>
<input name="name" /> <input name="name" />
@ -502,7 +502,7 @@
<h3>Edit: <%= response.label %></h3> <h3>Edit: <%= response.label %></h3>
<button type="button" class="button subtle" data-modal-close>Close</button> <button type="button" class="button subtle" data-modal-close>Close</button>
</div> </div>
<form method="post" action="/plugins/echonomy-framework/settings/responses" data-response-form> <form method="post" action="/plugins/economy-framework/settings/responses" data-response-form>
<input type="hidden" name="response_key" value="<%= response.key %>" /> <input type="hidden" name="response_key" value="<%= response.key %>" />
<div class="field"> <div class="field">
<label>Selection mode</label> <label>Selection mode</label>
@ -536,7 +536,7 @@
<% if (isMod) { %> <% if (isMod) { %>
<section class="card"> <section class="card">
<h2>Adjust user balance</h2> <h2>Adjust user balance</h2>
<form method="post" action="/plugins/echonomy-framework/accounts/adjust" class="form-grid"> <form method="post" action="/plugins/economy-framework/accounts/adjust" class="form-grid">
<div class="field"> <div class="field">
<label>Username</label> <label>Username</label>
<input name="username" /> <input name="username" />
@ -594,12 +594,12 @@
type="search" type="search"
placeholder="Search transactions" placeholder="Search transactions"
aria-label="Search transactions" aria-label="Search transactions"
data-table-filter="echonomy-transactions" data-table-filter="economy-transactions"
/> />
<div class="table-controls"> <div class="table-controls">
<label class="table-page-size"> <label class="table-page-size">
Show Show
<select data-table-size="echonomy-transactions"> <select data-table-size="economy-transactions">
<option value="25">25</option> <option value="25">25</option>
<option value="50">50</option> <option value="50">50</option>
<option value="100">100</option> <option value="100">100</option>
@ -611,8 +611,8 @@
</div> </div>
<div class="table-wrap"> <div class="table-wrap">
<table <table
class="table echonomy-table" class="table economy-table"
data-table="echonomy-transactions" data-table="economy-transactions"
data-pageable="true" data-pageable="true"
data-page-size="25" data-page-size="25"
data-page-sizes="25,50,100,250" data-page-sizes="25,50,100,250"
@ -680,7 +680,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="table-pagination" data-table-pagination="echonomy-transactions"> <div class="table-pagination" data-table-pagination="economy-transactions">
<button type="button" class="button subtle" data-page-prev>Previous</button> <button type="button" class="button subtle" data-page-prev>Previous</button>
<span class="table-page-label" data-page-label>Page 1 of 1</span> <span class="table-page-label" data-page-label>Page 1 of 1</span>
<button type="button" class="button subtle" data-page-next>Next</button> <button type="button" class="button subtle" data-page-next>Next</button>

View File

@ -1,5 +1,5 @@
{ {
"pluginId": "echonomy-games", "pluginId": "economy-games",
"pluginName": "Economy Games", "pluginName": "Economy Games",
"commands": [ "commands": [
{ {

View File

@ -1,7 +1,7 @@
{ {
"id": "echonomy-games", "id": "economy-games",
"name": "Economy Games", "name": "Economy Games",
"version": "0.1.6", "version": "0.1.7",
"description": "Cross-platform mini-games that use the Economy currency framework.", "description": "Cross-platform mini-games that use the Economy currency framework.",
"main": "index.js" "main": "index.js"
} }

View File

@ -214,7 +214,7 @@
</summary> </summary>
<div class="game-row__body"> <div class="game-row__body">
<section class="game-card"> <section class="game-card">
<form method="post" action="/plugins/echonomy-games/settings/hotpotato" class="form-grid"> <form method="post" action="/plugins/economy-games/settings/hotpotato" class="form-grid">
<div class="field"> <div class="field">
<label>Enable Hot Potato</label> <label>Enable Hot Potato</label>
<label class="switch"> <label class="switch">
@ -315,7 +315,7 @@
<summary>Edit replies</summary> <summary>Edit replies</summary>
<div class="reply-body"> <div class="reply-body">
<p class="hint">One reply per line. Tokens: {user}, {target}, {amount}, {payout}, {seconds}, {min}, {max}, {game}, {holder}, {loss}, {winners}, {trigger}.</p> <p class="hint">One reply per line. Tokens: {user}, {target}, {amount}, {payout}, {seconds}, {min}, {max}, {game}, {holder}, {loss}, {winners}, {trigger}.</p>
<form method="post" action="/plugins/echonomy-games/settings/responses" class="form-grid responses-grid"> <form method="post" action="/plugins/economy-games/settings/responses" class="form-grid responses-grid">
<% responsesByGame.hotpotato.forEach((response) => { %> <% responsesByGame.hotpotato.forEach((response) => { %>
<div class="field response-field"> <div class="field response-field">
<label><%= response.label %></label> <label><%= response.label %></label>
@ -359,7 +359,7 @@
</summary> </summary>
<div class="game-row__body"> <div class="game-row__body">
<section class="game-card"> <section class="game-card">
<form method="post" action="/plugins/echonomy-games/settings/coinflip" class="form-grid"> <form method="post" action="/plugins/economy-games/settings/coinflip" class="form-grid">
<div class="field"> <div class="field">
<label>Enable Coinflip</label> <label>Enable Coinflip</label>
<label class="switch"> <label class="switch">
@ -447,7 +447,7 @@
<summary>Edit replies</summary> <summary>Edit replies</summary>
<div class="reply-body"> <div class="reply-body">
<p class="hint">One reply per line. Tokens: {user}, {amount}, {payout}, {min}, {max}, {seconds}.</p> <p class="hint">One reply per line. Tokens: {user}, {amount}, {payout}, {min}, {max}, {seconds}.</p>
<form method="post" action="/plugins/echonomy-games/settings/responses" class="form-grid responses-grid"> <form method="post" action="/plugins/economy-games/settings/responses" class="form-grid responses-grid">
<% responsesByGame.coinflip.forEach((response) => { %> <% responsesByGame.coinflip.forEach((response) => { %>
<div class="field response-field"> <div class="field response-field">
<label><%= response.label %></label> <label><%= response.label %></label>
@ -491,7 +491,7 @@
</summary> </summary>
<div class="game-row__body"> <div class="game-row__body">
<section class="game-card"> <section class="game-card">
<form method="post" action="/plugins/echonomy-games/settings/mystery" class="form-grid"> <form method="post" action="/plugins/economy-games/settings/mystery" class="form-grid">
<div class="field"> <div class="field">
<label>Enable Mystery Box</label> <label>Enable Mystery Box</label>
<label class="switch"> <label class="switch">
@ -579,7 +579,7 @@
<summary>Edit replies</summary> <summary>Edit replies</summary>
<div class="reply-body"> <div class="reply-body">
<p class="hint">One reply per line. Tokens: {user}, {amount}, {payout}, {min}, {max}, {seconds}.</p> <p class="hint">One reply per line. Tokens: {user}, {amount}, {payout}, {min}, {max}, {seconds}.</p>
<form method="post" action="/plugins/echonomy-games/settings/responses" class="form-grid responses-grid"> <form method="post" action="/plugins/economy-games/settings/responses" class="form-grid responses-grid">
<% responsesByGame.mystery.forEach((response) => { %> <% responsesByGame.mystery.forEach((response) => { %>
<div class="field response-field"> <div class="field response-field">
<label><%= response.label %></label> <label><%= response.label %></label>

View File

@ -12,7 +12,7 @@ const ROUTE_ALIASES = Object.freeze({
"/admin/youtube-wizard": "youtube configuration config settings wizard location where integration", "/admin/youtube-wizard": "youtube configuration config settings wizard location where integration",
"/admin/plugins": "plugin plugins settings configuration management", "/admin/plugins": "plugin plugins settings configuration management",
"/admin/commands": "command commands custom command settings management", "/admin/commands": "command commands custom command settings management",
"/plugins/echonomy-framework": "economy echonomy currency points banking settings", "/plugins/economy-framework": "economy currency points banking settings",
"/plugins/moderation": "moderation mod tools sanctions bans timeouts settings" "/plugins/moderation": "moderation mod tools sanctions bans timeouts settings"
}); });

View File

@ -944,7 +944,7 @@ async function tryWhisper(client, username, message) {
} }
function distributeBanAssets(db, subjectId, { reason }) { function distributeBanAssets(db, subjectId, { reason }) {
const framework = global.lumiFrameworks?.echonomy; const framework = global.lumiFrameworks?.economy;
if (!framework) { if (!framework) {
return; return;
} }

View File

@ -1,7 +1,7 @@
{ {
"id": "moderation", "id": "moderation",
"name": "Moderation Center", "name": "Moderation Center",
"version": "0.1.3", "version": "0.1.4",
"description": "Cross-platform moderation actions, notes, and sanctions.", "description": "Cross-platform moderation actions, notes, and sanctions.",
"main": "index.js" "main": "index.js"
} }

View File

@ -5,6 +5,17 @@ const { db } = require("./db");
const pluginsDir = path.join(__dirname, "..", "..", "plugins"); const pluginsDir = path.join(__dirname, "..", "..", "plugins");
const cleanupHandlers = []; const cleanupHandlers = [];
const legacyEconomyStem = ["echo", "nomy"].join("");
const economyPluginAliases = {
"economy-framework": [`${legacyEconomyStem}-framework`],
"economy-games": [`${legacyEconomyStem}-games`]
};
const economyCommandAliases = [
{ from: `${legacyEconomyStem}:root`, to: "economy:root" },
{ from: `${legacyEconomyStem}-games:hotpotato`, to: "economy-games:hotpotato" },
{ from: `${legacyEconomyStem}-games:coinflip`, to: "economy-games:coinflip" },
{ from: `${legacyEconomyStem}-games:mystery`, to: "economy-games:mystery" }
];
function readJson(filePath) { function readJson(filePath) {
const raw = fs.readFileSync(filePath, "utf8"); const raw = fs.readFileSync(filePath, "utf8");
@ -36,7 +47,8 @@ function scanPluginDirectories() {
version: manifest.version || "0.0.0", version: manifest.version || "0.0.0",
description: manifest.description || "", description: manifest.description || "",
main: manifest.main || "index.js", main: manifest.main || "index.js",
dir: path.join(pluginsDir, entry.name) dir: path.join(pluginsDir, entry.name),
legacyIds: economyPluginAliases[manifest.id] || []
}); });
} catch { } catch {
continue; continue;
@ -55,6 +67,7 @@ function syncPluginRegistry() {
); );
for (const plugin of plugins) { for (const plugin of plugins) {
migratePluginAliases(plugin, now);
insert.run( insert.run(
plugin.id, plugin.id,
plugin.name, plugin.name,
@ -65,9 +78,70 @@ function syncPluginRegistry() {
now now
); );
} }
migrateCommandUsageAliases();
return plugins; return plugins;
} }
function migratePluginAliases(plugin, now) {
for (const legacyId of plugin.legacyIds || []) {
const existing = db.prepare("SELECT * FROM plugins WHERE id = ?").get(plugin.id);
const legacy = db.prepare("SELECT * FROM plugins WHERE id = ?").get(legacyId);
if (legacy && !existing) {
db.prepare(
"INSERT INTO plugins (id, name, version, enabled, source, path, installed_at, updated_at) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
).run(
plugin.id,
plugin.name,
plugin.version,
legacy.enabled,
legacy.source || "local",
plugin.dir,
legacy.installed_at || now,
now
);
} else if (legacy && existing) {
db.prepare("UPDATE plugins SET enabled = ?, updated_at = ? WHERE id = ?").run(
legacy.enabled,
now,
plugin.id
);
}
const settingsRows = db
.prepare("SELECT key, value, updated_at FROM plugin_settings WHERE plugin_id = ?")
.all(legacyId);
const copySetting = db.prepare(
"INSERT OR IGNORE INTO plugin_settings (plugin_id, key, value, updated_at) VALUES (?, ?, ?, ?)"
);
for (const row of settingsRows) {
copySetting.run(plugin.id, row.key, row.value, row.updated_at || now);
}
db.prepare("DELETE FROM plugin_settings WHERE plugin_id = ?").run(legacyId);
db.prepare("DELETE FROM plugins WHERE id = ?").run(legacyId);
}
}
function migrateCommandUsageAliases() {
const table = db
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'command_usage'")
.get();
if (!table) {
return;
}
for (const alias of economyCommandAliases) {
const oldRow = db.prepare("SELECT count, updated_at FROM command_usage WHERE command_id = ?").get(alias.from);
if (!oldRow) {
continue;
}
db.prepare(
"INSERT INTO command_usage (command_id, count, updated_at) VALUES (?, ?, ?) " +
"ON CONFLICT(command_id) DO UPDATE SET count = command_usage.count + excluded.count, updated_at = MAX(command_usage.updated_at, excluded.updated_at)"
).run(alias.to, oldRow.count || 0, oldRow.updated_at || Date.now());
db.prepare("DELETE FROM command_usage WHERE command_id = ?").run(alias.from);
}
}
function getPlugins() { function getPlugins() {
return db.prepare("SELECT * FROM plugins ORDER BY name").all(); return db.prepare("SELECT * FROM plugins ORDER BY name").all();
} }

View File

@ -422,15 +422,15 @@ function buildCommandUsageRows(limit) {
} }
function buildCurrencyRows(limit) { function buildCurrencyRows(limit) {
if (!tableExists("echonomy_accounts")) { if (!tableExists("economy_accounts")) {
return { rows: [], emptyMessage: "Currency framework not active." }; return { rows: [], emptyMessage: "Currency framework not active." };
} }
const rows = db const rows = db
.prepare( .prepare(
"SELECT user_profiles.internal_username AS username, echonomy_accounts.balance AS value " + "SELECT user_profiles.internal_username AS username, economy_accounts.balance AS value " +
"FROM echonomy_accounts " + "FROM economy_accounts " +
"JOIN user_profiles ON user_profiles.id = echonomy_accounts.user_id " + "JOIN user_profiles ON user_profiles.id = economy_accounts.user_id " +
"ORDER BY echonomy_accounts.balance DESC LIMIT ?" "ORDER BY economy_accounts.balance DESC LIMIT ?"
) )
.all(limit); .all(limit);
return { rows, valueLabel: getCurrencyLabel() }; return { rows, valueLabel: getCurrencyLabel() };
@ -494,11 +494,11 @@ function buildCommandIndex() {
if (command.id && !command.id.toString().includes(":")) { if (command.id && !command.id.toString().includes(":")) {
ids.push(`${plugin.id}:${command.id}`); ids.push(`${plugin.id}:${command.id}`);
} }
if (plugin.id === "echonomy-framework" && command.id === "root") { if (plugin.id === "economy-framework" && command.id === "root") {
ids.push("echonomy:root"); ids.push("economy:root");
} }
if (plugin.id === "echonomy-games" && command.id === "mysterybox") { if (plugin.id === "economy-games" && command.id === "mysterybox") {
ids.push("echonomy-games:mystery"); ids.push("economy-games:mystery");
} }
const label = subcommand ? `${prefix}${trigger} ${subcommand}` : `${prefix}${trigger}`; const label = subcommand ? `${prefix}${trigger} ${subcommand}` : `${prefix}${trigger}`;
addEntry(ids, label, commandId); addEntry(ids, label, commandId);
@ -588,8 +588,8 @@ function formatCommandId(commandId, prefix = "!") {
} }
function getCurrencyLabel() { function getCurrencyLabel() {
const plural = getPluginSetting("echonomy-framework", "currency_name_plural"); const plural = getPluginSetting("economy-framework", "currency_name_plural");
const singular = getPluginSetting("echonomy-framework", "currency_name"); const singular = getPluginSetting("economy-framework", "currency_name");
return plural || singular || "Coins"; return plural || singular || "Coins";
} }