add telegram bot
This commit is contained in:
parent
35df6780ec
commit
5b6d8cccb7
@ -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")
|
||||
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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')}`,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
9
backend/src/core/config/telegraf.config.ts
Normal file
9
backend/src/core/config/telegraf.config.ts
Normal 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'),
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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'))
|
||||
}
|
||||
|
||||
@ -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'],
|
||||
})
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
) {}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
) {}
|
||||
|
||||
|
||||
@ -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'),
|
||||
|
||||
17
backend/src/module/libs/telegram/telegram.button.ts
Normal file
17
backend/src/module/libs/telegram/telegram.button.ts
Normal 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',
|
||||
),
|
||||
]),
|
||||
}
|
||||
74
backend/src/module/libs/telegram/telegram.message.ts
Normal file
74
backend/src/module/libs/telegram/telegram.message.ts
Normal 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!`,
|
||||
}
|
||||
19
backend/src/module/libs/telegram/telegram.module.ts
Normal file
19
backend/src/module/libs/telegram/telegram.module.ts
Normal 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 {}
|
||||
159
backend/src/module/libs/telegram/telegram.service.ts
Normal file
159
backend/src/module/libs/telegram/telegram.service.ts
Normal 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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -60,7 +60,7 @@ export class NotificationService {
|
||||
|
||||
return {
|
||||
notificationSetting,
|
||||
token: telegramAuthToken.token,
|
||||
telegramAuthToken: telegramAuthToken.token,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@ -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
50
backend/src/shared/types/env.d.ts
vendored
Normal 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
|
||||
}
|
||||
@ -32,7 +32,11 @@ export async function generateToken(prismaService: PrismaService, user: User, ty
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
user: {
|
||||
include: {
|
||||
notificationSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -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'
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user