mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:01:08 +02:00
♻️ refactor: consolidate SessionService & TranscriptionService in @manacore/bot-services
Created shared services to eliminate code duplication across Matrix bots: **New Services in @manacore/bot-services:** - SessionService: User authentication via mana-core-auth (was duplicated in 11 bots) - TranscriptionService: Speech-to-text via mana-stt (was duplicated in 6 bots) **Migrated Bots:** - matrix-todo-bot: uses TranscriptionService - matrix-picture-bot: uses SessionService - matrix-clock-bot: uses TranscriptionService - matrix-zitare-bot: uses both SessionService & TranscriptionService **Code Reduction:** - Removed ~300 lines of duplicate code from migrated bots - Centralized service configuration via NestJS modules - Added comprehensive documentation in CLAUDE.md Remaining bots can be migrated following the same pattern documented in packages/bot-services/CLAUDE.md. Note: @storage/backend type-check fails due to pre-existing drizzle-orm issue Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
508ae124a9
commit
9b61831cb5
35 changed files with 1014 additions and 903 deletions
|
|
@ -181,12 +181,16 @@ User → matrix-mana-bot → @manacore/bot-services → Multiple Backends
|
|||
Das Package `@manacore/bot-services` stellt transport-agnostische Geschäftslogik bereit:
|
||||
|
||||
```typescript
|
||||
// Exportierte Services
|
||||
// Business Logic Services
|
||||
export { TodoModule, TodoService } from './todo';
|
||||
export { CalendarModule, CalendarService } from './calendar';
|
||||
export { AiModule, AiService } from './ai';
|
||||
export { ClockModule, ClockService } from './clock';
|
||||
|
||||
// Infrastructure Services (NEU: Konsolidiert aus 11+ Bots)
|
||||
export { SessionModule, SessionService } from './session'; // Auth via mana-core-auth
|
||||
export { TranscriptionModule, TranscriptionService } from './transcription'; // STT via mana-stt
|
||||
|
||||
// Storage Provider (pluggable)
|
||||
export { FileStorageProvider } from './shared/storage/file-storage.provider';
|
||||
export { MemoryStorageProvider } from './shared/storage/memory-storage.provider';
|
||||
|
|
@ -196,6 +200,15 @@ export { generateId, getTodayISO, formatDateDE } from './shared/utils';
|
|||
export { parseGermanDateKeyword } from './shared/date-parser';
|
||||
```
|
||||
|
||||
### 3.1.1 Konsolidierte Services
|
||||
|
||||
Die folgenden Services wurden aus den einzelnen Bots konsolidiert:
|
||||
|
||||
| Service | Vorher | Nachher | Bots |
|
||||
|---------|--------|---------|------|
|
||||
| `SessionService` | 11x dupliziert | 1x in bot-services | picture, contacts, chat, zitare, ... |
|
||||
| `TranscriptionService` | 6x dupliziert | 1x in bot-services | todo, clock, zitare, nutriphi, ... |
|
||||
|
||||
### 3.2 TodoService
|
||||
|
||||
Vollständige Aufgabenverwaltung mit deutscher Sprachunterstützung:
|
||||
|
|
|
|||
|
|
@ -35,16 +35,30 @@ This package provides **transport-agnostic** services that contain all business
|
|||
|
||||
## Available Services
|
||||
|
||||
### Business Logic Services
|
||||
|
||||
| Service | Storage | Description |
|
||||
|---------|---------|-------------|
|
||||
| `TodoService` | File (JSON) | Task management with projects, priorities, dates |
|
||||
| `CalendarService` | File (JSON) | Events, calendars, reminders |
|
||||
| `AiService` | In-memory | Ollama LLM integration, chat sessions, vision |
|
||||
| `ClockService` | External API | Timers, alarms, world clocks |
|
||||
| `NutritionService` | Placeholder | Meal tracking (to be implemented) |
|
||||
| `QuotesService` | Placeholder | Daily quotes (to be implemented) |
|
||||
| `StatsService` | Placeholder | Analytics reports (to be implemented) |
|
||||
| `DocsService` | Placeholder | Documentation generation (to be implemented) |
|
||||
|
||||
### Infrastructure Services
|
||||
|
||||
| Service | Storage | Description |
|
||||
|---------|---------|-------------|
|
||||
| `SessionService` | In-memory | User authentication via mana-core-auth |
|
||||
| `TranscriptionService` | External API | Speech-to-text via mana-stt service |
|
||||
|
||||
### Placeholder Services (to be implemented)
|
||||
|
||||
| Service | Description |
|
||||
|---------|-------------|
|
||||
| `NutritionService` | Meal tracking |
|
||||
| `QuotesService` | Daily quotes |
|
||||
| `StatsService` | Analytics reports |
|
||||
| `DocsService` | Documentation generation |
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
@ -52,7 +66,14 @@ This package provides **transport-agnostic** services that contain all business
|
|||
|
||||
```typescript
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TodoModule, CalendarModule, AiModule, ClockModule } from '@manacore/bot-services';
|
||||
import {
|
||||
TodoModule,
|
||||
CalendarModule,
|
||||
AiModule,
|
||||
ClockModule,
|
||||
SessionModule,
|
||||
TranscriptionModule,
|
||||
} from '@manacore/bot-services';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -63,11 +84,59 @@ import { TodoModule, CalendarModule, AiModule, ClockModule } from '@manacore/bot
|
|||
// External services
|
||||
AiModule.register({ baseUrl: 'http://ollama:11434' }),
|
||||
ClockModule.register({ apiUrl: 'http://clock-backend:3017/api/v1' }),
|
||||
|
||||
// Infrastructure services (use ConfigService by default)
|
||||
SessionModule.forRoot(),
|
||||
TranscriptionModule.forRoot(),
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
### Session Service (Authentication)
|
||||
|
||||
```typescript
|
||||
import { SessionService } from '@manacore/bot-services';
|
||||
|
||||
// Login a Matrix user
|
||||
const result = await sessionService.login(
|
||||
'@user:matrix.org',
|
||||
'email@example.com',
|
||||
'password'
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
// Get token for API calls
|
||||
const token = sessionService.getToken('@user:matrix.org');
|
||||
|
||||
// Check if logged in
|
||||
const isLoggedIn = sessionService.isLoggedIn('@user:matrix.org');
|
||||
}
|
||||
|
||||
// Logout
|
||||
sessionService.logout('@user:matrix.org');
|
||||
|
||||
// Store custom session data
|
||||
sessionService.setSessionData('@user:matrix.org', 'currentConversationId', 'abc123');
|
||||
const convId = sessionService.getSessionData<string>('@user:matrix.org', 'currentConversationId');
|
||||
```
|
||||
|
||||
### Transcription Service (Speech-to-Text)
|
||||
|
||||
```typescript
|
||||
import { TranscriptionService } from '@manacore/bot-services';
|
||||
|
||||
// Transcribe audio buffer
|
||||
const text = await transcriptionService.transcribe(audioBuffer, { language: 'de' });
|
||||
|
||||
// Get full response with metadata
|
||||
const result = await transcriptionService.transcribeWithMetadata(audioBuffer);
|
||||
console.log(result.text, result.language, result.model);
|
||||
|
||||
// Health check
|
||||
const isHealthy = await transcriptionService.checkHealth();
|
||||
```
|
||||
|
||||
### Direct Service Usage
|
||||
|
||||
```typescript
|
||||
|
|
@ -162,15 +231,86 @@ packages/bot-services/
|
|||
│ │ ├── utils.ts # Utility functions
|
||||
│ │ └── index.ts
|
||||
│ ├── todo/
|
||||
│ │ ├── types.ts
|
||||
│ │ ├── todo.service.ts
|
||||
│ │ ├── todo.module.ts
|
||||
│ │ └── index.ts
|
||||
│ ├── calendar/
|
||||
│ ├── ai/
|
||||
│ ├── clock/
|
||||
│ ├── session/ # NEW: User authentication
|
||||
│ │ ├── types.ts
|
||||
│ │ ├── session.service.ts
|
||||
│ │ ├── session.module.ts
|
||||
│ │ └── index.ts
|
||||
│ ├── transcription/ # NEW: Speech-to-text
|
||||
│ │ ├── types.ts
|
||||
│ │ ├── transcription.service.ts
|
||||
│ │ ├── transcription.module.ts
|
||||
│ │ └── index.ts
|
||||
│ └── ...
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
└── CLAUDE.md
|
||||
```
|
||||
|
||||
## Migrating Bots to Shared Services
|
||||
|
||||
To migrate a bot from local services to shared services:
|
||||
|
||||
### 1. Add dependency
|
||||
|
||||
```bash
|
||||
# In package.json
|
||||
"dependencies": {
|
||||
"@manacore/bot-services": "workspace:*",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Update module imports
|
||||
|
||||
```typescript
|
||||
// bot.module.ts - BEFORE
|
||||
import { SessionModule } from '../session/session.module';
|
||||
import { TranscriptionModule } from '../transcription/transcription.module';
|
||||
|
||||
@Module({
|
||||
imports: [SessionModule, TranscriptionModule],
|
||||
})
|
||||
|
||||
// bot.module.ts - AFTER
|
||||
import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
|
||||
|
||||
@Module({
|
||||
imports: [SessionModule.forRoot(), TranscriptionModule.forRoot()],
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Update service imports
|
||||
|
||||
```typescript
|
||||
// matrix.service.ts - BEFORE
|
||||
import { SessionService } from '../session/session.service';
|
||||
import { TranscriptionService } from '../transcription/transcription.service';
|
||||
|
||||
// matrix.service.ts - AFTER
|
||||
import { SessionService, TranscriptionService } from '@manacore/bot-services';
|
||||
```
|
||||
|
||||
### 4. Delete local modules
|
||||
|
||||
```bash
|
||||
rm -rf src/session/
|
||||
rm -rf src/transcription/
|
||||
```
|
||||
|
||||
### Migrated Bots
|
||||
|
||||
| Bot | SessionService | TranscriptionService |
|
||||
|-----|----------------|---------------------|
|
||||
| matrix-todo-bot | - | ✅ |
|
||||
| matrix-picture-bot | ✅ | - |
|
||||
| matrix-clock-bot | - | ✅ |
|
||||
| matrix-zitare-bot | ✅ | ✅ |
|
||||
| matrix-chat-bot | TODO | - |
|
||||
| matrix-contacts-bot | TODO | - |
|
||||
| matrix-nutriphi-bot | TODO | TODO |
|
||||
| matrix-project-doc-bot | - | TODO |
|
||||
| ... | ... | ... |
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
"./calendar": "./src/calendar/index.ts",
|
||||
"./clock": "./src/clock/index.ts",
|
||||
"./ai": "./src/ai/index.ts",
|
||||
"./session": "./src/session/index.ts",
|
||||
"./transcription": "./src/transcription/index.ts",
|
||||
"./nutrition": "./src/nutrition/index.ts",
|
||||
"./quotes": "./src/quotes/index.ts",
|
||||
"./stats": "./src/stats/index.ts",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,12 @@ export type {
|
|||
} from './todo';
|
||||
|
||||
// Calendar
|
||||
export { CalendarModule, CalendarModuleOptions, CalendarService, CALENDAR_STORAGE_PROVIDER } from './calendar';
|
||||
export {
|
||||
CalendarModule,
|
||||
CalendarModuleOptions,
|
||||
CalendarService,
|
||||
CALENDAR_STORAGE_PROVIDER,
|
||||
} from './calendar';
|
||||
export type {
|
||||
CalendarEvent,
|
||||
Calendar,
|
||||
|
|
@ -79,6 +84,23 @@ export type {
|
|||
TimeTrackingSummary,
|
||||
} from './clock';
|
||||
|
||||
// Session (User authentication via mana-core-auth)
|
||||
export {
|
||||
SessionModule,
|
||||
SessionService,
|
||||
SESSION_MODULE_OPTIONS,
|
||||
DEFAULT_SESSION_EXPIRY_MS,
|
||||
} from './session';
|
||||
export type { UserSession, LoginResult, SessionStats, SessionModuleOptions } from './session';
|
||||
|
||||
// Transcription (Speech-to-Text via mana-stt)
|
||||
export { TranscriptionModule, TranscriptionService, STT_MODULE_OPTIONS } from './transcription';
|
||||
export type {
|
||||
SttResponse,
|
||||
TranscriptionOptions,
|
||||
TranscriptionModuleOptions,
|
||||
} from './transcription';
|
||||
|
||||
// ===== Placeholder Services (to be implemented) =====
|
||||
|
||||
export { NutritionModule } from './nutrition';
|
||||
|
|
@ -96,7 +118,18 @@ export type { DocsServiceConfig, ProjectDoc } from './docs';
|
|||
// ===== Shared Utilities =====
|
||||
|
||||
export { FileStorageProvider, MemoryStorageProvider } from './shared';
|
||||
export type { StorageProvider, BaseEntity, UserEntity, ServiceConfig, Result, PaginationOptions, PaginatedResult, DateRange, Priority, ServiceStats } from './shared';
|
||||
export type {
|
||||
StorageProvider,
|
||||
BaseEntity,
|
||||
UserEntity,
|
||||
ServiceConfig,
|
||||
Result,
|
||||
PaginationOptions,
|
||||
PaginatedResult,
|
||||
DateRange,
|
||||
Priority,
|
||||
ServiceStats,
|
||||
} from './shared';
|
||||
export {
|
||||
generateId,
|
||||
getTodayISO,
|
||||
|
|
|
|||
4
packages/bot-services/src/session/index.ts
Normal file
4
packages/bot-services/src/session/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export { SessionService } from './session.service';
|
||||
export { SessionModule } from './session.module';
|
||||
export type { UserSession, LoginResult, SessionStats, SessionModuleOptions } from './types';
|
||||
export { SESSION_MODULE_OPTIONS, DEFAULT_SESSION_EXPIRY_MS } from './types';
|
||||
62
packages/bot-services/src/session/session.module.ts
Normal file
62
packages/bot-services/src/session/session.module.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { Module, DynamicModule, Global } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { SessionService } from './session.service';
|
||||
import { SessionModuleOptions, SESSION_MODULE_OPTIONS } from './types';
|
||||
|
||||
/**
|
||||
* Shared session management module for Matrix bots
|
||||
*
|
||||
* Provides SessionService for managing user authentication sessions.
|
||||
* Links Matrix user IDs to mana-core-auth JWT tokens.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // With explicit configuration
|
||||
* @Module({
|
||||
* imports: [
|
||||
* SessionModule.register({
|
||||
* authUrl: 'http://mana-core-auth:3001',
|
||||
* sessionExpiryMs: 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||
* })
|
||||
* ]
|
||||
* })
|
||||
*
|
||||
* // With ConfigService (reads from auth.url or MANA_CORE_AUTH_URL)
|
||||
* @Module({
|
||||
* imports: [SessionModule.forRoot()]
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
@Global()
|
||||
@Module({})
|
||||
export class SessionModule {
|
||||
/**
|
||||
* Register module with explicit options
|
||||
*/
|
||||
static register(options: SessionModuleOptions = {}): DynamicModule {
|
||||
return {
|
||||
module: SessionModule,
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: SESSION_MODULE_OPTIONS,
|
||||
useValue: options,
|
||||
},
|
||||
SessionService,
|
||||
],
|
||||
exports: [SessionService],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Register module with ConfigService (reads auth.url or MANA_CORE_AUTH_URL from config)
|
||||
*/
|
||||
static forRoot(): DynamicModule {
|
||||
return {
|
||||
module: SessionModule,
|
||||
imports: [ConfigModule],
|
||||
providers: [SessionService],
|
||||
exports: [SessionService],
|
||||
};
|
||||
}
|
||||
}
|
||||
235
packages/bot-services/src/session/session.service.ts
Normal file
235
packages/bot-services/src/session/session.service.ts
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
import { Injectable, Inject, Logger, Optional } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
UserSession,
|
||||
LoginResult,
|
||||
SessionStats,
|
||||
SessionModuleOptions,
|
||||
SESSION_MODULE_OPTIONS,
|
||||
DEFAULT_SESSION_EXPIRY_MS,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Shared session management service for Matrix bots
|
||||
*
|
||||
* Manages user authentication sessions linking Matrix user IDs to mana-core-auth JWT tokens.
|
||||
* Sessions are stored in-memory and automatically expire.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In NestJS module
|
||||
* imports: [SessionModule.register({ authUrl: 'http://mana-core-auth:3001' })]
|
||||
*
|
||||
* // In service/controller
|
||||
* const result = await sessionService.login(matrixUserId, email, password);
|
||||
* const token = sessionService.getToken(matrixUserId);
|
||||
* ```
|
||||
*/
|
||||
@Injectable()
|
||||
export class SessionService {
|
||||
private readonly logger = new Logger(SessionService.name);
|
||||
private sessions: Map<string, UserSession> = new Map();
|
||||
private readonly authUrl: string;
|
||||
private readonly sessionExpiryMs: number;
|
||||
private readonly loginPath: string;
|
||||
|
||||
constructor(
|
||||
@Optional() private configService: ConfigService,
|
||||
@Optional() @Inject(SESSION_MODULE_OPTIONS) private options?: SessionModuleOptions
|
||||
) {
|
||||
// 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.sessionExpiryMs = options?.sessionExpiryMs || DEFAULT_SESSION_EXPIRY_MS;
|
||||
this.loginPath = options?.loginPath || '/api/v1/auth/login';
|
||||
|
||||
this.logger.log(`Auth URL: ${this.authUrl}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Login a Matrix user with mana-core-auth credentials
|
||||
*
|
||||
* @param matrixUserId - Matrix user ID (e.g., "@user:matrix.mana.how")
|
||||
* @param email - User's email
|
||||
* @param password - User's password
|
||||
* @returns Login result with success status
|
||||
*/
|
||||
async login(matrixUserId: string, email: string, password: string): Promise<LoginResult> {
|
||||
try {
|
||||
const response = await fetch(`${this.authUrl}${this.loginPath}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = (await response.json().catch(() => ({}))) as { message?: string };
|
||||
return {
|
||||
success: false,
|
||||
error: errorData.message || 'Authentifizierung fehlgeschlagen',
|
||||
};
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { accessToken?: string; token?: string };
|
||||
const token = data.accessToken || data.token;
|
||||
|
||||
if (!token) {
|
||||
return { success: false, error: 'Kein Token erhalten' };
|
||||
}
|
||||
|
||||
// Store session with expiry
|
||||
this.sessions.set(matrixUserId, {
|
||||
token,
|
||||
email,
|
||||
expiresAt: new Date(Date.now() + this.sessionExpiryMs),
|
||||
});
|
||||
|
||||
this.logger.log(`User ${matrixUserId} logged in as ${email}`);
|
||||
return { success: true, email };
|
||||
} catch (error) {
|
||||
this.logger.error(`Login failed for ${matrixUserId}:`, error);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Verbindung zum Auth-Server fehlgeschlagen',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout a Matrix user
|
||||
*/
|
||||
logout(matrixUserId: string): void {
|
||||
this.sessions.delete(matrixUserId);
|
||||
this.logger.log(`User ${matrixUserId} logged out`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JWT token for a Matrix user (null if not logged in or expired)
|
||||
*/
|
||||
getToken(matrixUserId: string): string | null {
|
||||
const session = this.sessions.get(matrixUserId);
|
||||
|
||||
if (!session) return null;
|
||||
|
||||
// Check if token expired
|
||||
if (session.expiresAt < new Date()) {
|
||||
this.sessions.delete(matrixUserId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return session.token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a Matrix user is logged in
|
||||
*/
|
||||
isLoggedIn(matrixUserId: string): boolean {
|
||||
return this.getToken(matrixUserId) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full session object for a Matrix user
|
||||
*/
|
||||
getSession(matrixUserId: string): UserSession | null {
|
||||
const token = this.getToken(matrixUserId); // This handles expiry check
|
||||
if (!token) return null;
|
||||
return this.sessions.get(matrixUserId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email for a logged-in Matrix user
|
||||
*/
|
||||
getEmail(matrixUserId: string): string | null {
|
||||
const session = this.getSession(matrixUserId);
|
||||
return session?.email || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store custom data in a user's session
|
||||
*/
|
||||
setSessionData(matrixUserId: string, key: string, value: unknown): void {
|
||||
const session = this.sessions.get(matrixUserId);
|
||||
if (session) {
|
||||
session.data = session.data || {};
|
||||
session.data[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom data from a user's session
|
||||
*/
|
||||
getSessionData<T = unknown>(matrixUserId: string, key: string): T | null {
|
||||
const session = this.getSession(matrixUserId);
|
||||
return (session?.data?.[key] as T) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total session count (including expired)
|
||||
*/
|
||||
getSessionCount(): number {
|
||||
return this.sessions.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of active (non-expired) sessions
|
||||
*/
|
||||
getActiveSessionCount(): number {
|
||||
const now = new Date();
|
||||
let count = 0;
|
||||
for (const session of this.sessions.values()) {
|
||||
if (session.expiresAt > now) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session statistics
|
||||
*/
|
||||
getStats(): SessionStats {
|
||||
return {
|
||||
total: this.getSessionCount(),
|
||||
active: this.getActiveSessionCount(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up expired sessions (can be called periodically)
|
||||
*/
|
||||
cleanupExpiredSessions(): number {
|
||||
const now = new Date();
|
||||
let cleaned = 0;
|
||||
|
||||
for (const [userId, session] of this.sessions.entries()) {
|
||||
if (session.expiresAt < now) {
|
||||
this.sessions.delete(userId);
|
||||
cleaned++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleaned > 0) {
|
||||
this.logger.log(`Cleaned up ${cleaned} expired sessions`);
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active session user IDs
|
||||
*/
|
||||
getActiveUserIds(): string[] {
|
||||
const now = new Date();
|
||||
const userIds: string[] = [];
|
||||
|
||||
for (const [userId, session] of this.sessions.entries()) {
|
||||
if (session.expiresAt > now) {
|
||||
userIds.push(userId);
|
||||
}
|
||||
}
|
||||
|
||||
return userIds;
|
||||
}
|
||||
}
|
||||
55
packages/bot-services/src/session/types.ts
Normal file
55
packages/bot-services/src/session/types.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* Types for Matrix user session management
|
||||
*/
|
||||
|
||||
/**
|
||||
* User session data stored per Matrix user
|
||||
*/
|
||||
export interface UserSession {
|
||||
/** JWT token from mana-core-auth */
|
||||
token: string;
|
||||
/** User's email address */
|
||||
email: string;
|
||||
/** Token expiration time */
|
||||
expiresAt: Date;
|
||||
/** Additional session data (bot-specific) */
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Login result
|
||||
*/
|
||||
export interface LoginResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Session statistics
|
||||
*/
|
||||
export interface SessionStats {
|
||||
/** Total sessions (including expired) */
|
||||
total: number;
|
||||
/** Active (non-expired) sessions */
|
||||
active: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Session module configuration options
|
||||
*/
|
||||
export interface SessionModuleOptions {
|
||||
/** Mana Core Auth URL */
|
||||
authUrl?: string;
|
||||
/** Session expiry in milliseconds (default: 7 days) */
|
||||
sessionExpiryMs?: number;
|
||||
/** Custom login endpoint path */
|
||||
loginPath?: string;
|
||||
}
|
||||
|
||||
export const SESSION_MODULE_OPTIONS = 'SESSION_MODULE_OPTIONS';
|
||||
|
||||
/**
|
||||
* Default session expiry: 7 days in milliseconds
|
||||
*/
|
||||
export const DEFAULT_SESSION_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
4
packages/bot-services/src/transcription/index.ts
Normal file
4
packages/bot-services/src/transcription/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export { TranscriptionService } from './transcription.service';
|
||||
export { TranscriptionModule } from './transcription.module';
|
||||
export type { SttResponse, TranscriptionOptions, TranscriptionModuleOptions } from './types';
|
||||
export { STT_MODULE_OPTIONS } from './types';
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { Module, DynamicModule, Global } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { TranscriptionService } from './transcription.service';
|
||||
import { TranscriptionModuleOptions, STT_MODULE_OPTIONS } from './types';
|
||||
|
||||
/**
|
||||
* Shared Speech-to-Text transcription module
|
||||
*
|
||||
* Provides TranscriptionService for voice command processing in Matrix bots.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // With explicit configuration
|
||||
* @Module({
|
||||
* imports: [
|
||||
* TranscriptionModule.register({
|
||||
* sttUrl: 'http://mana-stt:3020',
|
||||
* defaultLanguage: 'de'
|
||||
* })
|
||||
* ]
|
||||
* })
|
||||
*
|
||||
* // With ConfigService (reads from stt.url or STT_URL)
|
||||
* @Module({
|
||||
* imports: [TranscriptionModule.forRoot()]
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
@Global()
|
||||
@Module({})
|
||||
export class TranscriptionModule {
|
||||
/**
|
||||
* Register module with explicit options
|
||||
*/
|
||||
static register(options: TranscriptionModuleOptions = {}): DynamicModule {
|
||||
return {
|
||||
module: TranscriptionModule,
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: STT_MODULE_OPTIONS,
|
||||
useValue: options,
|
||||
},
|
||||
TranscriptionService,
|
||||
],
|
||||
exports: [TranscriptionService],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Register module with ConfigService (reads stt.url or STT_URL from config)
|
||||
*/
|
||||
static forRoot(): DynamicModule {
|
||||
return {
|
||||
module: TranscriptionModule,
|
||||
imports: [ConfigModule],
|
||||
providers: [TranscriptionService],
|
||||
exports: [TranscriptionService],
|
||||
};
|
||||
}
|
||||
}
|
||||
140
packages/bot-services/src/transcription/transcription.service.ts
Normal file
140
packages/bot-services/src/transcription/transcription.service.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import { Injectable, Inject, Logger, Optional } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
SttResponse,
|
||||
TranscriptionOptions,
|
||||
STT_MODULE_OPTIONS,
|
||||
TranscriptionModuleOptions,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Shared Speech-to-Text transcription service
|
||||
*
|
||||
* Connects to mana-stt service to transcribe audio files.
|
||||
* Used by Matrix bots for voice command processing.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In NestJS module
|
||||
* imports: [TranscriptionModule.register({ sttUrl: 'http://mana-stt:3020' })]
|
||||
*
|
||||
* // In service
|
||||
* const text = await transcriptionService.transcribe(audioBuffer, { language: 'de' });
|
||||
* ```
|
||||
*/
|
||||
@Injectable()
|
||||
export class TranscriptionService {
|
||||
private readonly logger = new Logger(TranscriptionService.name);
|
||||
private readonly sttUrl: string;
|
||||
private readonly defaultLanguage: string;
|
||||
|
||||
constructor(
|
||||
@Optional() private configService: ConfigService,
|
||||
@Optional() @Inject(STT_MODULE_OPTIONS) private options?: TranscriptionModuleOptions
|
||||
) {
|
||||
// Priority: module options > config > environment > default
|
||||
this.sttUrl =
|
||||
options?.sttUrl ||
|
||||
this.configService?.get<string>('stt.url') ||
|
||||
this.configService?.get<string>('STT_URL') ||
|
||||
'http://localhost:3020';
|
||||
|
||||
this.defaultLanguage = options?.defaultLanguage || 'de';
|
||||
|
||||
this.logger.log(`STT Service URL: ${this.sttUrl}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transcribe audio buffer to text
|
||||
*
|
||||
* @param audioBuffer - Audio data (supports ogg, wav, mp3, etc.)
|
||||
* @param options - Transcription options (language, model)
|
||||
* @returns Transcribed text
|
||||
*/
|
||||
async transcribe(audioBuffer: Buffer, options?: TranscriptionOptions): Promise<string> {
|
||||
const language = options?.language || this.defaultLanguage;
|
||||
|
||||
const formData = new FormData();
|
||||
const blob = new Blob([new Uint8Array(audioBuffer)], { type: 'audio/ogg' });
|
||||
formData.append('file', blob, 'audio.ogg');
|
||||
formData.append('language', language);
|
||||
|
||||
if (options?.model) {
|
||||
formData.append('model', options.model);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.sttUrl}/transcribe`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`STT service error: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const result = (await response.json()) as SttResponse;
|
||||
this.logger.log(`Transcription completed: ${result.text.substring(0, 50)}...`);
|
||||
return result.text;
|
||||
} catch (error) {
|
||||
this.logger.error('Transcription failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transcribe audio and return full response with metadata
|
||||
*/
|
||||
async transcribeWithMetadata(
|
||||
audioBuffer: Buffer,
|
||||
options?: TranscriptionOptions
|
||||
): Promise<SttResponse> {
|
||||
const language = options?.language || this.defaultLanguage;
|
||||
|
||||
const formData = new FormData();
|
||||
const blob = new Blob([new Uint8Array(audioBuffer)], { type: 'audio/ogg' });
|
||||
formData.append('file', blob, 'audio.ogg');
|
||||
formData.append('language', language);
|
||||
|
||||
if (options?.model) {
|
||||
formData.append('model', options.model);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.sttUrl}/transcribe`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`STT service error: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
return (await response.json()) as SttResponse;
|
||||
} catch (error) {
|
||||
this.logger.error('Transcription failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if STT service is healthy
|
||||
*/
|
||||
async checkHealth(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${this.sttUrl}/health`);
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get STT service URL (for debugging/logging)
|
||||
*/
|
||||
getSttUrl(): string {
|
||||
return this.sttUrl;
|
||||
}
|
||||
}
|
||||
22
packages/bot-services/src/transcription/types.ts
Normal file
22
packages/bot-services/src/transcription/types.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* Types for Speech-to-Text transcription service
|
||||
*/
|
||||
|
||||
export interface SttResponse {
|
||||
text: string;
|
||||
language?: string;
|
||||
model?: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface TranscriptionOptions {
|
||||
language?: string;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export interface TranscriptionModuleOptions {
|
||||
sttUrl?: string;
|
||||
defaultLanguage?: string;
|
||||
}
|
||||
|
||||
export const STT_MODULE_OPTIONS = 'STT_MODULE_OPTIONS';
|
||||
690
pnpm-lock.yaml
generated
690
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -24,6 +24,7 @@
|
|||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@manacore/bot-services": "workspace:*",
|
||||
"@nestjs/common": "^10.4.17",
|
||||
"@nestjs/config": "^3.3.0",
|
||||
"@nestjs/core": "^10.4.17",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { MatrixService } from './matrix.service';
|
||||
import { ClockModule } from '../clock/clock.module';
|
||||
import { TranscriptionModule } from '../transcription/transcription.module';
|
||||
import { TranscriptionModule } from '@manacore/bot-services';
|
||||
|
||||
@Module({
|
||||
imports: [ClockModule, TranscriptionModule],
|
||||
imports: [ClockModule, TranscriptionModule.forRoot()],
|
||||
providers: [MatrixService],
|
||||
exports: [MatrixService],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { ClockService, Timer, Alarm } from '../clock/clock.service';
|
||||
import { TranscriptionService } from '../transcription/transcription.service';
|
||||
import { TranscriptionService } from '@manacore/bot-services';
|
||||
import { HELP_TEXT, WELCOME_TEXT } from '../config/configuration';
|
||||
|
||||
// Natural language keywords
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TranscriptionService } from './transcription.service';
|
||||
|
||||
@Module({
|
||||
providers: [TranscriptionService],
|
||||
exports: [TranscriptionService],
|
||||
})
|
||||
export class TranscriptionModule {}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
interface SttResponse {
|
||||
text: string;
|
||||
language?: string;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TranscriptionService {
|
||||
private readonly logger = new Logger(TranscriptionService.name);
|
||||
private readonly sttUrl: string;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.sttUrl = this.configService.get<string>('stt.url') || 'http://localhost:3020';
|
||||
this.logger.log(`STT Service URL: ${this.sttUrl}`);
|
||||
}
|
||||
|
||||
async transcribe(audioBuffer: Buffer, language: string = 'de'): Promise<string> {
|
||||
const formData = new FormData();
|
||||
const blob = new Blob([new Uint8Array(audioBuffer)], { type: 'audio/ogg' });
|
||||
formData.append('file', blob, 'audio.ogg');
|
||||
formData.append('language', language);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.sttUrl}/transcribe`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`STT service error: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const result: SttResponse = await response.json();
|
||||
this.logger.log(`Transcription completed: ${result.text.substring(0, 50)}...`);
|
||||
return result.text;
|
||||
} catch (error) {
|
||||
this.logger.error('Transcription failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async checkHealth(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${this.sttUrl}/health`);
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,9 @@
|
|||
"private": true,
|
||||
"main": "dist/main.js",
|
||||
"pnpm": {
|
||||
"neverBuiltDependencies": ["@matrix-org/matrix-sdk-crypto-nodejs"],
|
||||
"neverBuiltDependencies": [
|
||||
"@matrix-org/matrix-sdk-crypto-nodejs"
|
||||
],
|
||||
"overrides": {
|
||||
"@matrix-org/matrix-sdk-crypto-nodejs": "npm:empty-npm-package@1.0.0"
|
||||
}
|
||||
|
|
@ -22,6 +24,7 @@
|
|||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@manacore/bot-services": "workspace:*",
|
||||
"@nestjs/common": "^10.4.15",
|
||||
"@nestjs/config": "^3.3.0",
|
||||
"@nestjs/core": "^10.4.15",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { MatrixService } from './matrix.service';
|
||||
import { PictureModule } from '../picture/picture.module';
|
||||
import { SessionModule } from '../session/session.module';
|
||||
import { SessionModule } from '@manacore/bot-services';
|
||||
|
||||
@Module({
|
||||
imports: [PictureModule, SessionModule],
|
||||
imports: [PictureModule, SessionModule.forRoot()],
|
||||
providers: [MatrixService],
|
||||
exports: [MatrixService],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
LogLevel,
|
||||
} from 'matrix-bot-sdk';
|
||||
import { PictureService } from '../picture/picture.service';
|
||||
import { SessionService } from '../session/session.service';
|
||||
import { SessionService } from '@manacore/bot-services';
|
||||
import { HELP_MESSAGE } from '../config/configuration';
|
||||
|
||||
// Natural language keywords that trigger commands
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { SessionService } from './session.service';
|
||||
|
||||
@Module({
|
||||
providers: [SessionService],
|
||||
exports: [SessionService],
|
||||
})
|
||||
export class SessionModule {}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
interface UserSession {
|
||||
token: string;
|
||||
email: string;
|
||||
expiresAt: Date;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SessionService {
|
||||
private readonly logger = new Logger(SessionService.name);
|
||||
private sessions: Map<string, UserSession> = new Map();
|
||||
private authUrl: string;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.authUrl = this.configService.get<string>('auth.url') || 'http://localhost:3001';
|
||||
}
|
||||
|
||||
async login(
|
||||
matrixUserId: string,
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const response = await fetch(`${this.authUrl}/api/v1/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
return {
|
||||
success: false,
|
||||
error: errorData.message || 'Authentifizierung fehlgeschlagen',
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const token = data.accessToken || data.token;
|
||||
|
||||
if (!token) {
|
||||
return { success: false, error: 'Kein Token erhalten' };
|
||||
}
|
||||
|
||||
// Store session (7 days expiry)
|
||||
this.sessions.set(matrixUserId, {
|
||||
token,
|
||||
email,
|
||||
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||
});
|
||||
|
||||
this.logger.log(`User ${matrixUserId} logged in as ${email}`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
this.logger.error(`Login failed for ${matrixUserId}:`, error);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Verbindung zum Auth-Server fehlgeschlagen',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
logout(matrixUserId: string): void {
|
||||
this.sessions.delete(matrixUserId);
|
||||
this.logger.log(`User ${matrixUserId} logged out`);
|
||||
}
|
||||
|
||||
getToken(matrixUserId: string): string | null {
|
||||
const session = this.sessions.get(matrixUserId);
|
||||
|
||||
if (!session) return null;
|
||||
|
||||
// Check if token expired
|
||||
if (session.expiresAt < new Date()) {
|
||||
this.sessions.delete(matrixUserId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return session.token;
|
||||
}
|
||||
|
||||
isLoggedIn(matrixUserId: string): boolean {
|
||||
return this.getToken(matrixUserId) !== null;
|
||||
}
|
||||
|
||||
getSessionCount(): number {
|
||||
return this.sessions.size;
|
||||
}
|
||||
|
||||
getLoggedInCount(): number {
|
||||
const now = new Date();
|
||||
let count = 0;
|
||||
for (const session of this.sessions.values()) {
|
||||
if (session.expiresAt > now) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@manacore/bot-services": "workspace:*",
|
||||
"@nestjs/common": "^10.4.15",
|
||||
"@nestjs/config": "^3.3.0",
|
||||
"@nestjs/core": "^10.4.15",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { MatrixService } from './matrix.service';
|
||||
import { TodoModule } from '../todo/todo.module';
|
||||
import { TranscriptionModule } from '../transcription/transcription.module';
|
||||
import { TranscriptionModule } from '@manacore/bot-services';
|
||||
|
||||
@Module({
|
||||
imports: [TodoModule, TranscriptionModule],
|
||||
imports: [TodoModule, TranscriptionModule.forRoot()],
|
||||
providers: [MatrixService],
|
||||
exports: [MatrixService],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { TodoService, Task } from '../todo/todo.service';
|
||||
import { TranscriptionService } from '../transcription/transcription.service';
|
||||
import { TranscriptionService } from '@manacore/bot-services';
|
||||
import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration';
|
||||
|
||||
// Natural language keywords that trigger commands (German + English)
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TranscriptionService } from './transcription.service';
|
||||
|
||||
@Module({
|
||||
providers: [TranscriptionService],
|
||||
exports: [TranscriptionService],
|
||||
})
|
||||
export class TranscriptionModule {}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
interface SttResponse {
|
||||
text: string;
|
||||
language?: string;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TranscriptionService {
|
||||
private readonly logger = new Logger(TranscriptionService.name);
|
||||
private readonly sttUrl: string;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.sttUrl = this.configService.get<string>('stt.url') || 'http://localhost:3020';
|
||||
this.logger.log(`STT Service URL: ${this.sttUrl}`);
|
||||
}
|
||||
|
||||
async transcribe(audioBuffer: Buffer, language: string = 'de'): Promise<string> {
|
||||
const formData = new FormData();
|
||||
const blob = new Blob([new Uint8Array(audioBuffer)], { type: 'audio/ogg' });
|
||||
formData.append('file', blob, 'audio.ogg');
|
||||
formData.append('language', language);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.sttUrl}/transcribe`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`STT service error: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const result: SttResponse = await response.json();
|
||||
this.logger.log(`Transcription completed: ${result.text.substring(0, 50)}...`);
|
||||
return result.text;
|
||||
} catch (error) {
|
||||
this.logger.error('Transcription failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async checkHealth(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${this.sttUrl}/health`);
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@manacore/bot-services": "workspace:*",
|
||||
"@nestjs/common": "^10.4.15",
|
||||
"@nestjs/config": "^3.3.0",
|
||||
"@nestjs/core": "^10.4.15",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { MatrixService } from './matrix.service';
|
||||
import { QuotesModule } from '../quotes/quotes.module';
|
||||
import { SessionModule } from '../session/session.module';
|
||||
import { TranscriptionModule } from '../transcription/transcription.module';
|
||||
import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
|
||||
|
||||
@Module({
|
||||
imports: [QuotesModule, SessionModule, TranscriptionModule],
|
||||
imports: [QuotesModule, SessionModule.forRoot(), TranscriptionModule.forRoot()],
|
||||
providers: [MatrixService],
|
||||
exports: [MatrixService],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ import {
|
|||
} from 'matrix-bot-sdk';
|
||||
import { QuotesService } from '../quotes/quotes.service';
|
||||
import { ZitareService } from '../quotes/zitare.service';
|
||||
import { SessionService } from '../session/session.service';
|
||||
import { TranscriptionService } from '../transcription/transcription.service';
|
||||
import { SessionService, TranscriptionService } from '@manacore/bot-services';
|
||||
import { HELP_MESSAGE, Category } from '../config/configuration';
|
||||
|
||||
// Natural language keywords that trigger commands
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { SessionService } from './session.service';
|
||||
|
||||
@Module({
|
||||
providers: [SessionService],
|
||||
exports: [SessionService],
|
||||
})
|
||||
export class SessionModule {}
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
interface UserSession {
|
||||
token: string;
|
||||
email: string;
|
||||
expiresAt: Date;
|
||||
lastQuoteId?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SessionService {
|
||||
private readonly logger = new Logger(SessionService.name);
|
||||
private sessions: Map<string, UserSession> = new Map();
|
||||
private authUrl: string;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.authUrl = this.configService.get<string>('auth.url') || 'http://localhost:3001';
|
||||
}
|
||||
|
||||
async login(
|
||||
matrixUserId: string,
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const response = await fetch(`${this.authUrl}/api/v1/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
return {
|
||||
success: false,
|
||||
error: errorData.message || 'Authentifizierung fehlgeschlagen',
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const token = data.accessToken || data.token;
|
||||
|
||||
if (!token) {
|
||||
return { success: false, error: 'Kein Token erhalten' };
|
||||
}
|
||||
|
||||
// Store session (7 days expiry)
|
||||
this.sessions.set(matrixUserId, {
|
||||
token,
|
||||
email,
|
||||
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||
});
|
||||
|
||||
this.logger.log(`User ${matrixUserId} logged in as ${email}`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
this.logger.error(`Login failed for ${matrixUserId}:`, error);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Verbindung zum Auth-Server fehlgeschlagen',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
logout(matrixUserId: string): void {
|
||||
this.sessions.delete(matrixUserId);
|
||||
this.logger.log(`User ${matrixUserId} logged out`);
|
||||
}
|
||||
|
||||
getToken(matrixUserId: string): string | null {
|
||||
const session = this.sessions.get(matrixUserId);
|
||||
|
||||
if (!session) return null;
|
||||
|
||||
// Check if token expired
|
||||
if (session.expiresAt < new Date()) {
|
||||
this.sessions.delete(matrixUserId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return session.token;
|
||||
}
|
||||
|
||||
isLoggedIn(matrixUserId: string): boolean {
|
||||
return this.getToken(matrixUserId) !== null;
|
||||
}
|
||||
|
||||
setLastQuoteId(matrixUserId: string, quoteId: string): void {
|
||||
const session = this.sessions.get(matrixUserId);
|
||||
if (session) {
|
||||
session.lastQuoteId = quoteId;
|
||||
}
|
||||
}
|
||||
|
||||
getLastQuoteId(matrixUserId: string): string | null {
|
||||
const session = this.sessions.get(matrixUserId);
|
||||
return session?.lastQuoteId || null;
|
||||
}
|
||||
|
||||
getSessionCount(): number {
|
||||
return this.sessions.size;
|
||||
}
|
||||
|
||||
getLoggedInCount(): number {
|
||||
const now = new Date();
|
||||
let count = 0;
|
||||
for (const session of this.sessions.values()) {
|
||||
if (session.expiresAt > now) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TranscriptionService } from './transcription.service';
|
||||
|
||||
@Module({
|
||||
providers: [TranscriptionService],
|
||||
exports: [TranscriptionService],
|
||||
})
|
||||
export class TranscriptionModule {}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class TranscriptionService {
|
||||
private readonly logger = new Logger(TranscriptionService.name);
|
||||
private readonly sttUrl: string;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.sttUrl = this.configService.get<string>('stt.url') || 'http://localhost:3020';
|
||||
}
|
||||
|
||||
async transcribe(audioBuffer: Buffer, language: string = 'de'): Promise<string> {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
const blob = new Blob([new Uint8Array(audioBuffer)], { type: 'audio/ogg' });
|
||||
formData.append('file', blob, 'audio.ogg');
|
||||
formData.append('language', language);
|
||||
|
||||
const response = await fetch(`${this.sttUrl}/transcribe`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`STT service error: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = (await response.json()) as { text: string };
|
||||
this.logger.log(`Transcription result: ${result.text}`);
|
||||
return result.text;
|
||||
} catch (error) {
|
||||
this.logger.error('Transcription failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue