Scorekeeper changes.
This commit is contained in:
parent
7df051795a
commit
455233b1c4
@ -114,7 +114,7 @@ function cacheResponse(client, key, id, ttlSeconds) {
|
||||
*/
|
||||
function awardOutput(client, guildId, userId, amount) {
|
||||
if (client.scorekeeper && amount > 0) {
|
||||
client.scorekeeper.addOutput(guildId, userId, amount)
|
||||
client.scorekeeper.addOutput(guildId, userId, amount, 'AI_response')
|
||||
.catch(err => client.logger.error(`Scorekeeper error: ${err.message}`));
|
||||
}
|
||||
}
|
||||
@ -222,6 +222,12 @@ async function handleImage(client, message, resp, cfg) {
|
||||
client.logger.info(`Saved image: ${filePath}`);
|
||||
attachments.push(new AttachmentBuilder(buffer, { name: filename }));
|
||||
}
|
||||
// Award output points based on token usage for image generation
|
||||
const tokens = imgRes.usage?.total_tokens ?? count;
|
||||
if (client.scorekeeper && tokens > 0) {
|
||||
client.scorekeeper.addOutput(message.guild.id, message.author.id, tokens, 'image_generation')
|
||||
.catch(err => client.logger.error(`Scorekeeper error: ${err.message}`));
|
||||
}
|
||||
// Reply with attachments
|
||||
await message.reply({ content: promptText, files: attachments });
|
||||
} catch (err) {
|
||||
|
||||
@ -134,6 +134,12 @@ async function handleImageInteraction(client, interaction, resp, cfg, ephemeral)
|
||||
client.logger.info(`Saved image: ${filePath}`);
|
||||
attachments.push(new AttachmentBuilder(buffer, { name: filename }));
|
||||
}
|
||||
// Award output points based on token usage for image generation
|
||||
const tokens = imgRes.usage?.total_tokens ?? count;
|
||||
if (client.scorekeeper && tokens > 0) {
|
||||
client.scorekeeper.addOutput(interaction.guildId, interaction.user.id, tokens, 'image_generation')
|
||||
.catch(err => client.logger.error(`Scorekeeper error: ${err.message}`));
|
||||
}
|
||||
// Reply with attachments
|
||||
await interaction.editReply({ content: promptText, files: attachments });
|
||||
return true;
|
||||
@ -281,7 +287,7 @@ export const commands = [
|
||||
// Award output tokens
|
||||
const tokens = resp.usage?.total_tokens ?? resp.usage?.completion_tokens ?? 0;
|
||||
if (client.scorekeeper && tokens > 0) {
|
||||
client.scorekeeper.addOutput(interaction.guildId, interaction.user.id, tokens)
|
||||
client.scorekeeper.addOutput(interaction.guildId, interaction.user.id, tokens, 'AI_query')
|
||||
.catch(e => client.logger.error(`Scorekeeper error: ${e.message}`));
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@ -14,7 +14,7 @@ export const init = async (client, config) => {
|
||||
// Do not award zero or negative points
|
||||
if (points <= 0) return;
|
||||
try {
|
||||
await client.scorekeeper.addInput(message.guild.id, message.author.id, points);
|
||||
await client.scorekeeper.addInput(message.guild.id, message.author.id, points, 'message');
|
||||
} catch (error) {
|
||||
client.logger.error(`Error adding input points: ${error.message}`);
|
||||
}
|
||||
@ -92,7 +92,7 @@ function processVoiceLeave(client, guild, member, channelId) {
|
||||
const points = Math.min(Math.floor(duration), 30);
|
||||
if (points > 0) {
|
||||
try {
|
||||
client.scorekeeper.addInput(guild.id, member.id, points)
|
||||
client.scorekeeper.addInput(guild.id, member.id, points, 'voice_activity')
|
||||
.then(() => {
|
||||
client.logger.debug(`Added ${points} voice activity points for ${member.user.tag}`);
|
||||
})
|
||||
|
||||
@ -33,9 +33,23 @@ export const init = async (client, config) => {
|
||||
await checkCategoriesCollection(client);
|
||||
await checkEventsCollection(client);
|
||||
// Create scorekeeper interface on client
|
||||
client.scorekeeper = {
|
||||
addInput: (guildId, userId, amount) => addInput(client, guildId, userId, amount),
|
||||
addOutput: (guildId, userId, amount) => addOutput(client, guildId, userId, amount),
|
||||
client.scorekeeper = {
|
||||
/**
|
||||
* Add input points with optional reason for audit
|
||||
* @param {string} guildId
|
||||
* @param {string} userId
|
||||
* @param {number} amount
|
||||
* @param {string} [reason]
|
||||
*/
|
||||
addInput: (guildId, userId, amount, reason) => addInput(client, guildId, userId, amount, reason),
|
||||
/**
|
||||
* Add output points with optional reason for audit
|
||||
* @param {string} guildId
|
||||
* @param {string} userId
|
||||
* @param {number} amount
|
||||
* @param {string} [reason]
|
||||
*/
|
||||
addOutput: (guildId, userId, amount, reason) => addOutput(client, guildId, userId, amount, reason),
|
||||
addCommendation: (guildId, userId, amount = 1) => addCommendation(client, guildId, userId, amount),
|
||||
addCitation: (guildId, userId, amount = 1) => addCitation(client, guildId, userId, amount),
|
||||
getScore: (guildId, userId) => getScore(client, guildId, userId),
|
||||
@ -155,7 +169,15 @@ function setupDecayCron(client, schedule) {
|
||||
/**
|
||||
* Add input points for a user
|
||||
*/
|
||||
async function addInput(client, guildId, userId, amount) {
|
||||
/**
|
||||
* Add input points for a user and log an audit event
|
||||
* @param {import('discord.js').Client} client
|
||||
* @param {string} guildId
|
||||
* @param {string} userId
|
||||
* @param {number} amount
|
||||
* @param {string} [reason]
|
||||
*/
|
||||
async function addInput(client, guildId, userId, amount, reason = '') {
|
||||
if (!guildId || !userId || !amount || amount <= 0) {
|
||||
throw new Error(`Invalid parameters for addInput - guildId: ${guildId}, userId: ${userId}, amount: ${amount}`);
|
||||
}
|
||||
@ -170,9 +192,25 @@ async function addInput(client, guildId, userId, amount) {
|
||||
client.logger.debug(`Updating record ${scoreData.id} - input from ${scoreData.input} to ${newInput}`);
|
||||
|
||||
// Use direct update with ID to avoid duplicate records
|
||||
return await client.pb.collection('scorekeeper').update(scoreData.id, {
|
||||
const updatedRecord = await client.pb.collection('scorekeeper').update(scoreData.id, {
|
||||
input: newInput
|
||||
});
|
||||
// Log input change at info level
|
||||
client.logger.info(`[module:scorekeeper][addInput] guildId=${guildId}, userId=${userId}, recordId=${scoreData.id}, previousInput=${scoreData.input}, newInput=${newInput}, amount=${amount}, reason=${reason}`);
|
||||
// Audit event: log input change
|
||||
try {
|
||||
await client.pb.collection('scorekeeper_events').create({
|
||||
guildId,
|
||||
userId,
|
||||
type: 'input',
|
||||
amount,
|
||||
reason,
|
||||
awardedBy: client.user?.id
|
||||
});
|
||||
} catch (eventError) {
|
||||
client.logger.error(`[module:scorekeeper] Failed to log input event: ${eventError.message}`);
|
||||
}
|
||||
return updatedRecord;
|
||||
} catch (error) {
|
||||
client.logger.error(`Error adding input points: ${error.message}`);
|
||||
throw error;
|
||||
@ -182,7 +220,15 @@ async function addInput(client, guildId, userId, amount) {
|
||||
/**
|
||||
* Add output points for a user
|
||||
*/
|
||||
async function addOutput(client, guildId, userId, amount) {
|
||||
/**
|
||||
* Add output points for a user and log an audit event
|
||||
* @param {import('discord.js').Client} client
|
||||
* @param {string} guildId
|
||||
* @param {string} userId
|
||||
* @param {number} amount
|
||||
* @param {string} [reason]
|
||||
*/
|
||||
async function addOutput(client, guildId, userId, amount, reason = '') {
|
||||
if (!guildId || !userId || !amount || amount <= 0) {
|
||||
throw new Error('Invalid parameters for addOutput');
|
||||
}
|
||||
@ -197,9 +243,25 @@ async function addOutput(client, guildId, userId, amount) {
|
||||
client.logger.debug(`Updating record ${scoreData.id} - output from ${scoreData.output} to ${newOutput}`);
|
||||
|
||||
// Use direct update with ID to avoid duplicate records
|
||||
return await client.pb.collection('scorekeeper').update(scoreData.id, {
|
||||
const updatedRecord = await client.pb.collection('scorekeeper').update(scoreData.id, {
|
||||
output: newOutput
|
||||
});
|
||||
// Log output change at info level
|
||||
client.logger.info(`[module:scorekeeper][addOutput] guildId=${guildId}, userId=${userId}, recordId=${scoreData.id}, previousOutput=${scoreData.output}, newOutput=${newOutput}, amount=${amount}, reason=${reason}`);
|
||||
// Audit event: log output change
|
||||
try {
|
||||
await client.pb.collection('scorekeeper_events').create({
|
||||
guildId,
|
||||
userId,
|
||||
type: 'output',
|
||||
amount,
|
||||
reason,
|
||||
awardedBy: client.user?.id
|
||||
});
|
||||
} catch (eventError) {
|
||||
client.logger.error(`[module:scorekeeper] Failed to log output event: ${eventError.message}`);
|
||||
}
|
||||
return updatedRecord;
|
||||
} catch (error) {
|
||||
client.logger.error(`Error adding output points: ${error.message}`);
|
||||
throw error;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, EmbedBuilder } from 'discord.js';
|
||||
import { MessageFlags } from 'discord-api-types/v10';
|
||||
// Init function to handle autocomplete for /vc invite
|
||||
/**
|
||||
* tempvc module: temporary voice channel manager
|
||||
*
|
||||
@ -154,7 +155,13 @@ export const commands = [
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('invite')
|
||||
.setDescription('Invite a user to this channel')
|
||||
.addUserOption(opt => opt.setName('user').setDescription('User to invite').setRequired(true))
|
||||
// Autocomplete string option for user ID
|
||||
.addStringOption(opt =>
|
||||
opt.setName('user')
|
||||
.setDescription('User to invite')
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('kick')
|
||||
@ -223,10 +230,29 @@ export const commands = [
|
||||
await voice.setName(name);
|
||||
await interaction.reply({ content: `Channel renamed to **${name}**.`, flags: MessageFlags.Ephemeral });
|
||||
} else if (sub === 'invite') {
|
||||
const u = interaction.options.getUser('user', true);
|
||||
// Invitation: support both string (autocomplete) and user option types
|
||||
let userId;
|
||||
let memberToInvite;
|
||||
// Try string option first (autocomplete)
|
||||
try {
|
||||
userId = interaction.options.getString('user', true);
|
||||
memberToInvite = await guild.members.fetch(userId);
|
||||
} catch (e) {
|
||||
// Fallback to user option
|
||||
try {
|
||||
const user = interaction.options.getUser('user', true);
|
||||
userId = user.id;
|
||||
memberToInvite = await guild.members.fetch(userId);
|
||||
} catch {
|
||||
memberToInvite = null;
|
||||
}
|
||||
}
|
||||
if (!memberToInvite) {
|
||||
return interaction.reply({ content: 'User not found in this server.', flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
// grant view and connect
|
||||
await voice.permissionOverwrites.edit(u.id, { ViewChannel: true, Connect: true });
|
||||
await interaction.reply({ content: `Invited <@${u.id}>.`, flags: MessageFlags.Ephemeral });
|
||||
await voice.permissionOverwrites.edit(userId, { ViewChannel: true, Connect: true });
|
||||
await interaction.reply({ content: `Invited <@${userId}>.`, flags: MessageFlags.Ephemeral });
|
||||
} else if (sub === 'kick') {
|
||||
const u = interaction.options.getUser('user', true);
|
||||
const gm = await guild.members.fetch(u.id);
|
||||
@ -421,6 +447,40 @@ export const commands = [
|
||||
* Initialize module: load PB state and hook events
|
||||
*/
|
||||
export async function init(client) {
|
||||
// autocomplete for /vc invite
|
||||
client.on('interactionCreate', async interaction => {
|
||||
if (!interaction.isAutocomplete()) return;
|
||||
if (interaction.commandName !== 'vc') return;
|
||||
// Only handle autocomplete for the 'invite' subcommand
|
||||
let sub;
|
||||
try {
|
||||
sub = interaction.options.getSubcommand();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (sub !== 'invite') return;
|
||||
const focused = interaction.options.getFocused();
|
||||
const guild = interaction.guild;
|
||||
if (!guild) return;
|
||||
// Perform guild member search for autocomplete suggestions (prefix match)
|
||||
let choices = [];
|
||||
try {
|
||||
const members = await guild.members.search({ query: focused, limit: 25 });
|
||||
choices = members.map(m => ({ name: m.displayName || m.user.username, value: m.id }));
|
||||
} catch (err) {
|
||||
client.logger.error(`[module:tempvc] Autocomplete search failed: ${err.message}`);
|
||||
}
|
||||
// If no choices found or to support substring matching, fallback to cache filter
|
||||
if (choices.length === 0) {
|
||||
const str = String(focused).toLowerCase();
|
||||
choices = Array.from(guild.members.cache.values())
|
||||
.filter(m => (m.displayName || m.user.username).toLowerCase().includes(str))
|
||||
.slice(0, 25)
|
||||
.map(m => ({ name: m.displayName || m.user.username, value: m.id }));
|
||||
}
|
||||
// Respond with suggestions (max 25)
|
||||
await interaction.respond(choices);
|
||||
});
|
||||
// tempvc state: masters per guild, sessions map
|
||||
client.tempvc = { masters: new Map(), sessions: new Map() };
|
||||
// hook voice state updates
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user