mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 22:16:43 +02:00
feat(matrix-skilltree-bot): add Matrix bot for skill tree and XP management
- Skill management: create, list, view details, delete - XP tracking with level-up notifications - Branch filtering (intellect, body, creativity, social, practical, mindset, custom) - Activity history per skill or global - User statistics (total XP, skill count, highest level, streak) - German/English command aliases - Number-based reference system for ease of use - JWT auth via mana-core-auth - Health check endpoint on port 3326 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
edbe7502d3
commit
3ed1453ff4
17 changed files with 1272 additions and 0 deletions
|
|
@ -0,0 +1,8 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { SkilltreeService } from './skilltree.service';
|
||||
|
||||
@Module({
|
||||
providers: [SkilltreeService],
|
||||
exports: [SkilltreeService],
|
||||
})
|
||||
export class SkilltreeModule {}
|
||||
151
services/matrix-skilltree-bot/src/skilltree/skilltree.service.ts
Normal file
151
services/matrix-skilltree-bot/src/skilltree/skilltree.service.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
export type SkillBranch = 'intellect' | 'body' | 'creativity' | 'social' | 'practical' | 'mindset' | 'custom';
|
||||
|
||||
export interface Skill {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
branch: SkillBranch;
|
||||
parentId?: string;
|
||||
icon: string;
|
||||
color?: string;
|
||||
currentXp: number;
|
||||
totalXp: number;
|
||||
level: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Activity {
|
||||
id: string;
|
||||
skillId: string;
|
||||
xpEarned: number;
|
||||
description: string;
|
||||
duration?: number;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface UserStats {
|
||||
totalXp: number;
|
||||
totalSkills: number;
|
||||
highestLevel: number;
|
||||
streakDays: number;
|
||||
lastActivityDate?: string;
|
||||
}
|
||||
|
||||
export interface AddXpResult {
|
||||
skill: Skill;
|
||||
leveledUp: boolean;
|
||||
newLevel: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SkilltreeService {
|
||||
private readonly logger = new Logger(SkilltreeService.name);
|
||||
private backendUrl: string;
|
||||
private apiPrefix: string;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.backendUrl = this.configService.get<string>('skilltree.backendUrl') || 'http://localhost:3024';
|
||||
this.apiPrefix = this.configService.get<string>('skilltree.apiPrefix') || '/api/v1';
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
token: string,
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<{ data?: T; error?: string }> {
|
||||
try {
|
||||
const url = `${this.backendUrl}${this.apiPrefix}${endpoint}`;
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
return { error: errorData.message || `Fehler: ${response.status}` };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { data };
|
||||
} catch (error) {
|
||||
this.logger.error(`Request failed: ${endpoint}`, error);
|
||||
return { error: 'Verbindung zum Backend fehlgeschlagen' };
|
||||
}
|
||||
}
|
||||
|
||||
// Skill operations
|
||||
async getSkills(token: string, branch?: string): Promise<{ data?: { skills: Skill[] }; error?: string }> {
|
||||
const query = branch ? `?branch=${branch}` : '';
|
||||
return this.request<{ skills: Skill[] }>(token, `/skills${query}`);
|
||||
}
|
||||
|
||||
async getSkill(token: string, skillId: string): Promise<{ data?: { skill: Skill }; error?: string }> {
|
||||
return this.request<{ skill: Skill }>(token, `/skills/${skillId}`);
|
||||
}
|
||||
|
||||
async createSkill(
|
||||
token: string,
|
||||
name: string,
|
||||
branch: SkillBranch,
|
||||
description?: string
|
||||
): Promise<{ data?: { skill: Skill }; error?: string }> {
|
||||
return this.request<{ skill: Skill }>(token, '/skills', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name, branch, description }),
|
||||
});
|
||||
}
|
||||
|
||||
async deleteSkill(token: string, skillId: string): Promise<{ error?: string }> {
|
||||
return this.request(token, `/skills/${skillId}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
async addXp(
|
||||
token: string,
|
||||
skillId: string,
|
||||
xp: number,
|
||||
description: string,
|
||||
duration?: number
|
||||
): Promise<{ data?: AddXpResult; error?: string }> {
|
||||
return this.request<AddXpResult>(token, `/skills/${skillId}/xp`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ xp, description, duration }),
|
||||
});
|
||||
}
|
||||
|
||||
// Stats
|
||||
async getStats(token: string): Promise<{ data?: { stats: UserStats }; error?: string }> {
|
||||
return this.request<{ stats: UserStats }>(token, '/skills/stats');
|
||||
}
|
||||
|
||||
// Activities
|
||||
async getActivities(token: string, limit?: number): Promise<{ data?: { activities: Activity[] }; error?: string }> {
|
||||
const query = limit ? `?limit=${limit}` : '';
|
||||
return this.request<{ activities: Activity[] }>(token, `/activities${query}`);
|
||||
}
|
||||
|
||||
async getRecentActivities(token: string, limit?: number): Promise<{ data?: { activities: Activity[] }; error?: string }> {
|
||||
const query = limit ? `?limit=${limit}` : '';
|
||||
return this.request<{ activities: Activity[] }>(token, `/activities/recent${query}`);
|
||||
}
|
||||
|
||||
async getSkillActivities(token: string, skillId: string): Promise<{ data?: { activities: Activity[] }; error?: string }> {
|
||||
return this.request<{ activities: Activity[] }>(token, `/activities/skill/${skillId}`);
|
||||
}
|
||||
|
||||
async checkHealth(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${this.backendUrl}/health`);
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue