UNPKG

@aituber-onair/kizuna

Version:

A sophisticated bond system (絆 - Kizuna) for managing relationships between users and AI characters in AITuber OnAir.

428 lines 12.9 kB
/** * UserManager - User management class * * Manages creation, retrieval, and updates of Kizuna users */ import { generateUserId, parseUserId, getDisplayName, } from "./utils/userIdGenerator"; /** * User management class */ export class UserManager { constructor(config) { this.users = new Map(); this.config = config; } /** * Get or create user */ getOrCreateUser(context) { // Generate user ID const userId = this.generateUserIdFromContext(context); let user = this.users.get(userId); if (!user) { // Create new user user = this.createUser(userId, context); this.users.set(userId, user); this.log(`New user created: ${userId} (${user.displayName})`); } else { // Update existing user information this.updateUserActivity(user, context); } return user; } /** * Get user */ getUser(userId) { return this.users.get(userId) || null; } /** * Get all users */ getAllUsers() { return Array.from(this.users.values()); } /** * Get user count */ getUserCount() { return this.users.size; } /** * Get user count by platform */ getUserCountByPlatform() { const counts = { owner: 0, youtube: 0, twitch: 0, websocket: 0, }; for (const user of this.users.values()) { counts[user.type]++; } return counts; } /** * Get active users (users who accessed within specified period) */ getActiveUsers(hours = 24) { const cutoffTime = Date.now() - hours * 60 * 60 * 1000; return this.getAllUsers().filter((user) => new Date(user.lastSeen).getTime() > cutoffTime); } /** * Get top users (by points) */ getTopUsers(limit = 10) { return this.getAllUsers() .sort((a, b) => b.points - a.points) .slice(0, limit); } /** * Delete user */ deleteUser(userId) { const user = this.users.get(userId); if (!user) { return false; } // Cannot delete owner if (user.type === "owner") { this.log(`Cannot delete owner user: ${userId}`); return false; } this.users.delete(userId); this.log(`User deleted: ${userId}`); return true; } /** * Reset user points */ resetUserPoints(userId) { const user = this.users.get(userId); if (!user) { return false; } const oldPoints = user.points; user.points = user.type === "owner" ? this.config.owner.initialPoints : 0; user.level = this.calculateLevel(user.points); this.log(`User points reset: ${userId} (${oldPoints} -> ${user.points})`); return true; } /** * Grant achievement to user */ grantAchievement(userId, achievement) { const user = this.users.get(userId); if (!user) { return false; } // Check if user already has the same achievement const hasAchievement = user.achievements.some((a) => a.id === achievement.id); if (hasAchievement) { return false; } user.achievements.push({ ...achievement, earnedAt: new Date(), }); this.log(`Achievement granted to ${userId}: ${achievement.title}`); return true; } /** * Add user interaction record */ addInteractionRecord(userId, context, pointsEarned, appliedRules) { const user = this.users.get(userId); if (!user) { return; } const record = { id: this.generateInteractionId(), timestamp: new Date(), points: pointsEarned, message: context.message, emotion: context.emotion || undefined, platform: this.determineUserType(context), appliedRules, }; // Initialize interaction history if not exists if (!user.stats.interactionHistory) { user.stats.interactionHistory = []; } user.stats.interactionHistory.push(record); // Limit history to latest 100 entries if (user.stats.interactionHistory.length > 100) { user.stats.interactionHistory = user.stats.interactionHistory.slice(-100); } } /** * Export users in JSON format */ exportUsers() { const users = Object.fromEntries(this.users); return JSON.stringify(users, null, 2); } /** * Import users from JSON format */ importUsers(jsonData) { const result = { success: false, imported: 0, errors: [] }; try { const parsedData = JSON.parse(jsonData); for (const [userId, userData] of Object.entries(parsedData)) { try { const user = this.validateAndNormalizeUser(userId, userData); this.users.set(userId, user); result.imported++; } catch (error) { result.errors.push(`Failed to import user ${userId}: ${error}`); } } result.success = result.errors.length === 0; this.log(`Import completed: ${result.imported} users imported, ${result.errors.length} errors`); } catch (error) { result.errors.push(`Failed to parse JSON: ${error}`); } return result; } /** * Get user data as Map format (for internal processing) */ getUsersAsMap() { return new Map(this.users); } /** * Set user data from Map format (for internal processing) */ setUsersFromMap(users) { this.users = new Map(users); } // ============================================================================ // Private methods // ============================================================================ /** * Generate user ID from context */ generateUserIdFromContext(context) { // Use existing user ID if already set if (context.userId && context.userId !== "anonymous") { return context.userId; } // Extract username (platform-specific processing) const userName = this.extractUserNameFromContext(context); return generateUserId(context.platform, userName, context.isOwner); } /** * Extract username from context */ extractUserNameFromContext(context) { // Get username from metadata if (context.metadata?.userName) { return context.metadata.userName; } // Try to extract username from user ID try { const parsed = parseUserId(context.userId); return parsed.userName; } catch { // Anonymous user if parse fails return "anonymous"; } } /** * Create new user */ createUser(userId, context) { const userType = this.determineUserType(context); const displayName = getDisplayName(userId); // Set initial points const initialPoints = userType === "owner" ? this.config.owner.initialPoints : 0; const user = { id: userId, displayName, type: userType, points: initialPoints, level: this.calculateLevel(initialPoints), achievements: [], stats: this.createInitialStats(), firstSeen: new Date(), lastSeen: new Date(), customData: {}, }; // Grant owner-specific achievements if (userType === "owner") { this.grantOwnerAchievements(user); } return user; } /** * Determine user type */ determineUserType(context) { if (context.isOwner || context.platform === "chatForm") { return "owner"; } switch (context.platform) { case "youtube": return "youtube"; case "twitch": return "twitch"; case "websocket": case "vision": case "textFile": case "configAdvice": return "websocket"; default: return "websocket"; } } /** * Create initial statistics */ createInitialStats() { return { totalMessages: 1, totalPointsEarned: 0, dailyStreak: 1, favoriteEmotions: {}, todayMessages: 1, interactionHistory: [], }; } /** * Update user activity */ updateUserActivity(user, context) { const now = new Date(); const lastSeen = new Date(user.lastSeen); user.lastSeen = now; user.stats.totalMessages++; // Update today's message count if (this.isSameDay(now, lastSeen)) { user.stats.todayMessages++; } else { user.stats.todayMessages = 1; // Update consecutive login days if (this.isConsecutiveDay(now, lastSeen)) { user.stats.dailyStreak++; } else { user.stats.dailyStreak = 1; } } // Update emotion statistics if (context.emotion) { user.stats.favoriteEmotions[context.emotion] = (user.stats.favoriteEmotions[context.emotion] || 0) + 1; } } /** * Grant owner-specific achievements */ grantOwnerAchievements(user) { const ownerAchievements = [ { id: "first_adopter", title: "First Adopter", description: "Early adopter of AITuber OnAir", icon: "🏆", }, { id: "master_of_aituber", title: "Master of AITuber", description: "Master of AITuber", icon: "👑", }, ]; for (const achievement of ownerAchievements) { if (this.config.owner.exclusiveAchievements.includes(achievement.id)) { user.achievements.push({ ...achievement, earnedAt: new Date(), }); } } } /** * Calculate level */ calculateLevel(points) { // 1 level up per 100 points, max 10 levels return Math.min(Math.floor(points / 100) + 1, 10); } /** * Generate interaction ID */ generateInteractionId() { return `interaction_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`; } /** * Check if same day */ isSameDay(date1, date2) { return date1.toDateString() === date2.toDateString(); } /** * Check if consecutive day */ isConsecutiveDay(today, lastSeen) { const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); return this.isSameDay(yesterday, lastSeen); } /** * Validate and normalize user data */ validateAndNormalizeUser(userId, userData) { // Check required fields const requiredFields = ["id", "displayName", "type", "points", "level"]; if (typeof userData !== "object" || userData === null) { throw new Error("Invalid user data: must be an object"); } for (const field of requiredFields) { if (!(field in userData)) { throw new Error(`Missing required field: ${field}`); } } const data = userData; // Normalize Date objects const user = { ...data, firstSeen: new Date(data.firstSeen), lastSeen: new Date(data.lastSeen), achievements: data.achievements?.map((a) => { const achievement = a; return { ...achievement, earnedAt: new Date(achievement.earnedAt), }; }) || [], stats: { ...data.stats, interactionHistory: data.stats?.interactionHistory?.map((r) => { const record = r; return { ...record, timestamp: new Date(record.timestamp), }; }) || [], }, }; return user; } /** * Log output */ log(message) { if (this.config.dev.debugMode) { console.log(`[UserManager] ${message}`); } } } //# sourceMappingURL=UserManager.js.map