feat: add API modules for digest scheduling, org admin portal, and thread views
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { Body, Controller, Delete, Get, Post, Put, UseGuards } from '@nestjs/common';
|
||||
import { DigestService } from './digest.service';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/roles.guard';
|
||||
import { Roles } from '../auth/roles.decorator';
|
||||
import { CurrentTenantContext } from '../auth/current-tenant.decorator';
|
||||
import { TenantContext } from '../../common/tenant-context';
|
||||
import { UpdateDigestConfigDto } from './digest.dto';
|
||||
|
||||
@Controller('admin/digest')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles('OWNER', 'ADMIN')
|
||||
export class DigestController {
|
||||
constructor(private readonly digestService: DigestService) {}
|
||||
|
||||
@Get('config')
|
||||
getConfig(@CurrentTenantContext() ctx: TenantContext) {
|
||||
return this.digestService.getConfig(ctx.tenantId);
|
||||
}
|
||||
|
||||
@Put('config')
|
||||
upsertConfig(
|
||||
@CurrentTenantContext() ctx: TenantContext,
|
||||
@Body() body: UpdateDigestConfigDto,
|
||||
) {
|
||||
return this.digestService.upsertConfig(ctx.tenantId, body, ctx.adminId ?? 'unknown');
|
||||
}
|
||||
|
||||
@Delete('config')
|
||||
disableConfig(@CurrentTenantContext() ctx: TenantContext) {
|
||||
return this.digestService.disableConfig(ctx.tenantId);
|
||||
}
|
||||
|
||||
@Get('history')
|
||||
getHistory(@CurrentTenantContext() ctx: TenantContext) {
|
||||
return this.digestService.getHistory(ctx.tenantId);
|
||||
}
|
||||
|
||||
@Post('send-now')
|
||||
sendNow(@CurrentTenantContext() ctx: TenantContext) {
|
||||
return this.digestService.sendNow(ctx.tenantId, ctx.adminId ?? 'unknown');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { IsBoolean, IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
|
||||
|
||||
export class UpdateDigestConfigDto {
|
||||
@IsString()
|
||||
targetGroupJid: string;
|
||||
|
||||
@IsString()
|
||||
targetAccountId: string;
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Max(23)
|
||||
scheduleHour: number;
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Max(59)
|
||||
scheduleMinute: number;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isActive?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DigestController } from './digest.controller';
|
||||
import { DigestService } from './digest.service';
|
||||
import { digestQueueProvider } from '../../queues/digest.queue';
|
||||
|
||||
@Module({
|
||||
controllers: [DigestController],
|
||||
providers: [DigestService, digestQueueProvider],
|
||||
})
|
||||
export class DigestModule {}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Injectable, Inject, NotFoundException } from '@nestjs/common';
|
||||
import { Queue } from 'bullmq';
|
||||
import { DigestJobData } from '@tower/types';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { AuditAction } from '../audit/audit.types';
|
||||
import { DIGEST_QUEUE } from '../../queues/digest.queue';
|
||||
import { UpdateDigestConfigDto } from './digest.dto';
|
||||
|
||||
function todayMidnightUtc(): Date {
|
||||
const now = new Date();
|
||||
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DigestService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly audit: AuditService,
|
||||
@Inject(DIGEST_QUEUE) private readonly digestQueue: Queue<DigestJobData>,
|
||||
) {}
|
||||
|
||||
async getConfig(tenantId: string) {
|
||||
return this.prisma.digestConfig.findUnique({ where: { tenantId } });
|
||||
}
|
||||
|
||||
async upsertConfig(tenantId: string, dto: UpdateDigestConfigDto, adminId: string) {
|
||||
const config = await this.prisma.digestConfig.upsert({
|
||||
where: { tenantId },
|
||||
create: {
|
||||
tenantId,
|
||||
targetGroupJid: dto.targetGroupJid,
|
||||
targetAccountId: dto.targetAccountId,
|
||||
scheduleHour: dto.scheduleHour,
|
||||
scheduleMinute: dto.scheduleMinute,
|
||||
isActive: dto.isActive ?? true,
|
||||
},
|
||||
update: {
|
||||
targetGroupJid: dto.targetGroupJid,
|
||||
targetAccountId: dto.targetAccountId,
|
||||
scheduleHour: dto.scheduleHour,
|
||||
scheduleMinute: dto.scheduleMinute,
|
||||
...(dto.isActive !== undefined ? { isActive: dto.isActive } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
await this.audit.log({
|
||||
tenantId,
|
||||
actorId: adminId,
|
||||
action: AuditAction.DIGEST_SENT,
|
||||
resourceType: 'DigestConfig',
|
||||
resourceId: config.id,
|
||||
payload: { targetGroupJid: dto.targetGroupJid, scheduleHour: dto.scheduleHour, scheduleMinute: dto.scheduleMinute },
|
||||
});
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
async disableConfig(tenantId: string): Promise<void> {
|
||||
const config = await this.prisma.digestConfig.findUnique({ where: { tenantId } });
|
||||
if (!config) throw new NotFoundException('Digest config not found');
|
||||
await this.prisma.digestConfig.update({ where: { tenantId }, data: { isActive: false } });
|
||||
}
|
||||
|
||||
async getHistory(tenantId: string) {
|
||||
const digests = await this.prisma.digest.findMany({
|
||||
where: { tenantId },
|
||||
orderBy: { digestDate: 'desc' },
|
||||
take: 30,
|
||||
});
|
||||
return digests.map((d) => ({
|
||||
id: d.id,
|
||||
digestDate: d.digestDate.toISOString(),
|
||||
messageCount: (d.messageIds as string[]).length,
|
||||
sentAt: d.sentAt.toISOString(),
|
||||
}));
|
||||
}
|
||||
|
||||
async sendNow(tenantId: string, adminId: string): Promise<{ status: string }> {
|
||||
const config = await this.prisma.digestConfig.findUnique({ where: { tenantId } });
|
||||
if (!config) throw new NotFoundException('Digest config not found — configure via PUT /admin/digest/config first');
|
||||
|
||||
await this.digestQueue.add('digest', {
|
||||
tenantId,
|
||||
digestDate: todayMidnightUtc().toISOString(),
|
||||
triggeredBy: 'manual',
|
||||
}, { attempts: 3, backoff: { type: 'exponential', delay: 2000 } });
|
||||
|
||||
await this.audit.log({
|
||||
tenantId,
|
||||
actorId: adminId,
|
||||
action: AuditAction.DIGEST_SENT,
|
||||
resourceType: 'Digest',
|
||||
resourceId: tenantId,
|
||||
payload: { triggeredBy: 'manual' },
|
||||
});
|
||||
|
||||
return { status: 'queued' };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { OrgAdminJwtPayload } from '@tower/types';
|
||||
|
||||
export const CurrentOrgAdmin = createParamDecorator(
|
||||
(_data: unknown, ctx: ExecutionContext): OrgAdminJwtPayload => {
|
||||
return ctx.switchToHttp().getRequest().user;
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
|
||||
@Injectable()
|
||||
export class OrgAdminGuard implements CanActivate {
|
||||
constructor(private readonly jwtService: JwtService) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader?.startsWith('Bearer ')) throw new UnauthorizedException();
|
||||
|
||||
const token = authHeader.slice(7);
|
||||
try {
|
||||
const payload = this.jwtService.verify(token);
|
||||
if (payload.kind !== 'orgadmin') throw new UnauthorizedException('Not an org admin');
|
||||
req.user = payload;
|
||||
return true;
|
||||
} catch {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common';
|
||||
import { OrgService } from './org.service';
|
||||
import { OrgAdminGuard } from './org-admin.guard';
|
||||
import { Public } from '../auth/public.decorator';
|
||||
|
||||
@Controller('auth/org')
|
||||
export class OrgAuthController {
|
||||
constructor(private readonly orgService: OrgService) {}
|
||||
|
||||
@Public()
|
||||
@Post('login')
|
||||
login(@Body() body: { email: string; password: string }) {
|
||||
return this.orgService.login(body.email, body.password);
|
||||
}
|
||||
|
||||
@UseGuards(OrgAdminGuard)
|
||||
@Get('me')
|
||||
me(@Req() req: any) {
|
||||
return this.orgService.me(req.user.sub);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@nestjs/common';
|
||||
import { OrgService } from './org.service';
|
||||
import { OrgAdminGuard } from './org-admin.guard';
|
||||
import { CurrentOrgAdmin } from './org-admin.decorator';
|
||||
import { OrgAdminJwtPayload } from '@tower/types';
|
||||
|
||||
@Controller('org')
|
||||
@UseGuards(OrgAdminGuard)
|
||||
export class OrgController {
|
||||
constructor(private readonly orgService: OrgService) {}
|
||||
|
||||
@Get('dashboard')
|
||||
getDashboard(@CurrentOrgAdmin() admin: OrgAdminJwtPayload) {
|
||||
return this.orgService.getDashboard(admin.organizationId);
|
||||
}
|
||||
|
||||
@Get('tenants')
|
||||
getTenants(@CurrentOrgAdmin() admin: OrgAdminJwtPayload) {
|
||||
return this.orgService.getTenants(admin.organizationId);
|
||||
}
|
||||
|
||||
@Post('tenants')
|
||||
createTenant(
|
||||
@CurrentOrgAdmin() admin: OrgAdminJwtPayload,
|
||||
@Body() body: { name: string; slug: string },
|
||||
) {
|
||||
return this.orgService.createTenant(admin.organizationId, body, admin.role);
|
||||
}
|
||||
|
||||
@Get('tenants/:id')
|
||||
getTenant(@CurrentOrgAdmin() admin: OrgAdminJwtPayload, @Param('id') id: string) {
|
||||
return this.orgService.getTenant(admin.organizationId, id);
|
||||
}
|
||||
|
||||
@Get('rules')
|
||||
getRules(@CurrentOrgAdmin() admin: OrgAdminJwtPayload) {
|
||||
return this.orgService.getRules(admin.organizationId);
|
||||
}
|
||||
|
||||
@Post('rules')
|
||||
createRule(
|
||||
@CurrentOrgAdmin() admin: OrgAdminJwtPayload,
|
||||
@Body() body: { matchType: string; matchValue: string; action: string; priority?: number },
|
||||
) {
|
||||
return this.orgService.createRule(admin.organizationId, body);
|
||||
}
|
||||
|
||||
@Patch('rules/:id')
|
||||
updateRule(
|
||||
@CurrentOrgAdmin() admin: OrgAdminJwtPayload,
|
||||
@Param('id') id: string,
|
||||
@Body() body: { matchValue?: string; action?: string; priority?: number; isActive?: boolean },
|
||||
) {
|
||||
return this.orgService.updateRule(admin.organizationId, id, body);
|
||||
}
|
||||
|
||||
@Delete('rules/:id')
|
||||
deleteRule(@CurrentOrgAdmin() admin: OrgAdminJwtPayload, @Param('id') id: string) {
|
||||
return this.orgService.deleteRule(admin.organizationId, id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { OrgAuthController } from './org-auth.controller';
|
||||
import { OrgController } from './org.controller';
|
||||
import { OrgService } from './org.service';
|
||||
import { OrgAdminGuard } from './org-admin.guard';
|
||||
import { PrismaModule } from '../../prisma/prisma.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PrismaModule,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
secret: config.get<string>('JWT_SECRET') ?? '',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
controllers: [OrgAuthController, OrgController],
|
||||
providers: [OrgService, OrgAdminGuard],
|
||||
exports: [OrgAdminGuard],
|
||||
})
|
||||
export class OrgModule {}
|
||||
@@ -0,0 +1,165 @@
|
||||
import {
|
||||
ConflictException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { OrgAdminJwtPayload } from '@tower/types';
|
||||
|
||||
@Injectable()
|
||||
export class OrgService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly config: ConfigService,
|
||||
) {}
|
||||
|
||||
async login(email: string, password: string) {
|
||||
const admin = await this.prisma.orgAdmin.findFirst({ where: { email } });
|
||||
if (!admin) throw new UnauthorizedException('Invalid credentials');
|
||||
|
||||
const valid = await bcrypt.compare(password, admin.passwordHash);
|
||||
if (!valid) throw new UnauthorizedException('Invalid credentials');
|
||||
|
||||
const payload: OrgAdminJwtPayload = {
|
||||
kind: 'orgadmin',
|
||||
sub: admin.id,
|
||||
organizationId: admin.organizationId,
|
||||
email: admin.email,
|
||||
role: admin.role as 'ORG_OWNER' | 'ORG_ADMIN',
|
||||
};
|
||||
|
||||
const token = this.jwtService.sign(payload, {
|
||||
secret: this.config.get<string>('JWT_SECRET'),
|
||||
expiresIn: '7d',
|
||||
});
|
||||
|
||||
return {
|
||||
token,
|
||||
orgAdmin: { id: admin.id, email: admin.email, name: admin.name, role: admin.role, organizationId: admin.organizationId },
|
||||
};
|
||||
}
|
||||
|
||||
async me(adminId: string) {
|
||||
const admin = await this.prisma.orgAdmin.findUnique({
|
||||
where: { id: adminId },
|
||||
include: { organization: { select: { id: true, slug: true, name: true } } },
|
||||
});
|
||||
if (!admin) throw new UnauthorizedException('Org admin not found');
|
||||
return { id: admin.id, email: admin.email, name: admin.name, role: admin.role, organization: admin.organization };
|
||||
}
|
||||
|
||||
async getDashboard(organizationId: string) {
|
||||
const tenants = await this.prisma.tenant.findMany({
|
||||
where: { organizationId },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
isActive: true,
|
||||
_count: { select: { messages: true, towerUsers: true, admins: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
chapterCount: tenants.length,
|
||||
chapters: tenants.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
slug: t.slug,
|
||||
isActive: t.isActive,
|
||||
messageCount: t._count.messages,
|
||||
memberCount: t._count.towerUsers,
|
||||
adminCount: t._count.admins,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async getTenants(organizationId: string) {
|
||||
return this.prisma.tenant.findMany({
|
||||
where: { organizationId },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
_count: { select: { messages: true, towerUsers: true } },
|
||||
},
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
async getTenant(organizationId: string, tenantId: string) {
|
||||
const tenant = await this.prisma.tenant.findFirst({
|
||||
where: { id: tenantId, organizationId },
|
||||
include: {
|
||||
admins: { select: { id: true, email: true, role: true, createdAt: true } },
|
||||
_count: { select: { messages: true, towerUsers: true, groups: true } },
|
||||
},
|
||||
});
|
||||
if (!tenant) throw new NotFoundException('Chapter not found');
|
||||
return tenant;
|
||||
}
|
||||
|
||||
async createTenant(organizationId: string, body: { name: string; slug: string }, role: string) {
|
||||
if (role !== 'ORG_OWNER') throw new ForbiddenException('Only ORG_OWNER can create chapters');
|
||||
|
||||
const existing = await this.prisma.tenant.findUnique({ where: { slug: body.slug } });
|
||||
if (existing) throw new ConflictException('Tenant slug already taken');
|
||||
|
||||
return this.prisma.tenant.create({
|
||||
data: { name: body.name, slug: body.slug, organizationId },
|
||||
});
|
||||
}
|
||||
|
||||
async getRules(organizationId: string) {
|
||||
return this.prisma.orgRule.findMany({
|
||||
where: { organizationId },
|
||||
orderBy: [{ priority: 'asc' }, { createdAt: 'asc' }],
|
||||
});
|
||||
}
|
||||
|
||||
async createRule(organizationId: string, body: { matchType: string; matchValue: string; action: string; priority?: number }) {
|
||||
const existing = await this.prisma.orgRule.findUnique({
|
||||
where: { organizationId_matchType_matchValue: { organizationId, matchType: body.matchType as any, matchValue: body.matchValue } },
|
||||
});
|
||||
if (existing) throw new ConflictException('Rule already exists');
|
||||
|
||||
return this.prisma.orgRule.create({
|
||||
data: {
|
||||
organizationId,
|
||||
matchType: body.matchType as any,
|
||||
matchValue: body.matchValue,
|
||||
action: body.action as any,
|
||||
priority: body.priority ?? 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateRule(organizationId: string, ruleId: string, body: { matchValue?: string; action?: string; priority?: number; isActive?: boolean }) {
|
||||
const rule = await this.prisma.orgRule.findFirst({ where: { id: ruleId, organizationId } });
|
||||
if (!rule) throw new NotFoundException('Rule not found');
|
||||
|
||||
return this.prisma.orgRule.update({
|
||||
where: { id: ruleId },
|
||||
data: {
|
||||
...(body.matchValue !== undefined && { matchValue: body.matchValue }),
|
||||
...(body.action !== undefined && { action: body.action as any }),
|
||||
...(body.priority !== undefined && { priority: body.priority }),
|
||||
...(body.isActive !== undefined && { isActive: body.isActive }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async deleteRule(organizationId: string, ruleId: string) {
|
||||
const rule = await this.prisma.orgRule.findFirst({ where: { id: ruleId, organizationId } });
|
||||
if (!rule) throw new NotFoundException('Rule not found');
|
||||
await this.prisma.orgRule.delete({ where: { id: ruleId } });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
||||
import { ThreadsService } from './threads.service';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/roles.guard';
|
||||
import { Roles } from '../auth/roles.decorator';
|
||||
import { CurrentTenantContext } from '../auth/current-tenant.decorator';
|
||||
import { TenantContext } from '../../common/tenant-context';
|
||||
|
||||
@Controller('admin/threads')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles('OWNER', 'ADMIN')
|
||||
export class ThreadsController {
|
||||
constructor(private readonly threads: ThreadsService) {}
|
||||
|
||||
@Get()
|
||||
list(@CurrentTenantContext() ctx: TenantContext) {
|
||||
return this.threads.listThreads(ctx.tenantId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
get(
|
||||
@CurrentTenantContext() ctx: TenantContext,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
return this.threads.getThread(ctx.tenantId, id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ThreadsController } from './threads.controller';
|
||||
import { ThreadsService } from './threads.service';
|
||||
|
||||
@Module({
|
||||
controllers: [ThreadsController],
|
||||
providers: [ThreadsService],
|
||||
})
|
||||
export class ThreadsModule {}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class ThreadsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async listThreads(tenantId: string) {
|
||||
const threads = await this.prisma.thread.findMany({
|
||||
where: { tenantId },
|
||||
orderBy: { lastActivityAt: 'desc' },
|
||||
include: {
|
||||
_count: { select: { messages: true } },
|
||||
sourceGroup: { select: { name: true, platformId: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return threads.map((t) => ({
|
||||
id: t.id,
|
||||
topic: t.topic,
|
||||
sourceGroupName: t.sourceGroup.name,
|
||||
messageCount: t._count.messages,
|
||||
lastActivityAt: t.lastActivityAt.toISOString(),
|
||||
createdAt: t.createdAt.toISOString(),
|
||||
}));
|
||||
}
|
||||
|
||||
async getThread(tenantId: string, threadId: string) {
|
||||
const thread = await this.prisma.thread.findUnique({
|
||||
where: { id: threadId },
|
||||
include: {
|
||||
messages: {
|
||||
orderBy: { createdAt: 'asc' },
|
||||
select: {
|
||||
id: true,
|
||||
content: true,
|
||||
senderJid: true,
|
||||
senderName: true,
|
||||
tags: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
},
|
||||
},
|
||||
sourceGroup: { select: { name: true, platformId: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!thread || thread.tenantId !== tenantId) {
|
||||
throw new NotFoundException('Thread not found');
|
||||
}
|
||||
|
||||
return {
|
||||
id: thread.id,
|
||||
topic: thread.topic,
|
||||
sourceGroupName: thread.sourceGroup.name,
|
||||
messageCount: thread.messageCount,
|
||||
lastActivityAt: thread.lastActivityAt.toISOString(),
|
||||
createdAt: thread.createdAt.toISOString(),
|
||||
messages: thread.messages.map((m) => ({
|
||||
id: m.id,
|
||||
content: m.content,
|
||||
senderJid: m.senderJid,
|
||||
senderName: m.senderName,
|
||||
tags: m.tags,
|
||||
status: m.status,
|
||||
createdAt: m.createdAt.toISOString(),
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { Queue } from 'bullmq';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { DigestJobData } from '@tower/types';
|
||||
import { parseRedisUrl } from './redis-connection';
|
||||
|
||||
export const DIGEST_QUEUE = 'DIGEST_QUEUE';
|
||||
|
||||
export function createDigestQueue(redisUrl: string): Queue<DigestJobData> {
|
||||
return new Queue<DigestJobData>('tower.digest.generate.v1', {
|
||||
connection: parseRedisUrl(redisUrl),
|
||||
});
|
||||
}
|
||||
|
||||
export const digestQueueProvider: Provider = {
|
||||
provide: DIGEST_QUEUE,
|
||||
useFactory: (config: ConfigService) =>
|
||||
createDigestQueue(config.get<string>('REDIS_URL', 'redis://localhost:6379')),
|
||||
inject: [ConfigService],
|
||||
};
|
||||
Reference in New Issue
Block a user