Add level-up announcements, new plugins, scheduler overhaul, and bug fixes

- Level-up announcements with Twinbee/Parodius-themed messages and emoji flair
- New plugins: !howami (LLM stat roast), !vibe (room energy), !tldr (conversation summary)
- !game command as primary game lookup with !retro as alias
- Prefetch system: API data cached at 00:05 UTC for reliable scheduled posts
- Nightly maintenance job: prunes old data across 15+ tables
- Sentiment aggregation: new sentiment_stats table with backfill migration
- Access token moved from .env to device.json for security
- Retry logic (sendReact/sendWithRetry) for transient connection failures
- Trivia crash fix: optional chaining on threadEventId across all locations
- Bug fixes: COALESCE for NULL SUM in howami, unused params in vibe, dead try-catch in fun
- License updated from ISC to MIT
- README: weather section expanded, prefetch/maintenance documented

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-03-08 11:30:23 -07:00
parent 69be69e3b0
commit 07c8674fe0
17 changed files with 1109 additions and 135 deletions

View File

@@ -38,23 +38,8 @@ import { ToolsPlugin } from "./plugins/tools";
import { ReactionsPlugin } from "./plugins/reactions";
import { BotInfoPlugin } from "./plugins/botinfo";
import { RetroPlugin } from "./plugins/retro";
/**
* Persist a new access token to the .env file so it survives restarts.
*/
function updateEnvToken(newToken: string): void {
const envPath = path.resolve(process.cwd(), ".env");
if (!fs.existsSync(envPath)) return;
let content = fs.readFileSync(envPath, "utf-8");
if (content.match(/^MATRIX_ACCESS_TOKEN=.*/m)) {
content = content.replace(/^MATRIX_ACCESS_TOKEN=.*/m, `MATRIX_ACCESS_TOKEN=${newToken}`);
} else {
content += `\nMATRIX_ACCESS_TOKEN=${newToken}\n`;
}
fs.writeFileSync(envPath, content, "utf-8");
logger.info("Updated .env with new access token");
}
import { HowAmIPlugin } from "./plugins/howami";
import { VibePlugin } from "./plugins/vibe";
/**
* Ensure we have a valid access token. If the current token is invalid and a
@@ -70,12 +55,24 @@ async function resolveAccessToken(
const deviceFile = path.join(dataDir, "device.json");
const hasDeviceFile = fs.existsSync(deviceFile);
// Try to load the stored token from device.json first
let storedToken: string | undefined;
if (hasDeviceFile) {
try {
const data = JSON.parse(fs.readFileSync(deviceFile, "utf-8"));
storedToken = data.accessToken;
} catch { /* ignore */ }
}
// Prefer stored token (device.json) over env var
const effectiveToken = storedToken ?? currentToken;
// If we have a stored device identity AND a valid token, reuse them
if (hasDeviceFile && currentToken) {
const valid = await isTokenValid(homeserverUrl, currentToken);
if (hasDeviceFile && effectiveToken) {
const valid = await isTokenValid(homeserverUrl, effectiveToken);
if (valid) {
logger.info("Access token is valid, device identity exists");
return currentToken;
return effectiveToken;
}
logger.warn("Access token is invalid or expired");
}
@@ -83,7 +80,7 @@ async function resolveAccessToken(
// No device.json means fresh crypto store — we MUST do a password login
// to get a new token + device pair. Reusing an old token's device would
// cause OTK conflicts since the server has keys we don't have locally.
if (!hasDeviceFile && currentToken) {
if (!hasDeviceFile && effectiveToken) {
logger.info("No device.json found — fresh crypto store requires a new device via password login");
}
@@ -116,11 +113,15 @@ async function resolveAccessToken(
const loginResult = await loginWithPassword(homeserverUrl, botUserId, password, existingDeviceId);
// Save the new device ID
fs.writeFileSync(deviceFile, JSON.stringify({ deviceId: loginResult.device_id }), "utf-8");
// Save the new device ID and access token together
fs.writeFileSync(
deviceFile,
JSON.stringify({ deviceId: loginResult.device_id, accessToken: loginResult.access_token }),
"utf-8"
);
fs.chmodSync(deviceFile, 0o600);
logger.info(`Device identity saved: ${loginResult.device_id}`);
updateEnvToken(loginResult.access_token);
return loginResult.access_token;
}
@@ -237,6 +238,8 @@ async function main(): Promise<void> {
const reactionsPlugin = new ReactionsPlugin(botClient);
const botInfoPlugin = new BotInfoPlugin(botClient);
const retroPlugin = new RetroPlugin(botClient);
const howAmIPlugin = new HowAmIPlugin(botClient);
const vibePlugin = new VibePlugin(botClient);
const dailyScheduler = new DailyScheduler(
botClient,
@@ -282,6 +285,8 @@ async function main(): Promise<void> {
registry.register(reactionsPlugin);
registry.register(botInfoPlugin);
registry.register(retroPlugin);
registry.register(howAmIPlugin);
registry.register(vibePlugin);
registry.register(achievementsPlugin);
registry.register(dailyScheduler);