-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add stats back #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,6 +56,7 @@ Example: | |
| $help | ||
| $ping | ||
| $usage | ||
| $stats | ||
| ``` | ||
|
|
||
| ## AI Command Usage | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,6 +9,7 @@ import { openRouter } from "../../utils/ai.ts"; | |||||||||||||||||||||||||||||||||||||||||||||||||
| import { addToContext, getContext } from "../../utils/context.ts"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { pretty } from "../../utils/pretty.ts"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { sanitizeForPrompt } from "../../utils/sanitize.ts"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { recordUsage } from "../../utils/stats.ts"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { canUseAI, formatTimeLeft, setUsage } from "../../utils/usage.ts"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export let CURRENT_MODEL_INDEX = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -20,6 +21,9 @@ export default { | |||||||||||||||||||||||||||||||||||||||||||||||||
| async execute({ message, args, ctx }: CommandCallbackOpts) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (message.author.bot) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if ((message as any)._processedByAskai) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| (message as any)._processedByAskai = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const { imageUrl, mimeType } = getAttachmentData(message); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const question = parseQuestion(args, !!imageUrl); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -79,10 +83,27 @@ export default { | |||||||||||||||||||||||||||||||||||||||||||||||||
| await message.reply({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| content: pretty(result.text), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /// token usage | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const tokensUsedByModel = result.usage?.totalTokens ?? 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (tokensUsedByModel > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| await setUsage(userId, tokensUsedByModel); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // stats | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const profile = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| username: message.author.username, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| displayName: message.author.displayName, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| avatar: message.author.displayAvatarURL({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| extension: "png", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| size: 256, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| recordUsage(userId, profile, tokensUsedByModel).catch((err) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+95
to
+104
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The stats recording operation lacks error handling. If Confidence: 5/5 Suggested Fix
Suggested change
Wrap the stats recording in a try-catch block to prevent non-critical stats failures from affecting the user experience. The error is logged for debugging but doesn't interrupt the command flow since the primary operation (AI response) has already succeeded. Prompt for AICopy this prompt to your AI IDE to fix this issue locally: 📍 This suggestion applies to lines 95-104 |
||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("[Stats] Failed to record usage:", err); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| await message.reply( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "Sorry, I encountered an issue processing your request right now.", | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,64 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { AttachmentBuilder } from "discord.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { CommandCallbackOpts } from "../../types/command.ts"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getUserStats } from "../../utils/stats.ts"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { renderStatsCard } from "../../visuals/statsCard.ts"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: "stats", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: "Show your AI usage statistics with heatmap", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aliases: ["mystats", "usage-stats"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async execute({ message }: CommandCallbackOpts) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (message.author.bot) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const target = message.mentions?.users?.first() || message.author; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const userId = target.id; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const stats = await getUserStats(userId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!stats.hasData) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const who = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| target.id === message.author.id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? "You have" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : `${target.username} has`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await message.reply( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `${who} no AI usage recorded yet. Use \`$ai\` or \`,\` with a question to start tracking your stats.`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const member = message.guild | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? await message.guild.members.fetch(target.id).catch(() => null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const displayName = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| member?.displayName || stats.displayName || target.username; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const avatarUrl = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| target.displayAvatarURL({ extension: "png", size: 256 }) || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stats.avatar || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const buffer = await renderStatsCard({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| displayName, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handle: `@${target.username}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| avatarUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| brand: "Rael", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| heatmap: stats.heatmap, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lifetimeTokens: stats.lifetimeTokens, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| peakDayTokens: stats.peakDayTokens, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreak: stats.currentStreak, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| longestStreak: stats.longestStreak, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+42
to
+52
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Confidence: 4/5 Suggested Fix
Suggested change
Add validation after the Prompt for AICopy this prompt to your AI IDE to fix this issue locally: 📍 This suggestion applies to lines 42-52 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const attachment = new AttachmentBuilder(buffer, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: `stats-${target.id}.png`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await message.reply({ files: [attachment] }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("Stats command error:", err); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await message.reply("Could not generate your stats card right now."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,203 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import fs from "fs/promises"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import path from "path"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { fileURLToPath } from "url"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const __filename = fileURLToPath(import.meta.url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const __dirname = path.dirname(__filename); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const DATA_DIR = path.join(__dirname, "..", "..", "data"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const STATS_FILE = path.join(DATA_DIR, "stats.json"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface DailyRecord { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [date: string]: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface UserRecord { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| username: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| displayName: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| avatar: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lifetimeTokens: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| peakDayTokens: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| daily: DailyRecord; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface StatsStore { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| users: Record<string, UserRecord>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let store: StatsStore = { users: {} }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let isLoaded = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let writeQueued = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let isWriting = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function loadStore(): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isLoaded) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await fs.mkdir(DATA_DIR, { recursive: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const raw = await fs.readFile(STATS_FILE, "utf-8"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsed = JSON.parse(raw); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| store = parsed?.users ? parsed : { users: {} }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+39
to
+40
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Confidence: 5/5 Suggested Fix
Suggested change
Add proper validation to ensure the parsed JSON matches the expected Prompt for AICopy this prompt to your AI IDE to fix this issue locally: 📍 This suggestion applies to lines 39-40 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| store = { users: {} }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isLoaded = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function saveStore(): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isWriting) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| writeQueued = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isWriting = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await fs.mkdir(DATA_DIR, { recursive: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await fs.writeFile(STATS_FILE, JSON.stringify(store), "utf-8"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("Failed to save stats.json:", err); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isWriting = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (writeQueued) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| writeQueued = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await saveStore(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+47
to
+66
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Confidence: 5/5 Suggested Fix
Suggested change
Move Prompt for AICopy this prompt to your AI IDE to fix this issue locally: 📍 This suggestion applies to lines 47-66
Comment on lines
+47
to
+66
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Confidence: 5/5 Suggested FixThe current queuing mechanism is fundamentally flawed for handling multiple concurrent writes. Consider implementing a proper write queue using an array or using a debounced write approach with a minimum delay. However, this requires significant refactoring beyond a simple line-range fix.
Prompt for AICopy this prompt to your AI IDE to fix this issue locally: 📍 This suggestion applies to lines 47-66 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function getDayKey(date = new Date()): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return date.toISOString().slice(0, 10); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function recordUsage( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| profile: { username?: string; displayName?: string; avatar?: string }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tokens: number, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!userId) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await loadStore(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!store.users[userId]) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| store.users[userId] = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| username: "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| displayName: "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| avatar: "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lifetimeTokens: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| peakDayTokens: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| daily: {}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const user = store.users[userId]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (profile.username) user.username = profile.username; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (profile.displayName) user.displayName = profile.displayName; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (profile.avatar) user.avatar = profile.avatar; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const safeTokens = Math.max(0, Math.floor(tokens)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (safeTokens > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const today = getDayKey(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user.daily[today] = (user.daily[today] || 0) + safeTokens; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user.lifetimeTokens += safeTokens; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (user.daily[today] > user.peakDayTokens) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user.peakDayTokens = user.daily[today]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await saveStore(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same issue as Comment #1 in Confidence: 5/5 Suggested Fix
Suggested change
Wrap the Prompt for AICopy this prompt to your AI IDE to fix this issue locally: |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function computeStreaks(daily: DailyRecord = {}) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const activeDays = new Set( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Object.keys(daily).filter((d) => daily[d] && daily[d] > 0), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (activeDays.size === 0) return { currentStreak: 0, longestStreak: 0 }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Current streak | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let currentStreak = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cursor = new Date(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!activeDays.has(getDayKey(cursor))) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cursor.setUTCDate(cursor.getUTCDate() - 1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| while (activeDays.has(getDayKey(cursor))) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreak++; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cursor.setUTCDate(cursor.getUTCDate() - 1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+120
to
+127
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current streak calculation has a logic error. It creates a new Confidence: 5/5 Suggested Fix
Suggested change
Normalize the cursor date to UTC midnight before starting the streak calculation. This ensures consistency between the date used for checking and the date keys stored in the daily records. Prompt for AICopy this prompt to your AI IDE to fix this issue locally: 📍 This suggestion applies to lines 120-127 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Longest streak | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sorted = [...activeDays].sort(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let longestStreak = 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let run = 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = 1; i < sorted.length; i++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const prev = new Date(`${sorted[i - 1]}T00:00:00Z`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const curr = new Date(`${sorted[i]}T00:00:00Z`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const diff = Math.round((curr.getTime() - prev.getTime()) / 86400000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| run = diff === 1 ? run + 1 : 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (run > longestStreak) longestStreak = run; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { currentStreak, longestStreak }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function getUserStats(userId: string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await loadStore(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const user = store.users[userId]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!user) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| displayName: "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| username: "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| avatar: "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lifetimeTokens: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| peakDayTokens: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreak: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| longestStreak: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| heatmap: { columns: [], max: 0 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hasData: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { currentStreak, longestStreak } = computeStreaks(user.daily); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Build 30-week heatmap | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const weeks = 30; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const today = new Date(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const end = new Date( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end.setUTCDate(end.getUTCDate() + (6 - end.getUTCDay())); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const totalDays = weeks * 7; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const start = new Date(end); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| start.setUTCDate(start.getUTCDate() - (totalDays - 1)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const columns: number[][] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let max = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cursor = new Date(start); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let w = 0; w < weeks; w++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const week: number[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let d = 0; d < 7; d++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const value = user.daily[getDayKey(cursor)] || 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (value > max) max = value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| week.push(value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cursor.setUTCDate(cursor.getUTCDate() + 1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| columns.push(week); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| displayName: user.displayName, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| username: user.username, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| avatar: user.avatar, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lifetimeTokens: user.lifetimeTokens, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| peakDayTokens: user.peakDayTokens, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentStreak, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| longestStreak, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| heatmap: { columns, max }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hasData: user.lifetimeTokens > 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The double-reply prevention mechanism mutates the message object directly using
(message as any), which can cause memory leaks in long-running bots. The flag_processedByAskaiis never cleaned up, and this approach bypasses TypeScript's type safety. Additionally, there's a potential race condition if multiple handlers process the same message concurrently.Confidence: 4/5
Suggested Fix
Consider using a
Setto track processed message IDs with automatic cleanup:This approach:
anycastsPrompt for AI
Copy this prompt to your AI IDE to fix this issue locally:
📍 This suggestion applies to lines 23-24