feat(services): add telegram-project-doc-bot service

Add new NestJS-based Telegram bot for project documentation with:
- Drizzle ORM for database access
- OpenAI integration for AI features
- S3 storage support via AWS SDK
- Monorepo integration (dev scripts, database setup, MinIO bucket)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-01-27 03:29:08 +01:00
parent bff80b552a
commit 7c20d88649
28 changed files with 2365 additions and 180 deletions

View file

@ -0,0 +1,24 @@
import { Module, Global } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';
@Global()
@Module({
providers: [
{
provide: DATABASE_CONNECTION,
useFactory: (configService: ConfigService) => {
const connectionString = configService.get<string>('database.url');
const client = postgres(connectionString!);
return drizzle(client, { schema });
},
inject: [ConfigService],
},
],
exports: [DATABASE_CONNECTION],
})
export class DatabaseModule {}

View file

@ -0,0 +1,85 @@
import { pgTable, uuid, text, timestamp, integer, jsonb, boolean } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
// Projects table
export const projects = pgTable('projects', {
id: uuid('id').primaryKey().defaultRandom(),
telegramUserId: integer('telegram_user_id').notNull(),
name: text('name').notNull(),
description: text('description'),
status: text('status').default('active').notNull(), // active, archived, completed
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// Media items (photos, voice notes, text)
export const mediaItems = pgTable('media_items', {
id: uuid('id').primaryKey().defaultRandom(),
projectId: uuid('project_id')
.references(() => projects.id, { onDelete: 'cascade' })
.notNull(),
type: text('type').notNull(), // photo, voice, text
// Storage
storageKey: text('storage_key'), // S3 key for photo/voice
thumbnailKey: text('thumbnail_key'), // Thumbnail for photos
// Content
caption: text('caption'), // Original caption/text
transcription: text('transcription'), // Voice → Text
aiDescription: text('ai_description'), // Vision → Description
// Metadata
metadata: jsonb('metadata').$type<{
width?: number;
height?: number;
duration?: number;
mimeType?: string;
fileSize?: number;
}>(),
telegramFileId: text('telegram_file_id'),
orderIndex: integer('order_index').default(0).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// Generated blog posts
export const generations = pgTable('generations', {
id: uuid('id').primaryKey().defaultRandom(),
projectId: uuid('project_id')
.references(() => projects.id, { onDelete: 'cascade' })
.notNull(),
style: text('style').default('casual').notNull(),
content: text('content').notNull(), // Generated markdown
pdfKey: text('pdf_key'), // S3 key for PDF export
isLatest: boolean('is_latest').default(true).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// Relations
export const projectsRelations = relations(projects, ({ many }) => ({
mediaItems: many(mediaItems),
generations: many(generations),
}));
export const mediaItemsRelations = relations(mediaItems, ({ one }) => ({
project: one(projects, {
fields: [mediaItems.projectId],
references: [projects.id],
}),
}));
export const generationsRelations = relations(generations, ({ one }) => ({
project: one(projects, {
fields: [generations.projectId],
references: [projects.id],
}),
}));
// Types
export type Project = typeof projects.$inferSelect;
export type NewProject = typeof projects.$inferInsert;
export type MediaItem = typeof mediaItems.$inferSelect;
export type NewMediaItem = typeof mediaItems.$inferInsert;
export type Generation = typeof generations.$inferSelect;
export type NewGeneration = typeof generations.$inferInsert;