🐛 fix(matrix-web): handle Matrix SSO loginToken callback

Add loginWithLoginToken function to exchange Matrix SSO loginToken for credentials.
The app layout now detects the loginToken URL parameter and completes the SSO flow.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-02-01 15:02:47 +01:00
parent 9e9db590dc
commit dc0d425f61
53 changed files with 1550 additions and 230 deletions

View file

@ -148,6 +148,58 @@ export async function checkHomeserver(
}
}
/**
* Login with a Matrix SSO login token (for SSO/OAuth callback)
* This exchanges the loginToken from SSO redirect for proper credentials
*/
export async function loginWithLoginToken(
homeserver: string,
loginToken: string
): Promise<LoginResult> {
// Load polyfills first
await import('./polyfills');
const { createClient } = await import('matrix-js-sdk');
// Normalize homeserver URL
let baseUrl = homeserver.trim();
if (!baseUrl.startsWith('http://') && !baseUrl.startsWith('https://')) {
baseUrl = `https://${baseUrl}`;
}
// Remove trailing slash
baseUrl = baseUrl.replace(/\/$/, '');
const tempClient = createClient({ baseUrl });
try {
const response = await tempClient.login('m.login.token', {
token: loginToken,
initial_device_display_name: 'Manalink',
});
return {
success: true,
credentials: {
homeserver: baseUrl,
accessToken: response.access_token,
userId: response.user_id,
deviceId: response.device_id,
},
};
} catch (err) {
const message = err instanceof Error ? err.message : 'Login failed';
// Provide more helpful error messages
if (message.includes('M_UNKNOWN_TOKEN') || message.includes('M_FORBIDDEN')) {
return { success: false, error: 'Login token expired or invalid. Please try again.' };
}
if (message.includes('Failed to fetch') || message.includes('NetworkError')) {
return { success: false, error: 'Could not connect to homeserver' };
}
return { success: false, error: message };
}
}
/**
* Register a new account (if registration is open)
*/

View file

@ -3,6 +3,7 @@ export { matrixStore } from './store.svelte';
export {
loginWithPassword,
loginWithToken,
loginWithLoginToken,
discoverHomeserver,
checkHomeserver,
register,

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { matrixStore } from '$lib/matrix';
import { matrixStore, loginWithLoginToken } from '$lib/matrix';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { onMount, onDestroy } from 'svelte';
@ -143,7 +143,45 @@
return;
}
// Try to initialize Matrix
// Check for SSO loginToken in URL (Matrix SSO callback)
const urlParams = new URLSearchParams(window.location.search);
const loginToken = urlParams.get('loginToken');
if (loginToken) {
console.log('Found loginToken in URL, exchanging for credentials...');
// Exchange loginToken for Matrix credentials
const result = await loginWithLoginToken('matrix.mana.how', loginToken);
if (result.success && result.credentials) {
console.log('SSO login successful, initializing Matrix client...');
// Remove loginToken from URL to prevent re-processing on refresh
const cleanUrl = window.location.pathname;
window.history.replaceState({}, '', cleanUrl);
// Initialize with the new credentials
const initialized = await matrixStore.initialize(result.credentials);
if (!initialized) {
initError = matrixStore.error || 'Failed to initialize Matrix client';
}
loading = false;
return;
} else {
// SSO token exchange failed
initError = result.error || 'SSO login failed';
loading = false;
// Clear the URL and redirect to login
window.history.replaceState({}, '', '/login');
goto('/login');
return;
}
}
// Try to initialize Matrix with stored credentials
const success = await matrixStore.initialize();
if (!success) {

View file

@ -34,6 +34,10 @@
"types": "./dist/transcription/index.d.ts",
"default": "./dist/transcription/index.js"
},
"./credit": {
"types": "./dist/credit/index.d.ts",
"default": "./dist/credit/index.js"
},
"./nutrition": {
"types": "./dist/nutrition/index.d.ts",
"default": "./dist/nutrition/index.js"

View file

@ -0,0 +1,61 @@
import { Module, DynamicModule, Global } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { CreditService } from './credit.service';
import { CreditModuleOptions, CREDIT_MODULE_OPTIONS } from './types';
/**
* Shared credit management module for Matrix bots
*
* Provides CreditService for querying credit balances and formatting
* credit-related messages for Matrix chat display.
*
* @example
* ```typescript
* // With explicit configuration
* @Module({
* imports: [
* CreditModule.register({
* authUrl: 'http://mana-core-auth:3001',
* })
* ]
* })
*
* // With ConfigService (reads from auth.url or MANA_CORE_AUTH_URL)
* @Module({
* imports: [CreditModule.forRoot()]
* })
* ```
*/
@Global()
@Module({})
export class CreditModule {
/**
* Register module with explicit options
*/
static register(options: CreditModuleOptions = {}): DynamicModule {
return {
module: CreditModule,
imports: [ConfigModule],
providers: [
{
provide: CREDIT_MODULE_OPTIONS,
useValue: options,
},
CreditService,
],
exports: [CreditService],
};
}
/**
* Register module with ConfigService (reads MANA_CORE_AUTH_URL from config)
*/
static forRoot(): DynamicModule {
return {
module: CreditModule,
imports: [ConfigModule],
providers: [CreditService],
exports: [CreditService],
};
}
}

View file

@ -0,0 +1,316 @@
import { Injectable, Inject, Logger, Optional } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
CreditBalance,
CreditValidationResult,
CreditConsumeResult,
CreditModuleOptions,
CreditStatusMessage,
CreditErrorCode,
CREDIT_MODULE_OPTIONS,
} from './types';
/**
* Shared credit management service for Matrix bots
*
* Provides credit balance queries, validation, and formatted messages
* for displaying credit information in Matrix chat.
*
* @example
* ```typescript
* // In NestJS module
* imports: [CreditModule.register({ authUrl: 'http://mana-core-auth:3001' })]
*
* // In service/controller
* const balance = await creditService.getBalance(token);
* const statusMsg = creditService.formatStatusMessage(balance);
* ```
*/
@Injectable()
export class CreditService {
private readonly logger = new Logger(CreditService.name);
private readonly authUrl: string;
private readonly serviceKey?: string;
private readonly appId?: string;
constructor(
@Optional() private configService: ConfigService,
@Optional() @Inject(CREDIT_MODULE_OPTIONS) private options?: CreditModuleOptions
) {
// Priority: module options > config > environment > default
this.authUrl =
options?.authUrl ||
this.configService?.get<string>('auth.url') ||
this.configService?.get<string>('MANA_CORE_AUTH_URL') ||
'http://localhost:3001';
this.serviceKey =
options?.serviceKey || this.configService?.get<string>('MANA_CORE_SERVICE_KEY');
this.appId = options?.appId || this.configService?.get<string>('APP_ID');
this.logger.log(`Credit service initialized with auth URL: ${this.authUrl}`);
}
/**
* Get credit balance for a user
*
* @param token - User's JWT token
* @returns Credit balance information
*/
async getBalance(token: string): Promise<CreditBalance> {
try {
const response = await fetch(`${this.authUrl}/api/v1/credits/balance`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
this.logger.warn(`Failed to get credit balance: ${response.status}`);
return { balance: 0, hasCredits: false };
}
const data = (await response.json()) as { balance?: number; tier?: string };
const balance = data.balance ?? 0;
return {
balance,
hasCredits: balance > 0,
tier: data.tier,
};
} catch (error) {
this.logger.error('Error getting credit balance:', error);
return { balance: 0, hasCredits: false };
}
}
/**
* Validate if user has enough credits for an operation
*
* @param token - User's JWT token
* @param requiredCredits - Credits required for the operation
* @returns Validation result
*/
async validateCredits(token: string, requiredCredits: number): Promise<CreditValidationResult> {
const balance = await this.getBalance(token);
return {
hasCredits: balance.balance >= requiredCredits,
availableCredits: balance.balance,
requiredCredits,
error:
balance.balance < requiredCredits
? `Nicht genug Credits. Benötigt: ${requiredCredits}, Vorhanden: ${balance.balance.toFixed(2)}`
: undefined,
};
}
/**
* Format credit balance as a status message for Matrix
*
* @param balance - Credit balance or number
* @returns Formatted message with text and HTML versions
*/
formatBalanceMessage(balance: CreditBalance | number): CreditStatusMessage {
const credits = typeof balance === 'number' ? balance : balance.balance;
const hasCredits = credits > 0;
const icon = hasCredits ? '⚡' : '⚠️';
const creditsFormatted = credits.toFixed(2);
const text = `${icon} Credits: ${creditsFormatted}`;
const html = `${icon} <b>Credits:</b> ${creditsFormatted}`;
return { text, html };
}
/**
* Format a full status message with credit information
*
* @param email - User's email (logged in as)
* @param balance - Credit balance
* @param additionalInfo - Additional status info
* @returns Formatted status message
*/
formatStatusMessage(
email: string,
balance: CreditBalance,
additionalInfo?: Record<string, string>
): CreditStatusMessage {
const lines: string[] = [];
const htmlLines: string[] = [];
// Header
lines.push('🤖 Bot Status');
htmlLines.push('<b>🤖 Bot Status</b>');
// User info
lines.push(`👤 User: ${email}`);
htmlLines.push(`👤 <b>User:</b> ${email}`);
// Credits
const creditIcon = balance.hasCredits ? '⚡' : '⚠️';
const creditsFormatted = balance.balance.toFixed(2);
lines.push(`${creditIcon} Credits: ${creditsFormatted}`);
htmlLines.push(`${creditIcon} <b>Credits:</b> ${creditsFormatted}`);
// Tier if available
if (balance.tier) {
lines.push(`📊 Tier: ${balance.tier}`);
htmlLines.push(`📊 <b>Tier:</b> ${balance.tier}`);
}
// Additional info
if (additionalInfo) {
for (const [key, value] of Object.entries(additionalInfo)) {
lines.push(`${key}: ${value}`);
htmlLines.push(`<b>${key}:</b> ${value}`);
}
}
// Low credits warning
if (balance.balance < 10 && balance.balance > 0) {
lines.push('');
lines.push('⚠️ Nur noch wenig Credits!');
lines.push('👉 Credits kaufen: https://mana.how/credits');
htmlLines.push('<br>');
htmlLines.push('⚠️ <b>Nur noch wenig Credits!</b>');
htmlLines.push('👉 <a href="https://mana.how/credits">Credits kaufen</a>');
}
// No credits warning
if (!balance.hasCredits) {
lines.push('');
lines.push('❌ Keine Credits mehr!');
lines.push('👉 Credits kaufen: https://mana.how/credits');
htmlLines.push('<br>');
htmlLines.push('❌ <b>Keine Credits mehr!</b>');
htmlLines.push('👉 <a href="https://mana.how/credits">Credits kaufen</a>');
}
return {
text: lines.join('\n'),
html: htmlLines.join('<br>'),
};
}
/**
* Format an error message for insufficient credits
*
* @param required - Required credits
* @param available - Available credits
* @param operation - Operation name (optional)
* @returns Formatted error message
*/
formatInsufficientCreditsError(
required: number,
available: number,
operation?: string
): CreditStatusMessage {
const lines: string[] = [];
const htmlLines: string[] = [];
lines.push('❌ Nicht genug Credits');
htmlLines.push('❌ <b>Nicht genug Credits</b>');
if (operation) {
lines.push(`Operation: ${operation}`);
htmlLines.push(`<b>Operation:</b> ${operation}`);
}
lines.push(`Benötigt: ${required.toFixed(2)} Credits`);
lines.push(`Vorhanden: ${available.toFixed(2)} Credits`);
htmlLines.push(`<b>Benötigt:</b> ${required.toFixed(2)} Credits`);
htmlLines.push(`<b>Vorhanden:</b> ${available.toFixed(2)} Credits`);
lines.push('');
lines.push('👉 Credits kaufen: https://mana.how/credits');
htmlLines.push('<br>');
htmlLines.push('👉 <a href="https://mana.how/credits">Credits kaufen</a>');
return {
text: lines.join('\n'),
html: htmlLines.join('<br>'),
};
}
/**
* Format a success message after credit consumption
*
* @param consumed - Credits consumed
* @param remaining - Remaining credits
* @param operation - Operation description (optional)
* @returns Formatted success message
*/
formatCreditConsumedMessage(
consumed: number,
remaining: number,
operation?: string
): CreditStatusMessage {
const text = operation
? `${operation}\n⚡ -${consumed.toFixed(2)} Credits (${remaining.toFixed(2)} verbleibend)`
: `⚡ -${consumed.toFixed(2)} Credits (${remaining.toFixed(2)} verbleibend)`;
const html = operation
? `${operation}<br>⚡ -${consumed.toFixed(2)} Credits (${remaining.toFixed(2)} verbleibend)`
: `⚡ -${consumed.toFixed(2)} Credits (${remaining.toFixed(2)} verbleibend)`;
return { text, html };
}
/**
* Get error code from HTTP status
*
* @param status - HTTP status code
* @returns Credit error code
*/
getErrorCodeFromStatus(status: number): CreditErrorCode {
switch (status) {
case 401:
return CreditErrorCode.NOT_LOGGED_IN;
case 402:
return CreditErrorCode.INSUFFICIENT_CREDITS;
case 400:
return CreditErrorCode.INVALID_OPERATION;
default:
return CreditErrorCode.SERVICE_UNAVAILABLE;
}
}
/**
* Format a generic credit error message
*
* @param errorCode - Credit error code
* @returns Formatted error message
*/
formatErrorMessage(errorCode: CreditErrorCode): CreditStatusMessage {
let text: string;
let html: string;
switch (errorCode) {
case CreditErrorCode.INSUFFICIENT_CREDITS:
text = '❌ Nicht genug Credits\n👉 Credits kaufen: https://mana.how/credits';
html =
'❌ <b>Nicht genug Credits</b><br>👉 <a href="https://mana.how/credits">Credits kaufen</a>';
break;
case CreditErrorCode.NOT_LOGGED_IN:
text = '❌ Bitte zuerst einloggen: !login email passwort';
html = '❌ <b>Bitte zuerst einloggen:</b> <code>!login email passwort</code>';
break;
case CreditErrorCode.INVALID_OPERATION:
text = '❌ Ungültige Operation';
html = '❌ <b>Ungültige Operation</b>';
break;
case CreditErrorCode.SERVICE_UNAVAILABLE:
default:
text = '❌ Service temporär nicht verfügbar. Bitte später erneut versuchen.';
html = '❌ <b>Service temporär nicht verfügbar.</b> Bitte später erneut versuchen.';
break;
}
return { text, html };
}
}

View file

@ -0,0 +1,10 @@
export { CreditService } from './credit.service';
export { CreditModule } from './credit.module';
export type {
CreditBalance,
CreditValidationResult,
CreditConsumeResult,
CreditModuleOptions,
CreditStatusMessage,
} from './types';
export { CREDIT_MODULE_OPTIONS, CreditErrorCode } from './types';

View file

@ -0,0 +1,75 @@
/**
* Types for credit management in Matrix bots
*/
/**
* User credit balance information
*/
export interface CreditBalance {
/** Current credit balance */
balance: number;
/** Whether user has enough credits for basic operations */
hasCredits: boolean;
/** User's tier (if applicable) */
tier?: string;
}
/**
* Result of a credit validation check
*/
export interface CreditValidationResult {
/** Whether user has enough credits */
hasCredits: boolean;
/** Available credits */
availableCredits: number;
/** Required credits for the operation */
requiredCredits: number;
/** Error message if not enough credits */
error?: string;
}
/**
* Result of a credit consumption operation
*/
export interface CreditConsumeResult {
/** Whether credits were successfully consumed */
success: boolean;
/** New balance after consumption */
newBalance?: number;
/** Error message if failed */
error?: string;
}
/**
* Credit module configuration options
*/
export interface CreditModuleOptions {
/** Mana Core Auth URL */
authUrl?: string;
/** Service key for credit operations */
serviceKey?: string;
/** App ID for credit operations */
appId?: string;
}
export const CREDIT_MODULE_OPTIONS = 'CREDIT_MODULE_OPTIONS';
/**
* Credit error codes for structured error handling
*/
export enum CreditErrorCode {
INSUFFICIENT_CREDITS = 'INSUFFICIENT_CREDITS',
NOT_LOGGED_IN = 'NOT_LOGGED_IN',
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
INVALID_OPERATION = 'INVALID_OPERATION',
}
/**
* Formatted credit message for Matrix bots
*/
export interface CreditStatusMessage {
/** Plain text message */
text: string;
/** HTML formatted message (for Matrix) */
html: string;
}

View file

@ -101,6 +101,16 @@ export type {
TranscriptionModuleOptions,
} from './transcription/index.js';
// Credit (Credit balance and formatting for Matrix bots)
export { CreditModule, CreditService, CREDIT_MODULE_OPTIONS, CreditErrorCode } from './credit/index.js';
export type {
CreditBalance,
CreditValidationResult,
CreditConsumeResult,
CreditModuleOptions,
CreditStatusMessage,
} from './credit/index.js';
// ===== Placeholder Services (to be implemented) =====
export { NutritionModule } from './nutrition/index.js';

View file

@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { CalendarModule } from '../calendar/calendar.module';
import { TranscriptionModule } from '@manacore/bot-services';
import { TranscriptionModule, SessionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [
@ -9,6 +9,8 @@ import { TranscriptionModule } from '@manacore/bot-services';
TranscriptionModule.register({
sttUrl: process.env.STT_URL || 'http://localhost:3020',
}),
SessionModule.forRoot(),
CreditModule.forRoot(),
],
providers: [MatrixService],
exports: [MatrixService],

View file

@ -7,10 +7,12 @@ import {
KeywordCommandDetector,
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { TranscriptionService } from '@manacore/bot-services';
import { TranscriptionService, SessionService, CreditService } from '@manacore/bot-services';
import { CalendarService, CalendarEvent } from '../calendar/calendar.service';
import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration';
const EVENT_CREATE_CREDITS = 0.02;
@Injectable()
export class MatrixService extends BaseMatrixService {
private readonly keywordDetector = new KeywordCommandDetector(
@ -29,7 +31,9 @@ export class MatrixService extends BaseMatrixService {
constructor(
configService: ConfigService,
private readonly transcriptionService: TranscriptionService,
private calendarService: CalendarService
private calendarService: CalendarService,
private sessionService: SessionService,
private creditService: CreditService
) {
super(configService);
}
@ -157,6 +161,14 @@ export class MatrixService extends BaseMatrixService {
await this.handlePinHelp(roomId, event);
break;
case 'login':
await this.handleLogin(roomId, event, userId, args);
break;
case 'logout':
await this.handleLogout(roomId, event, userId);
break;
default:
// Unknown command - ignore silently
break;
@ -237,6 +249,21 @@ export class MatrixService extends BaseMatrixService {
return;
}
// Validate credits if user is logged in
const token = this.sessionService.getToken(userId);
if (token) {
const validation = await this.creditService.validateCredits(token, EVENT_CREATE_CREDITS);
if (!validation.hasCredits) {
const errorMsg = this.creditService.formatInsufficientCreditsError(
EVENT_CREATE_CREDITS,
validation.availableCredits,
'Termin erstellen'
);
await this.sendReply(roomId, event, errorMsg.text);
return;
}
}
const { title, startTime, endTime, isAllDay } = this.calendarService.parseEventInput(input);
if (!startTime || !endTime) {
@ -264,7 +291,15 @@ export class MatrixService extends BaseMatrixService {
);
const timeStr = this.calendarService.formatEventTime(calendarEvent);
await this.sendReply(roomId, event, `✅ Termin erstellt: **${title}**\n📆 ${timeStr}`);
let response = `✅ Termin erstellt: **${title}**\n📆 ${timeStr}`;
// Show credit deduction if logged in
if (token) {
const balance = await this.creditService.getBalance(token);
response += `\n⚡ -${EVENT_CREATE_CREDITS} Credits (${balance.balance.toFixed(2)} verbleibend)`;
}
await this.sendReply(roomId, event, response);
}
private async handleEventDetails(roomId: string, event: MatrixRoomEvent, userId: string, args: string) {
@ -339,16 +374,71 @@ export class MatrixService extends BaseMatrixService {
const events = await this.calendarService.getUpcomingEvents(userId, 7);
const todayEvents = await this.calendarService.getTodayEvents(userId);
const response = `📊 **Status**
// Check login status and credits
const token = this.sessionService.getToken(userId);
const session = this.sessionService.getSession(userId);
Termine heute: ${todayEvents.length}
Termine nächste 7 Tage: ${events.length}
let response = `📊 **Status**\n\n`;
response += `• Termine heute: ${todayEvents.length}\n`;
response += `• Termine nächste 7 Tage: ${events.length}\n\n`;
Bot: Online`;
if (token && session) {
const balance = await this.creditService.getBalance(token);
response += `👤 Angemeldet als: ${session.email}\n`;
response += `⚡ Credits: ${balance.balance.toFixed(2)}\n\n`;
} else {
response += `👤 Nicht angemeldet\n`;
response += `💡 Login: \`!login email passwort\`\n\n`;
}
response += `Bot: ✅ Online`;
await this.sendReply(roomId, event, response);
}
private async handleLogin(roomId: string, event: MatrixRoomEvent, userId: string, args: string) {
const parts = args.trim().split(/\s+/);
if (parts.length < 2) {
await this.sendReply(
roomId,
event,
'❌ Bitte gib Email und Passwort an.\n\nBeispiel: `!login email@example.com passwort`'
);
return;
}
const [email, password] = parts;
const result = await this.sessionService.login(userId, email, password);
if (!result.success) {
await this.sendReply(roomId, event, `❌ Login fehlgeschlagen: ${result.error}`);
return;
}
const token = this.sessionService.getToken(userId);
if (token) {
const balance = await this.creditService.getBalance(token);
await this.sendReply(
roomId,
event,
`✅ Erfolgreich angemeldet als **${email}**\n⚡ Credits: ${balance.balance.toFixed(2)}`
);
} else {
await this.sendReply(roomId, event, `✅ Erfolgreich angemeldet als **${email}**`);
}
}
private async handleLogout(roomId: string, event: MatrixRoomEvent, userId: string) {
const session = this.sessionService.getSession(userId);
if (!session) {
await this.sendReply(roomId, event, '❌ Du bist nicht angemeldet.');
return;
}
this.sessionService.logout(userId);
await this.sendReply(roomId, event, '✅ Erfolgreich abgemeldet.');
}
private async handlePinHelp(roomId: string, event: MatrixRoomEvent) {
try {
// Send help message

View file

@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { ChatModule } from '../chat/chat.module';
import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
import { SessionModule, TranscriptionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [
@ -10,6 +10,7 @@ import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
TranscriptionModule.register({
sttUrl: process.env.STT_URL || 'http://localhost:3020',
}),
CreditModule.forRoot(),
],
providers: [MatrixService],
exports: [MatrixService],

View file

@ -8,7 +8,7 @@ import {
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { ChatService, Conversation } from '../chat/chat.service';
import { SessionService, TranscriptionService } from '@manacore/bot-services';
import { SessionService, TranscriptionService, CreditService, CreditErrorCode } from '@manacore/bot-services';
import { HELP_MESSAGE, BRANCH_ICONS } from '../config/configuration';
@Injectable()
@ -27,7 +27,8 @@ export class MatrixService extends BaseMatrixService {
configService: ConfigService,
private readonly transcriptionService: TranscriptionService,
private chatService: ChatService,
private sessionService: SessionService
private sessionService: SessionService,
private creditService: CreditService
) {
super(configService);
}
@ -136,7 +137,7 @@ export class MatrixService extends BaseMatrixService {
break;
case 'status':
response = this.handleStatus(sender);
response = await this.handleStatus(sender);
break;
case 'chat':
@ -248,21 +249,38 @@ export class MatrixService extends BaseMatrixService {
return 'Erfolgreich abgemeldet.';
}
private handleStatus(sender: string): string {
private async handleStatus(sender: string): Promise<string> {
const isLoggedIn = this.sessionService.isLoggedIn(sender);
const email = this.sessionService.getEmail(sender);
const token = this.sessionService.getToken(sender);
const currentConv = this.getCurrentConversation(sender);
const selectedModel = this.getSelectedModel(sender);
let status = `**Bot Status**\n`;
status += `- Angemeldet: ${isLoggedIn ? 'Ja' : 'Nein'}\n`;
// Get credit balance if logged in
let creditBalance = { balance: 0, hasCredits: false };
if (token) {
creditBalance = await this.creditService.getBalance(token);
}
const additionalInfo: Record<string, string> = {};
if (currentConv) {
status += `- Aktuelles Gespraech: ${currentConv.substring(0, 8)}...\n`;
additionalInfo['🗨️ Gespraech'] = `${currentConv.substring(0, 8)}...`;
}
if (selectedModel) {
status += `- Gewaehltes Modell: ${selectedModel.substring(0, 8)}...\n`;
additionalInfo['🧠 Modell'] = `${selectedModel.substring(0, 8)}...`;
}
status += `- Aktive Sessions: ${this.sessionService.getSessionCount()}`;
return status;
if (!isLoggedIn) {
return `🤖 **Bot Status**\n\n❌ Nicht angemeldet.\n\nNutze \`!login email passwort\` zum Anmelden.`;
}
const statusMessage = this.creditService.formatStatusMessage(
email || 'Unbekannt',
creditBalance,
additionalInfo
);
return statusMessage.text;
}
// Quick chat (stateless)
@ -292,6 +310,16 @@ export class MatrixService extends BaseMatrixService {
);
if (result.error) {
// Handle 402 Payment Required (insufficient credits)
if (result.statusCode === 402) {
const balance = await this.creditService.getBalance(token);
const errorMsg = this.creditService.formatInsufficientCreditsError(
2, // AI Chat costs ~2 credits
balance.balance,
'AI Chat'
);
return errorMsg.text;
}
return `Fehler: ${result.error}`;
}
@ -465,6 +493,16 @@ Nutze \`!senden [nachricht]\` um zu chatten oder \`!verlauf\` fuer den Nachricht
// Get AI response
const completionResult = await this.chatService.createCompletion(token, messages, convResult.data!.modelId);
if (completionResult.error) {
// Handle 402 Payment Required (insufficient credits)
if (completionResult.statusCode === 402) {
const balance = await this.creditService.getBalance(token);
const errorMsg = this.creditService.formatInsufficientCreditsError(
2, // AI Chat costs ~2 credits
balance.balance,
'AI Chat'
);
return errorMsg.text;
}
return `Fehler bei AI-Antwort: ${completionResult.error}`;
}

View file

@ -59,7 +59,7 @@ export class ChatService {
path: string,
token: string,
options: RequestInit = {}
): Promise<{ data?: T; error?: string }> {
): Promise<{ data?: T; error?: string; statusCode?: number }> {
try {
const response = await fetch(this.getUrl(path), {
...options,
@ -72,7 +72,10 @@ export class ChatService {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
return { error: errorData.message || `HTTP ${response.status}` };
return {
error: errorData.message || `HTTP ${response.status}`,
statusCode: response.status,
};
}
const data = await response.json();
@ -118,7 +121,7 @@ export class ChatService {
messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
modelId: string,
options?: { temperature?: number; maxTokens?: number }
): Promise<{ data?: ChatCompletionResponse; error?: string }> {
): Promise<{ data?: ChatCompletionResponse; error?: string; statusCode?: number }> {
return this.request<ChatCompletionResponse>('/chat/completions', token, {
method: 'POST',
body: JSON.stringify({

View file

@ -1,10 +1,10 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { ClockModule } from '../clock/clock.module';
import { TranscriptionModule } from '@manacore/bot-services';
import { TranscriptionModule, SessionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [ClockModule, TranscriptionModule.forRoot()],
imports: [ClockModule, TranscriptionModule.forRoot(), SessionModule.forRoot(), CreditModule.forRoot()],
providers: [MatrixService],
exports: [MatrixService],
})

View file

@ -8,7 +8,7 @@ import {
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { ClockService } from '../clock/clock.service';
import { TranscriptionService } from '@manacore/bot-services';
import { TranscriptionService, SessionService, CreditService } from '@manacore/bot-services';
import { HELP_TEXT, WELCOME_TEXT } from '../config/configuration';
@Injectable()
@ -28,7 +28,9 @@ export class MatrixService extends BaseMatrixService {
constructor(
configService: ConfigService,
private clockService: ClockService,
private transcriptionService: TranscriptionService
private transcriptionService: TranscriptionService,
private sessionService: SessionService,
private creditService: CreditService
) {
super(configService);
}
@ -156,6 +158,14 @@ export class MatrixService extends BaseMatrixService {
await this.sendReply(roomId, event, HELP_TEXT);
break;
case 'login':
await this.handleLogin(roomId, event, userId, args);
break;
case 'logout':
await this.handleLogout(roomId, event, userId);
break;
case 'timer':
await this.handleTimerCommand(roomId, event, userId, args);
break;
@ -337,34 +347,76 @@ export class MatrixService extends BaseMatrixService {
}
}
private async handleStatusCommand(roomId: string, event: MatrixRoomEvent, userId: string) {
try {
const token = this.getToken(userId);
if (!token) {
await this.sendReply(roomId, event, 'Keine Authentifizierung.');
return;
}
const timer = await this.clockService.getRunningTimer(token);
if (!timer) {
await this.sendReply(roomId, event, 'Kein aktiver Timer.\n\nStarte einen mit `!timer 25m`');
return;
}
const remaining = this.clockService.formatDuration(timer.remainingSeconds);
const total = this.clockService.formatDuration(timer.durationSeconds);
const statusIcon = timer.status === 'running' ? '' : '';
const statusText = timer.status === 'running' ? 'Lauft' : 'Pausiert';
let response = `**${statusIcon} Timer ${statusText}**\n\n`;
response += `Verbleibend: ${remaining} / ${total}`;
if (timer.label) response += `\nLabel: ${timer.label}`;
await this.sendReply(roomId, event, response);
} catch (error) {
this.logger.error('Status failed:', error);
await this.sendReply(roomId, event, 'Fehler beim Abrufen des Status.');
private async handleLogin(roomId: string, event: MatrixRoomEvent, userId: string, args: string) {
const parts = args.split(' ');
if (parts.length < 2 || !parts[0] || !parts[1]) {
await this.sendReply(roomId, event, 'Verwendung: `!login email passwort`');
return;
}
const [email, password] = parts;
const result = await this.sessionService.login(userId, email, password);
if (result.success) {
const token = this.sessionService.getToken(userId);
if (token) {
const balance = await this.creditService.getBalance(token);
await this.sendReply(roomId, event,
`✅ Erfolgreich angemeldet als **${email}**\n⚡ Credits: ${balance.balance.toFixed(2)}`);
} else {
await this.sendReply(roomId, event, `✅ Erfolgreich angemeldet als **${email}**`);
}
} else {
await this.sendReply(roomId, event, `❌ Anmeldung fehlgeschlagen: ${result.error}`);
}
}
private async handleLogout(roomId: string, event: MatrixRoomEvent, userId: string) {
this.sessionService.logout(userId);
await this.sendReply(roomId, event, '👋 Erfolgreich abgemeldet.');
}
private async handleStatusCommand(roomId: string, event: MatrixRoomEvent, userId: string) {
// Auth-Status zuerst
const loggedIn = this.sessionService.isLoggedIn(userId);
const session = this.sessionService.getSession(userId);
const token = this.sessionService.getToken(userId);
let response = '**🕐 Clock Bot Status**\n\n';
if (loggedIn && session && token) {
const balance = await this.creditService.getBalance(token);
response += `👤 Angemeldet als: ${session.email}\n`;
response += `⚡ Credits: ${balance.balance.toFixed(2)}\n\n`;
} else {
response += `❌ Nicht angemeldet\n`;
response += `Nutze \`!login email passwort\` zum Anmelden.\n\n`;
}
// Timer-Status
try {
const timerToken = this.getToken(userId);
if (timerToken) {
const timer = await this.clockService.getRunningTimer(timerToken);
if (timer) {
const remaining = this.clockService.formatDuration(timer.remainingSeconds);
const total = this.clockService.formatDuration(timer.durationSeconds);
const statusIcon = timer.status === 'running' ? '▶️' : '⏸️';
const statusText = timer.status === 'running' ? 'Läuft' : 'Pausiert';
response += `**${statusIcon} Timer ${statusText}**\n`;
response += `Verbleibend: ${remaining} / ${total}`;
if (timer.label) response += `\nLabel: ${timer.label}`;
} else {
response += `Kein aktiver Timer.\n\nStarte einen mit \`!timer 25m\``;
}
} else {
response += `Timer-Status nicht verfügbar (nicht angemeldet).`;
}
} catch (error) {
this.logger.error('Timer status failed:', error);
response += `Timer-Status nicht verfügbar.`;
}
await this.sendReply(roomId, event, response);
}
private async handleTimersCommand(roomId: string, event: MatrixRoomEvent, userId: string) {
@ -603,11 +655,15 @@ export class MatrixService extends BaseMatrixService {
}
private getToken(userId: string): string | null {
// First check if user has a stored token
// SessionService hat Priorität (Login via mana-core-auth)
const sessionToken = this.sessionService.getToken(userId);
if (sessionToken) return sessionToken;
// Fallback auf clockService Token
const storedToken = this.clockService.getUserToken(userId);
if (storedToken) return storedToken;
// Fall back to demo token for development
// Entwicklungs-Fallback
return this.demoToken || null;
}
}

View file

@ -16,12 +16,16 @@ export default () => ({
export const HELP_TEXT = `**Clock Bot - Zeiterfassung per Chat**
**Account:**
- \`!login email passwort\` - Anmelden
- \`!logout\` - Abmelden
- \`!status\` - Account & Timer Status
**Timer (Stoppuhr):**
- \`!timer 25m\` oder \`!timer 1h30m\` - Timer erstellen & starten
- \`!stop\` - Laufenden Timer pausieren
- \`!resume\` - Pausierten Timer fortsetzen
- \`!reset\` - Timer zurucksetzen
- \`!status\` - Aktuellen Timer-Status anzeigen
- \`!timers\` - Alle Timer anzeigen
**Alarme (Wecker):**

View file

@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { ContactsModule } from '../contacts/contacts.module';
import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
import { SessionModule, TranscriptionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [
@ -10,6 +10,7 @@ import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
TranscriptionModule.register({
sttUrl: process.env.STT_URL || 'http://localhost:3020',
}),
CreditModule.forRoot(),
],
providers: [MatrixService],
exports: [MatrixService],

View file

@ -9,9 +9,11 @@ import {
UserListMapper,
} from '@manacore/matrix-bot-common';
import { ContactsService, Contact } from '../contacts/contacts.service';
import { SessionService, TranscriptionService } from '@manacore/bot-services';
import { SessionService, TranscriptionService, CreditService } from '@manacore/bot-services';
import { HELP_MESSAGE } from '../config/configuration';
const CONTACT_CREATE_CREDITS = 0.02;
// Natural language keyword detector
const keywordDetector = new KeywordCommandDetector([
...COMMON_KEYWORDS,
@ -29,7 +31,8 @@ export class MatrixService extends BaseMatrixService {
configService: ConfigService,
private readonly transcriptionService: TranscriptionService,
private contactsService: ContactsService,
private sessionService: SessionService
private sessionService: SessionService,
private creditService: CreditService
) {
super(configService);
}
@ -414,6 +417,18 @@ Sag "hilfe" fur alle Befehle!`;
return;
}
// Validate credits
const validation = await this.creditService.validateCredits(token, CONTACT_CREATE_CREDITS);
if (!validation.hasCredits) {
const errorMsg = this.creditService.formatInsufficientCreditsError(
CONTACT_CREATE_CREDITS,
validation.availableCredits,
'Kontakt erstellen'
);
await this.sendReply(roomId, event, errorMsg.text);
return;
}
if (args.length < 1) {
await this.sendReply(
roomId,
@ -433,10 +448,11 @@ Sag "hilfe" fur alle Befehle!`;
});
const name = contact.displayName || `${firstName} ${lastName || ''}`.trim();
const balance = await this.creditService.getBalance(token);
await this.sendReply(
roomId,
event,
`Kontakt **${name}** erstellt!\n\nNutze \`!kontakte\` um die Liste zu sehen oder \`!edit\` um weitere Daten hinzuzufugen.`
`Kontakt **${name}** erstellt!\n⚡ -${CONTACT_CREATE_CREDITS} Credits (${balance.balance.toFixed(2)} verbleibend)\n\nNutze \`!kontakte\` um die Liste zu sehen oder \`!edit\` um weitere Daten hinzuzufugen.`
);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler';
@ -693,13 +709,23 @@ Sag "hilfe" fur alle Befehle!`;
const result = await this.sessionService.login(sender, email, password);
if (result.success) {
await this.sendReply(
roomId,
event,
`Erfolgreich angemeldet!\n\nNutze \`!kontakte\` um deine Kontakte zu sehen.`
);
const token = this.sessionService.getToken(sender);
if (token) {
const balance = await this.creditService.getBalance(token);
await this.sendReply(
roomId,
event,
`✅ Erfolgreich angemeldet als **${email}**\n⚡ Credits: ${balance.balance.toFixed(2)}\n\nNutze \`!kontakte\` um deine Kontakte zu sehen.`
);
} else {
await this.sendReply(
roomId,
event,
`✅ Erfolgreich angemeldet!\n\nNutze \`!kontakte\` um deine Kontakte zu sehen.`
);
}
} else {
await this.sendReply(roomId, event, `Anmeldung fehlgeschlagen: ${result.error}`);
await this.sendReply(roomId, event, `Anmeldung fehlgeschlagen: ${result.error}`);
}
}
@ -707,14 +733,21 @@ Sag "hilfe" fur alle Befehle!`;
const backendHealthy = await this.contactsService.checkHealth();
const isLoggedIn = this.sessionService.isLoggedIn(sender);
const sessionCount = this.sessionService.getSessionCount();
const session = this.sessionService.getSession(sender);
const token = this.sessionService.getToken(sender);
const statusText = `**Contacts Bot Status**
let statusText = `**Contacts Bot Status**\n\n`;
statusText += `**Backend:** ${backendHealthy ? '✅ Online' : '❌ Offline'}\n`;
statusText += `**Aktive Sessions:** ${sessionCount}\n\n`;
**Backend:** ${backendHealthy ? 'Online' : 'Offline'}
**Dein Status:** ${isLoggedIn ? 'Angemeldet' : 'Nicht angemeldet'}
**Aktive Sessions:** ${sessionCount}
${!isLoggedIn ? 'Nutze `!login email passwort` um dich anzumelden.' : ''}`;
if (isLoggedIn && session && token) {
const balance = await this.creditService.getBalance(token);
statusText += `👤 Angemeldet als: ${session.email}\n`;
statusText += `⚡ Credits: ${balance.balance.toFixed(2)}\n`;
} else {
statusText += `👤 Nicht angemeldet\n`;
statusText += `💡 Login: \`!login email passwort\``;
}
await this.sendReply(roomId, event, statusText);
}

View file

@ -4,9 +4,16 @@ import { CommandRouterService } from './command-router.service';
import { HandlersModule } from '../handlers/handlers.module';
import { OrchestrationModule } from '../orchestration/orchestration.module';
import { VoiceModule } from '../voice/voice.module';
import { SessionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [forwardRef(() => HandlersModule), forwardRef(() => OrchestrationModule), VoiceModule],
imports: [
forwardRef(() => HandlersModule),
forwardRef(() => OrchestrationModule),
VoiceModule,
SessionModule.forRoot(),
CreditModule.forRoot(),
],
providers: [MatrixService, CommandRouterService],
exports: [MatrixService, CommandRouterService],
})

View file

@ -67,6 +67,18 @@ export class CommandRouterService {
description: 'Show help',
},
// Auth Commands
{
patterns: ['!login'],
handler: (ctx, args) => this.helpHandler.handleLogin(ctx, args),
description: 'Login with email and password',
},
{
patterns: ['!logout'],
handler: (ctx) => this.helpHandler.handleLogout(ctx),
description: 'Logout',
},
// AI Commands
{
patterns: ['!models', '!modelle'],

View file

@ -4,6 +4,7 @@ import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent } from '@manacore/m
import { CommandRouterService, CommandContext } from './command-router.service';
import { VoiceService, VoiceServiceError } from '../voice/voice.service';
import { VoiceFormatterService } from '../voice/voice-formatter.service';
import { SessionService, CreditService } from '@manacore/bot-services';
import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration';
@Injectable()
@ -16,7 +17,9 @@ export class MatrixService extends BaseMatrixService {
private commandRouter: CommandRouterService,
@Inject(forwardRef(() => VoiceService))
private voiceService: VoiceService,
private voiceFormatter: VoiceFormatterService
private voiceFormatter: VoiceFormatterService,
private sessionService: SessionService,
private creditService: CreditService
) {
super(configService);
this.voiceEnabled = configService.get('voice.enabled') !== false;

View file

@ -37,6 +37,11 @@ export default () => ({
// Help text for the unified bot
export const HELP_TEXT = `**🤖 Mana - Dein Assistent**
**👤 Account**
\`!login email passwort\` - Anmelden
\`!logout\` - Abmelden
\`!status\` - Account & Bot Status
**AI & Chat**
Schreib einfach eine Nachricht - ich antworte!
\`!model [name]\` - KI-Modell wechseln

View file

@ -7,9 +7,10 @@ import { HelpHandler } from './help.handler';
import { VoiceHandler } from './voice.handler';
import { BotModule } from '../bot/bot.module';
import { VoiceModule } from '../voice/voice.module';
import { SessionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [forwardRef(() => BotModule), VoiceModule],
imports: [forwardRef(() => BotModule), VoiceModule, SessionModule.forRoot(), CreditModule.forRoot()],
providers: [AiHandler, TodoHandler, CalendarHandler, ClockHandler, HelpHandler, VoiceHandler],
exports: [AiHandler, TodoHandler, CalendarHandler, ClockHandler, HelpHandler, VoiceHandler],
})

View file

@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AiService, TodoService } from '@manacore/bot-services';
import { AiService, TodoService, SessionService, CreditService } from '@manacore/bot-services';
import { CommandContext } from '../bot/command-router.service';
import { VoiceService } from '../voice/voice.service';
import { HELP_TEXT } from '../config/configuration';
@ -9,14 +9,61 @@ export class HelpHandler {
constructor(
private aiService: AiService,
private todoService: TodoService,
private voiceService: VoiceService
private voiceService: VoiceService,
private sessionService: SessionService,
private creditService: CreditService
) {}
async showHelp(ctx: CommandContext): Promise<string> {
return HELP_TEXT;
}
async handleLogin(ctx: CommandContext, args: string): Promise<string> {
const parts = args.split(' ');
if (parts.length < 2 || !parts[0] || !parts[1]) {
return 'Verwendung: `!login email passwort`';
}
const [email, password] = parts;
const result = await this.sessionService.login(ctx.userId, email, password);
if (result.success) {
const token = this.sessionService.getToken(ctx.userId);
if (token) {
const balance = await this.creditService.getBalance(token);
return `✅ Erfolgreich angemeldet als **${email}**\n⚡ Credits: ${balance.balance.toFixed(2)}`;
}
return `✅ Erfolgreich angemeldet als **${email}**`;
}
return `❌ Anmeldung fehlgeschlagen: ${result.error}`;
}
async handleLogout(ctx: CommandContext): Promise<string> {
this.sessionService.logout(ctx.userId);
return '👋 Erfolgreich abgemeldet.';
}
async showStatus(ctx: CommandContext): Promise<string> {
// Auth-Status zuerst
const loggedIn = this.sessionService.isLoggedIn(ctx.userId);
const session = this.sessionService.getSession(ctx.userId);
const token = this.sessionService.getToken(ctx.userId);
let authSection = '';
if (loggedIn && session && token) {
const balance = await this.creditService.getBalance(token);
authSection = `**👤 Account**
Angemeldet als: ${session.email}
Credits: ${balance.balance.toFixed(2)}
`;
} else {
authSection = `**👤 Account**
Nicht angemeldet
Nutze \`!login email passwort\`
`;
}
// Check services in parallel
const [aiConnected, todoStats, voiceHealth] = await Promise.all([
this.aiService.checkConnection(),
@ -36,7 +83,7 @@ export class HelpHandler {
return `**📊 Status**
**AI/Ollama**
${authSection}**AI/Ollama**
Verbindung: ${aiStatus}
Modell: \`${currentModel}\`

View file

@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { ManadeckModule } from '../manadeck/manadeck.module';
import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
import { SessionModule, TranscriptionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [
@ -10,6 +10,7 @@ import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
TranscriptionModule.register({
sttUrl: process.env.STT_URL || 'http://localhost:3020',
}),
CreditModule.forRoot(),
],
providers: [MatrixService],
exports: [MatrixService],

View file

@ -9,9 +9,12 @@ import {
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { ManadeckService, Deck, Card } from '../manadeck/manadeck.service';
import { SessionService, TranscriptionService } from '@manacore/bot-services';
import { SessionService, TranscriptionService, CreditService } from '@manacore/bot-services';
import { HELP_MESSAGE } from '../config/configuration';
const DECK_CREATE_CREDITS = 0.1;
const AI_DECK_GENERATE_CREDITS = 20;
@Injectable()
export class MatrixService extends BaseMatrixService {
// Store last shown decks/cards per user for reference by number
@ -36,7 +39,8 @@ export class MatrixService extends BaseMatrixService {
configService: ConfigService,
private readonly transcriptionService: TranscriptionService,
private manadeckService: ManadeckService,
private sessionService: SessionService
private sessionService: SessionService,
private creditService: CreditService
) {
super(configService);
}
@ -221,9 +225,15 @@ export class MatrixService extends BaseMatrixService {
const result = await this.sessionService.login(sender, email, password);
if (result.success) {
await this.sendHtml(roomId, `<p>Erfolgreich angemeldet als <strong>${email}</strong></p>`);
const token = this.sessionService.getToken(sender);
if (token) {
const balance = await this.creditService.getBalance(token);
await this.sendHtml(roomId, `<p>✅ Erfolgreich angemeldet als <strong>${email}</strong><br/>⚡ Credits: ${balance.balance.toFixed(2)}</p>`);
} else {
await this.sendHtml(roomId, `<p>✅ Erfolgreich angemeldet als <strong>${email}</strong></p>`);
}
} else {
await this.sendHtml(roomId, `<p>Login fehlgeschlagen: ${result.error}</p>`);
await this.sendHtml(roomId, `<p>Login fehlgeschlagen: ${result.error}</p>`);
}
}
@ -231,16 +241,24 @@ export class MatrixService extends BaseMatrixService {
const backendOk = await this.manadeckService.checkHealth();
const loggedIn = this.sessionService.isLoggedIn(sender);
const sessions = this.sessionService.getSessionCount();
const session = this.sessionService.getSession(sender);
const token = this.sessionService.getToken(sender);
await this.sendHtml(
roomId,
`<h3>ManaDeck Bot Status</h3>
<ul>
<li>Backend: ${backendOk ? 'Online' : 'Offline'}</li>
<li>Angemeldet: ${loggedIn ? 'Ja' : 'Nein'}</li>
<li>Aktive Sessions: ${sessions}</li>
</ul>`
);
let statusHtml = `<h3>ManaDeck Bot Status</h3><ul>`;
statusHtml += `<li>Backend: ${backendOk ? '✅ Online' : '❌ Offline'}</li>`;
statusHtml += `<li>Aktive Sessions: ${sessions}</li>`;
if (loggedIn && session && token) {
const balance = await this.creditService.getBalance(token);
statusHtml += `<li>👤 Angemeldet als: ${session.email}</li>`;
statusHtml += `<li>⚡ Credits: ${balance.balance.toFixed(2)}</li>`;
} else {
statusHtml += `<li>👤 Nicht angemeldet</li>`;
statusHtml += `<li>💡 Login: <code>!login email passwort</code></li>`;
}
statusHtml += `</ul>`;
await this.sendHtml(roomId, statusHtml);
}
// Deck handlers

View file

@ -1,10 +1,10 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { NutriPhiModule } from '../nutriphi/nutriphi.module';
import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
import { SessionModule, TranscriptionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [NutriPhiModule, SessionModule.forRoot(), TranscriptionModule.forRoot()],
imports: [NutriPhiModule, SessionModule.forRoot(), TranscriptionModule.forRoot(), CreditModule.forRoot()],
providers: [MatrixService],
exports: [MatrixService],
})

View file

@ -13,9 +13,11 @@ import {
DailySummary,
WeeklyStats,
} from '../nutriphi/nutriphi.service';
import { SessionService, TranscriptionService } from '@manacore/bot-services';
import { SessionService, TranscriptionService, CreditService } from '@manacore/bot-services';
import { HELP_MESSAGE, MEAL_TYPE_LABELS } from '../config/configuration';
const PHOTO_ANALYSIS_CREDITS = 3;
// Natural language keyword detector
const keywordDetector = new KeywordCommandDetector([
...COMMON_KEYWORDS,
@ -33,7 +35,8 @@ export class MatrixService extends BaseMatrixService {
configService: ConfigService,
private nutriphiService: NutriPhiService,
private sessionService: SessionService,
private transcriptionService: TranscriptionService
private transcriptionService: TranscriptionService,
private creditService: CreditService
) {
super(configService);
}
@ -256,10 +259,19 @@ Sag "hilfe" fur alle Befehle!`;
const result = await this.sessionService.login(sender, email, password);
if (result.success) {
await this.sendMessage(
roomId,
`Erfolgreich angemeldet!\n\nDu kannst jetzt Fotos analysieren und deine Ernahrung tracken.`
);
const token = this.sessionService.getToken(sender);
if (token) {
const balance = await this.creditService.getBalance(token);
await this.sendMessage(
roomId,
`✅ Erfolgreich angemeldet als **${email}**\n⚡ Credits: ${balance.balance.toFixed(2)}\n\nDu kannst jetzt Fotos analysieren und deine Ernahrung tracken.`
);
} else {
await this.sendMessage(
roomId,
`✅ Erfolgreich angemeldet!\n\nDu kannst jetzt Fotos analysieren und deine Ernahrung tracken.`
);
}
} else {
await this.sendMessage(roomId, `Anmeldung fehlgeschlagen: ${result.error}`);
}
@ -614,15 +626,21 @@ Sag "hilfe" fur alle Befehle!`;
const backendHealthy = await this.nutriphiService.checkHealth();
const isLoggedIn = this.sessionService.isLoggedIn(sender);
const sessionCount = this.sessionService.getSessionCount();
const loggedInCount = this.sessionService.getActiveSessionCount();
const session = this.sessionService.getSession(sender);
const token = this.sessionService.getToken(sender);
const statusText = `**NutriPhi Bot Status**
let statusText = `**NutriPhi Bot Status**\n\n`;
statusText += `**Backend:** ${backendHealthy ? '✅ Online' : '❌ Offline'}\n`;
statusText += `**Aktive Sessions:** ${sessionCount}\n\n`;
**Backend:** ${backendHealthy ? 'Online' : 'Offline'}
**Dein Status:** ${isLoggedIn ? 'Angemeldet' : 'Nicht angemeldet'}
**Aktive Sessions:** ${sessionCount} (${loggedInCount} angemeldet)
${!isLoggedIn ? 'Nutze `!login email passwort` um dich anzumelden.' : ''}`;
if (isLoggedIn && session && token) {
const balance = await this.creditService.getBalance(token);
statusText += `👤 Angemeldet als: ${session.email}\n`;
statusText += `⚡ Credits: ${balance.balance.toFixed(2)}\n`;
} else {
statusText += `👤 Nicht angemeldet\n`;
statusText += `💡 Login: \`!login email passwort\``;
}
await this.sendMessage(roomId, statusText);
}

View file

@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { OllamaModule } from '../ollama/ollama.module';
import { TranscriptionModule } from '@manacore/bot-services';
import { TranscriptionModule, SessionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [
@ -9,6 +9,8 @@ import { TranscriptionModule } from '@manacore/bot-services';
TranscriptionModule.register({
sttUrl: process.env.STT_URL || 'http://localhost:3020',
}),
SessionModule.forRoot(),
CreditModule.forRoot(),
],
providers: [MatrixService],
exports: [MatrixService],

View file

@ -7,10 +7,13 @@ import {
KeywordCommandDetector,
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { TranscriptionService } from '@manacore/bot-services';
import { TranscriptionService, SessionService, CreditService } from '@manacore/bot-services';
import { OllamaService } from '../ollama/ollama.service';
import { SYSTEM_PROMPTS } from '../config/configuration';
// Ollama is local, so credits are minimal
const OLLAMA_CHAT_CREDITS = 0.1;
interface UserSession {
systemPrompt: string;
model: string;
@ -39,7 +42,9 @@ export class MatrixService extends BaseMatrixService {
constructor(
configService: ConfigService,
private readonly transcriptionService: TranscriptionService,
private ollamaService: OllamaService
private ollamaService: OllamaService,
private sessionService: SessionService,
private creditService: CreditService
) {
super(configService);
}
@ -382,14 +387,23 @@ Schreibe einfach eine Nachricht und ich antworte!
const connected = await this.ollamaService.checkConnection();
const models = await this.ollamaService.listModels();
const session = this.getSession(sender);
const loggedIn = this.sessionService.isLoggedIn(sender);
const authSession = this.sessionService.getSession(sender);
const token = this.sessionService.getToken(sender);
const statusText = `**Ollama Status**
let statusText = `**Ollama Status**\n\n`;
statusText += `**Verbindung:** ${connected ? 'Online' : 'Offline'}\n`;
statusText += `**Modelle:** ${models.length}\n`;
statusText += `**Dein Modell:** \`${session.model}\`\n`;
statusText += `**Chat-Verlauf:** ${session.history.length} Nachrichten\n`;
**Verbindung:** ${connected ? 'Online' : 'Offline'}
**Modelle:** ${models.length}
**Dein Modell:** \`${session.model}\`
**Chat-Verlauf:** ${session.history.length} Nachrichten
**DSGVO:** Alle Daten lokal`;
if (loggedIn && authSession && token) {
const balance = await this.creditService.getBalance(token);
statusText += `**👤 Angemeldet als:** ${authSession.email}\n`;
statusText += `**⚡ Credits:** ${balance.balance.toFixed(2)}\n`;
}
statusText += `**DSGVO:** Alle Daten lokal`;
await this.sendMessage(roomId, statusText);
}

View file

@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { PictureModule } from '../picture/picture.module';
import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
import { SessionModule, TranscriptionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [
@ -10,6 +10,7 @@ import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
TranscriptionModule.register({
sttUrl: process.env.STT_URL || 'http://localhost:3020',
}),
CreditModule.forRoot(),
],
providers: [MatrixService],
exports: [MatrixService],

View file

@ -8,9 +8,12 @@ import {
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { PictureService } from '../picture/picture.service';
import { SessionService, TranscriptionService } from '@manacore/bot-services';
import { SessionService, TranscriptionService, CreditService } from '@manacore/bot-services';
import { HELP_MESSAGE } from '../config/configuration';
// Credit cost for image generation
const IMAGE_GENERATION_CREDITS = 10;
interface ParsedPrompt {
prompt: string;
negativePrompt?: string;
@ -38,7 +41,8 @@ export class MatrixService extends BaseMatrixService {
configService: ConfigService,
private readonly transcriptionService: TranscriptionService,
private pictureService: PictureService,
private sessionService: SessionService
private sessionService: SessionService,
private creditService: CreditService
) {
super(configService);
}
@ -239,6 +243,18 @@ Sag "hilfe" fur alle Befehle!`;
return;
}
// Validate credits before generating
const validation = await this.creditService.validateCredits(token, IMAGE_GENERATION_CREDITS);
if (!validation.hasCredits) {
const errorMsg = this.creditService.formatInsufficientCreditsError(
IMAGE_GENERATION_CREDITS,
validation.availableCredits,
'Bildgenerierung'
);
await this.sendMessage(roomId, errorMsg.text);
return;
}
// Check if user already has an active generation
if (this.activeGenerations.has(sender)) {
await this.sendMessage(
@ -461,11 +477,17 @@ Sag "hilfe" fur alle Befehle!`;
}
try {
const balance = await this.pictureService.getCredits(token);
await this.sendMessage(
roomId,
`**Dein Credit-Guthaben:** ${balance} Credits\n\nEine Bildgenerierung kostet 10 Credits.`
);
const balance = await this.creditService.getBalance(token);
const creditIcon = balance.hasCredits ? '⚡' : '⚠️';
let text = `${creditIcon} **Dein Credit-Guthaben:** ${balance.balance.toFixed(2)} Credits\n\n`;
text += `Eine Bildgenerierung kostet **${IMAGE_GENERATION_CREDITS} Credits**.`;
if (balance.balance < IMAGE_GENERATION_CREDITS) {
text += '\n\n⚠ Nicht genug Credits fur eine Generierung!';
text += '\n👉 Credits kaufen: https://mana.how/credits';
}
await this.sendMessage(roomId, text);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler';
await this.sendMessage(roomId, `Fehler: ${errorMsg}`);
@ -514,14 +536,30 @@ Sag "hilfe" fur alle Befehle!`;
private async handleStatus(roomId: string, sender: string) {
const backendHealthy = await this.pictureService.checkHealth();
const isLoggedIn = this.sessionService.isLoggedIn(sender);
const email = this.sessionService.getEmail(sender);
const token = this.sessionService.getToken(sender);
const sessionCount = this.sessionService.getSessionCount();
const currentModel = this.userModels.get(sender);
const hasActiveGeneration = this.activeGenerations.has(sender);
// Get credit balance if logged in
let creditInfo = '';
if (token) {
const balance = await this.creditService.getBalance(token);
const creditIcon = balance.hasCredits ? '⚡' : '⚠️';
creditInfo = `\n${creditIcon} **Credits:** ${balance.balance.toFixed(2)}`;
if (balance.balance < IMAGE_GENERATION_CREDITS && balance.balance > 0) {
creditInfo += '\n⚠ Nicht genug Credits fur eine Generierung!';
}
if (!balance.hasCredits) {
creditInfo += '\n👉 Credits kaufen: https://mana.how/credits';
}
}
const statusText = `**Picture Bot Status**
**Backend:** ${backendHealthy ? 'Online' : 'Offline'}
**Dein Status:** ${isLoggedIn ? 'Angemeldet' : 'Nicht angemeldet'}
**Dein Status:** ${isLoggedIn ? `Angemeldet (${email})` : 'Nicht angemeldet'}${creditInfo}
**Ausgewahltes Modell:** ${currentModel || 'Standard'}
**Aktive Generierung:** ${hasActiveGeneration ? 'Ja' : 'Nein'}
**Aktive Sessions:** ${sessionCount}

View file

@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { PlantaModule } from '../planta/planta.module';
import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
import { SessionModule, TranscriptionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [
@ -10,6 +10,7 @@ import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
TranscriptionModule.register({
sttUrl: process.env.STT_URL || 'http://localhost:3020',
}),
CreditModule.forRoot(),
],
providers: [MatrixService],
exports: [MatrixService],

View file

@ -9,7 +9,7 @@ import {
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { PlantaService, Plant } from '../planta/planta.service';
import { SessionService, TranscriptionService } from '@manacore/bot-services';
import { SessionService, TranscriptionService, CreditService } from '@manacore/bot-services';
import { HELP_MESSAGE } from '../config/configuration';
@Injectable()
@ -51,7 +51,8 @@ export class MatrixService extends BaseMatrixService {
configService: ConfigService,
private readonly transcriptionService: TranscriptionService,
private plantaService: PlantaService,
private sessionService: SessionService
private sessionService: SessionService,
private creditService: CreditService
) {
super(configService);
}
@ -211,9 +212,15 @@ export class MatrixService extends BaseMatrixService {
const result = await this.sessionService.login(sender, email, password);
if (result.success) {
await this.sendMessage(roomId, `<p>Erfolgreich angemeldet als <strong>${email}</strong></p>`);
const token = this.sessionService.getToken(sender);
if (token) {
const balance = await this.creditService.getBalance(token);
await this.sendMessage(roomId, `<p>✅ Erfolgreich angemeldet als <strong>${email}</strong><br/>⚡ Credits: ${balance.balance.toFixed(2)}</p>`);
} else {
await this.sendMessage(roomId, `<p>✅ Erfolgreich angemeldet als <strong>${email}</strong></p>`);
}
} else {
await this.sendMessage(roomId, `<p>Login fehlgeschlagen: ${result.error}</p>`);
await this.sendMessage(roomId, `<p>Login fehlgeschlagen: ${result.error}</p>`);
}
}
@ -221,16 +228,24 @@ export class MatrixService extends BaseMatrixService {
const backendOk = await this.plantaService.checkHealth();
const loggedIn = this.sessionService.isLoggedIn(sender);
const sessions = this.sessionService.getSessionCount();
const session = this.sessionService.getSession(sender);
const token = this.sessionService.getToken(sender);
await this.sendMessage(
roomId,
`<h3>Planta Bot Status</h3>
<ul>
<li>Backend: ${backendOk ? 'Online' : 'Offline'}</li>
<li>Angemeldet: ${loggedIn ? 'Ja' : 'Nein'}</li>
<li>Aktive Sessions: ${sessions}</li>
</ul>`
);
let statusHtml = `<h3>Planta Bot Status</h3><ul>`;
statusHtml += `<li>Backend: ${backendOk ? '✅ Online' : '❌ Offline'}</li>`;
statusHtml += `<li>Aktive Sessions: ${sessions}</li>`;
if (loggedIn && session && token) {
const balance = await this.creditService.getBalance(token);
statusHtml += `<li>👤 Angemeldet als: ${session.email}</li>`;
statusHtml += `<li>⚡ Credits: ${balance.balance.toFixed(2)}</li>`;
} else {
statusHtml += `<li>👤 Nicht angemeldet</li>`;
statusHtml += `<li>💡 Login: <code>!login email passwort</code></li>`;
}
statusHtml += `</ul>`;
await this.sendMessage(roomId, statusHtml);
}
// Plant handlers

View file

@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { PresiModule } from '../presi/presi.module';
import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
import { SessionModule, TranscriptionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [
@ -10,6 +10,7 @@ import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
TranscriptionModule.register({
sttUrl: process.env.STT_URL || 'http://localhost:3020',
}),
CreditModule.forRoot(),
],
providers: [MatrixService],
exports: [MatrixService],

View file

@ -9,7 +9,7 @@ import {
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { PresiService, Deck, Theme, SlideContent } from '../presi/presi.service';
import { SessionService, TranscriptionService } from '@manacore/bot-services';
import { SessionService, TranscriptionService, CreditService } from '@manacore/bot-services';
import { HELP_MESSAGE } from '../config/configuration';
@Injectable()
@ -32,7 +32,8 @@ export class MatrixService extends BaseMatrixService {
configService: ConfigService,
private readonly transcriptionService: TranscriptionService,
private presiService: PresiService,
private sessionService: SessionService
private sessionService: SessionService,
private creditService: CreditService
) {
super(configService);
}
@ -204,7 +205,13 @@ export class MatrixService extends BaseMatrixService {
const result = await this.sessionService.login(sender, email, password);
if (result.success) {
await this.sendMessage(roomId, `<p>Erfolgreich angemeldet als <strong>${email}</strong></p>`);
const token = this.sessionService.getToken(sender);
if (token) {
const balance = await this.creditService.getBalance(token);
await this.sendMessage(roomId, `<p>✅ Erfolgreich angemeldet als <strong>${email}</strong><br/>⚡ Credits: ${balance.balance.toFixed(2)}</p>`);
} else {
await this.sendMessage(roomId, `<p>✅ Erfolgreich angemeldet als <strong>${email}</strong></p>`);
}
} else {
await this.sendMessage(roomId, `<p>Login fehlgeschlagen: ${result.error}</p>`);
}
@ -214,16 +221,23 @@ export class MatrixService extends BaseMatrixService {
const backendOk = await this.presiService.checkHealth();
const loggedIn = this.sessionService.isLoggedIn(sender);
const sessions = this.sessionService.getSessionCount();
const session = this.sessionService.getSession(sender);
const token = this.sessionService.getToken(sender);
await this.sendMessage(
roomId,
`<h3>Presi Bot Status</h3>
<ul>
<li>Backend: ${backendOk ? 'Online' : 'Offline'}</li>
<li>Angemeldet: ${loggedIn ? 'Ja' : 'Nein'}</li>
<li>Aktive Sessions: ${sessions}</li>
</ul>`
);
let statusHtml = '<h3>Presi Bot Status</h3><ul>';
statusHtml += `<li>Backend: ${backendOk ? 'Online' : 'Offline'}</li>`;
statusHtml += `<li>Angemeldet: ${loggedIn ? 'Ja' : 'Nein'}</li>`;
if (loggedIn && session && token) {
const balance = await this.creditService.getBalance(token);
statusHtml += `<li>👤 Angemeldet als: ${session.email}</li>`;
statusHtml += `<li>⚡ Credits: ${balance.balance.toFixed(2)}</li>`;
}
statusHtml += `<li>Aktive Sessions: ${sessions}</li>`;
statusHtml += '</ul>';
await this.sendMessage(roomId, statusHtml);
}
// Deck handlers

View file

@ -3,9 +3,10 @@ import { MatrixService } from './matrix.service';
import { ProjectModule } from '../project/project.module';
import { MediaModule } from '../media/media.module';
import { GenerationModule } from '../generation/generation.module';
import { SessionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [ProjectModule, MediaModule, GenerationModule],
imports: [ProjectModule, MediaModule, GenerationModule, SessionModule.forRoot(), CreditModule.forRoot()],
providers: [MatrixService],
exports: [MatrixService],
})

View file

@ -10,6 +10,7 @@ import {
import { ProjectService } from '../project/project.service';
import { MediaService } from '../media/media.service';
import { GenerationService } from '../generation/generation.service';
import { SessionService, CreditService } from '@manacore/bot-services';
import { BLOG_STYLES } from '../config/configuration';
@Injectable()
@ -34,7 +35,9 @@ export class MatrixService extends BaseMatrixService {
configService: ConfigService,
private projectService: ProjectService,
private mediaService: MediaService,
private generationService: GenerationService
private generationService: GenerationService,
private sessionService: SessionService,
private creditService: CreditService
) {
super(configService);
this.allowedUsers = this.configService.get<string[]>('matrix.allowedUsers') || [];
@ -116,6 +119,16 @@ export class MatrixService extends BaseMatrixService {
case 'start':
await this.sendHelp(roomId);
break;
case 'login':
await this.handleLogin(roomId, sender, argString);
break;
case 'logout':
await this.handleLogout(roomId, sender);
break;
case 'auth':
case 'account':
await this.handleAuthStatus(roomId, sender);
break;
case 'new':
await this.createProject(roomId, sender, argString);
break;
@ -154,6 +167,11 @@ export class MatrixService extends BaseMatrixService {
Sammle Fotos, Sprachnotizen und Text für deine Projekte und erstelle daraus Blogbeiträge.
**Account:**
- \`!login email passwort\` - Anmelden
- \`!logout\` - Abmelden
- \`!auth\` - Account Status
**Projekt-Commands:**
- \`!new [Name]\` - Neues Projekt starten
- \`!projects\` - Alle Projekte anzeigen
@ -180,6 +198,53 @@ ${styles}
await this.sendMessage(roomId, helpText);
}
private async handleLogin(roomId: string, sender: string, args: string) {
const parts = args.split(' ');
if (parts.length < 2 || !parts[0] || !parts[1]) {
await this.sendMessage(roomId, 'Verwendung: `!login email passwort`');
return;
}
const [email, password] = parts;
const result = await this.sessionService.login(sender, email, password);
if (result.success) {
const token = this.sessionService.getToken(sender);
if (token) {
const balance = await this.creditService.getBalance(token);
await this.sendMessage(roomId,
`✅ Erfolgreich angemeldet als **${email}**\n⚡ Credits: ${balance.balance.toFixed(2)}`);
} else {
await this.sendMessage(roomId, `✅ Erfolgreich angemeldet als **${email}**`);
}
} else {
await this.sendMessage(roomId, `❌ Anmeldung fehlgeschlagen: ${result.error}`);
}
}
private async handleLogout(roomId: string, sender: string) {
this.sessionService.logout(sender);
await this.sendMessage(roomId, '👋 Erfolgreich abgemeldet.');
}
private async handleAuthStatus(roomId: string, sender: string) {
const loggedIn = this.sessionService.isLoggedIn(sender);
const session = this.sessionService.getSession(sender);
const token = this.sessionService.getToken(sender);
let response = '**📋 Account Status**\n\n';
if (loggedIn && session && token) {
const balance = await this.creditService.getBalance(token);
response += `👤 Angemeldet als: ${session.email}\n`;
response += `⚡ Credits: ${balance.balance.toFixed(2)}`;
} else {
response += `❌ Nicht angemeldet\n`;
response += `Nutze \`!login email passwort\` zum Anmelden.`;
}
await this.sendMessage(roomId, response);
}
private async createProject(roomId: string, sender: string, name: string) {
if (!name) {
await this.sendMessage(
@ -262,23 +327,34 @@ ${styles}
}
private async showStatus(roomId: string, sender: string) {
// Auth info
const loggedIn = this.sessionService.isLoggedIn(sender);
const session = this.sessionService.getSession(sender);
const token = this.sessionService.getToken(sender);
let authInfo = '';
if (loggedIn && session && token) {
const balance = await this.creditService.getBalance(token);
authInfo = `**👤 Account**\n${session.email} | ⚡ ${balance.balance.toFixed(2)} Credits\n\n`;
}
const projectId = this.activeProjects.get(sender);
if (!projectId) {
await this.sendMessage(roomId, 'Kein aktives Projekt.\n\nStarte mit: `!new Projektname`');
await this.sendMessage(roomId, `${authInfo}Kein aktives Projekt.\n\nStarte mit: \`!new Projektname\``);
return;
}
const project = await this.projectService.findById(projectId);
if (!project) {
this.activeProjects.delete(sender);
await this.sendMessage(roomId, 'Projekt nicht gefunden. Starte ein neues mit `!new`');
await this.sendMessage(roomId, `${authInfo}Projekt nicht gefunden. Starte ein neues mit \`!new\``);
return;
}
const stats = await this.projectService.getStats(projectId);
const latest = await this.generationService.getLatestGeneration(projectId);
let statusText = `**Projekt-Status**\n\n**Name:** ${project.name}\n**Status:** ${project.status}\n**Erstellt:** ${project.createdAt.toLocaleDateString('de-DE')}\n\n**Inhalte:**\n${stats.photos} Fotos\n${stats.voices} Sprachnotizen\n${stats.texts} Textnotizen\n**Gesamt:** ${stats.total} Einträge`;
let statusText = `${authInfo}**Projekt-Status**\n\n**Name:** ${project.name}\n**Status:** ${project.status}\n**Erstellt:** ${project.createdAt.toLocaleDateString('de-DE')}\n\n**Inhalte:**\n${stats.photos} Fotos\n${stats.voices} Sprachnotizen\n${stats.texts} Textnotizen\n**Gesamt:** ${stats.total} Einträge`;
if (latest) {
statusText += `\n\n**Letzte Generierung:**\n${latest.createdAt.toLocaleString('de-DE')} (${latest.style})`;

View file

@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { QuestionsModule } from '../questions/questions.module';
import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
import { SessionModule, TranscriptionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [
@ -10,6 +10,7 @@ import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
TranscriptionModule.register({
sttUrl: process.env.STT_URL || 'http://localhost:3020',
}),
CreditModule.forRoot(),
],
providers: [MatrixService],
exports: [MatrixService],

View file

@ -9,9 +9,14 @@ import {
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { QuestionsService, Question, Collection, Answer } from '../questions/questions.service';
import { SessionService, TranscriptionService } from '@manacore/bot-services';
import { SessionService, TranscriptionService, CreditService } from '@manacore/bot-services';
import { HELP_MESSAGE } from '../config/configuration';
const QUESTION_CREATE_CREDITS = 0.02;
const QUICK_RESEARCH_CREDITS = 5;
const STANDARD_RESEARCH_CREDITS = 10;
const DEEP_RESEARCH_CREDITS = 25;
@Injectable()
export class MatrixService extends BaseMatrixService {
// Store last shown items per user for reference by number
@ -34,7 +39,8 @@ export class MatrixService extends BaseMatrixService {
configService: ConfigService,
private questionsService: QuestionsService,
private sessionService: SessionService,
private readonly transcriptionService: TranscriptionService
private readonly transcriptionService: TranscriptionService,
private creditService: CreditService
) {
super(configService);
}
@ -220,9 +226,15 @@ export class MatrixService extends BaseMatrixService {
const result = await this.sessionService.login(sender, email, password);
if (result.success) {
await this.sendMessage(roomId, `<p>Erfolgreich angemeldet als <strong>${email}</strong></p>`);
const token = this.sessionService.getToken(sender);
if (token) {
const balance = await this.creditService.getBalance(token);
await this.sendMessage(roomId, `<p>✅ Erfolgreich angemeldet als <strong>${email}</strong><br/>⚡ Credits: ${balance.balance.toFixed(2)}</p>`);
} else {
await this.sendMessage(roomId, `<p>✅ Erfolgreich angemeldet als <strong>${email}</strong></p>`);
}
} else {
await this.sendMessage(roomId, `<p>Login fehlgeschlagen: ${result.error}</p>`);
await this.sendMessage(roomId, `<p>Login fehlgeschlagen: ${result.error}</p>`);
}
}
@ -230,16 +242,24 @@ export class MatrixService extends BaseMatrixService {
const backendOk = await this.questionsService.checkHealth();
const loggedIn = this.sessionService.isLoggedIn(sender);
const sessions = this.sessionService.getSessionCount();
const session = this.sessionService.getSession(sender);
const token = this.sessionService.getToken(sender);
await this.sendMessage(
roomId,
`<h3>Questions Bot Status</h3>
<ul>
<li>Backend: ${backendOk ? 'Online' : 'Offline'}</li>
<li>Angemeldet: ${loggedIn ? 'Ja' : 'Nein'}</li>
<li>Aktive Sessions: ${sessions}</li>
</ul>`
);
let statusHtml = `<h3>Questions Bot Status</h3><ul>`;
statusHtml += `<li>Backend: ${backendOk ? '✅ Online' : '❌ Offline'}</li>`;
statusHtml += `<li>Aktive Sessions: ${sessions}</li>`;
if (loggedIn && session && token) {
const balance = await this.creditService.getBalance(token);
statusHtml += `<li>👤 Angemeldet als: ${session.email}</li>`;
statusHtml += `<li>⚡ Credits: ${balance.balance.toFixed(2)}</li>`;
} else {
statusHtml += `<li>👤 Nicht angemeldet</li>`;
statusHtml += `<li>💡 Login: <code>!login email passwort</code></li>`;
}
statusHtml += `</ul>`;
await this.sendMessage(roomId, statusHtml);
}
// Question handlers

View file

@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { SkilltreeModule } from '../skilltree/skilltree.module';
import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
import { SessionModule, TranscriptionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [
@ -10,6 +10,7 @@ import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
TranscriptionModule.register({
sttUrl: process.env.STT_URL || 'http://localhost:3020',
}),
CreditModule.forRoot(),
],
providers: [MatrixService],
exports: [MatrixService],

View file

@ -9,7 +9,7 @@ import {
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { SkilltreeService, Skill, SkillBranch } from '../skilltree/skilltree.service';
import { SessionService, TranscriptionService } from '@manacore/bot-services';
import { SessionService, TranscriptionService, CreditService } from '@manacore/bot-services';
import { HELP_MESSAGE } from '../config/configuration';
@Injectable()
@ -55,7 +55,8 @@ export class MatrixService extends BaseMatrixService {
configService: ConfigService,
private skilltreeService: SkilltreeService,
private sessionService: SessionService,
private readonly transcriptionService: TranscriptionService
private readonly transcriptionService: TranscriptionService,
private creditService: CreditService
) {
super(configService);
}
@ -207,7 +208,13 @@ export class MatrixService extends BaseMatrixService {
const result = await this.sessionService.login(sender, email, password);
if (result.success) {
await this.sendMessage(roomId, `<p>Erfolgreich angemeldet als <strong>${email}</strong></p>`);
const token = this.sessionService.getToken(sender);
if (token) {
const balance = await this.creditService.getBalance(token);
await this.sendMessage(roomId, `<p>✅ Erfolgreich angemeldet als <strong>${email}</strong><br/>⚡ Credits: ${balance.balance.toFixed(2)}</p>`);
} else {
await this.sendMessage(roomId, `<p>✅ Erfolgreich angemeldet als <strong>${email}</strong></p>`);
}
} else {
await this.sendMessage(roomId, `<p>Login fehlgeschlagen: ${result.error}</p>`);
}
@ -217,16 +224,23 @@ export class MatrixService extends BaseMatrixService {
const backendOk = await this.skilltreeService.checkHealth();
const loggedIn = this.sessionService.isLoggedIn(sender);
const sessions = this.sessionService.getSessionCount();
const session = this.sessionService.getSession(sender);
const token = this.sessionService.getToken(sender);
await this.sendMessage(
roomId,
`<h3>Skilltree Bot Status</h3>
<ul>
<li>Backend: ${backendOk ? 'Online' : 'Offline'}</li>
<li>Angemeldet: ${loggedIn ? 'Ja' : 'Nein'}</li>
<li>Aktive Sessions: ${sessions}</li>
</ul>`
);
let statusHtml = '<h3>Skilltree Bot Status</h3><ul>';
statusHtml += `<li>Backend: ${backendOk ? 'Online' : 'Offline'}</li>`;
statusHtml += `<li>Angemeldet: ${loggedIn ? 'Ja' : 'Nein'}</li>`;
if (loggedIn && session && token) {
const balance = await this.creditService.getBalance(token);
statusHtml += `<li>👤 Angemeldet als: ${session.email}</li>`;
statusHtml += `<li>⚡ Credits: ${balance.balance.toFixed(2)}</li>`;
}
statusHtml += `<li>Aktive Sessions: ${sessions}</li>`;
statusHtml += '</ul>';
await this.sendMessage(roomId, statusHtml);
}
// Skill handlers

View file

@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { AnalyticsModule } from '../analytics/analytics.module';
import { UsersModule } from '../users/users.module';
import { TranscriptionModule } from '@manacore/bot-services';
import { TranscriptionModule, SessionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [
@ -11,6 +11,8 @@ import { TranscriptionModule } from '@manacore/bot-services';
TranscriptionModule.register({
sttUrl: process.env.STT_URL || 'http://localhost:3020',
}),
SessionModule.forRoot(),
CreditModule.forRoot(),
],
providers: [MatrixService],
exports: [MatrixService],

View file

@ -9,7 +9,7 @@ import {
} from '@manacore/matrix-bot-common';
import { AnalyticsService } from '../analytics/analytics.service';
import { UsersService } from '../users/users.service';
import { TranscriptionService } from '@manacore/bot-services';
import { TranscriptionService, SessionService, CreditService } from '@manacore/bot-services';
@Injectable()
export class MatrixService extends BaseMatrixService {
@ -28,7 +28,9 @@ export class MatrixService extends BaseMatrixService {
configService: ConfigService,
private analyticsService: AnalyticsService,
private usersService: UsersService,
private readonly transcriptionService: TranscriptionService
private readonly transcriptionService: TranscriptionService,
private sessionService: SessionService,
private creditService: CreditService
) {
super(configService);
this.reportRoomId = this.configService.get<string>('matrix.reportRoomId') || '';
@ -48,7 +50,7 @@ export class MatrixService extends BaseMatrixService {
roomId: string,
_event: MatrixRoomEvent,
message: string,
_sender: string
sender: string
): Promise<void> {
// Check for keyword commands first
const keywordCommand = this.keywordDetector.detect(message);
@ -58,8 +60,8 @@ export class MatrixService extends BaseMatrixService {
if (!message.startsWith('!')) return;
const [command] = message.slice(1).split(' ');
await this.handleCommand(roomId, command.toLowerCase());
const [command, ...args] = message.slice(1).split(' ');
await this.handleCommand(roomId, command.toLowerCase(), sender, args.join(' '));
}
protected override async handleAudioMessage(
@ -86,13 +88,25 @@ export class MatrixService extends BaseMatrixService {
}
}
private async handleCommand(roomId: string, command: string) {
private async handleCommand(roomId: string, command: string, sender: string, args: string) {
switch (command) {
case 'help':
case 'start':
await this.sendHelp(roomId);
break;
case 'login':
await this.handleLogin(roomId, sender, args);
break;
case 'logout':
await this.handleLogout(roomId, sender);
break;
case 'status':
await this.handleStatus(roomId, sender);
break;
case 'stats':
await this.sendStats(roomId);
break;
@ -121,7 +135,12 @@ export class MatrixService extends BaseMatrixService {
private async sendHelp(roomId: string) {
const helpText = `**📊 ManaCore Stats Bot (DSGVO-konform)**
**Befehle:**
**Account:**
- \`!login email passwort\` - Anmelden
- \`!logout\` - Abmelden
- \`!status\` - Account Status
**Statistiken:**
- \`!stats\` - Übersicht aller Apps (30 Tage)
- \`!today\` - Heutige Statistiken
- \`!week\` - Wochenstatistiken
@ -177,6 +196,53 @@ Daten von Umami Analytics (self-hosted).`;
await this.sendMessage(roomId, report);
}
private async handleLogin(roomId: string, sender: string, args: string) {
const parts = args.split(' ');
if (parts.length < 2 || !parts[0] || !parts[1]) {
await this.sendMessage(roomId, 'Verwendung: `!login email passwort`');
return;
}
const [email, password] = parts;
const result = await this.sessionService.login(sender, email, password);
if (result.success) {
const token = this.sessionService.getToken(sender);
if (token) {
const balance = await this.creditService.getBalance(token);
await this.sendMessage(roomId,
`✅ Erfolgreich angemeldet als **${email}**\n⚡ Credits: ${balance.balance.toFixed(2)}`);
} else {
await this.sendMessage(roomId, `✅ Erfolgreich angemeldet als **${email}**`);
}
} else {
await this.sendMessage(roomId, `❌ Anmeldung fehlgeschlagen: ${result.error}`);
}
}
private async handleLogout(roomId: string, sender: string) {
this.sessionService.logout(sender);
await this.sendMessage(roomId, '👋 Erfolgreich abgemeldet.');
}
private async handleStatus(roomId: string, sender: string) {
const loggedIn = this.sessionService.isLoggedIn(sender);
const session = this.sessionService.getSession(sender);
const token = this.sessionService.getToken(sender);
let response = '**📊 Stats Bot Status**\n\n';
if (loggedIn && session && token) {
const balance = await this.creditService.getBalance(token);
response += `👤 Angemeldet als: ${session.email}\n`;
response += `⚡ Credits: ${balance.balance.toFixed(2)}\n`;
} else {
response += `❌ Nicht angemeldet\n`;
response += `Nutze \`!login email passwort\` zum Anmelden.`;
}
await this.sendMessage(roomId, response);
}
// Public method for scheduled reports
async sendScheduledReport(report: string) {
if (!this.reportRoomId) {

View file

@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { StorageModule } from '../storage/storage.module';
import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
import { SessionModule, TranscriptionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [
@ -10,6 +10,7 @@ import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
TranscriptionModule.register({
sttUrl: process.env.STT_URL || 'http://localhost:3020',
}),
CreditModule.forRoot(),
],
providers: [MatrixService],
exports: [MatrixService],

View file

@ -9,7 +9,7 @@ import {
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { StorageService, StorageFile, Folder, ShareLink, TrashItem } from '../storage/storage.service';
import { SessionService, TranscriptionService } from '@manacore/bot-services';
import { SessionService, TranscriptionService, CreditService } from '@manacore/bot-services';
import { HELP_MESSAGE } from '../config/configuration';
@Injectable()
@ -36,7 +36,8 @@ export class MatrixService extends BaseMatrixService {
configService: ConfigService,
private storageService: StorageService,
private sessionService: SessionService,
private readonly transcriptionService: TranscriptionService
private readonly transcriptionService: TranscriptionService,
private creditService: CreditService
) {
super(configService);
}
@ -247,7 +248,13 @@ export class MatrixService extends BaseMatrixService {
const result = await this.sessionService.login(sender, email, password);
if (result.success) {
await this.sendMessage(roomId, `<p>Erfolgreich angemeldet als <strong>${email}</strong></p>`);
const token = this.sessionService.getToken(sender);
if (token) {
const balance = await this.creditService.getBalance(token);
await this.sendMessage(roomId, `<p>✅ Erfolgreich angemeldet als <strong>${email}</strong><br/>⚡ Credits: ${balance.balance.toFixed(2)}</p>`);
} else {
await this.sendMessage(roomId, `<p>✅ Erfolgreich angemeldet als <strong>${email}</strong></p>`);
}
} else {
await this.sendMessage(roomId, `<p>Login fehlgeschlagen: ${result.error}</p>`);
}
@ -257,16 +264,23 @@ export class MatrixService extends BaseMatrixService {
const backendOk = await this.storageService.checkHealth();
const loggedIn = this.sessionService.isLoggedIn(sender);
const sessions = this.sessionService.getSessionCount();
const session = this.sessionService.getSession(sender);
const token = this.sessionService.getToken(sender);
await this.sendMessage(
roomId,
`<h3>Storage Bot Status</h3>
<ul>
<li>Backend: ${backendOk ? 'Online' : 'Offline'}</li>
<li>Angemeldet: ${loggedIn ? 'Ja' : 'Nein'}</li>
<li>Aktive Sessions: ${sessions}</li>
</ul>`
);
let statusHtml = '<h3>Storage Bot Status</h3><ul>';
statusHtml += `<li>Backend: ${backendOk ? 'Online' : 'Offline'}</li>`;
statusHtml += `<li>Angemeldet: ${loggedIn ? 'Ja' : 'Nein'}</li>`;
if (loggedIn && session && token) {
const balance = await this.creditService.getBalance(token);
statusHtml += `<li>👤 Angemeldet als: ${session.email}</li>`;
statusHtml += `<li>⚡ Credits: ${balance.balance.toFixed(2)}</li>`;
}
statusHtml += `<li>Aktive Sessions: ${sessions}</li>`;
statusHtml += '</ul>';
await this.sendMessage(roomId, statusHtml);
}
// File handlers

View file

@ -1,10 +1,15 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { TodoModule } from '../todo/todo.module';
import { TranscriptionModule } from '@manacore/bot-services';
import { TranscriptionModule, SessionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [TodoModule, TranscriptionModule.forRoot()],
imports: [
TodoModule,
TranscriptionModule.forRoot(),
SessionModule.forRoot(),
CreditModule.forRoot(),
],
providers: [MatrixService],
exports: [MatrixService],
})

View file

@ -8,9 +8,12 @@ import {
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { TodoService, Task } from '../todo/todo.service';
import { TranscriptionService } from '@manacore/bot-services';
import { TranscriptionService, SessionService, CreditService } from '@manacore/bot-services';
import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration';
// Credit cost for task creation (micro-credits)
const TASK_CREATE_CREDITS = 0.02;
@Injectable()
export class MatrixService extends BaseMatrixService {
private readonly keywordDetector = new KeywordCommandDetector(
@ -29,7 +32,9 @@ export class MatrixService extends BaseMatrixService {
constructor(
configService: ConfigService,
private todoService: TodoService,
private transcriptionService: TranscriptionService
private transcriptionService: TranscriptionService,
private sessionService: SessionService,
private creditService: CreditService
) {
super(configService);
}
@ -113,6 +118,21 @@ export class MatrixService extends BaseMatrixService {
return;
}
// Check credits if user is logged in
const token = this.sessionService.getToken(sender);
if (token) {
const validation = await this.creditService.validateCredits(token, TASK_CREATE_CREDITS);
if (!validation.hasCredits) {
const errorMsg = this.creditService.formatInsufficientCreditsError(
TASK_CREATE_CREDITS,
validation.availableCredits,
'Aufgabe erstellen'
);
await this.sendReply(roomId, event, `Transkription: "${transcription}"\n\n${errorMsg.text}`);
return;
}
}
// Parse the transcription as a task input
const { title, priority, dueDate, project } = this.todoService.parseTaskInput(transcription);
@ -134,6 +154,12 @@ export class MatrixService extends BaseMatrixService {
responseText += `\n${details.join(' | ')}`;
}
// Show credit deduction if logged in
if (token) {
const balance = await this.creditService.getBalance(token);
responseText += `\n\n⚡ -${TASK_CREATE_CREDITS} Credits (${balance.balance.toFixed(2)} verbleibend)`;
}
await this.sendReply(roomId, event, responseText);
} catch (error) {
this.logger.error('Audio processing failed:', error);
@ -155,6 +181,14 @@ export class MatrixService extends BaseMatrixService {
await this.sendReply(roomId, event, HELP_TEXT);
break;
case 'login':
await this.handleLogin(roomId, event, userId, args);
break;
case 'logout':
await this.handleLogout(roomId, event, userId);
break;
case 'add':
case 'neu':
case 'neue':
@ -223,6 +257,21 @@ export class MatrixService extends BaseMatrixService {
return;
}
// Check credits if user is logged in
const token = this.sessionService.getToken(userId);
if (token) {
const validation = await this.creditService.validateCredits(token, TASK_CREATE_CREDITS);
if (!validation.hasCredits) {
const errorMsg = this.creditService.formatInsufficientCreditsError(
TASK_CREATE_CREDITS,
validation.availableCredits,
'Aufgabe erstellen'
);
await this.sendReply(roomId, event, errorMsg.text);
return;
}
}
const { title, priority, dueDate, project } = this.todoService.parseTaskInput(input);
const task = await this.todoService.createTask(userId, title, {
@ -242,6 +291,12 @@ export class MatrixService extends BaseMatrixService {
response += `\n${details.join(' | ')}`;
}
// Show credit deduction if logged in
if (token) {
const balance = await this.creditService.getBalance(token);
response += `\n\n⚡ -${TASK_CREATE_CREDITS} Credits (${balance.balance.toFixed(2)} verbleibend)`;
}
await this.sendReply(roomId, event, response);
}
@ -379,19 +434,60 @@ export class MatrixService extends BaseMatrixService {
private async handleStatus(roomId: string, event: MatrixRoomEvent, userId: string) {
const stats = await this.todoService.getStats(userId);
const isLoggedIn = this.sessionService.isLoggedIn(userId);
const email = this.sessionService.getEmail(userId);
const token = this.sessionService.getToken(userId);
// Get credit balance if logged in
let creditInfo = '';
if (token) {
const balance = await this.creditService.getBalance(token);
const creditIcon = balance.hasCredits ? '⚡' : '⚠️';
creditInfo = `\n${creditIcon} Credits: ${balance.balance.toFixed(2)}`;
if (balance.balance < 10 && balance.balance > 0) {
creditInfo += '\n⚠ Nur noch wenig Credits!';
}
if (!balance.hasCredits) {
creditInfo += '\n👉 Credits kaufen: https://mana.how/credits';
}
}
const response = `**Status**
👤 Angemeldet: ${isLoggedIn ? `Ja (${email})` : 'Nein'}${creditInfo}
- Offene Aufgaben: ${stats.pending}
- Heute faellig: ${stats.today}
- Erledigt: ${stats.completed}
- Gesamt: ${stats.total}
Bot: Online`;
Bot: Online${!isLoggedIn ? '\n\nTipp: Mit `!login email passwort` anmelden fuer Credit-Tracking' : ''}`;
await this.sendReply(roomId, event, response);
}
private async handleLogin(roomId: string, event: MatrixRoomEvent, userId: string, args: string) {
const parts = args.trim().split(/\s+/);
if (parts.length < 2) {
await this.sendReply(roomId, event, 'Verwendung: `!login email passwort`');
return;
}
const [email, password] = parts;
const result = await this.sessionService.login(userId, email, password);
if (result.success) {
await this.sendReply(roomId, event, `Erfolgreich angemeldet als **${email}**`);
} else {
await this.sendReply(roomId, event, `Anmeldung fehlgeschlagen: ${result.error}`);
}
}
private async handleLogout(roomId: string, event: MatrixRoomEvent, userId: string) {
this.sessionService.logout(userId);
await this.sendReply(roomId, event, 'Erfolgreich abgemeldet.');
}
private async handlePinHelp(roomId: string, event: MatrixRoomEvent) {
try {
// Send help message

View file

@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { TtsModule } from '../tts/tts.module';
import { TranscriptionModule } from '@manacore/bot-services';
import { TranscriptionModule, SessionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [
@ -9,6 +9,8 @@ import { TranscriptionModule } from '@manacore/bot-services';
TranscriptionModule.register({
sttUrl: process.env.STT_URL || 'http://localhost:3020',
}),
SessionModule.forRoot(),
CreditModule.forRoot(),
],
providers: [MatrixService],
})

View file

@ -8,7 +8,7 @@ import {
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { TtsService } from '../tts/tts.service';
import { TranscriptionService } from '@manacore/bot-services';
import { TranscriptionService, SessionService, CreditService } from '@manacore/bot-services';
import { HELP_TEXT, WELCOME_TEXT } from '../config/configuration';
interface UserSettings {
@ -38,7 +38,9 @@ export class MatrixService extends BaseMatrixService {
constructor(
configService: ConfigService,
private ttsService: TtsService,
private readonly transcriptionService: TranscriptionService
private readonly transcriptionService: TranscriptionService,
private sessionService: SessionService,
private creditService: CreditService
) {
super(configService);
this.defaultVoice = this.configService.get<string>('tts.defaultVoice') || 'af_heart';
@ -277,12 +279,21 @@ export class MatrixService extends BaseMatrixService {
private async handleStatusCommand(roomId: string, event: MatrixRoomEvent, userId: string) {
const settings = this.getUserSettings(userId);
const ttsHealthy = await this.ttsService.isHealthy();
const loggedIn = this.sessionService.isLoggedIn(userId);
const session = this.sessionService.getSession(userId);
const token = this.sessionService.getToken(userId);
let response = '**Aktuelle Einstellungen:**\n\n';
response += `Stimme: \`${settings.voice}\`\n`;
response += `Geschwindigkeit: ${settings.speed}x\n`;
response += `Max. Textlaenge: ${this.maxTextLength} Zeichen\n\n`;
response += `TTS-Service: ${ttsHealthy ? 'Online' : 'Offline'}`;
response += `TTS-Service: ${ttsHealthy ? 'Online' : 'Offline'}\n`;
if (loggedIn && session && token) {
const balance = await this.creditService.getBalance(token);
response += `\n👤 Angemeldet als: ${session.email}\n`;
response += `⚡ Credits: ${balance.balance.toFixed(2)}`;
}
await this.sendReply(roomId, event, response);
}

View file

@ -1,10 +1,10 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { QuotesModule } from '../quotes/quotes.module';
import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
import { SessionModule, TranscriptionModule, CreditModule } from '@manacore/bot-services';
@Module({
imports: [QuotesModule, SessionModule.forRoot(), TranscriptionModule.forRoot()],
imports: [QuotesModule, SessionModule.forRoot(), TranscriptionModule.forRoot(), CreditModule.forRoot()],
providers: [MatrixService],
exports: [MatrixService],
})

View file

@ -9,7 +9,7 @@ import {
} from '@manacore/matrix-bot-common';
import { QuotesService } from '../quotes/quotes.service';
import { ZitareService } from '../quotes/zitare.service';
import { SessionService, TranscriptionService } from '@manacore/bot-services';
import { SessionService, TranscriptionService, CreditService } from '@manacore/bot-services';
import { HELP_MESSAGE, Category } from '../config/configuration';
@Injectable()
@ -33,7 +33,8 @@ export class MatrixService extends BaseMatrixService {
private quotesService: QuotesService,
private zitareService: ZitareService,
private sessionService: SessionService,
private transcriptionService: TranscriptionService
private transcriptionService: TranscriptionService,
private creditService: CreditService
) {
super(configService);
}
@ -361,10 +362,19 @@ Sag "hilfe" fuer alle Befehle!`;
const result = await this.sessionService.login(sender, email, password);
if (result.success) {
await this.sendMessage(
roomId,
`Erfolgreich angemeldet!\n\nDu kannst jetzt Favoriten speichern und Listen verwalten.`
);
const token = this.sessionService.getToken(sender);
if (token) {
const balance = await this.creditService.getBalance(token);
await this.sendMessage(
roomId,
`✅ Erfolgreich angemeldet!\n⚡ Credits: ${balance.balance.toFixed(2)}\n\nDu kannst jetzt Favoriten speichern und Listen verwalten.`
);
} else {
await this.sendMessage(
roomId,
`Erfolgreich angemeldet!\n\nDu kannst jetzt Favoriten speichern und Listen verwalten.`
);
}
} else {
await this.sendMessage(roomId, `Anmeldung fehlgeschlagen: ${result.error}`);
}
@ -556,15 +566,22 @@ Sag "hilfe" fuer alle Befehle!`;
const isLoggedIn = this.sessionService.isLoggedIn(sender);
const sessionCount = this.sessionService.getSessionCount();
const totalQuotes = this.quotesService.getTotalCount();
const session = this.sessionService.getSession(sender);
const token = this.sessionService.getToken(sender);
const statusText = `**Zitare Bot Status**
let statusText = `**Zitare Bot Status**\n\n`;
statusText += `**Backend:** ${backendHealthy ? 'Online' : 'Offline'}\n`;
statusText += `**Dein Status:** ${isLoggedIn ? 'Angemeldet' : 'Nicht angemeldet'}\n`;
**Backend:** ${backendHealthy ? 'Online' : 'Offline'}
**Dein Status:** ${isLoggedIn ? 'Angemeldet' : 'Nicht angemeldet'}
**Aktive Sessions:** ${sessionCount}
**Verfuegbare Zitate:** ${totalQuotes}
if (isLoggedIn && session && token) {
const balance = await this.creditService.getBalance(token);
statusText += `**👤 Angemeldet als:** ${session.email}\n`;
statusText += `**⚡ Credits:** ${balance.balance.toFixed(2)}\n`;
}
${!isLoggedIn ? 'Nutze `!login email passwort` um dich anzumelden.' : ''}`;
statusText += `**Aktive Sessions:** ${sessionCount}\n`;
statusText += `**Verfuegbare Zitate:** ${totalQuotes}\n`;
statusText += `\n${!isLoggedIn ? 'Nutze `!login email passwort` um dich anzumelden.' : ''}`;
await this.sendMessage(roomId, statusText);
}