mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:01:09 +02:00
feat(bots): enable Redis SSO for todo-bot and calendar-bot
- Activate Redis session storage in both bots for cross-bot SSO - Update SessionHelper to async methods for Redis-backed SessionService - Fix async/await issues in todo-bot and calendar-bot matrix.service.ts - Remove unused imports from calendar-api and todo-api services - Add CALENDAR_BACKEND_URL and MANA_CORE_SERVICE_KEY to .env.development Note: SessionService methods are now async (Redis-backed). Other bots need their matrix.service.ts updated to await these async calls. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7bad849258
commit
2777f604fd
27 changed files with 2997 additions and 838 deletions
|
|
@ -3,7 +3,9 @@ import { AuthController } from './auth.controller';
|
|||
import { BetterAuthPassthroughController } from './better-auth-passthrough.controller';
|
||||
import { OidcController } from './oidc.controller';
|
||||
import { OidcLoginController } from './oidc-login.controller';
|
||||
import { MatrixSessionController } from './matrix-session.controller';
|
||||
import { BetterAuthService } from './services/better-auth.service';
|
||||
import { MatrixSessionService } from './services/matrix-session.service';
|
||||
import { ReferralsModule } from '../referrals/referrals.module';
|
||||
|
||||
@Module({
|
||||
|
|
@ -13,8 +15,9 @@ import { ReferralsModule } from '../referrals/referrals.module';
|
|||
BetterAuthPassthroughController,
|
||||
OidcController,
|
||||
OidcLoginController,
|
||||
MatrixSessionController,
|
||||
],
|
||||
providers: [BetterAuthService],
|
||||
exports: [BetterAuthService],
|
||||
providers: [BetterAuthService, MatrixSessionService],
|
||||
exports: [BetterAuthService, MatrixSessionService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
|
|
|||
208
services/mana-core-auth/src/auth/matrix-session.controller.ts
Normal file
208
services/mana-core-auth/src/auth/matrix-session.controller.ts
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Delete,
|
||||
Param,
|
||||
Body,
|
||||
Headers,
|
||||
UnauthorizedException,
|
||||
NotFoundException,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { MatrixSessionService } from './services/matrix-session.service';
|
||||
|
||||
/**
|
||||
* DTO for linking a Matrix user to a Mana account
|
||||
*/
|
||||
class LinkMatrixUserDto {
|
||||
/** Matrix user ID (e.g., @user:matrix.mana.how) */
|
||||
matrixUserId!: string;
|
||||
/** User's email (optional, for convenience) */
|
||||
email?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matrix Session Controller
|
||||
*
|
||||
* Provides endpoints for Matrix bot authentication via SSO.
|
||||
*
|
||||
* Endpoints:
|
||||
* - POST /api/v1/auth/matrix-user-links - Link Matrix user to Mana account
|
||||
* - GET /api/v1/auth/matrix-session/:matrixUserId - Get JWT for linked Matrix user
|
||||
* - DELETE /api/v1/auth/matrix-user-links/:matrixUserId - Unlink Matrix user
|
||||
* - GET /api/v1/auth/matrix-user-links/check/:matrixUserId - Check if user is linked
|
||||
*
|
||||
* Authentication:
|
||||
* - POST /link requires Bearer token (user authenticating)
|
||||
* - GET /session requires X-Service-Key (internal bot service)
|
||||
* - DELETE requires Bearer token (user unlinking)
|
||||
* - GET /check requires X-Service-Key (internal bot service)
|
||||
*/
|
||||
@Controller('api/v1/auth')
|
||||
export class MatrixSessionController {
|
||||
constructor(private readonly matrixSessionService: MatrixSessionService) {}
|
||||
|
||||
/**
|
||||
* Link a Matrix user ID to a Mana account
|
||||
*
|
||||
* Called by bots after successful !login command.
|
||||
* Requires the user's JWT token from login.
|
||||
*
|
||||
* @example
|
||||
* POST /api/v1/auth/matrix-user-links
|
||||
* Authorization: Bearer <jwt-token>
|
||||
* Body: { "matrixUserId": "@user:matrix.mana.how", "email": "user@example.com" }
|
||||
*/
|
||||
@Post('matrix-user-links')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
async linkMatrixUser(
|
||||
@Body() dto: LinkMatrixUserDto,
|
||||
@Headers('authorization') authHeader?: string,
|
||||
@Headers('x-service-key') serviceKey?: string
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
// Two auth methods: Bearer token (from user login) or Service key (from bot)
|
||||
let manaUserId: string;
|
||||
|
||||
if (serviceKey && this.matrixSessionService.validateServiceKey(serviceKey)) {
|
||||
// Service key auth - must provide userId in body
|
||||
const bodyWithUserId = dto as LinkMatrixUserDto & { userId?: string };
|
||||
if (!bodyWithUserId.userId) {
|
||||
throw new UnauthorizedException('userId required when using service key');
|
||||
}
|
||||
manaUserId = bodyWithUserId.userId;
|
||||
} else if (authHeader?.startsWith('Bearer ')) {
|
||||
// JWT auth - extract user ID from token
|
||||
const token = authHeader.substring(7);
|
||||
const payload = this.decodeToken(token);
|
||||
if (!payload?.sub) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
manaUserId = payload.sub;
|
||||
} else {
|
||||
throw new UnauthorizedException('Authentication required');
|
||||
}
|
||||
|
||||
if (!dto.matrixUserId) {
|
||||
throw new UnauthorizedException('matrixUserId is required');
|
||||
}
|
||||
|
||||
await this.matrixSessionService.linkMatrixUser(dto.matrixUserId, manaUserId, dto.email);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Matrix user ${dto.matrixUserId} linked successfully`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a JWT token for a linked Matrix user
|
||||
*
|
||||
* Called by bots to auto-authenticate users.
|
||||
* Requires service key (internal service authentication).
|
||||
*
|
||||
* @example
|
||||
* GET /api/v1/auth/matrix-session/@user:matrix.mana.how
|
||||
* X-Service-Key: <service-key>
|
||||
*/
|
||||
@Get('matrix-session/:matrixUserId')
|
||||
async getMatrixSession(
|
||||
@Param('matrixUserId') matrixUserId: string,
|
||||
@Headers('x-service-key') serviceKey?: string
|
||||
): Promise<{ token: string; email: string }> {
|
||||
// Require service key for this endpoint
|
||||
if (!serviceKey || !this.matrixSessionService.validateServiceKey(serviceKey)) {
|
||||
throw new UnauthorizedException('Valid service key required');
|
||||
}
|
||||
|
||||
const result = await this.matrixSessionService.getSessionForMatrixUser(
|
||||
decodeURIComponent(matrixUserId)
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
throw new NotFoundException('No link found for this Matrix user');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlink a Matrix user from a Mana account
|
||||
*
|
||||
* Called when user wants to disconnect their Matrix account.
|
||||
* Requires the user's JWT token.
|
||||
*
|
||||
* @example
|
||||
* DELETE /api/v1/auth/matrix-user-links/@user:matrix.mana.how
|
||||
* Authorization: Bearer <jwt-token>
|
||||
*/
|
||||
@Delete('matrix-user-links/:matrixUserId')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async unlinkMatrixUser(
|
||||
@Param('matrixUserId') matrixUserId: string,
|
||||
@Headers('authorization') authHeader?: string,
|
||||
@Headers('x-service-key') serviceKey?: string
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
// Allow both Bearer token and service key
|
||||
if (
|
||||
!authHeader?.startsWith('Bearer ') &&
|
||||
!this.matrixSessionService.validateServiceKey(serviceKey || '')
|
||||
) {
|
||||
throw new UnauthorizedException('Authentication required');
|
||||
}
|
||||
|
||||
const deleted = await this.matrixSessionService.unlinkMatrixUser(
|
||||
decodeURIComponent(matrixUserId)
|
||||
);
|
||||
|
||||
if (!deleted) {
|
||||
throw new NotFoundException('No link found for this Matrix user');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Matrix user ${matrixUserId} unlinked successfully`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a Matrix user is linked
|
||||
*
|
||||
* Requires service key (internal service authentication).
|
||||
*
|
||||
* @example
|
||||
* GET /api/v1/auth/matrix-user-links/check/@user:matrix.mana.how
|
||||
* X-Service-Key: <service-key>
|
||||
*/
|
||||
@Get('matrix-user-links/check/:matrixUserId')
|
||||
async checkMatrixLink(
|
||||
@Param('matrixUserId') matrixUserId: string,
|
||||
@Headers('x-service-key') serviceKey?: string
|
||||
): Promise<{ linked: boolean }> {
|
||||
// Require service key for this endpoint
|
||||
if (!serviceKey || !this.matrixSessionService.validateServiceKey(serviceKey)) {
|
||||
throw new UnauthorizedException('Valid service key required');
|
||||
}
|
||||
|
||||
const linked = await this.matrixSessionService.isLinked(decodeURIComponent(matrixUserId));
|
||||
|
||||
return { linked };
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode JWT token to get payload (without verification)
|
||||
* Note: This is used only to extract user ID after the bot has verified the token
|
||||
*/
|
||||
private decodeToken(token: string): { sub?: string } | null {
|
||||
try {
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) return null;
|
||||
|
||||
const payload = Buffer.from(parts[1], 'base64url').toString('utf-8');
|
||||
return JSON.parse(payload);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1346,6 +1346,66 @@ export class BetterAuthService {
|
|||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Matrix Bot SSO Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Generate a JWT token for a specific user (used by Matrix bots)
|
||||
*
|
||||
* This method generates a fresh JWT token for an existing user,
|
||||
* without requiring password authentication. It's used by the
|
||||
* Matrix-SSO-Link system to auto-authenticate bot users.
|
||||
*
|
||||
* @param userId - Mana Core Auth user ID
|
||||
* @returns JWT access token or null if user not found
|
||||
*/
|
||||
async generateTokenForUser(userId: string): Promise<string | null> {
|
||||
try {
|
||||
const db = getDb(this.databaseUrl);
|
||||
const { users } = await import('../../db/schema/auth.schema');
|
||||
const { eq } = await import('drizzle-orm');
|
||||
|
||||
// Get user from database
|
||||
const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1);
|
||||
|
||||
if (!user || user.deletedAt) {
|
||||
this.logger.warn('generateTokenForUser: User not found', { userId });
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate JWT using Better Auth's signJWT
|
||||
const api = this.auth.api as any;
|
||||
|
||||
const jwtResult = await api.signJWT({
|
||||
body: {
|
||||
payload: {
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
role: user.role || 'user',
|
||||
sid: `bot-session-${Date.now()}`, // Pseudo session ID for bots
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const token = jwtResult?.token;
|
||||
|
||||
if (!token) {
|
||||
this.logger.error('generateTokenForUser: signJWT returned empty token');
|
||||
return null;
|
||||
}
|
||||
|
||||
this.logger.debug('Generated token for user via Matrix-SSO-Link', { userId });
|
||||
return token;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
'generateTokenForUser failed',
|
||||
error instanceof Error ? error.stack : undefined
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// SSO Methods
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -0,0 +1,191 @@
|
|||
import { Injectable, Logger, UnauthorizedException, NotFoundException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { getDb } from '../../db/connection';
|
||||
import { matrixUserLinks, users } from '../../db/schema/auth.schema';
|
||||
import { BetterAuthService } from './better-auth.service';
|
||||
|
||||
/**
|
||||
* Matrix Session Service
|
||||
*
|
||||
* Manages the link between Matrix user IDs and Mana Core Auth accounts.
|
||||
* Enables automatic bot authentication for users who have linked their accounts.
|
||||
*
|
||||
* Flow:
|
||||
* 1. User logs into a Matrix bot via !login email password
|
||||
* 2. Bot calls POST /api/v1/auth/matrix-user-links to store the link
|
||||
* 3. Later, bot can call GET /api/v1/auth/matrix-session/:matrixUserId
|
||||
* 4. If a link exists, a fresh JWT token is returned
|
||||
*/
|
||||
@Injectable()
|
||||
export class MatrixSessionService {
|
||||
private readonly logger = new Logger(MatrixSessionService.name);
|
||||
private readonly db;
|
||||
private readonly serviceKey: string;
|
||||
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly betterAuthService: BetterAuthService
|
||||
) {
|
||||
const databaseUrl = this.configService.get<string>('DATABASE_URL');
|
||||
if (!databaseUrl) {
|
||||
throw new Error('DATABASE_URL is required');
|
||||
}
|
||||
this.db = getDb(databaseUrl);
|
||||
this.serviceKey = this.configService.get<string>('MANA_CORE_SERVICE_KEY', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate service key from X-Service-Key header
|
||||
*/
|
||||
validateServiceKey(providedKey: string): boolean {
|
||||
if (!this.serviceKey) {
|
||||
this.logger.warn('MANA_CORE_SERVICE_KEY not configured - service key validation disabled');
|
||||
return false;
|
||||
}
|
||||
return providedKey === this.serviceKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update a link between a Matrix user ID and a Mana user
|
||||
*
|
||||
* @param matrixUserId - Matrix user ID (e.g., @user:matrix.mana.how)
|
||||
* @param manaUserId - Mana Core Auth user ID
|
||||
* @param email - User's email (optional, for convenience)
|
||||
*/
|
||||
async linkMatrixUser(matrixUserId: string, manaUserId: string, email?: string): Promise<void> {
|
||||
// Check if link already exists
|
||||
const existing = await this.db
|
||||
.select()
|
||||
.from(matrixUserLinks)
|
||||
.where(eq(matrixUserLinks.matrixUserId, matrixUserId))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// Update existing link
|
||||
await this.db
|
||||
.update(matrixUserLinks)
|
||||
.set({
|
||||
userId: manaUserId,
|
||||
email,
|
||||
lastUsedAt: new Date(),
|
||||
})
|
||||
.where(eq(matrixUserLinks.matrixUserId, matrixUserId));
|
||||
|
||||
this.logger.log(`Updated Matrix link: ${matrixUserId} -> ${manaUserId}`);
|
||||
} else {
|
||||
// Create new link
|
||||
await this.db.insert(matrixUserLinks).values({
|
||||
id: nanoid(),
|
||||
matrixUserId,
|
||||
userId: manaUserId,
|
||||
email,
|
||||
linkedAt: new Date(),
|
||||
});
|
||||
|
||||
this.logger.log(`Created Matrix link: ${matrixUserId} -> ${manaUserId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a link for a Matrix user ID
|
||||
*/
|
||||
async unlinkMatrixUser(matrixUserId: string): Promise<boolean> {
|
||||
const result = await this.db
|
||||
.delete(matrixUserLinks)
|
||||
.where(eq(matrixUserLinks.matrixUserId, matrixUserId))
|
||||
.returning();
|
||||
|
||||
if (result.length > 0) {
|
||||
this.logger.log(`Removed Matrix link: ${matrixUserId}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a fresh JWT token for a linked Matrix user
|
||||
*
|
||||
* @param matrixUserId - Matrix user ID
|
||||
* @returns JWT token or null if no link exists
|
||||
*/
|
||||
async getSessionForMatrixUser(
|
||||
matrixUserId: string
|
||||
): Promise<{ token: string; email: string } | null> {
|
||||
// Find the link
|
||||
const links = await this.db
|
||||
.select({
|
||||
userId: matrixUserLinks.userId,
|
||||
email: matrixUserLinks.email,
|
||||
})
|
||||
.from(matrixUserLinks)
|
||||
.where(eq(matrixUserLinks.matrixUserId, matrixUserId))
|
||||
.limit(1);
|
||||
|
||||
if (links.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const link = links[0];
|
||||
|
||||
// Update last used timestamp
|
||||
await this.db
|
||||
.update(matrixUserLinks)
|
||||
.set({ lastUsedAt: new Date() })
|
||||
.where(eq(matrixUserLinks.matrixUserId, matrixUserId));
|
||||
|
||||
// Get user details if email not stored
|
||||
let email = link.email;
|
||||
if (!email) {
|
||||
const userRecords = await this.db
|
||||
.select({ email: users.email })
|
||||
.from(users)
|
||||
.where(eq(users.id, link.userId))
|
||||
.limit(1);
|
||||
|
||||
if (userRecords.length > 0) {
|
||||
email = userRecords[0].email;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a fresh JWT token for this user
|
||||
const token = await this.betterAuthService.generateTokenForUser(link.userId);
|
||||
|
||||
if (!token) {
|
||||
this.logger.error(`Failed to generate token for user ${link.userId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
this.logger.debug(`Generated token for Matrix user ${matrixUserId}`);
|
||||
return { token, email: email || '' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Matrix links for a Mana user
|
||||
*/
|
||||
async getLinksForUser(manaUserId: string): Promise<{ matrixUserId: string; linkedAt: Date }[]> {
|
||||
const links = await this.db
|
||||
.select({
|
||||
matrixUserId: matrixUserLinks.matrixUserId,
|
||||
linkedAt: matrixUserLinks.linkedAt,
|
||||
})
|
||||
.from(matrixUserLinks)
|
||||
.where(eq(matrixUserLinks.userId, manaUserId));
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a Matrix user is linked
|
||||
*/
|
||||
async isLinked(matrixUserId: string): Promise<boolean> {
|
||||
const links = await this.db
|
||||
.select({ id: matrixUserLinks.id })
|
||||
.from(matrixUserLinks)
|
||||
.where(eq(matrixUserLinks.matrixUserId, matrixUserId))
|
||||
.limit(1);
|
||||
|
||||
return links.length > 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -187,6 +187,26 @@ export const oauthConsents = authSchema.table('oauth_consents', {
|
|||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Matrix User Links table (for Bot SSO)
|
||||
// Links Matrix user IDs to Mana user accounts for automatic bot authentication
|
||||
export const matrixUserLinks = authSchema.table(
|
||||
'matrix_user_links',
|
||||
{
|
||||
id: text('id').primaryKey(), // nanoid
|
||||
matrixUserId: text('matrix_user_id').unique().notNull(), // e.g., @user:matrix.mana.how
|
||||
userId: text('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
linkedAt: timestamp('linked_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
lastUsedAt: timestamp('last_used_at', { withTimezone: true }),
|
||||
// Optional: store email for convenience (denormalized from users table)
|
||||
email: text('email'),
|
||||
},
|
||||
(table) => ({
|
||||
userIdIdx: index('matrix_user_links_user_id_idx').on(table.userId),
|
||||
})
|
||||
);
|
||||
|
||||
// User settings table (synced across all apps)
|
||||
export const userSettings = authSchema.table('user_settings', {
|
||||
userId: text('user_id')
|
||||
|
|
|
|||
|
|
@ -1,18 +1,35 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { MatrixService } from './matrix.service';
|
||||
import { CalendarModule } from '../calendar/calendar.module';
|
||||
import { TranscriptionModule, SessionModule, CreditModule } from '@manacore/bot-services';
|
||||
import {
|
||||
TranscriptionModule,
|
||||
SessionModule,
|
||||
CreditModule,
|
||||
CalendarApiService,
|
||||
} from '@manacore/bot-services';
|
||||
|
||||
// Factory provider for CalendarApiService
|
||||
const calendarApiServiceProvider = {
|
||||
provide: CalendarApiService,
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const baseUrl = configService.get<string>('CALENDAR_BACKEND_URL', 'http://localhost:3014');
|
||||
return new CalendarApiService(baseUrl);
|
||||
},
|
||||
inject: [ConfigService],
|
||||
};
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule,
|
||||
CalendarModule,
|
||||
TranscriptionModule.register({
|
||||
sttUrl: process.env.STT_URL || 'http://localhost:3020',
|
||||
}),
|
||||
SessionModule.forRoot(),
|
||||
SessionModule.forRoot({ storageMode: 'redis' }),
|
||||
CreditModule.forRoot(),
|
||||
],
|
||||
providers: [MatrixService],
|
||||
providers: [MatrixService, calendarApiServiceProvider],
|
||||
exports: [MatrixService],
|
||||
})
|
||||
export class BotModule {}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,13 @@ import {
|
|||
KeywordCommandDetector,
|
||||
COMMON_KEYWORDS,
|
||||
} from '@manacore/matrix-bot-common';
|
||||
import { TranscriptionService, SessionService, CreditService } from '@manacore/bot-services';
|
||||
import {
|
||||
TranscriptionService,
|
||||
SessionService,
|
||||
CreditService,
|
||||
CalendarApiService,
|
||||
CalendarEvent as ApiCalendarEvent,
|
||||
} from '@manacore/bot-services';
|
||||
import { CalendarService, CalendarEvent } from '../calendar/calendar.service';
|
||||
import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration';
|
||||
|
||||
|
|
@ -19,7 +25,10 @@ export class MatrixService extends BaseMatrixService {
|
|||
[
|
||||
...COMMON_KEYWORDS,
|
||||
{ keywords: ['was kannst du'], command: 'help' },
|
||||
{ keywords: ['was steht heute an', 'termine heute', 'heute termine', "today's events"], command: 'today' },
|
||||
{
|
||||
keywords: ['was steht heute an', 'termine heute', 'heute termine', "today's events"],
|
||||
command: 'today',
|
||||
},
|
||||
{ keywords: ['termine morgen', 'morgen termine', 'was ist morgen'], command: 'tomorrow' },
|
||||
{ keywords: ['diese woche', 'wochenübersicht', 'week', 'woche'], command: 'week' },
|
||||
{ keywords: ['zeige kalender', 'meine kalender', 'calendars'], command: 'calendars' },
|
||||
|
|
@ -32,12 +41,39 @@ export class MatrixService extends BaseMatrixService {
|
|||
configService: ConfigService,
|
||||
private readonly transcriptionService: TranscriptionService,
|
||||
private calendarService: CalendarService,
|
||||
private calendarApiService: CalendarApiService,
|
||||
private sessionService: SessionService,
|
||||
private creditService: CreditService
|
||||
) {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is logged in and has a valid token for API access
|
||||
*/
|
||||
private async getToken(userId: string): Promise<string | null> {
|
||||
return this.sessionService.getToken(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize event from API or local format to common format
|
||||
*/
|
||||
private normalizeEvent(event: CalendarEvent | ApiCalendarEvent): CalendarEvent {
|
||||
return {
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
description: event.description || null,
|
||||
location: event.location || null,
|
||||
startTime: event.startTime,
|
||||
endTime: event.endTime,
|
||||
isAllDay: event.isAllDay,
|
||||
calendarId: event.calendarId || '',
|
||||
calendarName: (event as CalendarEvent).calendarName || 'Kalender',
|
||||
createdAt: event.createdAt || new Date().toISOString(),
|
||||
userId: event.userId || '',
|
||||
};
|
||||
}
|
||||
|
||||
protected override async handleAudioMessage(
|
||||
roomId: string,
|
||||
event: MatrixRoomEvent,
|
||||
|
|
@ -64,9 +100,11 @@ export class MatrixService extends BaseMatrixService {
|
|||
|
||||
protected getConfig(): MatrixBotConfig {
|
||||
return {
|
||||
homeserverUrl: this.configService.get<string>('matrix.homeserverUrl') || 'http://localhost:8008',
|
||||
homeserverUrl:
|
||||
this.configService.get<string>('matrix.homeserverUrl') || 'http://localhost:8008',
|
||||
accessToken: this.configService.get<string>('matrix.accessToken') || '',
|
||||
storagePath: this.configService.get<string>('matrix.storagePath') || './data/bot-storage.json',
|
||||
storagePath:
|
||||
this.configService.get<string>('matrix.storagePath') || './data/bot-storage.json',
|
||||
allowedRooms: this.configService.get<string[]>('matrix.allowedRooms') || [],
|
||||
};
|
||||
}
|
||||
|
|
@ -176,7 +214,17 @@ export class MatrixService extends BaseMatrixService {
|
|||
}
|
||||
|
||||
private async handleTodayEvents(roomId: string, event: MatrixRoomEvent, userId: string) {
|
||||
const events = await this.calendarService.getTodayEvents(userId);
|
||||
const token = await this.getToken(userId);
|
||||
let events: CalendarEvent[];
|
||||
|
||||
if (token) {
|
||||
// Use API service
|
||||
const apiEvents = await this.calendarApiService.getTodayEvents(token);
|
||||
events = apiEvents.map((e) => this.normalizeEvent(e));
|
||||
} else {
|
||||
// Use local storage
|
||||
events = await this.calendarService.getTodayEvents(userId);
|
||||
}
|
||||
|
||||
if (events.length === 0) {
|
||||
await this.sendReply(
|
||||
|
|
@ -187,12 +235,31 @@ export class MatrixService extends BaseMatrixService {
|
|||
return;
|
||||
}
|
||||
|
||||
const response = this.formatEventList('📅 **Termine heute:**', events);
|
||||
let response = this.formatEventList('📅 **Termine heute:**', events);
|
||||
if (token) {
|
||||
response += '\n\n🔄 Synchronisiert';
|
||||
}
|
||||
await this.sendReply(roomId, event, response);
|
||||
}
|
||||
|
||||
private async handleTomorrowEvents(roomId: string, event: MatrixRoomEvent, userId: string) {
|
||||
const events = await this.calendarService.getTomorrowEvents(userId);
|
||||
const token = await this.getToken(userId);
|
||||
let events: CalendarEvent[];
|
||||
|
||||
if (token) {
|
||||
// Use API service - get events for tomorrow
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const tomorrowStr = tomorrow.toISOString().split('T')[0];
|
||||
const apiEvents = await this.calendarApiService.getEvents(token, {
|
||||
start: tomorrowStr,
|
||||
end: tomorrowStr,
|
||||
});
|
||||
events = apiEvents.map((e) => this.normalizeEvent(e));
|
||||
} else {
|
||||
// Use local storage
|
||||
events = await this.calendarService.getTomorrowEvents(userId);
|
||||
}
|
||||
|
||||
if (events.length === 0) {
|
||||
await this.sendReply(
|
||||
|
|
@ -203,12 +270,25 @@ export class MatrixService extends BaseMatrixService {
|
|||
return;
|
||||
}
|
||||
|
||||
const response = this.formatEventList('📅 **Termine morgen:**', events);
|
||||
let response = this.formatEventList('📅 **Termine morgen:**', events);
|
||||
if (token) {
|
||||
response += '\n\n🔄 Synchronisiert';
|
||||
}
|
||||
await this.sendReply(roomId, event, response);
|
||||
}
|
||||
|
||||
private async handleWeekEvents(roomId: string, event: MatrixRoomEvent, userId: string) {
|
||||
const events = await this.calendarService.getWeekEvents(userId);
|
||||
const token = await this.getToken(userId);
|
||||
let events: CalendarEvent[];
|
||||
|
||||
if (token) {
|
||||
// Use API service
|
||||
const apiEvents = await this.calendarApiService.getUpcomingEvents(token, 7);
|
||||
events = apiEvents.map((e) => this.normalizeEvent(e));
|
||||
} else {
|
||||
// Use local storage
|
||||
events = await this.calendarService.getWeekEvents(userId);
|
||||
}
|
||||
|
||||
if (events.length === 0) {
|
||||
await this.sendReply(
|
||||
|
|
@ -219,12 +299,25 @@ export class MatrixService extends BaseMatrixService {
|
|||
return;
|
||||
}
|
||||
|
||||
const response = this.formatEventList('📅 **Termine diese Woche:**', events);
|
||||
let response = this.formatEventList('📅 **Termine diese Woche:**', events);
|
||||
if (token) {
|
||||
response += '\n\n🔄 Synchronisiert';
|
||||
}
|
||||
await this.sendReply(roomId, event, response);
|
||||
}
|
||||
|
||||
private async handleUpcomingEvents(roomId: string, event: MatrixRoomEvent, userId: string) {
|
||||
const events = await this.calendarService.getUpcomingEvents(userId, 14);
|
||||
const token = await this.getToken(userId);
|
||||
let events: CalendarEvent[];
|
||||
|
||||
if (token) {
|
||||
// Use API service
|
||||
const apiEvents = await this.calendarApiService.getUpcomingEvents(token, 14);
|
||||
events = apiEvents.map((e) => this.normalizeEvent(e));
|
||||
} else {
|
||||
// Use local storage
|
||||
events = await this.calendarService.getUpcomingEvents(userId, 14);
|
||||
}
|
||||
|
||||
if (events.length === 0) {
|
||||
await this.sendReply(
|
||||
|
|
@ -235,11 +328,19 @@ export class MatrixService extends BaseMatrixService {
|
|||
return;
|
||||
}
|
||||
|
||||
const response = this.formatEventList('📅 **Anstehende Termine:**', events);
|
||||
let response = this.formatEventList('📅 **Anstehende Termine:**', events);
|
||||
if (token) {
|
||||
response += '\n\n🔄 Synchronisiert';
|
||||
}
|
||||
await this.sendReply(roomId, event, response);
|
||||
}
|
||||
|
||||
private async handleCreateEvent(roomId: string, event: MatrixRoomEvent, userId: string, input: string) {
|
||||
private async handleCreateEvent(
|
||||
roomId: string,
|
||||
event: MatrixRoomEvent,
|
||||
userId: string,
|
||||
input: string
|
||||
) {
|
||||
if (!input.trim()) {
|
||||
await this.sendReply(
|
||||
roomId,
|
||||
|
|
@ -249,8 +350,10 @@ export class MatrixService extends BaseMatrixService {
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if user is logged in
|
||||
const token = await this.getToken(userId);
|
||||
|
||||
// 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) {
|
||||
|
|
@ -264,45 +367,87 @@ export class MatrixService extends BaseMatrixService {
|
|||
}
|
||||
}
|
||||
|
||||
const { title, startTime, endTime, isAllDay } = this.calendarService.parseEventInput(input);
|
||||
let calendarEvent: CalendarEvent;
|
||||
|
||||
if (!startTime || !endTime) {
|
||||
await this.sendReply(
|
||||
roomId,
|
||||
event,
|
||||
'❌ Konnte Datum/Uhrzeit nicht erkennen.\n\nBeispiele:\n• `!termin Meeting morgen um 14:00`\n• `!termin Arzt am 15.02. um 10:00`\n• `!termin Urlaub am 01.03. ganztägig`'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (token) {
|
||||
// Use API service
|
||||
const { title, startTime, endTime, isAllDay, location } =
|
||||
this.calendarApiService.parseEventInput(input);
|
||||
|
||||
if (!title) {
|
||||
await this.sendReply(roomId, event, '❌ Bitte gib einen Titel für den Termin an.');
|
||||
return;
|
||||
}
|
||||
|
||||
const calendarEvent = await this.calendarService.createEvent(
|
||||
userId,
|
||||
title,
|
||||
startTime,
|
||||
endTime,
|
||||
{
|
||||
isAllDay,
|
||||
if (!startTime || !endTime) {
|
||||
await this.sendReply(
|
||||
roomId,
|
||||
event,
|
||||
'❌ Konnte Datum/Uhrzeit nicht erkennen.\n\nBeispiele:\n• `!termin Meeting morgen um 14:00`\n• `!termin Arzt am 15.02. um 10:00`\n• `!termin Urlaub am 01.03. ganztägig`'
|
||||
);
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
if (!title) {
|
||||
await this.sendReply(roomId, event, '❌ Bitte gib einen Titel für den Termin an.');
|
||||
return;
|
||||
}
|
||||
|
||||
const apiEvent = await this.calendarApiService.createEvent(token, {
|
||||
title,
|
||||
startTime,
|
||||
endTime,
|
||||
isAllDay,
|
||||
location: location || undefined,
|
||||
});
|
||||
|
||||
if (!apiEvent) {
|
||||
await this.sendReply(
|
||||
roomId,
|
||||
event,
|
||||
'❌ Fehler beim Erstellen des Termins. Bitte versuche es erneut.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
calendarEvent = this.normalizeEvent(apiEvent);
|
||||
} else {
|
||||
// Use local storage
|
||||
const { title, startTime, endTime, isAllDay } = this.calendarService.parseEventInput(input);
|
||||
|
||||
if (!startTime || !endTime) {
|
||||
await this.sendReply(
|
||||
roomId,
|
||||
event,
|
||||
'❌ Konnte Datum/Uhrzeit nicht erkennen.\n\nBeispiele:\n• `!termin Meeting morgen um 14:00`\n• `!termin Arzt am 15.02. um 10:00`\n• `!termin Urlaub am 01.03. ganztägig`'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!title) {
|
||||
await this.sendReply(roomId, event, '❌ Bitte gib einen Titel für den Termin an.');
|
||||
return;
|
||||
}
|
||||
|
||||
calendarEvent = await this.calendarService.createEvent(userId, title, startTime, endTime, {
|
||||
isAllDay,
|
||||
});
|
||||
}
|
||||
|
||||
const timeStr = this.calendarService.formatEventTime(calendarEvent);
|
||||
let response = `✅ Termin erstellt: **${title}**\n📆 ${timeStr}`;
|
||||
let response = `✅ Termin erstellt: **${calendarEvent.title}**\n📆 ${timeStr}`;
|
||||
|
||||
// Show credit deduction if logged in
|
||||
// Show credit deduction and sync status if logged in
|
||||
if (token) {
|
||||
const balance = await this.creditService.getBalance(token);
|
||||
response += `\n⚡ -${EVENT_CREATE_CREDITS} Credits (${balance.balance.toFixed(2)} verbleibend)`;
|
||||
response += '\n🔄 Synchronisiert mit calendar-backend';
|
||||
}
|
||||
|
||||
await this.sendReply(roomId, event, response);
|
||||
}
|
||||
|
||||
private async handleEventDetails(roomId: string, event: MatrixRoomEvent, userId: string, args: string) {
|
||||
private async handleEventDetails(
|
||||
roomId: string,
|
||||
event: MatrixRoomEvent,
|
||||
userId: string,
|
||||
args: string
|
||||
) {
|
||||
const eventNumber = parseInt(args.trim());
|
||||
|
||||
if (isNaN(eventNumber) || eventNumber < 1) {
|
||||
|
|
@ -314,7 +459,19 @@ export class MatrixService extends BaseMatrixService {
|
|||
return;
|
||||
}
|
||||
|
||||
const calendarEvent = await this.calendarService.getEventByIndex(userId, eventNumber);
|
||||
const token = await this.getToken(userId);
|
||||
let calendarEvent: CalendarEvent | null = null;
|
||||
|
||||
if (token) {
|
||||
// Use API service - get event list first
|
||||
const apiEvents = await this.calendarApiService.getUpcomingEvents(token, 30);
|
||||
if (eventNumber > 0 && eventNumber <= apiEvents.length) {
|
||||
calendarEvent = this.normalizeEvent(apiEvents[eventNumber - 1]);
|
||||
}
|
||||
} else {
|
||||
// Use local storage
|
||||
calendarEvent = await this.calendarService.getEventByIndex(userId, eventNumber);
|
||||
}
|
||||
|
||||
if (!calendarEvent) {
|
||||
await this.sendReply(roomId, event, `❌ Termin #${eventNumber} nicht gefunden.`);
|
||||
|
|
@ -334,10 +491,19 @@ export class MatrixService extends BaseMatrixService {
|
|||
response += `\n📝 ${calendarEvent.description}`;
|
||||
}
|
||||
|
||||
if (token) {
|
||||
response += '\n\n🔄 Synchronisiert';
|
||||
}
|
||||
|
||||
await this.sendReply(roomId, event, response);
|
||||
}
|
||||
|
||||
private async handleDeleteEvent(roomId: string, event: MatrixRoomEvent, userId: string, args: string) {
|
||||
private async handleDeleteEvent(
|
||||
roomId: string,
|
||||
event: MatrixRoomEvent,
|
||||
userId: string,
|
||||
args: string
|
||||
) {
|
||||
const eventNumber = parseInt(args.trim());
|
||||
|
||||
if (isNaN(eventNumber) || eventNumber < 1) {
|
||||
|
|
@ -349,34 +515,80 @@ export class MatrixService extends BaseMatrixService {
|
|||
return;
|
||||
}
|
||||
|
||||
const deletedEvent = await this.calendarService.deleteEvent(userId, eventNumber);
|
||||
const token = await this.getToken(userId);
|
||||
let deletedEvent: CalendarEvent | null = null;
|
||||
|
||||
if (token) {
|
||||
// Use API service - get event list first to find event by index
|
||||
const apiEvents = await this.calendarApiService.getUpcomingEvents(token, 30);
|
||||
if (eventNumber > 0 && eventNumber <= apiEvents.length) {
|
||||
const targetEvent = apiEvents[eventNumber - 1];
|
||||
const success = await this.calendarApiService.deleteEvent(token, targetEvent.id);
|
||||
if (success) {
|
||||
deletedEvent = this.normalizeEvent(targetEvent);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use local storage
|
||||
deletedEvent = await this.calendarService.deleteEvent(userId, eventNumber);
|
||||
}
|
||||
|
||||
if (!deletedEvent) {
|
||||
await this.sendReply(roomId, event, `❌ Termin #${eventNumber} nicht gefunden.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sendReply(roomId, event, `🗑️ Gelöscht: ${deletedEvent.title}`);
|
||||
let response = `🗑️ Gelöscht: ${deletedEvent.title}`;
|
||||
if (token) {
|
||||
response += '\n\n🔄 Synchronisiert';
|
||||
}
|
||||
await this.sendReply(roomId, event, response);
|
||||
}
|
||||
|
||||
private async handleCalendars(roomId: string, event: MatrixRoomEvent, userId: string) {
|
||||
const calendars = await this.calendarService.getCalendars(userId);
|
||||
const token = await this.getToken(userId);
|
||||
let calendars: { name: string }[];
|
||||
|
||||
if (token) {
|
||||
// Use API service
|
||||
calendars = await this.calendarApiService.getCalendars(token);
|
||||
} else {
|
||||
// Use local storage
|
||||
calendars = await this.calendarService.getCalendars(userId);
|
||||
}
|
||||
|
||||
let response = '📁 **Deine Kalender:**\n\n';
|
||||
for (const calendar of calendars) {
|
||||
response += `• ${calendar.name}\n`;
|
||||
}
|
||||
|
||||
if (token) {
|
||||
response += '\n🔄 Synchronisiert';
|
||||
}
|
||||
|
||||
await this.sendReply(roomId, event, response);
|
||||
}
|
||||
|
||||
private async handleStatus(roomId: string, event: MatrixRoomEvent, userId: string) {
|
||||
const events = await this.calendarService.getUpcomingEvents(userId, 7);
|
||||
const todayEvents = await this.calendarService.getTodayEvents(userId);
|
||||
const token = await this.getToken(userId);
|
||||
const session = await this.sessionService.getSession(userId);
|
||||
|
||||
// Check login status and credits
|
||||
const token = this.sessionService.getToken(userId);
|
||||
const session = this.sessionService.getSession(userId);
|
||||
let todayEvents: CalendarEvent[];
|
||||
let events: CalendarEvent[];
|
||||
|
||||
if (token) {
|
||||
// Use API service
|
||||
const apiTodayEvents = await this.calendarApiService.getTodayEvents(token);
|
||||
const apiEvents = await this.calendarApiService.getUpcomingEvents(token, 7);
|
||||
todayEvents = apiTodayEvents.map((e) => this.normalizeEvent(e));
|
||||
events = apiEvents.map((e) => this.normalizeEvent(e));
|
||||
} else {
|
||||
// Use local storage
|
||||
todayEvents = await this.calendarService.getTodayEvents(userId);
|
||||
events = await this.calendarService.getUpcomingEvents(userId, 7);
|
||||
}
|
||||
|
||||
const syncStatus = token ? '🔄 Synchronisiert mit calendar-backend' : '💾 Lokaler Speicher';
|
||||
|
||||
let response = `📊 **Status**\n\n`;
|
||||
response += `• Termine heute: ${todayEvents.length}\n`;
|
||||
|
|
@ -388,9 +600,10 @@ export class MatrixService extends BaseMatrixService {
|
|||
response += `⚡ Credits: ${balance.balance.toFixed(2)}\n\n`;
|
||||
} else {
|
||||
response += `👤 Nicht angemeldet\n`;
|
||||
response += `💡 Login: \`!login email passwort\`\n\n`;
|
||||
response += `💡 Login: \`!login email passwort\` für Synchronisation mit calendar-web\n\n`;
|
||||
}
|
||||
|
||||
response += `${syncStatus}\n`;
|
||||
response += `Bot: ✅ Online`;
|
||||
|
||||
await this.sendReply(roomId, event, response);
|
||||
|
|
@ -415,7 +628,7 @@ export class MatrixService extends BaseMatrixService {
|
|||
return;
|
||||
}
|
||||
|
||||
const token = this.sessionService.getToken(userId);
|
||||
const token = await this.sessionService.getToken(userId);
|
||||
if (token) {
|
||||
const balance = await this.creditService.getBalance(token);
|
||||
await this.sendReply(
|
||||
|
|
@ -429,7 +642,7 @@ export class MatrixService extends BaseMatrixService {
|
|||
}
|
||||
|
||||
private async handleLogout(roomId: string, event: MatrixRoomEvent, userId: string) {
|
||||
const session = this.sessionService.getSession(userId);
|
||||
const session = await this.sessionService.getSession(userId);
|
||||
if (!session) {
|
||||
await this.sendReply(roomId, event, '❌ Du bist nicht angemeldet.');
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -47,9 +47,11 @@ export class MatrixService extends BaseMatrixService {
|
|||
|
||||
protected getConfig(): MatrixBotConfig {
|
||||
return {
|
||||
homeserverUrl: this.configService.get<string>('matrix.homeserverUrl') || 'http://localhost:8008',
|
||||
homeserverUrl:
|
||||
this.configService.get<string>('matrix.homeserverUrl') || 'http://localhost:8008',
|
||||
accessToken: this.configService.get<string>('matrix.accessToken') || '',
|
||||
storagePath: this.configService.get<string>('matrix.storagePath') || './data/bot-storage.json',
|
||||
storagePath:
|
||||
this.configService.get<string>('matrix.storagePath') || './data/bot-storage.json',
|
||||
allowedRooms: this.configService.get<string[]>('matrix.allowedRooms') || [],
|
||||
};
|
||||
}
|
||||
|
|
@ -207,8 +209,8 @@ export class MatrixService extends BaseMatrixService {
|
|||
}
|
||||
}
|
||||
|
||||
private requireAuth(sender: string): string {
|
||||
const token = this.sessionService.getToken(sender);
|
||||
private async requireAuth(sender: string): Promise<string> {
|
||||
const token = await this.sessionService.getToken(sender);
|
||||
if (!token) {
|
||||
throw new Error('Nicht angemeldet. Nutze <code>!login email passwort</code>');
|
||||
}
|
||||
|
|
@ -226,12 +228,18 @@ export class MatrixService extends BaseMatrixService {
|
|||
const result = await this.sessionService.login(sender, email, password);
|
||||
|
||||
if (result.success) {
|
||||
const token = this.sessionService.getToken(sender);
|
||||
const token = await 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>`);
|
||||
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>`);
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`<p>✅ Erfolgreich angemeldet als <strong>${email}</strong></p>`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await this.sendMessage(roomId, `<p>❌ Login fehlgeschlagen: ${result.error}</p>`);
|
||||
|
|
@ -240,10 +248,10 @@ export class MatrixService extends BaseMatrixService {
|
|||
|
||||
private async handleStatus(roomId: string, sender: string) {
|
||||
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);
|
||||
const loggedIn = await this.sessionService.isLoggedIn(sender);
|
||||
const sessions = await this.sessionService.getSessionCount();
|
||||
const session = await this.sessionService.getSession(sender);
|
||||
const token = await this.sessionService.getToken(sender);
|
||||
|
||||
let statusHtml = `<h3>Questions Bot Status</h3><ul>`;
|
||||
statusHtml += `<li>Backend: ${backendOk ? '✅ Online' : '❌ Offline'}</li>`;
|
||||
|
|
@ -264,7 +272,7 @@ export class MatrixService extends BaseMatrixService {
|
|||
|
||||
// Question handlers
|
||||
private async handleListQuestions(roomId: string, sender: string, statusFilter?: string) {
|
||||
const token = this.requireAuth(sender);
|
||||
const token = await this.requireAuth(sender);
|
||||
|
||||
const options: Record<string, string> = {};
|
||||
if (statusFilter) {
|
||||
|
|
@ -306,13 +314,14 @@ export class MatrixService extends BaseMatrixService {
|
|||
html += `<li>${status} ${priority}<strong>${q.title}</strong></li>`;
|
||||
}
|
||||
html += '</ol>';
|
||||
html += '<p><em>Nutze <code>!frage [nr]</code> fuer Details oder <code>!recherche [nr]</code></em></p>';
|
||||
html +=
|
||||
'<p><em>Nutze <code>!frage [nr]</code> fuer Details oder <code>!recherche [nr]</code></em></p>';
|
||||
|
||||
await this.sendMessage(roomId, html);
|
||||
}
|
||||
|
||||
private async handleQuestionDetails(roomId: string, sender: string, numberStr: string) {
|
||||
const token = this.requireAuth(sender);
|
||||
const token = await this.requireAuth(sender);
|
||||
const question = this.getQuestionByNumber(sender, numberStr);
|
||||
|
||||
if (!question) {
|
||||
|
|
@ -339,7 +348,8 @@ export class MatrixService extends BaseMatrixService {
|
|||
if (q.tags?.length) html += `<li>Tags: ${q.tags.join(', ')}</li>`;
|
||||
if (q.category) html += `<li>Kategorie: ${q.category}</li>`;
|
||||
html += `<li>Erstellt: ${new Date(q.createdAt).toLocaleDateString('de-DE')}</li>`;
|
||||
if (q.answeredAt) html += `<li>Beantwortet: ${new Date(q.answeredAt).toLocaleDateString('de-DE')}</li>`;
|
||||
if (q.answeredAt)
|
||||
html += `<li>Beantwortet: ${new Date(q.answeredAt).toLocaleDateString('de-DE')}</li>`;
|
||||
html += '</ul>';
|
||||
|
||||
html += `<p><em>Nutze <code>!recherche ${numberStr}</code> um eine Recherche zu starten</em></p>`;
|
||||
|
|
@ -353,7 +363,7 @@ export class MatrixService extends BaseMatrixService {
|
|||
return;
|
||||
}
|
||||
|
||||
const token = this.requireAuth(sender);
|
||||
const token = await this.requireAuth(sender);
|
||||
const result = await this.questionsService.createQuestion(token, title);
|
||||
|
||||
if (result.error) {
|
||||
|
|
@ -370,7 +380,7 @@ export class MatrixService extends BaseMatrixService {
|
|||
}
|
||||
|
||||
private async handleDeleteQuestion(roomId: string, sender: string, numberStr: string) {
|
||||
const token = this.requireAuth(sender);
|
||||
const token = await this.requireAuth(sender);
|
||||
const question = this.getQuestionByNumber(sender, numberStr);
|
||||
|
||||
if (!question) {
|
||||
|
|
@ -390,7 +400,7 @@ export class MatrixService extends BaseMatrixService {
|
|||
}
|
||||
|
||||
private async handleArchiveQuestion(roomId: string, sender: string, numberStr: string) {
|
||||
const token = this.requireAuth(sender);
|
||||
const token = await this.requireAuth(sender);
|
||||
const question = this.getQuestionByNumber(sender, numberStr);
|
||||
|
||||
if (!question) {
|
||||
|
|
@ -409,8 +419,13 @@ export class MatrixService extends BaseMatrixService {
|
|||
}
|
||||
|
||||
// Research handlers
|
||||
private async handleStartResearch(roomId: string, sender: string, numberStr: string, depthStr?: string) {
|
||||
const token = this.requireAuth(sender);
|
||||
private async handleStartResearch(
|
||||
roomId: string,
|
||||
sender: string,
|
||||
numberStr: string,
|
||||
depthStr?: string
|
||||
) {
|
||||
const token = await this.requireAuth(sender);
|
||||
const question = this.getQuestionByNumber(sender, numberStr);
|
||||
|
||||
if (!question) {
|
||||
|
|
@ -428,7 +443,10 @@ export class MatrixService extends BaseMatrixService {
|
|||
};
|
||||
const depth = depthMap[depthStr?.toLowerCase() || ''] || 'quick';
|
||||
|
||||
await this.sendMessage(roomId, `<p>Starte ${depth}-Recherche fuer: <strong>${question.title}</strong>...</p>`);
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`<p>Starte ${depth}-Recherche fuer: <strong>${question.title}</strong>...</p>`
|
||||
);
|
||||
|
||||
const result = await this.questionsService.startResearch(token, question.id, depth);
|
||||
|
||||
|
|
@ -466,7 +484,7 @@ export class MatrixService extends BaseMatrixService {
|
|||
}
|
||||
|
||||
private async handleResearchResult(roomId: string, sender: string, numberStr: string) {
|
||||
const token = this.requireAuth(sender);
|
||||
const token = await this.requireAuth(sender);
|
||||
const question = this.getQuestionByNumber(sender, numberStr);
|
||||
|
||||
if (!question) {
|
||||
|
|
@ -511,7 +529,7 @@ export class MatrixService extends BaseMatrixService {
|
|||
}
|
||||
|
||||
private async handleSources(roomId: string, sender: string, numberStr: string) {
|
||||
const token = this.requireAuth(sender);
|
||||
const token = await this.requireAuth(sender);
|
||||
const question = this.getQuestionByNumber(sender, numberStr);
|
||||
|
||||
if (!question) {
|
||||
|
|
@ -535,7 +553,9 @@ export class MatrixService extends BaseMatrixService {
|
|||
|
||||
let html = `<h3>Quellen fuer: ${question.title}</h3><ol>`;
|
||||
for (const source of sources.slice(0, 10)) {
|
||||
const relevance = source.relevanceScore ? ` (${Math.round(source.relevanceScore * 100)}%)` : '';
|
||||
const relevance = source.relevanceScore
|
||||
? ` (${Math.round(source.relevanceScore * 100)}%)`
|
||||
: '';
|
||||
html += `<li><a href="${source.url}">${source.title}</a>${relevance}<br/><em>${source.domain}</em></li>`;
|
||||
}
|
||||
html += '</ol>';
|
||||
|
|
@ -549,7 +569,7 @@ export class MatrixService extends BaseMatrixService {
|
|||
|
||||
// Answer handlers
|
||||
private async handleAnswer(roomId: string, sender: string, numberStr: string) {
|
||||
const token = this.requireAuth(sender);
|
||||
const token = await this.requireAuth(sender);
|
||||
const question = this.getQuestionByNumber(sender, numberStr);
|
||||
|
||||
if (!question) {
|
||||
|
|
@ -579,7 +599,9 @@ export class MatrixService extends BaseMatrixService {
|
|||
const answer = answers[0];
|
||||
const accepted = answer.isAccepted ? ' ✅' : '';
|
||||
const rating = answer.rating ? ` (${answer.rating}/5 Sterne)` : '';
|
||||
const confidence = answer.confidence ? ` [${Math.round(answer.confidence * 100)}% Konfidenz]` : '';
|
||||
const confidence = answer.confidence
|
||||
? ` [${Math.round(answer.confidence * 100)}% Konfidenz]`
|
||||
: '';
|
||||
|
||||
let html = `<h3>Antwort${accepted}${rating}</h3>`;
|
||||
html += `<p><em>Model: ${answer.modelId}${confidence}</em></p>`;
|
||||
|
|
@ -599,11 +621,19 @@ export class MatrixService extends BaseMatrixService {
|
|||
await this.sendMessage(roomId, html);
|
||||
}
|
||||
|
||||
private async handleRateAnswer(roomId: string, sender: string, numberStr: string, ratingStr: string) {
|
||||
const token = this.requireAuth(sender);
|
||||
private async handleRateAnswer(
|
||||
roomId: string,
|
||||
sender: string,
|
||||
numberStr: string,
|
||||
ratingStr: string
|
||||
) {
|
||||
const token = await this.requireAuth(sender);
|
||||
|
||||
if (!this.answersMapper.hasList(sender)) {
|
||||
await this.sendMessage(roomId, '<p>Zeige zuerst eine Antwort mit <code>!antwort [nr]</code></p>');
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
'<p>Zeige zuerst eine Antwort mit <code>!antwort [nr]</code></p>'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -630,10 +660,13 @@ export class MatrixService extends BaseMatrixService {
|
|||
}
|
||||
|
||||
private async handleAcceptAnswer(roomId: string, sender: string, numberStr: string) {
|
||||
const token = this.requireAuth(sender);
|
||||
const token = await this.requireAuth(sender);
|
||||
|
||||
if (!this.answersMapper.hasList(sender)) {
|
||||
await this.sendMessage(roomId, '<p>Zeige zuerst eine Antwort mit <code>!antwort [nr]</code></p>');
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
'<p>Zeige zuerst eine Antwort mit <code>!antwort [nr]</code></p>'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -655,7 +688,7 @@ export class MatrixService extends BaseMatrixService {
|
|||
|
||||
// Collection handlers
|
||||
private async handleListCollections(roomId: string, sender: string) {
|
||||
const token = this.requireAuth(sender);
|
||||
const token = await this.requireAuth(sender);
|
||||
const result = await this.questionsService.getCollections(token);
|
||||
|
||||
if (result.error) {
|
||||
|
|
@ -691,7 +724,7 @@ export class MatrixService extends BaseMatrixService {
|
|||
return;
|
||||
}
|
||||
|
||||
const token = this.requireAuth(sender);
|
||||
const token = await this.requireAuth(sender);
|
||||
const result = await this.questionsService.createCollection(token, name);
|
||||
|
||||
if (result.error) {
|
||||
|
|
@ -700,7 +733,10 @@ export class MatrixService extends BaseMatrixService {
|
|||
}
|
||||
|
||||
this.collectionsMapper.clearList(sender);
|
||||
await this.sendMessage(roomId, `<p>Sammlung <strong>${result.data!.name}</strong> erstellt.</p>`);
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`<p>Sammlung <strong>${result.data!.name}</strong> erstellt.</p>`
|
||||
);
|
||||
}
|
||||
|
||||
// Search handler
|
||||
|
|
@ -710,7 +746,7 @@ export class MatrixService extends BaseMatrixService {
|
|||
return;
|
||||
}
|
||||
|
||||
const token = this.requireAuth(sender);
|
||||
const token = await this.requireAuth(sender);
|
||||
const result = await this.questionsService.getQuestions(token, { search: query });
|
||||
|
||||
if (result.error) {
|
||||
|
|
@ -745,10 +781,10 @@ export class MatrixService extends BaseMatrixService {
|
|||
|
||||
private getStatusEmoji(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
open: '❓', // Question mark
|
||||
open: '❓', // Question mark
|
||||
researching: '🔍', // Magnifying glass
|
||||
answered: '✅', // Check mark
|
||||
archived: '📦', // Package
|
||||
answered: '✅', // Check mark
|
||||
archived: '📦', // Package
|
||||
};
|
||||
return map[status] || '❓';
|
||||
}
|
||||
|
|
@ -765,8 +801,8 @@ export class MatrixService extends BaseMatrixService {
|
|||
|
||||
private getPriorityIndicator(priority: string): string {
|
||||
const map: Record<string, string> = {
|
||||
urgent: '🔴 ', // Red circle
|
||||
high: '🟠 ', // Orange circle
|
||||
urgent: '🔴 ', // Red circle
|
||||
high: '🟠 ', // Orange circle
|
||||
normal: '',
|
||||
low: '',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -206,11 +206,13 @@ Daten von Umami Analytics (self-hosted).`;
|
|||
const result = await this.sessionService.login(sender, email, password);
|
||||
|
||||
if (result.success) {
|
||||
const token = this.sessionService.getToken(sender);
|
||||
const token = await 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)}`);
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`✅ Erfolgreich angemeldet als **${email}**\n⚡ Credits: ${balance.balance.toFixed(2)}`
|
||||
);
|
||||
} else {
|
||||
await this.sendMessage(roomId, `✅ Erfolgreich angemeldet als **${email}**`);
|
||||
}
|
||||
|
|
@ -220,14 +222,14 @@ Daten von Umami Analytics (self-hosted).`;
|
|||
}
|
||||
|
||||
private async handleLogout(roomId: string, sender: string) {
|
||||
this.sessionService.logout(sender);
|
||||
await 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);
|
||||
const loggedIn = await this.sessionService.isLoggedIn(sender);
|
||||
const session = await this.sessionService.getSession(sender);
|
||||
const token = await this.sessionService.getToken(sender);
|
||||
|
||||
let response = '**📊 Stats Bot Status**\n\n';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,33 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { MatrixService } from './matrix.service';
|
||||
import { TodoModule } from '../todo/todo.module';
|
||||
import { TranscriptionModule, SessionModule, CreditModule } from '@manacore/bot-services';
|
||||
import {
|
||||
TranscriptionModule,
|
||||
SessionModule,
|
||||
CreditModule,
|
||||
TodoApiService,
|
||||
} from '@manacore/bot-services';
|
||||
|
||||
// Factory provider for TodoApiService
|
||||
const todoApiServiceProvider = {
|
||||
provide: TodoApiService,
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const baseUrl = configService.get<string>('TODO_BACKEND_URL', 'http://localhost:3018');
|
||||
return new TodoApiService(baseUrl);
|
||||
},
|
||||
inject: [ConfigService],
|
||||
};
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule,
|
||||
TodoModule,
|
||||
TranscriptionModule.forRoot(),
|
||||
SessionModule.forRoot(),
|
||||
SessionModule.forRoot({ storageMode: 'redis' }),
|
||||
CreditModule.forRoot(),
|
||||
],
|
||||
providers: [MatrixService],
|
||||
providers: [MatrixService, todoApiServiceProvider],
|
||||
exports: [MatrixService],
|
||||
})
|
||||
export class BotModule {}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,13 @@ import {
|
|||
COMMON_KEYWORDS,
|
||||
} from '@manacore/matrix-bot-common';
|
||||
import { TodoService, Task } from '../todo/todo.service';
|
||||
import { TranscriptionService, SessionService, CreditService } from '@manacore/bot-services';
|
||||
import {
|
||||
TranscriptionService,
|
||||
SessionService,
|
||||
CreditService,
|
||||
TodoApiService,
|
||||
Task as ApiTask,
|
||||
} from '@manacore/bot-services';
|
||||
import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration';
|
||||
|
||||
// Credit cost for task creation (micro-credits)
|
||||
|
|
@ -20,7 +26,10 @@ export class MatrixService extends BaseMatrixService {
|
|||
[
|
||||
...COMMON_KEYWORDS,
|
||||
{ keywords: ['was kannst du'], command: 'help' },
|
||||
{ keywords: ['zeige aufgaben', 'meine aufgaben', 'was muss ich', 'show tasks', 'list'], command: 'list' },
|
||||
{
|
||||
keywords: ['zeige aufgaben', 'meine aufgaben', 'was muss ich', 'show tasks', 'list'],
|
||||
command: 'list',
|
||||
},
|
||||
{ keywords: ['heute', 'today', 'was steht an'], command: 'today' },
|
||||
{ keywords: ['inbox', 'eingang', 'ohne datum'], command: 'inbox' },
|
||||
{ keywords: ['projekte', 'projects'], command: 'projects' },
|
||||
|
|
@ -32,6 +41,7 @@ export class MatrixService extends BaseMatrixService {
|
|||
constructor(
|
||||
configService: ConfigService,
|
||||
private todoService: TodoService,
|
||||
private todoApiService: TodoApiService,
|
||||
private transcriptionService: TranscriptionService,
|
||||
private sessionService: SessionService,
|
||||
private creditService: CreditService
|
||||
|
|
@ -39,11 +49,38 @@ export class MatrixService extends BaseMatrixService {
|
|||
super(configService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is logged in and has a valid token for API access
|
||||
*/
|
||||
private async getToken(userId: string): Promise<string | null> {
|
||||
return this.sessionService.getToken(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize task from API or local format to common format
|
||||
*/
|
||||
private normalizeTask(task: Task | ApiTask): Task {
|
||||
return {
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
completed: task.completed,
|
||||
priority: task.priority,
|
||||
dueDate: task.dueDate,
|
||||
project: task.project,
|
||||
labels: task.labels || [],
|
||||
createdAt: task.createdAt,
|
||||
completedAt: task.completedAt || null,
|
||||
userId: task.userId,
|
||||
};
|
||||
}
|
||||
|
||||
protected getConfig(): MatrixBotConfig {
|
||||
return {
|
||||
homeserverUrl: this.configService.get<string>('matrix.homeserverUrl') || 'http://localhost:8008',
|
||||
homeserverUrl:
|
||||
this.configService.get<string>('matrix.homeserverUrl') || 'http://localhost:8008',
|
||||
accessToken: this.configService.get<string>('matrix.accessToken') || '',
|
||||
storagePath: this.configService.get<string>('matrix.storagePath') || './data/bot-storage.json',
|
||||
storagePath:
|
||||
this.configService.get<string>('matrix.storagePath') || './data/bot-storage.json',
|
||||
allowedRooms: this.configService.get<string[]>('matrix.allowedRooms') || [],
|
||||
};
|
||||
}
|
||||
|
|
@ -74,11 +111,7 @@ export class MatrixService extends BaseMatrixService {
|
|||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error handling message: ${error}`);
|
||||
await this.sendReply(
|
||||
roomId,
|
||||
event,
|
||||
'Ein Fehler ist aufgetreten. Bitte versuche es erneut.'
|
||||
);
|
||||
await this.sendReply(roomId, event, 'Ein Fehler ist aufgetreten. Bitte versuche es erneut.');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -118,8 +151,10 @@ export class MatrixService extends BaseMatrixService {
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if user is logged in
|
||||
const token = await this.getToken(sender);
|
||||
|
||||
// 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) {
|
||||
|
|
@ -128,36 +163,59 @@ export class MatrixService extends BaseMatrixService {
|
|||
validation.availableCredits,
|
||||
'Aufgabe erstellen'
|
||||
);
|
||||
await this.sendReply(roomId, event, `Transkription: "${transcription}"\n\n${errorMsg.text}`);
|
||||
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);
|
||||
let task: Task;
|
||||
|
||||
// Create the task
|
||||
const task = await this.todoService.createTask(sender, title, {
|
||||
priority,
|
||||
dueDate,
|
||||
project,
|
||||
});
|
||||
if (token) {
|
||||
// Use API service (syncs with todo-web and mobile)
|
||||
const { title, priority, dueDate, project } =
|
||||
this.todoApiService.parseTaskInput(transcription);
|
||||
const apiTask = await this.todoApiService.createTask(token, { title, priority, dueDate });
|
||||
if (!apiTask) {
|
||||
await this.sendReply(
|
||||
roomId,
|
||||
event,
|
||||
`Transkription: "${transcription}"\n\nFehler beim Erstellen der Aufgabe.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
task = this.normalizeTask(apiTask);
|
||||
task.project = project;
|
||||
} else {
|
||||
// Use local storage (offline mode)
|
||||
const { title, priority, dueDate, project } =
|
||||
this.todoService.parseTaskInput(transcription);
|
||||
task = await this.todoService.createTask(sender, title, {
|
||||
priority,
|
||||
dueDate,
|
||||
project,
|
||||
});
|
||||
}
|
||||
|
||||
let responseText = `Transkription: "${transcription}"\n\nAufgabe erstellt: **${task.title}**`;
|
||||
|
||||
const details: string[] = [];
|
||||
if (priority < 4) details.push(`Prioritat ${priority}`);
|
||||
if (dueDate) details.push(`Datum: ${this.formatDate(dueDate)}`);
|
||||
if (project) details.push(`Projekt: ${project}`);
|
||||
if (task.priority < 4) details.push(`Prioritat ${task.priority}`);
|
||||
if (task.dueDate) details.push(`Datum: ${this.formatDate(task.dueDate)}`);
|
||||
if (task.project) details.push(`Projekt: ${task.project}`);
|
||||
|
||||
if (details.length > 0) {
|
||||
responseText += `\n${details.join(' | ')}`;
|
||||
}
|
||||
|
||||
// Show credit deduction if logged in
|
||||
// Show credit deduction and sync status if logged in
|
||||
if (token) {
|
||||
const balance = await this.creditService.getBalance(token);
|
||||
responseText += `\n\n⚡ -${TASK_CREATE_CREDITS} Credits (${balance.balance.toFixed(2)} verbleibend)`;
|
||||
responseText += '\n🔄 Synchronisiert mit todo-backend';
|
||||
}
|
||||
|
||||
await this.sendReply(roomId, event, responseText);
|
||||
|
|
@ -247,7 +305,12 @@ export class MatrixService extends BaseMatrixService {
|
|||
}
|
||||
}
|
||||
|
||||
private async handleAddTask(roomId: string, event: MatrixRoomEvent, userId: string, input: string) {
|
||||
private async handleAddTask(
|
||||
roomId: string,
|
||||
event: MatrixRoomEvent,
|
||||
userId: string,
|
||||
input: string
|
||||
) {
|
||||
if (!input.trim()) {
|
||||
await this.sendReply(
|
||||
roomId,
|
||||
|
|
@ -257,8 +320,10 @@ export class MatrixService extends BaseMatrixService {
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if user is logged in
|
||||
const token = await this.getToken(userId);
|
||||
|
||||
// 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) {
|
||||
|
|
@ -272,36 +337,65 @@ export class MatrixService extends BaseMatrixService {
|
|||
}
|
||||
}
|
||||
|
||||
const { title, priority, dueDate, project } = this.todoService.parseTaskInput(input);
|
||||
let task: Task;
|
||||
|
||||
const task = await this.todoService.createTask(userId, title, {
|
||||
priority,
|
||||
dueDate,
|
||||
project,
|
||||
});
|
||||
if (token) {
|
||||
// Use API service (syncs with todo-web and mobile)
|
||||
const { title, priority, dueDate, project } = this.todoApiService.parseTaskInput(input);
|
||||
const apiTask = await this.todoApiService.createTask(token, { title, priority, dueDate });
|
||||
if (!apiTask) {
|
||||
await this.sendReply(
|
||||
roomId,
|
||||
event,
|
||||
'Fehler beim Erstellen der Aufgabe. Bitte versuche es erneut.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
task = this.normalizeTask(apiTask);
|
||||
task.project = project; // Note: project handling via API needs project ID lookup
|
||||
} else {
|
||||
// Use local storage (offline mode)
|
||||
const { title, priority, dueDate, project } = this.todoService.parseTaskInput(input);
|
||||
task = await this.todoService.createTask(userId, title, {
|
||||
priority,
|
||||
dueDate,
|
||||
project,
|
||||
});
|
||||
}
|
||||
|
||||
let response = `Aufgabe erstellt: **${task.title}**`;
|
||||
|
||||
const details: string[] = [];
|
||||
if (priority < 4) details.push(`Prioritaet ${priority}`);
|
||||
if (dueDate) details.push(`Datum: ${this.formatDate(dueDate)}`);
|
||||
if (project) details.push(`Projekt: ${project}`);
|
||||
if (task.priority < 4) details.push(`Prioritaet ${task.priority}`);
|
||||
if (task.dueDate) details.push(`Datum: ${this.formatDate(task.dueDate)}`);
|
||||
if (task.project) details.push(`Projekt: ${task.project}`);
|
||||
|
||||
if (details.length > 0) {
|
||||
response += `\n${details.join(' | ')}`;
|
||||
}
|
||||
|
||||
// Show credit deduction if logged in
|
||||
// Show credit deduction and sync status if logged in
|
||||
if (token) {
|
||||
const balance = await this.creditService.getBalance(token);
|
||||
response += `\n\n⚡ -${TASK_CREATE_CREDITS} Credits (${balance.balance.toFixed(2)} verbleibend)`;
|
||||
response += '\n🔄 Synchronisiert mit todo-backend';
|
||||
}
|
||||
|
||||
await this.sendReply(roomId, event, response);
|
||||
}
|
||||
|
||||
private async handleListTasks(roomId: string, event: MatrixRoomEvent, userId: string) {
|
||||
const tasks = await this.todoService.getAllPendingTasks(userId);
|
||||
const token = await this.getToken(userId);
|
||||
let tasks: Task[];
|
||||
|
||||
if (token) {
|
||||
// Use API service
|
||||
const apiTasks = await this.todoApiService.getTasks(token, { completed: false });
|
||||
tasks = apiTasks.map((t) => this.normalizeTask(t));
|
||||
} else {
|
||||
// Use local storage
|
||||
tasks = await this.todoService.getAllPendingTasks(userId);
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
await this.sendReply(
|
||||
|
|
@ -312,12 +406,25 @@ export class MatrixService extends BaseMatrixService {
|
|||
return;
|
||||
}
|
||||
|
||||
const response = this.formatTaskList('**Alle offenen Aufgaben:**', tasks);
|
||||
let response = this.formatTaskList('**Alle offenen Aufgaben:**', tasks);
|
||||
if (token) {
|
||||
response += '\n\n🔄 Synchronisiert';
|
||||
}
|
||||
await this.sendReply(roomId, event, response);
|
||||
}
|
||||
|
||||
private async handleTodayTasks(roomId: string, event: MatrixRoomEvent, userId: string) {
|
||||
const tasks = await this.todoService.getTodayTasks(userId);
|
||||
const token = await this.getToken(userId);
|
||||
let tasks: Task[];
|
||||
|
||||
if (token) {
|
||||
// Use API service
|
||||
const apiTasks = await this.todoApiService.getTodayTasks(token);
|
||||
tasks = apiTasks.map((t) => this.normalizeTask(t));
|
||||
} else {
|
||||
// Use local storage
|
||||
tasks = await this.todoService.getTodayTasks(userId);
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
await this.sendReply(
|
||||
|
|
@ -328,23 +435,44 @@ export class MatrixService extends BaseMatrixService {
|
|||
return;
|
||||
}
|
||||
|
||||
const response = this.formatTaskList('**Aufgaben fuer heute:**', tasks);
|
||||
let response = this.formatTaskList('**Aufgaben fuer heute:**', tasks);
|
||||
if (token) {
|
||||
response += '\n\n🔄 Synchronisiert';
|
||||
}
|
||||
await this.sendReply(roomId, event, response);
|
||||
}
|
||||
|
||||
private async handleInboxTasks(roomId: string, event: MatrixRoomEvent, userId: string) {
|
||||
const tasks = await this.todoService.getInboxTasks(userId);
|
||||
const token = await this.getToken(userId);
|
||||
let tasks: Task[];
|
||||
|
||||
if (token) {
|
||||
// Use API service
|
||||
const apiTasks = await this.todoApiService.getInboxTasks(token);
|
||||
tasks = apiTasks.map((t) => this.normalizeTask(t));
|
||||
} else {
|
||||
// Use local storage
|
||||
tasks = await this.todoService.getInboxTasks(userId);
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
await this.sendReply(roomId, event, 'Inbox ist leer.\n\nAufgaben ohne Datum landen hier.');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = this.formatTaskList('**Inbox (ohne Datum):**', tasks);
|
||||
let response = this.formatTaskList('**Inbox (ohne Datum):**', tasks);
|
||||
if (token) {
|
||||
response += '\n\n🔄 Synchronisiert';
|
||||
}
|
||||
await this.sendReply(roomId, event, response);
|
||||
}
|
||||
|
||||
private async handleCompleteTask(roomId: string, event: MatrixRoomEvent, userId: string, args: string) {
|
||||
private async handleCompleteTask(
|
||||
roomId: string,
|
||||
event: MatrixRoomEvent,
|
||||
userId: string,
|
||||
args: string
|
||||
) {
|
||||
const taskNumber = parseInt(args.trim());
|
||||
|
||||
if (isNaN(taskNumber) || taskNumber < 1) {
|
||||
|
|
@ -356,17 +484,42 @@ export class MatrixService extends BaseMatrixService {
|
|||
return;
|
||||
}
|
||||
|
||||
const task = await this.todoService.completeTask(userId, taskNumber);
|
||||
const token = await this.getToken(userId);
|
||||
let task: Task | null = null;
|
||||
|
||||
if (token) {
|
||||
// Use API service - need to get task list first to find task by index
|
||||
const apiTasks = await this.todoApiService.getTasks(token, { completed: false });
|
||||
if (taskNumber > 0 && taskNumber <= apiTasks.length) {
|
||||
const targetTask = apiTasks[taskNumber - 1];
|
||||
const completedTask = await this.todoApiService.completeTask(token, targetTask.id);
|
||||
if (completedTask) {
|
||||
task = this.normalizeTask(completedTask);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use local storage
|
||||
task = await this.todoService.completeTask(userId, taskNumber);
|
||||
}
|
||||
|
||||
if (!task) {
|
||||
await this.sendReply(roomId, event, `Aufgabe #${taskNumber} nicht gefunden.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sendReply(roomId, event, `Erledigt: ~~${task.title}~~`);
|
||||
let response = `Erledigt: ~~${task.title}~~`;
|
||||
if (token) {
|
||||
response += '\n\n🔄 Synchronisiert';
|
||||
}
|
||||
await this.sendReply(roomId, event, response);
|
||||
}
|
||||
|
||||
private async handleDeleteTask(roomId: string, event: MatrixRoomEvent, userId: string, args: string) {
|
||||
private async handleDeleteTask(
|
||||
roomId: string,
|
||||
event: MatrixRoomEvent,
|
||||
userId: string,
|
||||
args: string
|
||||
) {
|
||||
const taskNumber = parseInt(args.trim());
|
||||
|
||||
if (isNaN(taskNumber) || taskNumber < 1) {
|
||||
|
|
@ -378,18 +531,48 @@ export class MatrixService extends BaseMatrixService {
|
|||
return;
|
||||
}
|
||||
|
||||
const task = await this.todoService.deleteTask(userId, taskNumber);
|
||||
const token = await this.getToken(userId);
|
||||
let task: Task | null = null;
|
||||
|
||||
if (token) {
|
||||
// Use API service - need to get task list first to find task by index
|
||||
const apiTasks = await this.todoApiService.getTasks(token, { completed: false });
|
||||
if (taskNumber > 0 && taskNumber <= apiTasks.length) {
|
||||
const targetTask = apiTasks[taskNumber - 1];
|
||||
const deleted = await this.todoApiService.deleteTask(token, targetTask.id);
|
||||
if (deleted) {
|
||||
task = this.normalizeTask(targetTask);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use local storage
|
||||
task = await this.todoService.deleteTask(userId, taskNumber);
|
||||
}
|
||||
|
||||
if (!task) {
|
||||
await this.sendReply(roomId, event, `Aufgabe #${taskNumber} nicht gefunden.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sendReply(roomId, event, `Geloescht: ${task.title}`);
|
||||
let response = `Geloescht: ${task.title}`;
|
||||
if (token) {
|
||||
response += '\n\n🔄 Synchronisiert';
|
||||
}
|
||||
await this.sendReply(roomId, event, response);
|
||||
}
|
||||
|
||||
private async handleProjects(roomId: string, event: MatrixRoomEvent, userId: string) {
|
||||
const projects = await this.todoService.getProjects(userId);
|
||||
const token = await this.getToken(userId);
|
||||
let projects: { name: string }[];
|
||||
|
||||
if (token) {
|
||||
// Use API service
|
||||
const apiProjects = await this.todoApiService.getProjects(token);
|
||||
projects = apiProjects;
|
||||
} else {
|
||||
// Use local storage
|
||||
projects = await this.todoService.getProjects(userId);
|
||||
}
|
||||
|
||||
if (projects.length === 0) {
|
||||
await this.sendReply(
|
||||
|
|
@ -405,11 +588,19 @@ export class MatrixService extends BaseMatrixService {
|
|||
response += `- ${project.name}\n`;
|
||||
}
|
||||
response += '\nZeige Projektaufgaben mit `!project [Name]`';
|
||||
if (token) {
|
||||
response += '\n\n🔄 Synchronisiert';
|
||||
}
|
||||
|
||||
await this.sendReply(roomId, event, response);
|
||||
}
|
||||
|
||||
private async handleProjectTasks(roomId: string, event: MatrixRoomEvent, userId: string, args: string) {
|
||||
private async handleProjectTasks(
|
||||
roomId: string,
|
||||
event: MatrixRoomEvent,
|
||||
userId: string,
|
||||
args: string
|
||||
) {
|
||||
const projectName = args.trim();
|
||||
|
||||
if (!projectName) {
|
||||
|
|
@ -421,22 +612,50 @@ export class MatrixService extends BaseMatrixService {
|
|||
return;
|
||||
}
|
||||
|
||||
const tasks = await this.todoService.getProjectTasks(userId, projectName);
|
||||
const token = await this.getToken(userId);
|
||||
let tasks: Task[];
|
||||
|
||||
if (token) {
|
||||
// Use API service - need to find project ID first
|
||||
const projects = await this.todoApiService.getProjects(token);
|
||||
const project = projects.find((p) => p.name.toLowerCase() === projectName.toLowerCase());
|
||||
if (project) {
|
||||
const apiTasks = await this.todoApiService.getProjectTasks(token, project.id);
|
||||
tasks = apiTasks.map((t) => this.normalizeTask(t));
|
||||
} else {
|
||||
tasks = [];
|
||||
}
|
||||
} else {
|
||||
// Use local storage
|
||||
tasks = await this.todoService.getProjectTasks(userId, projectName);
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
await this.sendReply(roomId, event, `Keine Aufgaben im Projekt "${projectName}".`);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = this.formatTaskList(`**Projekt: ${projectName}**`, tasks);
|
||||
let response = this.formatTaskList(`**Projekt: ${projectName}**`, tasks);
|
||||
if (token) {
|
||||
response += '\n\n🔄 Synchronisiert';
|
||||
}
|
||||
await this.sendReply(roomId, event, response);
|
||||
}
|
||||
|
||||
private async handleStatus(roomId: string, event: MatrixRoomEvent, userId: string) {
|
||||
const stats = await this.todoService.getStats(userId);
|
||||
const isLoggedIn = this.sessionService.isLoggedIn(userId);
|
||||
const token = await this.getToken(userId);
|
||||
const isLoggedIn = await this.sessionService.isLoggedIn(userId);
|
||||
const email = this.sessionService.getEmail(userId);
|
||||
const token = this.sessionService.getToken(userId);
|
||||
|
||||
let stats: { total: number; completed: number; pending: number; today: number };
|
||||
|
||||
if (token) {
|
||||
// Use API service
|
||||
stats = await this.todoApiService.getStats(token);
|
||||
} else {
|
||||
// Use local storage
|
||||
stats = await this.todoService.getStats(userId);
|
||||
}
|
||||
|
||||
// Get credit balance if logged in
|
||||
let creditInfo = '';
|
||||
|
|
@ -452,6 +671,8 @@ export class MatrixService extends BaseMatrixService {
|
|||
}
|
||||
}
|
||||
|
||||
const syncStatus = token ? '🔄 Synchronisiert mit todo-backend' : '💾 Lokaler Speicher';
|
||||
|
||||
const response = `**Status**
|
||||
|
||||
👤 Angemeldet: ${isLoggedIn ? `Ja (${email})` : 'Nein'}${creditInfo}
|
||||
|
|
@ -461,7 +682,8 @@ export class MatrixService extends BaseMatrixService {
|
|||
- Erledigt: ${stats.completed}
|
||||
- Gesamt: ${stats.total}
|
||||
|
||||
Bot: Online${!isLoggedIn ? '\n\nTipp: Mit `!login email passwort` anmelden fuer Credit-Tracking' : ''}`;
|
||||
${syncStatus}
|
||||
Bot: Online${!isLoggedIn ? '\n\nTipp: Mit `!login email passwort` anmelden fuer Synchronisation mit todo-web' : ''}`;
|
||||
|
||||
await this.sendReply(roomId, event, response);
|
||||
}
|
||||
|
|
@ -501,11 +723,7 @@ Bot: Online${!isLoggedIn ? '\n\nTipp: Mit `!login email passwort` anmelden fuer
|
|||
await this.sendReply(roomId, event, 'Hilfe wurde angepinnt!');
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to pin help:', error);
|
||||
await this.sendReply(
|
||||
roomId,
|
||||
event,
|
||||
'Konnte Hilfe nicht anpinnen (fehlende Berechtigung?)'
|
||||
);
|
||||
await this.sendReply(roomId, event, 'Konnte Hilfe nicht anpinnen (fehlende Berechtigung?)');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue