managarten/services/matrix-project-doc-bot/src/project/project.service.ts
Claude 7c5e9e3c49
feat(matrix): add Stats Bot and Project Doc Bot services
Complete GDPR-compliant bot suite for Matrix:

matrix-stats-bot (port 3312):
- Analytics reports from Umami
- Commands: !stats, !today, !week, !realtime, !users
- Scheduled daily/weekly reports to Matrix room

matrix-project-doc-bot (port 3313):
- Project documentation with photos, voice, text
- Voice transcription via OpenAI Whisper
- Blog generation with 5 styles (casual, technical, tutorial, social, story)
- Commands: !new, !projects, !switch, !status, !generate, !export
- Uses PostgreSQL + S3 (MinIO) for storage

Changes:
- docker-compose.macmini.yml: Added both Matrix bots
- health-check.sh: Added health checks for both bots

Environment variables required:
- MATRIX_STATS_BOT_TOKEN, MATRIX_PROJECT_DOC_BOT_TOKEN
- OPENAI_API_KEY (for Project Doc Bot)

https://claude.ai/code/session_01E3r5aFW3YLAhEJfsL2ryhv
2026-01-28 00:44:28 +00:00

74 lines
2 KiB
TypeScript

import { Injectable, Inject, Logger } from '@nestjs/common';
import { eq, desc } from 'drizzle-orm';
import { DATABASE_CONNECTION } from '../database/database.module';
import { projects, projectItems } from '../database/schema';
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
import type * as schema from '../database/schema';
type Database = PostgresJsDatabase<typeof schema>;
interface CreateProjectInput {
matrixUserId: string;
name: string;
}
@Injectable()
export class ProjectService {
private readonly logger = new Logger(ProjectService.name);
constructor(@Inject(DATABASE_CONNECTION) private db: Database) {}
async create(input: CreateProjectInput) {
const [project] = await this.db
.insert(projects)
.values({
matrixUserId: input.matrixUserId,
name: input.name,
})
.returning();
this.logger.log(`Created project ${project.id} for user ${input.matrixUserId}`);
return project;
}
async findById(id: string) {
const [project] = await this.db.select().from(projects).where(eq(projects.id, id));
return project;
}
async findByUser(matrixUserId: string) {
return this.db
.select()
.from(projects)
.where(eq(projects.matrixUserId, matrixUserId))
.orderBy(desc(projects.createdAt));
}
async update(id: string, data: Partial<typeof projects.$inferInsert>) {
const [project] = await this.db
.update(projects)
.set({ ...data, updatedAt: new Date() })
.where(eq(projects.id, id))
.returning();
return project;
}
async getStats(projectId: string) {
const items = await this.db.select().from(projectItems).where(eq(projectItems.projectId, projectId));
return {
photos: items.filter((i) => i.type === 'photo').length,
voices: items.filter((i) => i.type === 'voice').length,
texts: items.filter((i) => i.type === 'text').length,
total: items.length,
};
}
async getItems(projectId: string) {
return this.db
.select()
.from(projectItems)
.where(eq(projectItems.projectId, projectId))
.orderBy(projectItems.createdAt);
}
}