add telegram bot

This commit is contained in:
Sergey Krylov 2025-07-21 06:47:16 +03:00
parent 35df6780ec
commit 5b6d8cccb7
27 changed files with 403 additions and 28 deletions

View File

@ -93,7 +93,7 @@ model User {
followings Follow[] @relation(name: "followings")
notifications Notification[]
notificationSettings NotificationSettings?
telegramId String? @default("telegram_id")
telegramId String? @unique @map("telegram_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

View File

@ -3,13 +3,14 @@ import { ApolloDriverConfig } from '@nestjs/apollo'
import { ConfigService } from '@nestjs/config'
import { Request, Response } from 'express'
import * as path from 'node:path'
import { ProcessEnv } from '../../shared/types/env'
type ContextType = {
req: Request
res: Response
}
export function getGraphQLConfig(configService: ConfigService): ApolloDriverConfig {
export function getGraphQLConfig(configService: ConfigService<ProcessEnv>): ApolloDriverConfig {
return {
playground: isDev(configService),
path: configService.getOrThrow('GRAPHQL_PREFIX'),

View File

@ -1,7 +1,8 @@
import { TypeLiveKitOptions } from '@/src/module/libs/livekit/type/livekit.type'
import { ConfigService } from '@nestjs/config'
import { ProcessEnv } from '../../shared/types/env'
export function getLiveKitConfig(configService: ConfigService): TypeLiveKitOptions {
export function getLiveKitConfig(configService: ConfigService<ProcessEnv>): TypeLiveKitOptions {
return {
apiSecret: configService.getOrThrow('LIVEKIT_API_SECRET'),
apiKey: configService.getOrThrow('LIVEKIT_API_KEY'),

View File

@ -1,7 +1,8 @@
import { MailerOptions } from '@nestjs-modules/mailer'
import { ConfigService } from '@nestjs/config'
import type { ProcessEnv } from '../../shared/types/env';
import type { ConfigService } from '@nestjs/config';
import type { MailerOptions } from '@nestjs-modules/mailer';
export function getMailConfig(configService: ConfigService): MailerOptions {
export function getMailConfig(configService: ConfigService<ProcessEnv>): MailerOptions {
return {
transport: {
host: configService.getOrThrow<string>('MAIL_HOST'),
@ -15,5 +16,5 @@ export function getMailConfig(configService: ConfigService): MailerOptions {
defaults: {
from: `"TeaStream" ${configService.getOrThrow<string>('MAIL_LOGIN')}`,
},
}
};
}

View File

@ -0,0 +1,9 @@
import { ConfigService } from '@nestjs/config'
import { TelegrafModuleOptions } from 'nestjs-telegraf'
import { ProcessEnv } from '../../shared/types/env';
export function getTelegrafOptions(configService: ConfigService<ProcessEnv>): TelegrafModuleOptions {
return {
token: configService.getOrThrow('TELEGRAM_BOT_TOKEN'),
}
}

View File

@ -15,6 +15,7 @@ import { FollowModule } from '@/src/module/follow/follow.module'
import { LiveKitModule } from '@/src/module/libs/livekit/livekit.module'
import { MailModule } from '@/src/module/libs/mail/mail.module'
import { StorageModule } from '@/src/module/libs/storage/storage.module'
import { TelegramModule } from '@/src/module/libs/telegram/telegram.module'
import { NotificationModule } from '@/src/module/notification/notification.module'
import { IngressModule } from '@/src/module/stream/ingress/ingress.module'
import { StreamModule } from '@/src/module/stream/stream.module'
@ -49,6 +50,7 @@ import { RedisModule } from './redis/redis.module'
useFactory: getLiveKitConfig,
inject: [ConfigService],
}),
TelegramModule,
AccountModule,
SessionModule,
VerificationModule,

View File

@ -1,11 +1,12 @@
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import Redis from 'ioredis'
import { ProcessEnv } from '../../shared/types/env';
@Injectable()
export class RedisService extends Redis {
constructor(
private readonly configService: ConfigService,
private readonly configService: ConfigService<ProcessEnv>,
) {
super(configService.getOrThrow('REDIS_URI'))
}

View File

@ -16,21 +16,21 @@ async function bootstrap() {
const config = app.get(ConfigService)
const redis = app.get(RedisService)
app.use(cookieParser(config.getOrThrow<string>('COOKIE_SECRET')))
app.use(config.getOrThrow<string>('GRAPHQL_PREFIX'), graphqlUploadExpress())
app.use(cookieParser(config.getOrThrow('COOKIE_SECRET')))
app.use(config.getOrThrow('GRAPHQL_PREFIX'), graphqlUploadExpress())
app.useGlobalPipes(new ValidationPipe({
transform: true,
}))
app.use(session({
secret: config.getOrThrow<string>('SESSION_SECRET'),
name: config.getOrThrow<string>('SESSION_NAME'),
secret: config.getOrThrow('SESSION_SECRET'),
name: config.getOrThrow('SESSION_NAME'),
resave: false,
saveUninitialized: false,
cookie: {
domain: config.getOrThrow<string>('SESSION_DOMAIN'),
maxAge: ms(config.getOrThrow<StringValue>('SESSION_MAX_AGE')),
domain: config.getOrThrow('SESSION_DOMAIN'),
maxAge: ms(config.getOrThrow('SESSION_MAX_AGE')),
httpOnly: parseBoolean(config.getOrThrow('SESSION_HTTP_ONLY')),
secure: parseBoolean(config.getOrThrow('SESSION_SECURE')),
sameSite: 'lax',
@ -42,7 +42,7 @@ async function bootstrap() {
}))
app.enableCors({
origin: config.getOrThrow<string>('ALLOWED_ORIGIN'),
origin: config.getOrThrow('ALLOWED_ORIGIN'),
credentials: true,
exposedHeaders: ['set-cookie'],
})

View File

@ -1,7 +1,7 @@
import { PrismaService } from '@/src/core/prisma/prisma.service'
import { DeactivateAccountInput } from '@/src/module/auth/deactivate/inputs/deactivate-account.input'
import { MailService } from '@/src/module/libs/mail/mail.service'
import { SessionInfo } from '@/src/shared/types/session-metadata.types'
import { TelegramService } from '@/src/module/libs/telegram/telegram.service';
import { generateToken } from '@/src/shared/util/generate-token.util'
import { getSessionMetadata } from '@/src/shared/util/session-metadata.util'
import { destroySession } from '@/src/shared/util/session.util'
@ -10,13 +10,15 @@ import { ConfigService } from '@nestjs/config'
import { TokenType, User } from '@prisma/generated'
import { verify } from 'argon2'
import { Request } from 'express'
import { ProcessEnv } from '../../../shared/types/env'
@Injectable()
export class DeactivateService {
constructor(
private readonly prismaService: PrismaService,
private readonly configService: ConfigService,
private readonly configService: ConfigService<ProcessEnv>,
private readonly mailService: MailService,
private readonly telegramService: TelegramService
) {
}
@ -89,6 +91,9 @@ export class DeactivateService {
const metadata = getSessionMetadata(req, userAgent)
await this.mailService.sendDeactivateToken(user.email, deactivateToken.token, metadata)
if (deactivateToken.user?.notificationSettings?.telegramNotifications && deactivateToken.user?.telegramId) {
await this.telegramService.sendDeactivateToken(deactivateToken.user.telegramId, deactivateToken.token, metadata)
}
return true
}
}

View File

@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
import { PasswordRecoveryService } from './password-recovery.service';
import { PasswordRecoveryResolver } from './password-recovery.resolver';
import { Module } from '@nestjs/common'
import { PasswordRecoveryService } from './password-recovery.service'
import { PasswordRecoveryResolver } from './password-recovery.resolver'
@Module({
providers: [PasswordRecoveryResolver, PasswordRecoveryService],

View File

@ -2,6 +2,7 @@ import { PrismaService } from '@/src/core/prisma/prisma.service'
import { NewPasswordInput } from '@/src/module/auth/password-recovery/inputs/new-password.input'
import { ResetPasswordInput } from '@/src/module/auth/password-recovery/inputs/reset-password.input'
import { MailService } from '@/src/module/libs/mail/mail.service'
import { TelegramService } from '@/src/module/libs/telegram/telegram.service';
import { generateToken } from '@/src/shared/util/generate-token.util'
import { getSessionMetadata } from '@/src/shared/util/session-metadata.util'
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
@ -14,6 +15,7 @@ export class PasswordRecoveryService {
constructor(
private readonly prismaService: PrismaService,
private readonly mailService: MailService,
private readonly telegramService: TelegramService
) {
}
@ -21,6 +23,7 @@ export class PasswordRecoveryService {
const { email } = input
const user = await this.prismaService.user.findFirst({
where: { email },
include: { notificationSettings: true },
})
if (!user) {
throw new NotFoundException('Пользователь с такой почтой не найден')
@ -30,6 +33,10 @@ export class PasswordRecoveryService {
const metadata = getSessionMetadata(req, userAgent)
await this.mailService.sendPasswordResetToken(user.email, resetToken.token, metadata)
if (resetToken.user?.notificationSettings?.telegramNotifications && resetToken?.user.telegramId) {
await this.telegramService.sendPasswordResetToken(resetToken.user.telegramId, resetToken.token, metadata)
}
return true
}

View File

@ -16,13 +16,14 @@ import { verify } from 'argon2'
import { Request } from 'express'
import { SessionData } from 'express-session'
import { TOTP } from 'otpauth'
import { ProcessEnv } from '../../../shared/types/env'
@Injectable()
export class SessionService {
constructor(
private readonly prismaService: PrismaService,
private readonly redisService: RedisService,
private readonly configService: ConfigService,
private readonly configService: ConfigService<ProcessEnv>,
private readonly verificationService: VerificationService,
) {}

View File

@ -1,6 +1,7 @@
import { PrismaService } from '@/src/core/prisma/prisma.service'
import { MailService } from '@/src/module/libs/mail/mail.service'
import { StorageService } from '@/src/module/libs/storage/storage.service'
import { TelegramService } from '@/src/module/libs/telegram/telegram.service'
import { Injectable } from '@nestjs/common'
import { Cron, CronExpression } from '@nestjs/schedule'
@ -10,6 +11,7 @@ export class CronService {
private readonly prismaService: PrismaService,
private readonly mailService: MailService,
private readonly storageService: StorageService,
private readonly telegramService: TelegramService,
) {
}
@ -25,11 +27,18 @@ export class CronService {
lte: sevenDayAgo,
},
},
include: {
notificationSettings: true,
},
})
for (const user of deactivatedAccounts) {
console.log('Deactivate user', user.name, user.email)
await this.mailService.sendAccountDeletion(user.email)
if (user?.telegramId) {
await this.telegramService.sendAccountDeletionToken(user?.telegramId)
}
if (user.avatar) {
await this.storageService.remove(user.avatar)
}

View File

@ -1,4 +1,5 @@
import { PrismaService } from '@/src/core/prisma/prisma.service'
import { TelegramService } from '@/src/module/libs/telegram/telegram.service';
import { NotificationService } from '@/src/module/notification/notification.service'
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'
import { User } from '@prisma/generated'
@ -8,6 +9,7 @@ export class FollowService {
constructor(
private readonly prismaService: PrismaService,
private readonly notificationService: NotificationService,
private readonly telegramService: TelegramService,
) {
}
@ -68,6 +70,9 @@ export class FollowService {
if (follow.following.notificationSettings?.siteNotifications) {
await this.notificationService.createNewFollowing(follow.following.id, follow.follower)
}
if (follow.following.notificationSettings?.telegramNotifications && follow.following.telegramId) {
await this.telegramService.sendNewFollowing(follow.following.telegramId, follow.follower)
}
return follow
}

View File

@ -3,6 +3,7 @@ import { DeactivateTemplate } from '@/src/module/libs/mail/templates/deactivate.
import PasswordRecoveryTemplate from '@/src/module/libs/mail/templates/password-recovery.template'
import { SessionInfo } from '@/src/shared/types/session-metadata.types'
import { Token } from '@prisma/generated'
import { ProcessEnv } from '../../../shared/types/env';
import VerificationTemplate from './templates/verification.template'
import { MailerService } from '@nestjs-modules/mailer'
import { Injectable } from '@nestjs/common'
@ -12,7 +13,7 @@ import { render } from '@react-email/components'
@Injectable()
export class MailService {
constructor(
private readonly configService: ConfigService,
private readonly configService: ConfigService<ProcessEnv>,
private readonly mailerService: MailerService,
) {}

View File

@ -7,6 +7,7 @@ import {
} from '@aws-sdk/client-s3'
import { BadRequestException, Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { ProcessEnv } from '../../../shared/types/env';
@Injectable()
export class StorageService {
@ -14,7 +15,7 @@ export class StorageService {
private readonly bucket: string
constructor(
private readonly configService: ConfigService,
private readonly configService: ConfigService<ProcessEnv>,
) {
this.client = new S3Client({
endpoint: this.configService.getOrThrow('S3_ENDPOINT'),

View File

@ -0,0 +1,17 @@
import { Markup } from 'telegraf'
export const BUTTONS = {
authSuccess: Markup.inlineKeyboard([
[
Markup.button.callback('📜 Мои подписки', 'follows'),
Markup.button.callback('👤 Просмотреть профиль', 'me'),
],
[Markup.button.url('🌐 На сайт', 'https://teastream.ru')],
]),
profile: Markup.inlineKeyboard([
Markup.button.url(
'⚙️ Настройки аккаунта',
'https://teastream.ru/dashboard/settings',
),
]),
}

View File

@ -0,0 +1,74 @@
import { SessionInfo } from '@/src/shared/types/session-metadata.types'
import type { User } from '@prisma/generated'
export const MESSAGES = {
welcome:
'<b>👋 Добро пожаловать в TeaStream Bot!</b>\n\n'
+ 'Чтобы получать уведомления и улучшить ваш опыт использования платформы, давайте свяжем ваш Telegram аккаунт с TeaStream.\n\n'
+ 'Нажмите на кнопку ниже и перейдите в раздел <b>Уведомления</b>, чтобы завершить настройку.',
authSuccess: '🎉 Вы успешно авторизовались и Telegram аккаунт связан с TeaStream!\n\n',
invalidToken: '❌ Недействительный или просроченный токен.',
profile: (user: User, followersCount: number) => '<b>👤 Профиль пользователя:</b>\n\n'
+ `👤 Имя пользователя: <b>${user.name}</b>\n`
+ `📧 Email: <b>${user.email}</b>\n`
+ `👥 Количество подписчиков: <b>${followersCount}</b>\n`
+ `📝 О себе: <b>${user.bio ?? 'Не указано'}</b>\n\n`
+ '🔧 Нажмите на кнопку ниже, чтобы перейти к настройкам профиля.',
follows: (user: User) =>
`📺 <a href="https://teastream.ru/${user.name}">${user.name}</a>`,
resetPassword: (token: string, metadata: SessionInfo) =>
`<b>🔒 Сброс пароля</b>\n\n`
+ `Вы запросили сброс пароля для вашей учетной записи на платформе <b>TeaStream</b>.\n\n`
+ `Чтобы создать новый пароль, пожалуйста, перейдите по следующей ссылке:\n\n`
+ `<b><a href="https://teastream.ru/account/recovery/${token}">Сбросить пароль</a></b>\n\n`
+ `📅 <b>Дата запроса:</b> ${new Date().toLocaleDateString()} в ${new Date().toLocaleTimeString()}\n\n`
+ `🖥️ <b>Информация о запросе:</b>\n\n`
+ `🌍 <b>Расположение:</b> ${metadata.location.country}, ${metadata.location.city}\n`
+ `📱 <b>Операционная система:</b> ${metadata.device.os}\n`
+ `🌐 <b>Браузер:</b> ${metadata.device.browser}\n`
+ `💻 <b>IP-адрес:</b> ${metadata.ip}\n\n`
+ `Если вы не делали этот запрос, просто проигнорируйте это сообщение.\n\n`
+ `Спасибо за использование <b>TeaStream</b>! 🚀`,
deactivate: (token: string, metadata: SessionInfo) =>
`<b>⚠️ Запрос на деактивацию аккаунта</b>\n\n`
+ `Вы инициировали процесс деактивации вашего аккаунта на платформе <b>Teastream</b>.\n\n`
+ `Для завершения операции, пожалуйста, подтвердите свой запрос, введя следующий код подтверждения:\n\n`
+ `<b>Код подтверждения: ${token}</b>\n\n`
+ `📅 <b>Дата запроса:</b> ${new Date().toLocaleDateString()} в ${new Date().toLocaleTimeString()}\n\n`
+ `🖥️ <b>Информация о запросе:</b>\n\n`
+ `• 🌍 <b>Расположение:</b> ${metadata.location.country}, ${metadata.location.city}\n`
+ `• 📱 <b>Операционная система:</b> ${metadata.device.os}\n`
+ `• 🌐 <b>Браузер:</b> ${metadata.device.browser}\n`
+ `• 💻 <b>IP-адрес:</b> ${metadata.ip}\n\n`
+ `<b>Что произойдет после деактивации?</b>\n\n`
+ `1. Вы автоматически выйдете из системы и потеряете доступ к аккаунту.\n`
+ `2. Если вы не отмените деактивацию в течение 7 дней, ваш аккаунт будет <b>безвозвратно удален</b> со всей вашей информацией, данными и подписками.\n\n`
+ `<b>⏳ Обратите внимание:</b> Если в течение 7 дней вы передумаете, вы можете обратиться в нашу поддержку для восстановления доступа к вашему аккаунту до момента его полного удаления.\n\n`
+ `После удаления аккаунта восстановить его будет невозможно, и все данные будут потеряны без возможности восстановления.\n\n`
+ `Если вы передумали, просто проигнорируйте это сообщение. Ваш аккаунт останется активным.\n\n`
+ `Спасибо, что пользуетесь <b>TeaStream</b>! Мы всегда рады видеть вас на нашей платформе и надеемся, что вы останетесь с нами. 🚀\n\n`
+ `С уважением,\n`
+ `Команда TeaStream`,
accountDeleted:
`<b>⚠️ Ваш аккаунт был полностью удалён.</b>\n\n`
+ `Ваш аккаунт был полностью стерт из базы данных Teastream. Все ваши данные и информация были удалены безвозвратно. ❌\n\n`
+ `🔒 Вы больше не будете получать уведомления в Telegram и на почту.\n\n`
+ `Если вы захотите вернуться на платформу, вы можете зарегистрироваться по следующей ссылке:\n`
+ `<b><a href="https://teastream.ru/account/create">Зарегистрироваться на Teastream</a></b>\n\n`
+ `Спасибо, что были с нами! Мы всегда будем рады видеть вас на платформе. 🚀\n\n`
+ `С уважением,\n`
+ `Команда TeaStream`,
streamStart: (channel: User) =>
`<b>📡 На канале ${channel.displayName} началась трансляция!</b>\n\n`
+ `Смотрите здесь: <a href="https://teastream.ru/${channel.name}">Перейти к трансляции</a>`,
newFollowing: (follower: User, followersCount: number) =>
`<b>У вас новый подписчик!</b>\n\nЭто пользователь <a href="https://teastream.ru/${follower.name}">${follower.displayName}</a>\n\nИтоговое количество подписчиков на вашем канале: ${followersCount}`,
enableTwoFactor:
`🔐 Обеспечьте свою безопасность!\n\n`
+ `Включите двухфакторную аутентификацию в <a href="https://teastream.ru/dashboard/settings">настройках аккаунта</a>.`,
verifyChannel:
`<b>🎉 Поздравляем! Ваш канал верифицирован</b>\n\n`
+ `Мы рады сообщить, что ваш канал теперь верифицирован, и вы получили официальный значок.\n\n`
+ `Значок верификации подтверждает подлинность вашего канала и улучшает доверие зрителей.\n\n`
+ `Спасибо, что вы с нами и продолжаете развивать свой канал вместе с TeaStream!`,
}

View File

@ -0,0 +1,19 @@
import { getTelegrafOptions } from '@/src/core/config/telegraf.config'
import { Global, Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { TelegrafModule } from 'nestjs-telegraf'
import { TelegramService } from './telegram.service'
@Global()
@Module({
imports: [
TelegrafModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: getTelegrafOptions,
}),
],
providers: [TelegramService],
exports: [TelegramService],
})
export class TelegramModule {}

View File

@ -0,0 +1,159 @@
import { SessionInfo } from '@/src/shared/types/session-metadata.types'
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import {
Action,
Command, Ctx, Start, Update,
} from 'nestjs-telegraf'
import { Context, Telegraf } from 'telegraf'
import { PrismaService } from '@/src/core/prisma/prisma.service'
import { BUTTONS } from '@/src/module/libs/telegram/telegram.button'
import { MESSAGES } from '@/src/module/libs/telegram/telegram.message'
import { TokenType, User } from '@prisma/generated'
import { ProcessEnv } from '../../../shared/types/env'
@Update()
@Injectable()
export class TelegramService extends Telegraf {
private readonly _token: string
constructor(
private readonly prismaService: PrismaService,
private readonly configService: ConfigService<ProcessEnv>,
) {
super(configService.getOrThrow('TELEGRAM_BOT_TOKEN'))
this._token = configService.getOrThrow('TELEGRAM_BOT_TOKEN')
}
@Start()
public async onStart(@Ctx() ctx: Context) {
const chatId = ctx.chat?.id.toString()
// @ts-ignore
const token = ctx.message.text.split(' ')[1] as string
if (token) {
const authToken = await this.prismaService.token.findUnique({
where: {
token,
type: TokenType.TELEGRAM_AUTH,
},
})
if (!authToken?.userId) {
await ctx.reply('Токен не найден')
return
}
const hasExpired = new Date(authToken.expiresIn) < new Date()
if (hasExpired) {
await ctx.reply(MESSAGES.invalidToken)
return
}
await this.connectTelegram(authToken.userId, chatId!)
await this.prismaService.token.delete({
where: { id: authToken.id },
})
await ctx.replyWithHTML(MESSAGES.authSuccess, BUTTONS.authSuccess)
return
}
const user = await this.findUserByChatId(chatId!)
if (user) {
return await this.onMe(ctx)
}
await ctx.replyWithHTML(MESSAGES.welcome, BUTTONS.profile)
}
@Command('me')
@Action('me')
public async onMe(@Ctx() ctx: Context) {
const chatId = ctx.chat?.id.toString()
if (typeof chatId === 'undefined') {
throw new NotFoundException('Пользователь не найден')
}
const user = await this.findUserByChatId(chatId)
if (!user) {
throw new NotFoundException('Пользователь не найден')
}
const followersCount = await this.prismaService.follow.count({
where: { followingId: user.id },
})
await ctx.replyWithHTML(MESSAGES.profile(user, followersCount), BUTTONS.profile)
}
@Command('follows')
@Action('follows')
public async onFollow(@Ctx() ctx: Context) {
const chatId = ctx.chat?.id
if (typeof chatId === 'undefined') {
throw new NotFoundException('Пользователь не найден')
}
const user = await this.findUserByChatId(chatId.toString())
if (!user) {
throw new NotFoundException('Пользователь не найден')
}
const follows = await this.prismaService.follow.findMany({
where: { followerId: user.id },
include: { following: true },
})
if (follows.length > 0) {
const followList = follows.map(follow => MESSAGES.follows(follow.following)).join('\n')
const message = `<b>Каналы, на которые Вы подписаны</b>\n\n${followList}`
await ctx.replyWithHTML(message)
return
}
await ctx.replyWithHTML('❌ <b>У Вас нет подписок</b>')
}
public async sendPasswordResetToken(chatId: string, token: string, metadata: SessionInfo) {
await this.telegram.sendMessage(chatId, MESSAGES.resetPassword(token, metadata), { parse_mode: 'HTML' })
}
public async sendDeactivateToken(chatId: string, token: string, metadata: SessionInfo) {
await this.telegram.sendMessage(chatId, MESSAGES.deactivate(token, metadata), { parse_mode: 'HTML' })
}
public async sendAccountDeletionToken(chatId: string) {
await this.telegram.sendMessage(chatId, MESSAGES.accountDeleted, { parse_mode: 'HTML' })
}
public async sendStreamStart(chatId: string, channel: User) {
await this.telegram.sendMessage(chatId, MESSAGES.streamStart(channel), { parse_mode: 'HTML' })
}
public async sendNewFollowing(chatId: string, follower: User) {
const user = await this.findUserByChatId(chatId)
if (!user) {
throw new NotFoundException('Пользователь не найден')
}
await this.telegram.sendMessage(chatId, MESSAGES.newFollowing(follower, user.followings.length), { parse_mode: 'HTML' })
}
private async connectTelegram(userId: string, chatId: string) {
return this.prismaService.user.update({
where: { id: userId },
data: { telegramId: chatId },
})
}
private async findUserByChatId(chatId: string) {
return this.prismaService.user.findUnique({
where: { telegramId: chatId },
include: {
followings: true,
followers: true,
},
})
}
}

View File

@ -60,7 +60,7 @@ export class NotificationService {
return {
notificationSetting,
token: telegramAuthToken.token,
telegramAuthToken: telegramAuthToken.token,
}
}

View File

@ -9,13 +9,14 @@ import { ConfigService } from '@nestjs/config'
import * as Upload from 'graphql-upload/Upload'
import { AccessToken } from 'livekit-server-sdk'
import sharp from 'sharp'
import { ProcessEnv } from '../../shared/types/env';
@Injectable()
export class StreamService {
constructor(
private readonly prismaService: PrismaService,
private readonly storageService: StorageService,
private readonly configService: ConfigService,
private readonly configService: ConfigService<ProcessEnv>,
) {
}

View File

@ -1,5 +1,6 @@
import { PrismaService } from '@/src/core/prisma/prisma.service'
import { LiveKitService } from '@/src/module/libs/livekit/livekit.service'
import { TelegramService } from '@/src/module/libs/telegram/telegram.service';
import { NotificationService } from '@/src/module/notification/notification.service'
import { Injectable } from '@nestjs/common'
@ -9,6 +10,7 @@ export class WebhookService {
private prismaService: PrismaService,
private liveKitService: LiveKitService,
private readonly notificationService: NotificationService,
private readonly telegramService: TelegramService,
) {
}
@ -41,6 +43,9 @@ export class WebhookService {
if (follower.notificationSettings?.siteNotifications) {
await this.notificationService.createStreamStart(follower.id, stream.user!)
}
if (follower.notificationSettings?.telegramNotifications && follower.telegramId) {
await this.telegramService.sendStreamStart(follower.telegramId, stream.user!)
}
}
}

50
backend/src/shared/types/env.d.ts vendored Normal file
View File

@ -0,0 +1,50 @@
import { StringValue } from '../util/ms.util'
export interface ProcessEnv {
NODE_ENV: 'development' | 'production'
APPLICATION_PORT: number
APPLICATION_URL: string
ALLOWED_ORIGIN: string
COOKIE_SECRET: string
SESSION_SECRET: string
SESSION_NAME: string
SESSION_DOMAIN: string
SESSION_MAX_AGE: StringValue
SESSION_HTTP_ONLY: boolean
SESSION_SECURE: boolean
SESSION_FOLDER: string
GRAPHQL_PREFIX: string
POSTGRES_USER: string
POSTGRES_PASSWORD: string
POSTGRES_HOST: string
POSTGRES_PORT: number
POSTGRES_DATABASE: string
POSTGRES_URI: string
REDIS_USER: string
REDIS_PASSWORD: string
REDIS_HOST: string
REDIS_PORT: number
REDIS_URI: string
MAIL_HOST: string
MAIL_PORT: number
MAIL_LOGIN: string
MAIL_PASSWORD: string
S3_ENDPOINT: string
S3_REGION: string
S3_ACCESS_KEY_ID: string
S3_SECRET_KEY_ID: string
S3_BUCKET_NAME: string
LIVEKIT_URL: string
LIVEKIT_API_KEY: string
LIVEKIT_API_SECRET: string
TELEGRAM_BOT_TOKEN: string
}

View File

@ -32,7 +32,11 @@ export async function generateToken(prismaService: PrismaService, user: User, ty
},
},
include: {
user: true,
user: {
include: {
notificationSettings: true,
},
},
},
})
}

View File

@ -1,10 +1,11 @@
import { ConfigService } from '@nestjs/config'
import * as dotenv from 'dotenv'
import * as process from 'node:process'
import { ProcessEnv } from '../types/env';
dotenv.config()
export function isDev(configService: ConfigService) {
export function isDev(configService: ConfigService<ProcessEnv>) {
return configService.getOrThrow('NODE_ENV') === 'development'
}

View File

@ -3,6 +3,7 @@ import { InternalServerErrorException } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { User } from '@prisma/generated'
import { Request } from 'express'
import { ProcessEnv } from '../types/env'
export function saveSession(req: Request, user: User, metadata: SessionInfo) {
return new Promise((resolve, reject) => {
@ -20,7 +21,7 @@ export function saveSession(req: Request, user: User, metadata: SessionInfo) {
})
}
export function destroySession(req: Request, configService: ConfigService) {
export function destroySession(req: Request, configService: ConfigService<ProcessEnv>) {
return new Promise((resolve, reject) => {
req.session.destroy((error) => {
if (error) {