@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
JavaScript
/**
* 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